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
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ include LICENSE
include README.md
include requirements.txt
include .env.example
recursive-include docksec/templates *.html
recursive-include docksec/templates *
recursive-include tests *
global-exclude *.py[cod] __pycache__ *.so .DS_Store
15 changes: 6 additions & 9 deletions docksec/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,14 @@ def get_html_template() -> str:
"""
Load the HTML report template from the templates directory.
"""
template_path = os.path.join(TEMPLATES_DIR, "report_template.html")
try:
if os.path.exists(template_path):
template_path = os.path.join(TEMPLATES_DIR, "report.html.j2")
if os.path.exists(template_path):
try:
with open(template_path, 'r', encoding='utf-8') as f:
return f.read()
else:
# Fallback for when running from a location where templates might not be correctly linked
# This is a safety measure
return "<html><body><h1>Docker Security Report</h1><p>Template missing at " + template_path + "</p></body></html>"
except Exception as e:
return f"<html><body><h1>Error</h1><p>{str(e)}</p></body></html>"
except Exception as e:
return f"<html><body><h1>Error</h1><p>{str(e)}</p></body></html>"
return "<html><body><h1>Docker Security Report</h1><p>Template missing in " + TEMPLATES_DIR + "</p></body></html>"


# For backward compatibility with existing code that imports html_template
Expand Down
382 changes: 26 additions & 356 deletions docksec/report_generator.py

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ python-dotenv>=1.0,<2
colorama>=0.4.6,<1
rich>=13.0,<16
fpdf2>=2.8,<3
jinja2>=3.1.0
ruamel.yaml>=0.18.6
setuptools>=82.0.1

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"fpdf2>=2.8,<3",
"setuptools>=65.0.0",
"ruamel.yaml>=0.18.6",
"jinja2>=3.1.0",
],
extras_require={
"dev": [
Expand Down Expand Up @@ -60,6 +61,6 @@
],
include_package_data=True,
package_data={
'docksec': ['templates/*.html'],
'docksec': ['templates/*'],
},
)
18 changes: 18 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,24 @@ def test_get_html_template(self):
self.assertIn("<html", template.lower())
self.assertIn("Docker Security Report", template)

@patch("os.path.exists", return_value=False)
def test_get_html_template_missing(self, mock_exists):
"""Test HTML template loading when template file is missing."""
from docksec.config import get_html_template

template = get_html_template()
self.assertIn("Template missing", template)

@patch("os.path.exists", return_value=True)
@patch("builtins.open", side_effect=IOError("Permission denied"))
def test_get_html_template_error(self, mock_open, mock_exists):
"""Test HTML template loading when reading fails."""
from docksec.config import get_html_template

template = get_html_template()
self.assertIn("Error", template)
self.assertIn("Permission denied", template)

def test_results_dir_default(self):
"""Test that RESULTS_DIR defaults to home directory."""
from docksec.config import RESULTS_DIR
Expand Down
83 changes: 83 additions & 0 deletions tests/test_report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,89 @@ def test_html_omits_ai_section_without_findings(tmp_path):
assert "<h2>AI Dockerfile Analysis</h2>" not in content


def test_html_renders_vulnerabilities_table_and_truncation(tmp_path):
vulns = [
{
"VulnerabilityID": f"CVE-2024-{1000 + i}",
"Severity": "CRITICAL" if i % 2 == 0 else "HIGH",
"PkgName": f"package-{i}",
"InstalledVersion": f"1.0.{i}",
"Title": f"Vulnerability title for issue {i}",
"CVSS": 8.5,
"Status": "fixed" if i % 2 == 0 else "affected",
}
for i in range(60)
]
rg = ReportGenerator(image_name="test-image", results_dir=str(tmp_path))
results = make_results(vulns)
output_path = rg.generate_html_report(results)
with open(output_path, encoding="utf-8") as f:
content = f.read()

assert "<h2>Detailed Vulnerabilities</h2>" in content
assert "Total vulnerabilities:</strong> 60" in content
assert "Showing 50 of 60 vulnerabilities" in content
assert "CVE-2024-1000" in content
assert "CVE-2024-1049" in content
# The 51st vulnerability (index 50, id 1050) should not appear in the table
assert "CVE-2024-1050" not in content


def test_html_empty_vulnerabilities_renders_success_state(tmp_path):
rg = ReportGenerator(image_name="test-image", results_dir=str(tmp_path))
results = make_results([])
output_path = rg.generate_html_report(results)
with open(output_path, encoding="utf-8") as f:
content = f.read()

assert "No vulnerabilities found" in content
assert "<h2>Detailed Vulnerabilities</h2>" not in content


def test_html_renders_image_info_and_config_analysis(tmp_path):
rg = ReportGenerator(image_name="test-image", results_dir=str(tmp_path))
results = make_results([])
results["image_info"] = {
"size": 104857600, # 100 MB
"created": "2026-01-01T12:00:00Z",
"architecture": "arm64",
"os": "alpine",
}
results["config_analysis"] = {
"high_risk": ["Root user configured"],
"medium_risk": ["Missing HEALTHCHECK"],
"low_risk": ["No label provided"],
}
output_path = rg.generate_html_report(results)
with open(output_path, encoding="utf-8") as f:
content = f.read()

assert "<h2>Image Information</h2>" in content
assert "100.0 MB" in content
assert "arm64" in content
assert "alpine" in content
assert "<h2>Configuration Analysis</h2>" in content
assert "Root user configured" in content
assert "Missing HEALTHCHECK" in content
assert "No label provided" in content


def test_html_renders_dockerfile_scan_results(tmp_path):
rg = ReportGenerator(image_name="test-image", results_dir=str(tmp_path))
results = make_results([])
results["dockerfile_scan"] = {
"skipped": False,
"success": False,
"output": "DL3006 Always tag the version of an image explicitly",
}
output_path = rg.generate_html_report(results)
with open(output_path, encoding="utf-8") as f:
content = f.read()

assert "<h2>Dockerfile Scan Results</h2>" in content
assert "DL3006 Always tag the version of an image explicitly" in content


# ---------- MARKDOWN REPORT TESTS ----------


Expand Down
Loading