Skip to content

Commit 6a3876f

Browse files
committed
KatanaScan: add --dast katana flags -aff and -iqp
1 parent 073ca93 commit 6a3876f

1 file changed

Lines changed: 94 additions & 5 deletions

File tree

Collectors/KatanaScan.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
192233
def 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:
211252
def 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

Comments
 (0)