Skip to content
Open
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 @@ -136,6 +136,9 @@ docksec Dockerfile --scan-only --quiet

# Disable colored output (also honors the NO_COLOR env var)
docksec Dockerfile --no-color

# Enable INFO logging for command output details
docksec --verbose --image-only -i myapp:latest
```

Every scan ends with a result summary: a severity table, the security score with a
Expand Down
8 changes: 7 additions & 1 deletion docksec/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def main() -> None:
parser.add_argument('--provider', choices=LLMProvider.values(),
help='LLM provider to use (default: openai, can also set LLM_PROVIDER env var)')
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('-v', '--verbose', action='store_true', help='Enable INFO-level logs by setting DOCKSEC_LOG_LEVEL=INFO')
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)')
Expand All @@ -82,6 +83,11 @@ def main() -> None:
# Configure the terminal output layer before anything is printed.
from docksec import output
no_color = args.no_color or bool(os.getenv("NO_COLOR"))

# --verbose raises verbosity to INFO, but only when not already explicitly set.
if args.verbose:
os.environ.setdefault("DOCKSEC_LOG_LEVEL", "INFO")

if no_color:
# Every Rich console (including the AI-findings console in utils) honors
# NO_COLOR, so set it before those modules are imported.
Expand Down Expand Up @@ -679,4 +685,4 @@ def _suggest_next_command(args, results, run_ai, run_compose_analysis):
return ""

if __name__ == "__main__":
main()
main()
54 changes: 52 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def tearDown(self):
shutil.rmtree(self.test_dir)

@patch('sys.argv', ['docksec', 'Dockerfile', '-i', 'test:latest', '--compact-output'])
@patch('docksec.cli.DockerSecurityScanner', create=True)
@patch('docksec.docker_scanner.DockerSecurityScanner')
def test_compact_output_flag(self, mock_scanner_class):
"""Test --compact-output flag is parsed correctly."""
# Mock scanner instance
Expand All @@ -52,7 +52,7 @@ def test_compact_output_flag(self, mock_scanner_class):
pass

@patch('sys.argv', ['docksec', '--image-only', '-i', 'test:latest', '--skip-ai-scoring'])
@patch('docksec.cli.DockerSecurityScanner', create=True)
@patch('docksec.docker_scanner.DockerSecurityScanner')
def test_skip_ai_scoring_flag(self, mock_scanner_class):
"""Test --skip-ai-scoring flag is parsed correctly."""
# Mock scanner instance
Expand Down Expand Up @@ -137,6 +137,56 @@ def test_use_cache_env_var_disabled(self):
scanner.use_cache = os.getenv("DOCKSEC_USE_CACHE", "true").lower() == "true"

self.assertFalse(scanner.use_cache)

@patch('docksec.docker_scanner.DockerSecurityScanner')
@patch('sys.argv', ['docksec', '--image-only', '--verbose', '-i', 'alpine:latest'])
def test_verbose_flag_sets_log_level(self, mock_scanner_class):
"""`-v/--verbose` should set DOCKSEC_LOG_LEVEL to INFO when unset."""
mock_scanner = Mock()
mock_scanner_class.return_value = mock_scanner
mock_scanner.run_image_only_scan.return_value = {
'json_data': [],
'dockerfile_scan': {'skipped': True},
'image_scan': {'skipped': False},
'scan_mode': 'image_only',
}
mock_scanner.get_security_score.return_value = 90.0
mock_scanner.generate_all_reports.return_value = {}

with patch.dict(os.environ, {}, clear=True):
from docksec.cli import main

with patch('docksec.cli.output', create=True) as mock_output:
with patch('builtins.print'):
mock_output.report_results.return_value = None
main()

self.assertEqual(os.getenv("DOCKSEC_LOG_LEVEL"), "INFO")

@patch('docksec.docker_scanner.DockerSecurityScanner')
@patch('sys.argv', ['docksec', '--image-only', '--verbose', '-i', 'alpine:latest'])
def test_verbose_flag_does_not_override_existing_log_level(self, mock_scanner_class):
"""Existing DOCKSEC_LOG_LEVEL should keep priority over `--verbose`."""
mock_scanner = Mock()
mock_scanner_class.return_value = mock_scanner
mock_scanner.run_image_only_scan.return_value = {
'json_data': [],
'dockerfile_scan': {'skipped': True},
'image_scan': {'skipped': False},
'scan_mode': 'image_only',
}
mock_scanner.get_security_score.return_value = 90.0
mock_scanner.generate_all_reports.return_value = {}

with patch.dict(os.environ, {'DOCKSEC_LOG_LEVEL': 'WARNING'}):
from docksec.cli import main

with patch('docksec.cli.output', create=True) as mock_output:
with patch('builtins.print'):
mock_output.report_results.return_value = None
main()

self.assertEqual(os.getenv("DOCKSEC_LOG_LEVEL"), "WARNING")

@patch('sys.argv', ['docksec', 'Dockerfile', '-i', 'test:latest', '--provider', 'anthropic'])
@patch('docksec.cli.DockerSecurityScanner', create=True)
Expand Down