Coverage for src \ sec_report_kit \ report \ html_renderer.py: 100%
18 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 08:06 +0530
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 08:06 +0530
1from __future__ import annotations
3import datetime as dt
4import html
6from sec_report_kit.models import Finding
9def _esc(value: str) -> str:
10 return html.escape(value, quote=True)
13def render_html_report(target_ref: str, source_label: str, findings: list[Finding], counts: dict[str, int]) -> str:
14 generated_at = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
15 total = len(findings)
17 rows = []
18 for item in findings:
19 vuln_id = _esc(item.vulnerability_id)
20 if item.primary_url:
21 vuln_cell = (
22 f'<a href="{_esc(item.primary_url)}" target="_blank" rel="noopener noreferrer">{vuln_id}</a>'
23 )
24 else:
25 vuln_cell = vuln_id
27 rows.append(
28 "<tr>"
29 f"<td class=\"sev {_esc(item.severity).lower()}\">{_esc(item.severity)}</td>"
30 f"<td>{vuln_cell}</td>"
31 f"<td>{_esc(item.package)}</td>"
32 f"<td>{_esc(item.installed_version)}</td>"
33 f"<td>{_esc(item.fixed_version)}</td>"
34 f"<td>{_esc(item.title)}</td>"
35 f"<td>{_esc(item.target)}</td>"
36 f"<td>{_esc(item.source_type)}</td>"
37 "</tr>"
38 )
40 rows_html = "\n".join(rows) if rows else "<tr><td colspan=\"8\">No vulnerabilities found.</td></tr>"
42 return f"""<!doctype html>
43<html lang=\"en\">
44<head>
45 <meta charset=\"utf-8\" />
46 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
47 <title>Security Report</title>
48 <style>
49 body {{ font-family: Segoe UI, Arial, sans-serif; margin: 24px; color: #1f2937; }}
50 h1 {{ margin: 0 0 8px 0; }}
51 .meta {{ margin-bottom: 18px; color: #4b5563; }}
52 .cards {{ display: grid; grid-template-columns: repeat(6, minmax(110px, 1fr)); gap: 10px; margin-bottom: 18px; }}
53 .card {{ border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #f9fafb; }}
54 .label {{ font-size: 12px; color: #6b7280; text-transform: uppercase; }}
55 .value {{ font-size: 24px; font-weight: 700; margin-top: 4px; }}
56 .filters {{ display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; align-items: center; }}
57 .filters label {{ font-size: 13px; color: #374151; display: flex; flex-direction: column; gap: 3px; }}
58 .filters select, .filters input[type=text] {{
59 font-size: 13px; padding: 5px 8px; border: 1px solid #d1d5db;
60 border-radius: 6px; background: #fff; color: #1f2937; min-width: 140px;
61 }}
62 .filters input[type=text] {{ min-width: 220px; }}
63 .filters button {{
64 font-size: 13px; padding: 6px 14px; border: 1px solid #d1d5db;
65 border-radius: 6px; background: #f3f4f6; cursor: pointer; color: #374151;
66 align-self: flex-end;
67 }}
68 .filters button:hover {{ background: #e5e7eb; }}
69 #result-count {{ font-size: 13px; color: #6b7280; align-self: flex-end; margin-left: auto; }}
70 table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
71 thead th {{ text-align: left; border-bottom: 2px solid #d1d5db; padding: 8px; background: #f3f4f6; position: sticky; top: 0; }}
72 tbody td {{ border-bottom: 1px solid #e5e7eb; padding: 8px; vertical-align: top; }}
73 tbody tr.hidden {{ display: none; }}
74 .sev {{ font-weight: 700; }}
75 .critical {{ color: #b91c1c; }}
76 .high {{ color: #c2410c; }}
77 .medium {{ color: #b45309; }}
78 .low {{ color: #1d4ed8; }}
79 .unknown {{ color: #6b7280; }}
80 a {{ color: #1d4ed8; text-decoration: none; }}
81 a:hover {{ text-decoration: underline; }}
82 .report-footer {{
83 margin-top: 36px;
84 padding-top: 14px;
85 border-top: 1px solid #e5e7eb;
86 text-align: center;
87 font-size: 12px;
88 color: #6b7280;
89 }}
90 </style>
91</head>
92<body>
93 <h1>Security Vulnerability Report</h1>
94 <div class=\"meta\">Source: <strong>{_esc(source_label)}</strong> | Target: <strong>{_esc(target_ref)}</strong> | Generated: {_esc(generated_at)}</div>
96 <div class=\"cards\">
97 <div class=\"card\"><div class=\"label\">Total</div><div class=\"value\">{total}</div></div>
98 <div class=\"card\"><div class=\"label\">Critical</div><div class=\"value critical\">{counts['CRITICAL']}</div></div>
99 <div class=\"card\"><div class=\"label\">High</div><div class=\"value high\">{counts['HIGH']}</div></div>
100 <div class=\"card\"><div class=\"label\">Medium</div><div class=\"value medium\">{counts['MEDIUM']}</div></div>
101 <div class=\"card\"><div class=\"label\">Low</div><div class=\"value low\">{counts['LOW']}</div></div>
102 <div class=\"card\"><div class=\"label\">Unknown</div><div class=\"value unknown\">{counts['UNKNOWN']}</div></div>
103 </div>
105 <div class=\"filters\">
106 <label>Severity
107 <select id=\"f-severity\">
108 <option value=\"\">All</option>
109 <option value=\"critical\">Critical</option>
110 <option value=\"high\">High</option>
111 <option value=\"medium\">Medium</option>
112 <option value=\"low\">Low</option>
113 <option value=\"unknown\">Unknown</option>
114 </select>
115 </label>
116 <label>Vulnerability ID
117 <input type=\"text\" id=\"f-vuln\" placeholder=\"e.g. CVE-2024-\" />
118 </label>
119 <label>Package
120 <input type=\"text\" id=\"f-pkg\" placeholder=\"e.g. openssl\" />
121 </label>
122 <label>Target
123 <input type=\"text\" id=\"f-target\" placeholder=\"e.g. usr/lib\" />
124 </label>
125 <label>Type
126 <input type=\"text\" id=\"f-type\" placeholder=\"e.g. os-pkgs\" />
127 </label>
128 <label>Title / keyword
129 <input type=\"text\" id=\"f-title\" placeholder=\"e.g. buffer overflow\" />
130 </label>
131 <button onclick=\"clearFilters()\">Clear</button>
132 <span id=\"result-count\"></span>
133 </div>
135 <table id=\"vuln-table\">
136 <thead>
137 <tr>
138 <th>Severity</th>
139 <th>Vulnerability</th>
140 <th>Package</th>
141 <th>Installed</th>
142 <th>Fixed</th>
143 <th>Title</th>
144 <th>Target</th>
145 <th>Type</th>
146 </tr>
147 </thead>
148 <tbody id=\"vuln-body\">
149 {rows_html}
150 </tbody>
151 </table>
153 <script>
154 const filterIds = ['f-severity', 'f-vuln', 'f-pkg', 'f-target', 'f-type', 'f-title'];
155 // col indices: severity=0, vuln=1, pkg=2, installed=3, fixed=4, title=5, target=6, type=7
156 const colMap = {{ 'f-severity': 0, 'f-vuln': 1, 'f-pkg': 2, 'f-target': 6, 'f-type': 7, 'f-title': 5 }};
158 function applyFilters() {{
159 const filters = {{}};
160 filterIds.forEach(id => {{
161 const el = document.getElementById(id);
162 filters[id] = el.value.trim().toLowerCase();
163 }});
165 const rows = document.querySelectorAll('#vuln-body tr');
166 let visible = 0;
167 rows.forEach(row => {{
168 const cells = row.querySelectorAll('td');
169 if (!cells.length) return;
170 let show = true;
171 for (const [id, col] of Object.entries(colMap)) {{
172 const val = filters[id];
173 if (!val) continue;
174 const cellText = (cells[col]?.textContent || '').trim().toLowerCase();
175 if (id === 'f-severity') {{
176 if (cellText !== val) {{ show = false; break; }}
177 }} else {{
178 if (!cellText.includes(val)) {{ show = false; break; }}
179 }}
180 }}
181 row.classList.toggle('hidden', !show);
182 if (show) visible++;
183 }});
185 const total = rows.length;
186 document.getElementById('result-count').textContent =
187 visible === total ? `${{total}} rows` : `${{visible}} of ${{total}} rows`;
188 }}
190 function clearFilters() {{
191 filterIds.forEach(id => {{ document.getElementById(id).value = ''; }});
192 applyFilters();
193 }}
195 filterIds.forEach(id => {{
196 const el = document.getElementById(id);
197 el.addEventListener('input', applyFilters);
198 el.addEventListener('change', applyFilters);
199 }});
201 applyFilters();
202 </script>
204 <footer class="report-footer">
205 This report is generated by
206 <a href="https://github.com/ShanKonduru/sec-report-kit" target="_blank" rel="noopener noreferrer">sec-report-kit</a>
207 utility
208 |
209 Connect with the developer:
210 <a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer">Shan Konduru</a>
211 </footer>
212</body>
213</html>
214"""