@@ -18,8 +18,10 @@ import (
1818 "github.com/urfave/cli/v3"
1919)
2020
21- var logger * zerolog.Logger
22- var version string
21+ var (
22+ logger * zerolog.Logger
23+ version string
24+ )
2325
2426type taskCallbackFn = func (* cli.Command , * engine.Engine ) error
2527
@@ -29,23 +31,28 @@ func main() {
2931 }
3032
3133 baseFlags := []cli.Flag {
32- & cli.BoolFlag {Name : "debug" , Aliases : []string {"d" }},
33- & cli.BoolFlag {Name : "remote" , Aliases : []string {"r" }},
34- & cli.BoolFlag {Name : "headfull" , Aliases : []string {"H" }},
35- & cli.IntFlag {Name : "concurrency" , Aliases : []string {"c" }},
36- & cli.IntFlag {Name : "port" , Value : 9222 },
37- & cli.StringFlag {Name : "device-type" , Value : "desktop" , Action : validDeviceType },
38- & cli.StringFlag {Name : "device-size" , Value : "large" , Action : validDeviceSize },
39- & cli.StringFlag {Name : "host" , Value : "127.0.0.1" },
40- & cli.StringFlag {Name : "user-agent" , Value : "chrome" },
34+ & cli.BoolFlag {Name : "debug" , Aliases : []string {"d" }, Usage : "enable debug logging" },
35+ & cli.BoolFlag {Name : "remote" , Aliases : []string {"r" }, Usage : "use a remote Chrome DevTools instance" },
36+ & cli.BoolFlag {Name : "headfull" , Aliases : []string {"H" }, Usage : "run browser in headfull mode" },
37+ & cli.IntFlag {Name : "concurrency" , Aliases : []string {"c" }, Usage : "number of concurrent workers" },
38+ & cli.IntFlag {Name : "port" , Value : 9222 , Usage : "remote DevTools port" },
39+ & cli.StringFlag {Name : "device-type" , Value : "desktop" , Usage : "device type (desktop/mobile/tablet)" , Action : validDeviceType },
40+ & cli.StringFlag {Name : "device-size" , Value : "large" , Usage : "device size preset" , Action : validDeviceSize },
41+ & cli.StringFlag {Name : "host" , Value : "127.0.0.1" , Usage : "remote DevTools host" },
42+ & cli.StringFlag {Name : "user-agent" , Value : "chrome" , Usage : "browser user-agent preset" },
43+ & cli.StringFlag {
44+ Name : "rules-dir" ,
45+ Value : "rules" ,
46+ Usage : "directory containing Sigma rules (default: rules/)" ,
47+ },
4148 }
4249
4350 withOutputFlags := append ([]cli.Flag {
44- & cli.StringFlag {Name : "output" , Value : "pisces.ndjson" , Aliases : []string {"o" }},
51+ & cli.StringFlag {Name : "output" , Value : "pisces.ndjson" , Aliases : []string {"o" }, Usage : "output NDJSON file" },
4552 }, baseFlags ... )
4653
4754 withWaitFlags := append ([]cli.Flag {
48- & cli.IntFlag {Name : "wait" , Value : 300 , Aliases : []string {"w" }},
55+ & cli.IntFlag {Name : "wait" , Value : 300 , Aliases : []string {"w" }, Usage : "wait time (seconds) after load before analysis" },
4956 }, withOutputFlags ... )
5057
5158 ver := version
@@ -64,9 +71,9 @@ func main() {
6471 Arguments : baseArgs ,
6572 Flags : withWaitFlags ,
6673 Action : func (ctx context.Context , cmd * cli.Command ) error {
67- params := map [string ]any {}
68- params [ "wait" ] = cmd .Int ("wait" )
69-
74+ params := map [string ]any {
75+ "wait" : cmd .Int ("wait" ),
76+ }
7077 return runTask (ctx , cmd , "analyze" , params , outputResultJson )
7178 },
7279 },
@@ -84,46 +91,10 @@ func main() {
8491 Usage : "Screenshot one or more URLs" ,
8592 Arguments : baseArgs ,
8693 Flags : append ([]cli.Flag {
87- & cli.StringFlag {Name : "output-dir" , Value : "tmp/" , Aliases : []string {"o" }},
94+ & cli.StringFlag {Name : "output-dir" , Value : "tmp/" , Aliases : []string {"o" }, Usage : "directory for screenshots" },
8895 }, baseFlags ... ),
8996 Action : func (ctx context.Context , cmd * cli.Command ) error {
90- return runTask (ctx , cmd , "screenshot" , map [string ]any {}, func (cmd * cli.Command , e * engine.Engine ) error {
91- outputDir := cmd .String ("output-dir" )
92- err := os .MkdirAll (outputDir , 0755 )
93- if err != nil {
94- return err
95- }
96-
97- for r := range e .Results () {
98- if r .Error != nil {
99- logger .Warn ().Msgf ("result error: %v" , r .Error )
100- continue
101- }
102-
103- logger .Info ().Msgf ("result for %s (duration %s)" , r .URL , r .Elapsed .String ())
104-
105- sr , ok := r .Result .(* engine.ScreenshotResult )
106- if ! ok {
107- return errors .New ("screenshot result error" )
108- }
109-
110- fileName , err := urlToFilename (r .URL )
111- if err != nil {
112- return err
113- }
114-
115- out , err := os .Create (filepath .Join (outputDir , fmt .Sprintf ("%s.png" , fileName )))
116- if err != nil {
117- return err
118- }
119-
120- if _ , err = out .Write (* sr .Buffer ); err != nil {
121- return err
122- }
123- }
124-
125- return nil
126- })
97+ return runTask (ctx , cmd , "screenshot" , map [string ]any {}, screenshotCallback )
12798 },
12899 },
129100 },
@@ -176,38 +147,117 @@ func outputResultJson(cmd *cli.Command, e *engine.Engine) error {
176147 output := cmd .String ("output" )
177148 out , err := os .Create (output )
178149 if err != nil {
179- panic (err )
150+ return fmt .Errorf ("create output file: %w" , err )
151+ }
152+ defer func () {
153+ if cerr := out .Close (); cerr != nil {
154+ logger .Warn ().Err (cerr ).Msg ("file close error" )
155+ }
156+ }()
157+
158+ rulesDir := cmd .String ("rules-dir" )
159+ if rulesDir == "" {
160+ rulesDir = "rules"
180161 }
181162
163+ if rulesDir != "rules" {
164+ logger .Info ().Str ("rules_dir" , rulesDir ).Msg ("using custom Sigma rules directory" )
165+ }
166+
167+ if err := engine .InitSigmaEngine (rulesDir , logger ); err != nil {
168+ logger .Warn ().Err (err ).Msg ("failed to initialize Sigma engine" )
169+ }
170+
171+ ctx := context .Background ()
172+
182173 for r := range e .Results () {
183174 if r .Error != nil {
184175 logger .Warn ().Msgf ("result error: %v" , r .Error )
185176 continue
186177 }
187178
188- logger .Info ().Msgf ("result for %s (duration %s)" , r .URL , r .Elapsed .String ())
179+ matches , err := engine .EvaluateSigmaResult (ctx , r , logger )
180+ if err != nil {
181+ logger .Warn ().Err (err ).Msg ("sigma evaluation error" )
182+ } else {
183+ for _ , m := range matches {
184+ if ! m .Match {
185+ continue
186+ }
187+ logger .Info ().
188+ Str ("rule_id" , m .Rule .ID ).
189+ Str ("title" , m .Rule .Title ).
190+ Str ("url" , r .URL ).
191+ Msg ("[+] SIGMA ALERT: rule matched for URL" )
192+ }
193+ }
194+
195+ logger .Info ().
196+ Str ("url" , r .URL ).
197+ Str ("duration" , r .Elapsed .String ()).
198+ Msg ("result" )
189199
190200 line , err := json .Marshal (r .Result )
191201 if err != nil {
192- logger .Warn ().Msgf ("result json marshal error: %v" , r .Error )
202+ logger .Warn ().Err (err ).Msg ("result JSON marshal error" )
203+ continue
193204 }
194-
195205 if _ , err = out .Write (line ); err != nil {
196- logger .Warn ().Msgf ("result json write error: %v" , r .Error )
206+ logger .Warn ().Err (err ).Msg ("result JSON write error" )
207+ continue
197208 }
198-
199209 if _ , err = out .Write ([]byte ("\n " )); err != nil {
200- logger .Warn ().Msgf ("result json write error: %v" , r .Error )
210+ logger .Warn ().Err (err ).Msg ("result JSON newline write error" )
211+ continue
201212 }
202-
203213 logger .Debug ().Msgf ("wrote to file %s" , output )
204214 }
205215
206- if err = out .Close (); err != nil {
207- logger .Warn ().Msgf ("file close error: %v" , err )
216+ logger .Info ().Msg ("done" )
217+ return nil
218+ }
219+
220+ func screenshotCallback (cmd * cli.Command , e * engine.Engine ) error {
221+ outputDir := cmd .String ("output-dir" )
222+ if err := os .MkdirAll (outputDir , 0o755 ); err != nil {
223+ return fmt .Errorf ("create output dir: %w" , err )
208224 }
209225
210- logger .Info ().Msg ("done" )
226+ for r := range e .Results () {
227+ if r .Error != nil {
228+ logger .Warn ().Msgf ("result error: %v" , r .Error )
229+ continue
230+ }
231+
232+ logger .Info ().
233+ Str ("url" , r .URL ).
234+ Str ("duration" , r .Elapsed .String ()).
235+ Msg ("screenshot result" )
236+
237+ sr , ok := r .Result .(* engine.ScreenshotResult )
238+ if ! ok {
239+ return errors .New ("screenshot result error: unexpected type" )
240+ }
241+
242+ fileName , err := urlToFilename (r .URL )
243+ if err != nil {
244+ return fmt .Errorf ("build screenshot filename: %w" , err )
245+ }
246+
247+ outPath := filepath .Join (outputDir , fmt .Sprintf ("%s.png" , fileName ))
248+ out , err := os .Create (outPath )
249+ if err != nil {
250+ return fmt .Errorf ("create screenshot file: %w" , err )
251+ }
252+
253+ if _ , err = out .Write (* sr .Buffer ); err != nil {
254+ out .Close ()
255+ return fmt .Errorf ("write screenshot file: %w" , err )
256+ }
257+ if err := out .Close (); err != nil {
258+ return fmt .Errorf ("close screenshot file: %w" , err )
259+ }
260+ }
211261
212262 return nil
213263}
@@ -216,15 +266,13 @@ func validDeviceType(ctx context.Context, cmd *cli.Command, v string) error {
216266 if ! browser .IsValidDeviceType (v ) {
217267 return fmt .Errorf ("invalid device type: %v" , v )
218268 }
219-
220269 return nil
221270}
222271
223272func validDeviceSize (ctx context.Context , cmd * cli.Command , v string ) error {
224273 if ! browser .IsValidDeviceSize (v ) {
225274 return fmt .Errorf ("invalid device size: %v" , v )
226275 }
227-
228276 return nil
229277}
230278
@@ -237,14 +285,15 @@ func urlToFilename(taskURL string) (string, error) {
237285 domain := u .Host
238286 path := u .Path
239287
240- combined := domain + path
241- combined = strings .Trim (combined , "/" )
288+ combined := strings .Trim (domain + path , "/" )
242289
243290 safe := strings .Map (func (r rune ) rune {
244- if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' || r == '.' {
291+ switch r {
292+ case '/' , '\\' , ':' , '*' , '?' , '"' , '<' , '>' , '|' , '.' :
245293 return '_'
294+ default :
295+ return r
246296 }
247- return r
248297 }, combined )
249298
250299 if safe == "" {
0 commit comments