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 Symfony \Component \String \Slugger \AsciiSlugger ;
12+ use Symfony \Component \Yaml \Yaml ;
13+ use Symfony \Component \Yaml \Exception \ParseException ;
14+ use App \Entity \SigmaRule ;
15+ use App \Entity \SigmaRuleVersion ;
16+
17+ #[AsCommand(
18+ name: 'app:sigma:load-rules ' ,
19+ description: 'Loads *.yml rules files into Sentinel-Kit database in the backend application ' ,
20+ )]
21+ class SigmaRulesLoadCommand extends Command
22+ {
23+ private $ entityManager ;
24+
25+ public function __construct (EntityManagerInterface $ entityManager )
26+ {
27+ parent ::__construct ();
28+ $ this ->entityManager = $ entityManager ;
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+ $ directory = '/detection-rules/sigma ' ;
40+
41+ if (!is_dir ($ directory )) {
42+ $ io ->error ("Directory does not exist: $ directory " );
43+ return Command::FAILURE ;
44+ }
45+
46+ $ yamlFiles = $ this ->findYamlFiles ($ directory );
47+
48+ $ processedCount = 0 ;
49+ $ errorCount = 0 ;
50+
51+ foreach ($ yamlFiles as $ filePath ) {
52+ $ relativePath = str_replace ($ directory . DIRECTORY_SEPARATOR , '' , $ filePath );
53+ $ io ->text ("Processing: " . $ relativePath );
54+
55+ try {
56+ $ content = file_get_contents ($ filePath );
57+ if ($ content === false ) {
58+ $ io ->error ("Could not read file: $ filePath " );
59+ $ errorCount ++;
60+ continue ;
61+ }
62+
63+ $ yamlData = Yaml::parse ($ content );
64+
65+ if ($ yamlData === null ) {
66+ $ io ->warning ("File contains no valid YAML data: $ filePath " );
67+ continue ;
68+ }
69+
70+ $ this ->storeSigmaRule ($ content , $ yamlData , $ relativePath , $ io );
71+ $ processedCount ++;
72+ } catch (ParseException $ e ) {
73+ $ io ->error ("YAML parse error in file " . $ relativePath . ": " . $ e ->getMessage ());
74+ $ errorCount ++;
75+ } catch (\Exception $ e ) {
76+ $ io ->error ("Error processing file " . $ relativePath . ": " . $ e ->getMessage ());
77+ $ errorCount ++;
78+ }
79+ }
80+
81+ try {
82+ $ this ->entityManager ->flush ();
83+ } catch (\Exception $ e ) {
84+ $ io ->error ("Error flushing to database: " . $ e ->getMessage ());
85+ return Command::FAILURE ;
86+ }
87+
88+ $ io ->section ("Summary " );
89+ $ io ->text ("Total files processed: $ processedCount " );
90+ if ($ errorCount > 0 ) {
91+ $ io ->text ("Files with errors: $ errorCount " );
92+ return Command::FAILURE ;
93+ } else {
94+ $ io ->success ("Sigma rules loaded successfully. " );
95+ }
96+
97+
98+ return Command::SUCCESS ;
99+ }
100+
101+ /**
102+ * Recursively find all YAML files in a directory
103+ */
104+ private function findYamlFiles (string $ directory ): array
105+ {
106+ $ yamlFiles = [];
107+ $ iterator = new \RecursiveIteratorIterator (
108+ new \RecursiveDirectoryIterator ($ directory , \RecursiveDirectoryIterator::SKIP_DOTS )
109+ );
110+
111+ foreach ($ iterator as $ file ) {
112+ if ($ file ->isFile () && in_array ($ file ->getExtension (), ['yml ' , 'yaml ' ])) {
113+ $ yamlFiles [] = $ file ->getRealPath ();
114+ }
115+ }
116+
117+ return $ yamlFiles ;
118+ }
119+
120+ /**
121+ * Store Sigma Rule and its version into the database
122+ */
123+ private function storeSigmaRule (string $ content , array $ yamlData , string $ filePath ,SymfonyStyle $ io ): void
124+ {
125+ $ slugger = new AsciiSlugger ();
126+ $ title = '' ;
127+ $ description = null ;
128+ $ filename = $ slugger ->slug (pathinfo ($ filePath , PATHINFO_FILENAME ));
129+
130+ if (!empty ($ yamlData ['title ' ])) {
131+ $ title = $ yamlData ['title ' ];
132+ }else {
133+ $ title = substr ($ filename , 0 , strlen ($ filename ) - 4 );
134+ }
135+
136+ if (!empty ($ yamlData ['description ' ])) {
137+ $ description = $ yamlData ['description ' ];
138+ }
139+
140+ $ r = $ this ->entityManager ->GetRepository (SigmaRule::class)->findOneBy (['title ' => $ title ]);
141+ if ($ r ) {
142+ $ io ->warning (sprintf ('Rule with title "%s" already exists already exists in database ' , $ title ));
143+ return ;
144+ }
145+
146+ $ rd = $ this ->entityManager ->getRepository (SigmaRuleVersion::class)->findOneBy (['hash ' => md5 ($ content )]);
147+ if ($ rd ) {
148+ $ io ->warning (sprintf ('Rule "%s" ignored - content already exists in %s ' , $ filePath , $ title , $ description ));
149+ return ;
150+ }
151+
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 );
161+
162+ try {
163+ $ this ->entityManager ->persist ($ rule );
164+ }catch (\Exception $ e ){
165+ $ io ->error (sprintf ("Error storing rule: %s - %s " , $ filePath , $ e ->getMessage ()));
166+ return ;
167+ }
168+
169+ return ;
170+ }
171+ }
0 commit comments