Skip to content

Commit 7154f3b

Browse files
committed
Sigma to elastalert conversion on rule enabled
1 parent 8182b27 commit 7154f3b

File tree

10 files changed

+497
-27
lines changed

10 files changed

+497
-27
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ services:
8888
- ELASTALERT_MODE=daemon
8989
volumes:
9090
- ./data/rulesets/sigma_ruleset:/app/sigma_ruleset:ro
91+
- ./config/elastalert/defaults.yml:/app/defaults.yml:ro
9192
- ./config/elastalert/elastalert_config.yml:/app/elastalert_config.yml:ro
9293
- ./config/elastalert_ruleset:/app/elastalert_ruleset
9394
- sentinel-kit_certificates_elasticsearch:/app/certs:ro
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace App\Command;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Input\InputOption;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
use Symfony\Component\Console\Style\SymfonyStyle;
11+
use Doctrine\ORM\EntityManagerInterface;
12+
use App\Entity\SigmaRule;
13+
14+
#[AsCommand(
15+
name: 'app:elastalert:clear',
16+
description: 'Clear all Elastalert rules from the backend application',
17+
)]
18+
class ElastalertClearCommand extends Command
19+
{
20+
21+
private EntityManagerInterface $entityManager;
22+
23+
public function __construct(EntityManagerInterface $entityManager)
24+
{
25+
parent::__construct();
26+
$this->entityManager = $entityManager;
27+
}
28+
29+
protected function configure(): void
30+
{
31+
$this->addOption(
32+
'force',
33+
'f',
34+
InputOption::VALUE_NONE,
35+
'Force deletion without confirmation'
36+
);
37+
}
38+
39+
protected function execute(InputInterface $input, OutputInterface $output): int
40+
{
41+
$io = new SymfonyStyle($input, $output);
42+
$force = $input->getOption('force');
43+
44+
$elastalertDir = '/detection-rules/elastalert';
45+
$files = glob($elastalertDir . '/*.yml');
46+
$fileCount = count($files);
47+
48+
if (!$force) {
49+
$io->warning([
50+
'This command clears ALL ElastAlert rules and should ONLY be used for debugging purposes!',
51+
"ElastAlert files to be deleted: $fileCount",
52+
'This operation will disrupt active monitoring and alerting.',
53+
'Use this command only in development or debugging scenarios.'
54+
]);
55+
56+
if (!$io->confirm('Are you sure you want to proceed with clearing all ElastAlert rules?', false)) {
57+
$io->text('Operation cancelled.');
58+
return Command::SUCCESS;
59+
}
60+
}
61+
62+
$this->clearElastalertDirectory();
63+
64+
$rules = $this->entityManager->getRepository(SigmaRule::class)->findAll();
65+
foreach ($rules as $rule) {
66+
$this->entityManager->remove($rule);
67+
}
68+
$this->entityManager->flush();
69+
70+
$io->success("$fileCount ElastAlert rules have been cleared successfully.");
71+
return Command::SUCCESS;
72+
}
73+
74+
private function clearElastalertDirectory(): void
75+
{
76+
$elastalertDir = '/detection-rules/elastalert';
77+
$files = glob($elastalertDir . '/*.yml');
78+
foreach ($files as $file) {
79+
unlink($file);
80+
}
81+
}
82+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace App\Command;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
use Doctrine\ORM\EntityManagerInterface;
11+
use App\Service\ElastalertRuleValidator;
12+
use App\Entity\SigmaRule;
13+
14+
#[AsCommand(
15+
name: 'app:elastalert:sync',
16+
description: 'Synchronize Elastalert rules from enabled sigma rules',
17+
)]
18+
class ElastalertSyncCommand extends Command
19+
{
20+
21+
private EntityManagerInterface $entityManager;
22+
private ElastalertRuleValidator $elastalertValidator;
23+
24+
public function __construct(EntityManagerInterface $entityManager, ElastalertRuleValidator $elastalertValidator)
25+
{
26+
parent::__construct();
27+
$this->entityManager = $entityManager;
28+
$this->elastalertValidator = $elastalertValidator;
29+
}
30+
31+
protected function configure(): void
32+
{
33+
}
34+
35+
protected function execute(InputInterface $input, OutputInterface $output): int
36+
{
37+
$io = new SymfonyStyle($input, $output);
38+
39+
$this->clearElastalertDirectory();
40+
41+
$rules = $this->entityManager->getRepository(SigmaRule::class)->findBy(['active' => true]);
42+
foreach ($rules as $rule) {
43+
$e = $this->elastalertValidator->createElastalertRule($rule->getRuleLatestVersion());
44+
if (isset($e['error'])) {
45+
$io->error('Failed to create Elastalert rule for Sigma rule "' . $rule->getTitle() . '": ' . $e['error']);
46+
$rule->setActive(false);
47+
$this->entityManager->persist($rule);
48+
} else {
49+
$io->writeln('Elastalert rule created for Sigma rule "' . $rule->getTitle() . '" at ' . $e['filePath']);
50+
}
51+
}
52+
53+
try{
54+
$this->entityManager->flush();
55+
} catch (\Exception $e) {
56+
$io->error('Failed to flush changes to database: ' . $e->getMessage());
57+
$this->clearElastalertDirectory();
58+
return Command::FAILURE;
59+
}
60+
61+
return Command::SUCCESS;
62+
}
63+
64+
private function clearElastalertDirectory(): void
65+
{
66+
$elastalertDir = '/detection-rules/elastalert';
67+
$files = glob($elastalertDir . '/*.yml');
68+
foreach ($files as $file) {
69+
unlink($file);
70+
}
71+
}
72+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace App\Command;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Input\InputOption;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
use Symfony\Component\Console\Style\SymfonyStyle;
11+
use Doctrine\ORM\EntityManagerInterface;
12+
use App\Entity\SigmaRule;
13+
use Symfony\Component\Console\Input\ArrayInput;
14+
15+
#[AsCommand(
16+
name: 'app:sigma:clear',
17+
description: 'Clear all Sigma rules from the backend application',
18+
)]
19+
class SigmaRulesClearCommand extends Command
20+
{
21+
22+
private EntityManagerInterface $entityManager;
23+
24+
public function __construct(EntityManagerInterface $entityManager)
25+
{
26+
parent::__construct();
27+
$this->entityManager = $entityManager;
28+
}
29+
30+
protected function configure(): void
31+
{
32+
$this->addOption(
33+
'force',
34+
'f',
35+
InputOption::VALUE_NONE,
36+
'Force deletion without confirmation'
37+
);
38+
}
39+
40+
protected function execute(InputInterface $input, OutputInterface $output): int
41+
{
42+
$io = new SymfonyStyle($input, $output);
43+
$force = $input->getOption('force');
44+
45+
$rules = $this->entityManager->getRepository(SigmaRule::class)->findAll();
46+
$ruleCount = count($rules);
47+
48+
if ($ruleCount === 0) {
49+
$io->info('No Sigma rules found to clear.');
50+
return Command::SUCCESS;
51+
}
52+
53+
$io->warning([
54+
'This operation will permanently delete ALL Sigma rules!',
55+
"Total rules to be deleted: $ruleCount",
56+
'This action is irreversible.'
57+
]);
58+
59+
if (!$force) {
60+
if (!$io->confirm('Are you sure you want to proceed?', false)) {
61+
$io->text('Operation cancelled.');
62+
return Command::SUCCESS;
63+
}
64+
}
65+
66+
$syncCommand = $this->getApplication()->find('app:elastalert:clear');
67+
$syncInput = new ArrayInput(['--force' => true]);
68+
$syncCommand->run($syncInput, $output);
69+
70+
71+
foreach ($rules as $rule) {
72+
$this->entityManager->remove($rule);
73+
}
74+
75+
$this->entityManager->flush();
76+
77+
$io->success("$ruleCount Sigma rules have been cleared successfully.");
78+
return Command::SUCCESS;
79+
}
80+
}

sentinel-kit_server_backend/src/Command/SigmaRulesLoadCommand.php

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Symfony\Component\Console\Attribute\AsCommand;
66
use Symfony\Component\Console\Command\Command;
77
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Input\ArrayInput;
89
use Symfony\Component\Console\Output\OutputInterface;
910
use Symfony\Component\Console\Style\SymfonyStyle;
1011
use Doctrine\ORM\EntityManagerInterface;
@@ -14,6 +15,7 @@
1415
use App\Entity\SigmaRule;
1516
use App\Entity\SigmaRuleVersion;
1617
use App\Service\SigmaRuleValidator;
18+
use Symfony\Component\Console\Input\InputOption;
1719

1820
#[AsCommand(
1921
name: 'app:sigma:load-rules',
@@ -33,11 +35,18 @@ public function __construct(EntityManagerInterface $entityManager, SigmaRuleVali
3335

3436
protected function configure(): void
3537
{
38+
$this->addOption(
39+
'auto-enable',
40+
null,
41+
InputOption::VALUE_NONE,
42+
'Automatically enable imported rules'
43+
);
3644
}
3745

3846
protected function execute(InputInterface $input, OutputInterface $output): int
3947
{
4048
$io = new SymfonyStyle($input, $output);
49+
$autoEnable = $input->getOption('auto-enable');
4150

4251
$directory = '/detection-rules/sigma';
4352

@@ -105,10 +114,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
105114
$errorCount++;
106115
continue;
107116
}
108-
109-
$finalYamlData = $validationResult['yamlData'];
110117

111-
$this->storeSigmaRule($completedContent, $finalYamlData, $filename, $relativePath, $io);
118+
$this->storeSigmaRule(Yaml::dump($validationResult['yamlData']), $validationResult['yamlData'], $filename, $relativePath, $autoEnable, $io);
112119
$processedCount++;
113120
} catch (ParseException $e) {
114121
$io->error("YAML parse error in file " . $relativePath . ": " . $e->getMessage());
@@ -130,13 +137,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int
130137
$io->text("Total files processed: $processedCount");
131138
if ($errorCount > 0) {
132139
$io->text("Files with errors: $errorCount");
133-
return Command::FAILURE;
134-
} else {
135-
$io->success("Sigma rules loaded successfully.");
136140
}
137141

142+
if ($processedCount > 0) {
143+
$io->success("$processedCount Sigma rules loaded successfully.");
144+
145+
$io->text("Synchronizing Elastalert rules...");
146+
$syncCommand = $this->getApplication()->find('app:elastalert:sync');
147+
$syncInput = new ArrayInput([]);
148+
$syncReturnCode = $syncCommand->run($syncInput, $output);
149+
150+
if ($syncReturnCode === Command::SUCCESS) {
151+
$io->success("Elastalert synchronization completed successfully.");
152+
} else {
153+
$io->warning("Elastalert synchronization failed, but Sigma rules were imported successfully.");
154+
}
155+
156+
return ($errorCount > 0) ? Command::FAILURE : Command::SUCCESS;
157+
}
138158

139-
return Command::SUCCESS;
159+
$io->error("No rules were successfully imported.");
160+
return Command::FAILURE;
140161
}
141162

142163
/**
@@ -161,21 +182,20 @@ private function findYamlFiles(string $directory): array
161182
/**
162183
* Store Sigma Rule and its version into the database
163184
*/
164-
private function storeSigmaRule(string $content, array $yamlData, string $filename, string $filePath, SymfonyStyle $io): void
185+
private function storeSigmaRule(string $content, array $yamlData, string $filename, string $filePath, bool $autoEnable, SymfonyStyle $io): void
165186
{
166187

167188
$rule = new SigmaRule();
168189
$rule->setTitle($yamlData['title']);
169190
$rule->setDescription($yamlData['description']);
170191
$rule->setFilename($filename);
171-
$rule->setActive(false);
192+
$rule->setActive($autoEnable);
172193

173194
$ruleVersion = new SigmaRuleVersion();
174195
$ruleVersion->setContent($content);
175196
$ruleVersion->setLevel($yamlData['level']);
176197
$rule->addVersion($ruleVersion);
177198

178-
179199
$r = $this->entityManager->GetRepository(SigmaRule::class)->findOneBy(['title' => $yamlData['title']]);
180200
if ($r) {
181201
$io->warning(sprintf('Rule with title "%s" already exists in database', $yamlData['title']));

0 commit comments

Comments
 (0)