diff --git a/README.md b/README.md index 1db48cc..7b5d439 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,9 @@ docksec --image-only -i myapp:latest # Fast scan only (no AI) docksec Dockerfile --scan-only +# Choose which severity levels the image scan reports (default: CRITICAL,HIGH) +docksec -i myapp:latest --image-only --severity CRITICAL,HIGH,MEDIUM + # Reduce output to warnings, errors, and the result summary docksec Dockerfile --scan-only --quiet diff --git a/docksec/cli.py b/docksec/cli.py index 2f4b489..230cd7a 100644 --- a/docksec/cli.py +++ b/docksec/cli.py @@ -52,6 +52,7 @@ def main() -> None: parser.add_argument('--model', help='Model name to use (e.g., gpt-4o, claude-haiku-4-5, gemini-1.5-pro, llama3.1)') parser.add_argument('--compact-output', action='store_true', help='Use compact output format (less verbose)') parser.add_argument('--skip-ai-scoring', action='store_true', help='Skip AI-based security scoring (use local scoring only)') + parser.add_argument('--severity', help='Comma-separated severity levels to scan for (default: CRITICAL,HIGH; or set DOCKSEC_DEFAULT_SEVERITY)') parser.add_argument('--quiet', action='store_true', help='Reduce output to warnings, errors, and the result summary') parser.add_argument('--no-color', action='store_true', help='Disable colored output (also honors the NO_COLOR env var)') parser.add_argument('--version', action='version', version=f'DockSec {get_version()}') @@ -76,7 +77,21 @@ def main() -> None: # Set compact output mode if requested if args.compact_output: os.environ["DOCKSEC_COMPACT_OUTPUT"] = "true" - + + # Resolve the severity filter: CLI flag > DOCKSEC_DEFAULT_SEVERITY env > default. + from docksec.config_manager import get_config + from docksec.enums import Severity + severity = args.severity or get_config().default_severity + severity_list = [s.strip().upper() for s in severity.split(',') if s.strip()] + invalid_severities = [s for s in severity_list if s not in Severity.values()] + if not severity_list or invalid_severities: + output.error( + f"Invalid --severity value '{severity}'. " + f"Valid levels: {', '.join(Severity.values())}" + ) + sys.exit(1) + severity = ','.join(severity_list) + # Validate argument combinations if args.image_only and args.ai_only: output.error("--image-only and --ai-only cannot be used together (AI analysis requires a Dockerfile)") @@ -162,6 +177,8 @@ def main() -> None: output.banner(get_version(), mode_desc) output.kv("Reports", RESULTS_DIR) + if run_scan: + output.kv("Severity", severity) if run_ai: config = get_config() output.kv("AI Provider", str(config.llm_provider)) @@ -238,7 +255,7 @@ def main() -> None: skip_ai_scoring=args.skip_ai_scoring ) output.info(f"Scanning Compose file: {args.compose}") - results = orchestrator.run_full_scan("CRITICAL,HIGH") + results = orchestrator.run_full_scan(severity) # We need a scanner instance just for scoring and reporting scanner = DockerSecurityScanner(None, None, scan_only=not run_ai, skip_ai_scoring=args.skip_ai_scoring) @@ -258,10 +275,10 @@ def main() -> None: if args.image_only: # Image-only scan - skip Dockerfile analysis output.info(f"Scanning Docker image: {args.image}") - results = scanner.run_image_only_scan("CRITICAL,HIGH") + results = scanner.run_image_only_scan(severity) else: # Full scan including Dockerfile - results = scanner.run_full_scan("CRITICAL,HIGH") + results = scanner.run_full_scan(severity) # Calculate security score scanner.analysis_score = scanner.get_security_score(results) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8d4aed3..295fd28 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Redesigned terminal output**: a consolidated result summary with a box-drawing severity table, the security score with a color-coded rating, a "Quick take" action block highlighting the most important findings, the list of generated reports, and a suggested next command. - `--quiet` flag to reduce output to warnings, errors, and the result summary. - `--no-color` flag (also honors the `NO_COLOR` environment variable) to disable colored output. +- `--severity` flag to choose which severity levels the image vulnerability scan reports (default `CRITICAL,HIGH`; also settable via `DOCKSEC_DEFAULT_SEVERITY`). Invalid values are rejected with a clear error. ### Changed - **Cleaner terminal output**: internal logs now write to `stderr` instead of `stdout` and stay quiet in CLI mode, so raw location-tagged log lines no longer interleave with the tool's user-facing messages. Set `DOCKSEC_LOG_LEVEL` to restore verbose logging. diff --git a/tests/test_cli.py b/tests/test_cli.py index 9bb811d..7922b37 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -163,6 +163,39 @@ def test_model_flag_sets_env(self, mock_scanner_class): # This tests that the env var would be set pass + @patch('sys.argv', ['docksec', '--image-only', '-i', 'test:latest', '--severity', 'BOGUS']) + def test_invalid_severity_is_rejected(self): + """An invalid --severity value should error out and exit non-zero.""" + from docksec.cli import main + + with patch('builtins.print'): + with self.assertRaises(SystemExit) as ctx: + main() + self.assertEqual(ctx.exception.code, 1) + + @patch('sys.argv', ['docksec', '--image-only', '-i', 'test:latest', '--severity', 'critical']) + @patch('docksec.docker_scanner.DockerSecurityScanner') + def test_severity_flag_threads_to_scanner(self, mock_scanner_class): + """A valid --severity should be normalized and passed to the scan call.""" + from docksec.cli import main + + scanner = Mock() + mock_scanner_class.return_value = scanner + scanner.run_image_only_scan.return_value = { + 'json_data': [], + 'dockerfile_scan': {'skipped': True}, + 'image_scan': {'skipped': False}, + 'scan_mode': 'image_only', + } + scanner.get_security_score.return_value = 90.0 + scanner.generate_all_reports.return_value = {'json': 'x'} + scanner.RESULTS_DIR = '/tmp' + + main() + + # 'critical' is normalized to 'CRITICAL' and passed to the image scan. + scanner.run_image_only_scan.assert_called_once_with('CRITICAL') + @patch('sys.argv', ['docksec', '/no/such/docksec_dockerfile_xyz', '--quiet', '--no-color']) def test_quiet_and_no_color_flags_are_accepted(self): """--quiet and --no-color must parse (argparse exits 2 on unknown flags)."""