Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 21 additions & 4 deletions docksec/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}')
Expand All @@ -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)")
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down
Loading