@@ -189,6 +189,47 @@ def run_katana(
189189 temp .unlink ()
190190
191191
192+ def run_katana_dast (
193+ url : str ,
194+ use_headless : bool ,
195+ ) -> tuple [bool , list [str ]]:
196+ """Run katana for one URL in JSONL mode and return raw non-empty lines."""
197+ print (f"[+] dast: crawling { url } " )
198+
199+ cmd = ["katana" , "-u" , url ] + KATANA_BASE_OPTS .copy ()
200+ cmd [cmd .index ("-crawl-scope" ) + 1 ] = url
201+ cmd += ["-jsonl" , "-aff" , "-iqp" ]
202+ if use_headless :
203+ cmd .append ("-headless" )
204+
205+ try :
206+ proc = subprocess .run (
207+ cmd ,
208+ stdout = subprocess .PIPE ,
209+ stderr = subprocess .DEVNULL ,
210+ text = True ,
211+ check = True ,
212+ )
213+ except (subprocess .CalledProcessError , OSError ) as exc :
214+ print (f"[!] Error in dast for { url } : { exc } " , file = sys .stderr )
215+ return False , []
216+
217+ lines = [line for line in proc .stdout .splitlines () if line .strip ()]
218+ return True , lines
219+
220+
221+ def dedupe_preserve_order (lines : list [str ]) -> list [str ]:
222+ """Return deduplicated lines while preserving first-seen order."""
223+ seen : set [str ] = set ()
224+ ordered : list [str ] = []
225+ for line in lines :
226+ if line in seen :
227+ continue
228+ seen .add (line )
229+ ordered .append (line )
230+ return ordered
231+
232+
192233def merge_origin_results (mode : str , seed_urls : list [str ], seed_results : list [list [str ]]) -> list [str ]:
193234 """Merge multiple katana runs for a single origin into one deduplicated output."""
194235 merged : set [str ] = set ()
@@ -211,7 +252,10 @@ def write_origin_results(origin: str, out_dir: Path, lines: list[str]) -> Path:
211252def parse_args (argv = None ) -> argparse .Namespace :
212253 """Parse CLI arguments."""
213254 parser = create_argument_parser (
214- description = "Run katana in selected modes: all, files, paths."
255+ description = (
256+ "Run katana in selected modes (all/files/paths) or in --dast mode "
257+ "that writes a single katana-dast.jsonl file."
258+ )
215259 )
216260 parser .add_argument (
217261 "-i" ,
@@ -233,7 +277,15 @@ def parse_args(argv=None) -> argparse.Namespace:
233277 dest = "modes" ,
234278 action = "append" ,
235279 choices = VISIBLE_MODES ,
236- help = "Crawl mode to run. Use 'everything' for all, files, and paths. Repeat the flag if needed." ,
280+ help = (
281+ "Crawl mode to run. Use 'everything' for all, files, and paths. "
282+ "Repeat the flag if needed. Required unless --dast is used."
283+ ),
284+ )
285+ parser .add_argument (
286+ "--dast" ,
287+ action = "store_true" ,
288+ help = "Run DAST mode and write a single katana-dast.jsonl file. Cannot be used with -m/--mode." ,
237289 )
238290 parser .add_argument (
239291 "-b" ,
@@ -269,11 +321,17 @@ def main(argv=None) -> int:
269321 print (f"[!] URL file not found: { args .input } " , file = sys .stderr )
270322 return 1
271323
272- selected_modes = resolve_selected_modes (args )
273- if not selected_modes :
274- print ("[!] Please specify at least one crawl mode with -m/--mode." , file = sys .stderr )
324+ if args .dast and args .modes :
325+ print ("[!] --dast cannot be combined with -m/--mode." , file = sys .stderr )
275326 return 1
276327
328+ selected_modes : list [str ] = []
329+ if not args .dast :
330+ selected_modes = resolve_selected_modes (args )
331+ if not selected_modes :
332+ print ("[!] Please specify at least one crawl mode with -m/--mode." , file = sys .stderr )
333+ return 1
334+
277335 try :
278336 urls = read_input_urls (args .input )
279337 except OSError as exc :
@@ -285,6 +343,37 @@ def main(argv=None) -> int:
285343 return 1
286344
287345 output_root = args .output_dir
346+
347+ if args .dast :
348+ output_root .mkdir (parents = True , exist_ok = True )
349+ dast_lines : list [str ] = []
350+ failures : list [str ] = []
351+
352+ for url in urls :
353+ success , lines = run_katana_dast (
354+ url = url ,
355+ use_headless = args .browser ,
356+ )
357+ if not success :
358+ failures .append (url )
359+ continue
360+ dast_lines .extend (lines )
361+
362+ final_lines = dedupe_preserve_order (dast_lines )
363+ dast_output = output_root / "katana-dast.jsonl"
364+ dast_output .write_text (
365+ "\n " .join (final_lines ) + ("\n " if final_lines else "" ),
366+ encoding = "utf-8" ,
367+ )
368+
369+ if failures :
370+ print (f"[!] Katana failed for { len (failures )} target(s) in dast mode." , file = sys .stderr )
371+ for url in failures :
372+ print (f" - { url } " , file = sys .stderr )
373+ return 1
374+
375+ return 0
376+
288377 failures : list [tuple [str , str ]] = []
289378 grouped_urls = group_urls_by_origin (urls )
290379
0 commit comments