86 lines
3.2 KiB
Python
86 lines
3.2 KiB
Python
#!/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())
|