#!/usr/bin/env python3 """Run the installed filesystem scanner concurrently on explicitly named exports.""" import argparse import concurrent.futures import hashlib import json import subprocess import sys from pathlib import Path def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("archives", nargs="+", type=Path) parser.add_argument("--output", required=True, type=Path) parser.add_argument("--ioc-file", type=Path) parser.add_argument("--workers", type=int, default=4) args = parser.parse_args() if args.workers < 1 or args.workers > 4: parser.error("workers must be 1-4") names = [p.name.removesuffix(".tar.gz").removesuffix(".tgz") for p in args.archives] if len(set(names)) != len(names): parser.error("Archive names must be unique") if args.output.exists(): parser.error("Output must be new") for archive in args.archives: if not archive.is_file(): parser.error(f"Not a readable regular archive: {archive.name}") args.output.mkdir(parents=True, mode=0o700) if args.ioc_file: # Preserve the exact small case rule input used for this run. data = args.ioc_file.read_bytes() (args.output / "case-iocs.csv").write_bytes(data) args.ioc_file = args.output / "case-iocs.csv" code = Path(__file__).resolve().parents[1] / "src/netscaler_triage" manifest = { "archives": [p.name for p in args.archives], "workers": args.workers, "tool_sha256": { str(p.relative_to(code)): hashlib.sha256(p.read_bytes()).hexdigest() for p in [ code / "filesystem.py", code / "cli.py", code / "data/filesystem_rules.json", code / "data/threat_intel.json", ] }, } (args.output / "run_manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") def run(item: tuple[Path, str]) -> dict: archive, name = item command = [ sys.executable, "-m", "netscaler_triage", "filesystem", str(archive), "--output", str(args.output / name), "--max-output-bytes", str(256 * 1024**2), "--json", ] if args.ioc_file: command.extend(["--ioc-file", str(args.ioc_file)]) print(json.dumps({"started": archive.name}), flush=True) with ( (args.output / (name + ".stdout.json")).open("w") as stdout, (args.output / (name + ".progress.log")).open("w") as stderr, ): result = subprocess.run(command, stdout=stdout, stderr=stderr, check=False) row = {"archive": archive.name, "output": name, "exit_code": result.returncode} print(json.dumps(row), flush=True) return row with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool: results = list(pool.map(run, zip(args.archives, names, strict=True))) (args.output / "batch_summary.json").write_text(json.dumps(results, indent=2) + "\n") return int(any(r["exit_code"] != 0 for r in results)) if __name__ == "__main__": raise SystemExit(main())