Skip to content

Commit 7bdaa9e

Browse files
committed
feat(sdk): timestamped run directories preserve scan history
Each argus scan creates a new timestamped subdirectory: argus-results/2026-04-12T07-24-50Z/ argus-results/2026-04-12T11-30-22Z/ argus-results/latest -> 2026-04-12T11-30-22Z/ Previous scan results are never overwritten. The 'latest' symlink always points to the most recent run for scripting convenience. Applied to all scan modes: source, container, and DAST.
1 parent e43d605 commit 7bdaa9e

1 file changed

Lines changed: 42 additions & 4 deletions

File tree

argus/cli.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,41 @@
1313
FORMAT_CHOICES = ["terminal", "markdown", "sarif", "json"]
1414

1515

16+
def _make_run_dir(base_dir: str) -> str:
17+
"""Create a timestamped subdirectory for this scan run.
18+
19+
Each run gets its own directory so previous results are never
20+
overwritten. A 'latest' symlink points to the newest run.
21+
22+
Structure:
23+
argus-results/
24+
├── 2026-04-12T07-24-50Z/
25+
│ ├── argus.log
26+
│ ├── argus-audit.json
27+
│ └── ...
28+
└── latest -> 2026-04-12T07-24-50Z/
29+
"""
30+
from datetime import datetime, timezone
31+
32+
base = Path(base_dir)
33+
base.mkdir(parents=True, exist_ok=True)
34+
35+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
36+
run_dir = base / ts
37+
run_dir.mkdir(parents=True, exist_ok=True)
38+
39+
# Update 'latest' symlink
40+
latest = base / "latest"
41+
try:
42+
if latest.is_symlink() or latest.exists():
43+
latest.unlink()
44+
latest.symlink_to(ts)
45+
except OSError:
46+
pass # Symlinks may not work on all platforms
47+
48+
return str(run_dir)
49+
50+
1651
def build_parser() -> argparse.ArgumentParser:
1752
"""Build the top-level argument parser with scan and report subcommands."""
1853
parser = argparse.ArgumentParser(
@@ -402,8 +437,9 @@ def _cmd_source_scan(args: argparse.Namespace) -> int:
402437
if args.formats:
403438
config.reporting.formats = args.formats
404439

405-
# Initialize audit trail
406-
output_dir = config.reporting.output_dir
440+
# Initialize audit trail — each run gets a timestamped subdirectory
441+
output_dir = _make_run_dir(config.reporting.output_dir)
442+
config.reporting.output_dir = output_dir
407443
log = get_logger("argus", output_dir=output_dir, verbose=args.verbose)
408444
manifest = create_manifest(
409445
config_path=args.config,
@@ -492,7 +528,8 @@ def _cmd_container_scan(args: argparse.Namespace) -> int:
492528
if args.scanners:
493529
config["scanners"] = [s.strip() for s in args.scanners.split(",")]
494530

495-
output_dir = args.output_dir or config.get("output_dir", "./argus-results")
531+
base_dir = args.output_dir or config.get("output_dir", "./argus-results")
532+
output_dir = _make_run_dir(base_dir)
496533
formats = args.formats or ["terminal", "markdown"]
497534

498535
# Run
@@ -539,7 +576,8 @@ def _cmd_dast_scan(args: argparse.Namespace) -> int:
539576
"""Run DAST scanning lifecycle (start target, scan with ZAP, cleanup)."""
540577
from argus.dast import DastEngine
541578

542-
output_dir = args.output_dir or "./argus-results"
579+
base_dir = args.output_dir or "./argus-results"
580+
output_dir = _make_run_dir(base_dir)
543581
formats = args.formats or ["terminal", "markdown"]
544582

545583
# Parse env vars from --env KEY=VALUE flags

0 commit comments

Comments
 (0)