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

1from __future__ import annotations 

2 

3import datetime as dt 

4import html 

5 

6from sec_report_kit.models import Finding 

7 

8 

9def _esc(value: str) -> str: 

10 return html.escape(value, quote=True) 

11 

12 

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) 

16 

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 

26 

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 ) 

39 

40 rows_html = "\n".join(rows) if rows else "<tr><td colspan=\"8\">No vulnerabilities found.</td></tr>" 

41 

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> 

95 

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> 

104 

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> 

134 

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> 

152 

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 }}; 

157 

158 function applyFilters() {{ 

159 const filters = {{}}; 

160 filterIds.forEach(id => {{ 

161 const el = document.getElementById(id); 

162 filters[id] = el.value.trim().toLowerCase(); 

163 }}); 

164 

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 }}); 

184 

185 const total = rows.length; 

186 document.getElementById('result-count').textContent = 

187 visible === total ? `${{total}} rows` : `${{visible}} of ${{total}} rows`; 

188 }} 

189 

190 function clearFilters() {{ 

191 filterIds.forEach(id => {{ document.getElementById(id).value = ''; }}); 

192 applyFilters(); 

193 }} 

194 

195 filterIds.forEach(id => {{ 

196 const el = document.getElementById(id); 

197 el.addEventListener('input', applyFilters); 

198 el.addEventListener('change', applyFilters); 

199 }}); 

200 

201 applyFilters(); 

202 </script> 

203 

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 &nbsp;|&nbsp; 

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"""