11"""Argus CLI — command-line interface for security scanning."""
22
33import argparse
4+ import os
45import sys
56import threading
67import time
@@ -147,6 +148,7 @@ def build_parser() -> argparse.ArgumentParser:
147148 _build_validate_parser (subparsers )
148149 _build_mcp_parser (subparsers )
149150 _build_completion_parser (subparsers )
151+ _build_cache_parser (subparsers )
150152
151153 return parser
152154
@@ -307,6 +309,12 @@ def _build_scan_parser(subparsers: argparse._SubParsersAction) -> None:
307309 help = "Allow local tool versions that differ from argus-pinned versions. "
308310 "Use in airgapped environments where tool updates are constrained." ,
309311 )
312+ scan_parser .add_argument (
313+ "--no-cache" ,
314+ action = "store_true" ,
315+ help = "Disable DB cache volume mounts. Forces scanners to re-download "
316+ "vulnerability databases on every container run." ,
317+ )
310318
311319 # Container-specific flags (used with: argus scan container)
312320 container_group = scan_parser .add_argument_group (
@@ -633,6 +641,33 @@ def _build_completion_parser(subparsers: argparse._SubParsersAction) -> None:
633641 )
634642
635643
644+ def _build_cache_parser (subparsers : argparse ._SubParsersAction ) -> None :
645+ """Add the 'cache' subcommand for managing scanner DB caches."""
646+ cache_parser = subparsers .add_parser (
647+ "cache" ,
648+ help = "Manage scanner database caches" ,
649+ description = (
650+ "Manage cached vulnerability databases used by container-based scanners.\n \n "
651+ "Argus caches scanner databases (Trivy, Grype, ClamAV, etc.) in the system\n "
652+ "temp directory so container runs don't re-download hundreds of MB each time.\n "
653+ "The cache persists across runs within a session but is cleaned on reboot.\n \n "
654+ "Cache location: $TMPDIR/argus-cache (override with ARGUS_CACHE_DIR)\n "
655+ "For persistent caching: export ARGUS_CACHE_DIR=~/.argus/cache"
656+ ),
657+ formatter_class = argparse .RawDescriptionHelpFormatter ,
658+ )
659+ cache_sub = cache_parser .add_subparsers (dest = "cache_action" )
660+
661+ cache_sub .add_parser (
662+ "info" ,
663+ help = "Show cache location and size per scanner" ,
664+ )
665+ cache_sub .add_parser (
666+ "clean" ,
667+ help = "Remove all cached scanner databases" ,
668+ )
669+
670+
636671def _build_report_parser (subparsers : argparse ._SubParsersAction ) -> None :
637672 """Add the 'report' subcommand."""
638673 report_parser = subparsers .add_parser (
@@ -828,6 +863,7 @@ def _cmd_source_scan(args: argparse.Namespace) -> int:
828863 exclude = getattr (args , "exclude" , "" ),
829864 parallel = not getattr (args , "no_parallel" , False ),
830865 allow_local_versions = getattr (args , "allow_local_versions" , False ),
866+ no_cache = getattr (args , "no_cache" , False ),
831867 )
832868 log .info (
833869 "Scan complete: %d scanner(s), %d finding(s)" ,
@@ -1211,6 +1247,53 @@ def cmd_completion(args: argparse.Namespace) -> int:
12111247 return EXIT_SUCCESS
12121248
12131249
1250+ def cmd_cache (args : argparse .Namespace ) -> int :
1251+ """Manage scanner database caches."""
1252+ from argus .containers import CACHE_MOUNTS , _default_cache_root
1253+
1254+ cache_root = _default_cache_root ()
1255+ action = getattr (args , "cache_action" , None )
1256+
1257+ if action == "clean" :
1258+ if cache_root .exists ():
1259+ import shutil
1260+ shutil .rmtree (cache_root )
1261+ print (f"Removed cache directory: { cache_root } " )
1262+ else :
1263+ print ("No cache directory found." )
1264+ return EXIT_SUCCESS
1265+
1266+ # Default: info
1267+ print (f"Cache directory: { cache_root } " )
1268+ if os .environ .get ("ARGUS_CACHE_DIR" ):
1269+ print (f" (set by ARGUS_CACHE_DIR)" )
1270+ print ()
1271+
1272+ total_size = 0
1273+ for scanner_key in sorted (CACHE_MOUNTS ):
1274+ scanner_dir = cache_root / scanner_key
1275+ if scanner_dir .exists ():
1276+ size = sum (f .stat ().st_size for f in scanner_dir .rglob ("*" ) if f .is_file ())
1277+ total_size += size
1278+ print (f" { scanner_key :<15} { _format_size (size )} " )
1279+ else :
1280+ print (f" { scanner_key :<15} (not cached)" )
1281+
1282+ print (f"\n { 'Total' :<15} { _format_size (total_size )} " )
1283+ return EXIT_SUCCESS
1284+
1285+
1286+ def _format_size (size_bytes : int ) -> str :
1287+ """Format byte count as human-readable string."""
1288+ if size_bytes < 1024 :
1289+ return f"{ size_bytes } B"
1290+ if size_bytes < 1024 * 1024 :
1291+ return f"{ size_bytes / 1024 :.1f} KB"
1292+ if size_bytes < 1024 * 1024 * 1024 :
1293+ return f"{ size_bytes / (1024 * 1024 ):.1f} MB"
1294+ return f"{ size_bytes / (1024 * 1024 * 1024 ):.1f} GB"
1295+
1296+
12141297def cmd_mcp (args : argparse .Namespace ) -> int :
12151298 """Start the MCP server for AI assistant integration."""
12161299 try :
@@ -1245,6 +1328,7 @@ def _generate_zsh_completion(scanners: str) -> str:
12451328 'validate:Validate an argus.yml configuration file'
12461329 'mcp:Start the MCP server for AI assistant integration'
12471330 'completion:Generate shell completion script'
1331+ 'cache:Manage scanner database caches'
12481332 )
12491333
12501334 scanners=({ scanners } )
@@ -1280,6 +1364,9 @@ def _generate_zsh_completion(scanners: str) -> str:
12801364 '--no-timestamp[Flat output directory]'
12811365 '--fail-fast[Abort on first failure]'
12821366 '--timeout[Per-scanner timeout]:seconds:'
1367+ '--no-parallel[Run scanners sequentially]'
1368+ '--allow-local-versions[Skip version enforcement]'
1369+ '--no-cache[Disable DB cache volume mounts]'
12831370 )
12841371
12851372 scan_container=(
@@ -1351,7 +1438,7 @@ def _generate_bash_completion(scanners: str) -> str:
13511438 cur="${{COMP_WORDS[COMP_CWORD]}}"
13521439 prev="${{COMP_WORDS[COMP_CWORD-1]}}"
13531440
1354- commands="init scan classify collect report validate mcp completion"
1441+ commands="init scan classify collect report validate mcp completion cache "
13551442 scanners="{ scanners } "
13561443 severity="critical high medium low none"
13571444 formats="terminal markdown sarif json"
@@ -1375,7 +1462,7 @@ def _generate_bash_completion(scanners: str) -> str:
13751462 --scan-type) COMPREPLY=($(compgen -W "baseline full" -- "$cur")); return ;;
13761463 --path|-p|--output-dir|-o|--config|-c|--output-vars) COMPREPLY=($(compgen -d -- "$cur")); return ;;
13771464 esac
1378- COMPREPLY=($(compgen -W "--path --config --output-dir --severity-threshold --format --output-vars --list --verbose --no-spinner --no-timestamp --fail-fast --timeout" -- "$cur"))
1465+ COMPREPLY=($(compgen -W "--path --config --output-dir --severity-threshold --format --output-vars --list --verbose --no-spinner --no-timestamp --fail-fast --timeout --no-cache --no-parallel --allow-local-versions " -- "$cur"))
13791466 ;;
13801467 report)
13811468 if [ "$COMP_CWORD" -eq 2 ]; then
@@ -1769,6 +1856,7 @@ def main(argv: list[str] | None = None) -> None:
17691856 "validate" : cmd_validate ,
17701857 "mcp" : cmd_mcp ,
17711858 "completion" : cmd_completion ,
1859+ "cache" : cmd_cache ,
17721860 }
17731861
17741862 handler = handlers .get (args .command )
0 commit comments