Skip to content

Commit 8182b27

Browse files
committed
Sigma rules edition, bulk actions and rule severity implementation
1 parent 39c0e96 commit 8182b27

File tree

11 files changed

+857
-103
lines changed

11 files changed

+857
-103
lines changed

sentinel-kit_server_backend/src/Command/SigmaRulesLoadCommand.php

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Symfony\Component\Yaml\Exception\ParseException;
1414
use App\Entity\SigmaRule;
1515
use App\Entity\SigmaRuleVersion;
16+
use App\Service\SigmaRuleValidator;
1617

1718
#[AsCommand(
1819
name: 'app:sigma:load-rules',
@@ -21,11 +22,13 @@
2122
class SigmaRulesLoadCommand extends Command
2223
{
2324
private $entityManager;
25+
private $validator;
2426

25-
public function __construct(EntityManagerInterface $entityManager)
27+
public function __construct(EntityManagerInterface $entityManager, SigmaRuleValidator $validator)
2628
{
2729
parent::__construct();
2830
$this->entityManager = $entityManager;
31+
$this->validator = $validator;
2932
}
3033

3134
protected function configure(): void
@@ -60,14 +63,52 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6063
continue;
6164
}
6265

63-
$yamlData = Yaml::parse($content);
66+
$slugger = new AsciiSlugger();
67+
$filename = $slugger->slug(pathinfo($relativePath, PATHINFO_FILENAME));
68+
$titleFromFilename = pathinfo($relativePath, PATHINFO_FILENAME);
69+
70+
try {
71+
$yamlData = Yaml::parse($content);
72+
} catch (ParseException $e) {
73+
$io->error("YAML parse error in file " . $relativePath . ": " . $e->getMessage());
74+
$errorCount++;
75+
continue;
76+
}
77+
78+
if (!$yamlData) {
79+
$io->error("Empty or invalid YAML content in file: " . $relativePath);
80+
$errorCount++;
81+
continue;
82+
}
83+
84+
if (empty($yamlData['title'])) {
85+
$yamlData['title'] = $titleFromFilename;
86+
}
87+
88+
if (empty($yamlData['description'])) {
89+
$yamlData['description'] = '';
90+
}
91+
92+
$completedContent = Yaml::dump($yamlData);
93+
94+
$validationResult = $this->validator->validateSigmaRuleContent($completedContent);
6495

65-
if ($yamlData === null) {
66-
$io->warning("File contains no valid YAML data: $filePath");
96+
if (isset($validationResult['error'])) {
97+
$io->error("Validation error in file " . $relativePath . ": " . $validationResult['error']);
98+
$errorCount++;
99+
continue;
100+
}
101+
102+
if (!empty($validationResult['missingFields'])) {
103+
$missingFieldsString = implode(", ", $validationResult['missingFields']);
104+
$io->warning("File " . $relativePath . " is missing required fields: " . $missingFieldsString . " - Skipping");
105+
$errorCount++;
67106
continue;
68107
}
69108

70-
$this->storeSigmaRule($content, $yamlData, $relativePath, $io);
109+
$finalYamlData = $validationResult['yamlData'];
110+
111+
$this->storeSigmaRule($completedContent, $finalYamlData, $filename, $relativePath, $io);
71112
$processedCount++;
72113
} catch (ParseException $e) {
73114
$io->error("YAML parse error in file " . $relativePath . ": " . $e->getMessage());
@@ -120,44 +161,33 @@ private function findYamlFiles(string $directory): array
120161
/**
121162
* Store Sigma Rule and its version into the database
122163
*/
123-
private function storeSigmaRule(string $content, array $yamlData, string $filePath,SymfonyStyle $io): void
164+
private function storeSigmaRule(string $content, array $yamlData, string $filename, string $filePath, SymfonyStyle $io): void
124165
{
125-
$slugger = new AsciiSlugger();
126-
$title = '';
127-
$description = null;
128-
$filename = $slugger->slug(pathinfo($filePath, PATHINFO_FILENAME));
166+
167+
$rule = new SigmaRule();
168+
$rule->setTitle($yamlData['title']);
169+
$rule->setDescription($yamlData['description']);
170+
$rule->setFilename($filename);
171+
$rule->setActive(false);
129172

130-
if (!empty($yamlData['title'])) {
131-
$title = $yamlData['title'];
132-
}else{
133-
$title = substr($filename, 0, strlen($filename) - 4);
134-
}
173+
$ruleVersion = new SigmaRuleVersion();
174+
$ruleVersion->setContent($content);
175+
$ruleVersion->setLevel($yamlData['level']);
176+
$rule->addVersion($ruleVersion);
135177

136-
if (!empty($yamlData['description'])) {
137-
$description = $yamlData['description'];
138-
}
139178

140-
$r = $this->entityManager->GetRepository(SigmaRule::class)->findOneBy(['title' => $title]);
179+
$r = $this->entityManager->GetRepository(SigmaRule::class)->findOneBy(['title' => $yamlData['title']]);
141180
if ($r) {
142-
$io->warning(sprintf('Rule with title "%s" already exists already exists in database', $title));
181+
$io->warning(sprintf('Rule with title "%s" already exists in database', $yamlData['title']));
143182
return;
144183
}
145184

146-
$rd = $this->entityManager->getRepository(SigmaRuleVersion::class)->findOneBy(['hash' => md5($content)]);
185+
$rd = $this->entityManager->getRepository(SigmaRuleVersion::class)->findOneBy(['hash' => $ruleVersion->getHash()]);
147186
if ($rd) {
148-
$io->warning(sprintf('Rule "%s" ignored - content already exists in %s', $filePath, $title, $description));
187+
$io->warning(sprintf('Rule "%s" ignored - content already exists', $filePath));
149188
return;
150189
}
151190

152-
$rule = new SigmaRule();
153-
$rule->setFilename($filename);
154-
$rule->setTitle($title);
155-
$rule->setDescription($description);
156-
$rule->setActive(false);
157-
158-
$ruleVersion = new SigmaRuleVersion();
159-
$ruleVersion->setContent($content);
160-
$rule->addVersion($ruleVersion);
161191

162192
try{
163193
$this->entityManager->persist($rule);

sentinel-kit_server_backend/src/Controller/SigmaController.php

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,75 @@
77
use Symfony\Component\HttpFoundation\JsonResponse;
88
use Symfony\Component\Routing\Annotation\Route;
99
use Symfony\Component\Serializer\SerializerInterface;
10+
use Symfony\Component\Yaml\Yaml;
11+
use Symfony\Component\Yaml\Exception\ParseException;
1012
use Doctrine\ORM\EntityManagerInterface;
1113
use App\Entity\SigmaRule;
14+
use App\Entity\SigmaRuleVersion;
15+
use App\Service\SigmaRuleValidator;
1216

1317
class SigmaController extends AbstractController{
1418

1519
private $entityManger;
1620
private $serializer;
21+
private $validator;
1722

18-
public function __construct(EntityManagerInterface $em, SerializerInterface $serializer){
23+
public function __construct(EntityManagerInterface $em, SerializerInterface $serializer, SigmaRuleValidator $validator){
1924
$this->entityManger = $em;
2025
$this->serializer = $serializer;
26+
$this->validator = $validator;
27+
}
28+
29+
30+
31+
#[Route('/api/rules/sigma/add_rule', name: 'app_sigma_add_rule', methods: ['POST'])]
32+
public function SaveNewSigmaRule(Request $request): JsonResponse {
33+
$data = json_decode($request->getContent(), true);
34+
if (!isset($data['rule_content'])) {
35+
return new JsonResponse(['error' => 'Missing rule content'], Response::HTTP_BAD_REQUEST);
36+
}
37+
38+
$ruleContent = $data['rule_content'];
39+
$validationResult = $this->validator->validateSigmaRuleContent($ruleContent);
40+
41+
if (isset($validationResult['error'])) {
42+
return new JsonResponse(['error' => $validationResult['error']], Response::HTTP_BAD_REQUEST);
43+
}
44+
45+
if (!empty($validationResult['missingFields'])) {
46+
$missingFieldsString = implode(", ", $validationResult['missingFields']);
47+
return new JsonResponse(['error' => "Missing required fields: " . $missingFieldsString], Response::HTTP_BAD_REQUEST);
48+
}
49+
50+
$yamlData = $validationResult['yamlData'];
51+
52+
$existingRule = $this->entityManger->getRepository(SigmaRule::class)->findOneBy(['title' => $yamlData['title']]);
53+
if ($existingRule) {
54+
return new JsonResponse(['error' => 'A rule with this title already exists'], Response::HTTP_BAD_REQUEST);
55+
}
56+
57+
$newRule = new SigmaRule();
58+
$newRuleVersion = new SigmaRuleVersion();
59+
$newRule->setTitle($yamlData['title']);
60+
$newRule->setDescription($yamlData['description']);
61+
$newRule->setActive(false);
62+
$newRuleVersion->setContent(Yaml::dump($yamlData, 4));
63+
$newRuleVersion->setLevel($yamlData['level']);
64+
$newRule->addVersion($newRuleVersion);
65+
66+
$existingVersion = $this->entityManger->getRepository(SigmaRuleVersion::class)->findOneBy(['hash' => $newRuleVersion->getHash()]);
67+
if ($existingVersion) {
68+
return new JsonResponse(['error' => 'A rule with the exact same content already exists'], Response::HTTP_BAD_REQUEST);
69+
}
70+
71+
try{
72+
$this->entityManger->persist($newRule);
73+
$this->entityManger->flush();
74+
} catch (\Exception $e) {
75+
return new JsonResponse(['error' => 'Failed to save the rule: ' . $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
76+
}
77+
78+
return new JsonResponse(['error' => '', 'rule_id' => $newRule->getId()], Response::HTTP_OK);
2179
}
2280

2381

@@ -29,7 +87,7 @@ public function listRulesSummary(Request $request): Response
2987
}
3088

3189
#[Route('/api/rules/sigma/{ruleId}/status', name:'app_sigma_change_rule_status', methods: ['PUT'])]
32-
public function editRuleStatus(Request $request, int $ruleId): Response{
90+
public function editRuleStatus(Request $request, int $ruleId): JsonResponse{
3391
$rule = $this->entityManger->getRepository(SigmaRule::class)->find($ruleId);
3492
if(!$rule){
3593
return new JsonResponse(['error' => 'Rule not found'], Response::HTTP_NOT_FOUND);
@@ -44,7 +102,7 @@ public function editRuleStatus(Request $request, int $ruleId): Response{
44102
}
45103

46104
#[Route('api/rules/sigma/{ruleId}/details', name:'app_sigma_get_rule', methods: ['GET'])]
47-
public function getRule(Request $request, int $ruleId): Response {
105+
public function getRule(Request $request, int $ruleId): JsonResponse {
48106
$rule = $this->entityManger->getRepository(SigmaRule::class)->find($ruleId);
49107
if (!$rule) {
50108
return new JsonResponse(['error' => 'Rule not found'], Response::HTTP_NOT_FOUND);
@@ -54,8 +112,77 @@ public function getRule(Request $request, int $ruleId): Response {
54112
return new JsonResponse($serializedRule, Response::HTTP_OK, [], true);
55113
}
56114

115+
#[Route('/api/rules/sigma/{ruleId}/add_version', name: 'app_sigma_add_version', methods: ['POST'])]
116+
public function addRuleVersion(Request $request, int $ruleId): JsonResponse {
117+
$rule = $this->entityManger->getRepository(SigmaRule::class)->find($ruleId);
118+
if (!$rule) {
119+
return new JsonResponse(['error' => 'Rule not found'], Response::HTTP_NOT_FOUND);
120+
}
121+
122+
$data = json_decode($request->getContent(), true);
123+
if (!isset($data['rule_content'])) {
124+
return new JsonResponse(['error' => 'Missing rule content'], Response::HTTP_BAD_REQUEST);
125+
}
126+
127+
$ruleContent = $data['rule_content'];
128+
$validationResult = $this->validator->validateSigmaRuleContent($ruleContent);
129+
130+
if (isset($validationResult['error'])) {
131+
return new JsonResponse(['error' => $validationResult['error']], Response::HTTP_BAD_REQUEST);
132+
}
133+
134+
if (!empty($validationResult['missingFields'])) {
135+
$missingFieldsString = implode(", ", $validationResult['missingFields']);
136+
return new JsonResponse(['error' => "Missing required fields: " . $missingFieldsString], Response::HTTP_BAD_REQUEST);
137+
}
138+
139+
$yamlData = $validationResult['yamlData'];
140+
141+
$newRuleVersion = new SigmaRuleVersion();
142+
$newRuleVersion->setContent($ruleContent);
143+
$newRuleVersion->setLevel($yamlData['level']);
144+
$newRuleVersion->setRule($rule);
145+
146+
$existingVersion = $this->entityManger->getRepository(SigmaRuleVersion::class)->findOneBy(['hash' => $newRuleVersion->getHash()]);
147+
if ($existingVersion) {
148+
return new JsonResponse(['error' => 'A version with the exact same content already exists'], Response::HTTP_BAD_REQUEST);
149+
}
150+
151+
if ($rule->getTitle() !== $yamlData['title']) {
152+
$rule->setTitle($yamlData['title']);
153+
}
154+
if ($rule->getDescription() !== $yamlData['description']) {
155+
$rule->setDescription($yamlData['description']);
156+
}
157+
158+
$existingRulesWithTitle = $this->entityManger->getRepository(SigmaRule::class)->findBy(['title' => $yamlData['title']]);
159+
foreach ($existingRulesWithTitle as $existingRule) {
160+
if ($existingRule->getId() !== $rule->getId()) {
161+
return new JsonResponse(['error' => 'Another rule with this title already exists'], Response::HTTP_BAD_REQUEST);
162+
}
163+
}
164+
165+
$latestVersion = $this->entityManger->getRepository(SigmaRuleVersion::class)->findOneBy(
166+
['rule' => $rule],
167+
['id' => 'DESC']
168+
);
169+
if ($latestVersion && $latestVersion->getHash() === $newRuleVersion->getHash()) {
170+
return new JsonResponse(['error' => 'The new version content is identical to the latest version'], Response::HTTP_BAD_REQUEST);
171+
}
172+
173+
174+
try {
175+
$this->entityManger->persist($newRuleVersion);
176+
$this->entityManger->flush();
177+
} catch (\Exception $e) {
178+
return new JsonResponse(['error' => 'Failed to save the new version: ' . $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
179+
}
180+
181+
return new JsonResponse(['error' => '', 'version_id' => $newRuleVersion->getId()], Response::HTTP_OK);
182+
}
183+
57184
#[Route('api/rules/sigma/{ruleId}', name:'app_sigma_delete_rule', methods: ['DELETE'])]
58-
public function deleteRule(Request $request, int $ruleId): Response {
185+
public function deleteRule(Request $request, int $ruleId): JsonResponse {
59186
$rule = $this->entityManger->getRepository(SigmaRule::class)->find($ruleId);
60187
if (!$rule) {
61188
return new JsonResponse(['error' => 'Rule not found'], Response::HTTP_NOT_FOUND);

sentinel-kit_server_backend/src/Entity/SigmaRule.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\DBAL\Types\Types;
99
use Doctrine\ORM\Mapping as ORM;
1010
use Symfony\Component\Serializer\Annotation\Groups;
11+
use Symfony\Component\String\Slugger\AsciiSlugger;
1112

1213
#[ORM\Entity(repositoryClass: SigmaRuleRepository::class)]
1314
class SigmaRule
@@ -131,6 +132,8 @@ public function getTitle(): ?string
131132
public function setTitle(string $title): static
132133
{
133134
$this->title = trim($title);
135+
$slugger = new AsciiSlugger();
136+
$this->setFilename($slugger->slug($title)->lower());
134137

135138
return $this;
136139
}

sentinel-kit_server_backend/src/Entity/SigmaRuleVersion.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class SigmaRuleVersion
2020
#[Groups(['rule_details'])]
2121
private ?string $content = null;
2222

23+
#[ORM\Column(length: 20)]
24+
#[Groups(['rule_details'])]
25+
private ?string $level = 'informational';
26+
2327
#[ORM\Column(length: 64, unique: true)]
2428
private ?string $hash = null;
2529

@@ -54,6 +58,25 @@ public function setContent(string $content): static
5458
return $this;
5559
}
5660

61+
public function getLevel(): ?string
62+
{
63+
return $this->level;
64+
}
65+
66+
public function setLevel(string $level): static
67+
{
68+
$allowedLevels = ['informational', 'low', 'medium', 'high', 'critical'];
69+
if (!in_array($level, $allowedLevels)) {
70+
throw new \InvalidArgumentException(
71+
sprintf('Invalid level "%s". Allowed values are: %s', $level, implode(', ', $allowedLevels))
72+
);
73+
}
74+
75+
$this->level = $level;
76+
77+
return $this;
78+
}
79+
5780
public function getHash(): ?string
5881
{
5982
return $this->hash;

sentinel-kit_server_backend/src/Repository/SigmaRuleRepository.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,23 @@ public function __construct(ManagerRegistry $registry)
2222
* @return array
2323
*/
2424
public function summaryFindAll() : array{
25-
$qb = $this->createQueryBuilder("r")->orderBy("r.title","ASC")->select("r.id","r.title","r.description","r.active","r.createdOn","r.createdOn");
25+
$qb = $this->createQueryBuilder("r");
26+
27+
$subQuery = $this->createQueryBuilder('r2')
28+
->select('MAX(v2.createdOn)')
29+
->innerJoin('r2.versions', 'v2')
30+
->where('r2.id = r.id')
31+
->getDQL();
32+
33+
$qb->leftJoin(
34+
'r.versions',
35+
'v',
36+
Join::WITH,
37+
$qb->expr()->eq('v.createdOn', '(' . $subQuery . ')')
38+
)
39+
->select("r.id","r.title","r.description","r.active","r.createdOn","v.level")
40+
->orderBy("r.title","ASC");
41+
2642
return $qb->getQuery()->getResult();
2743
}
2844

0 commit comments

Comments
 (0)