| @@ -185,6 +185,33 @@ def _md_table(rows: list[dict[str, str]]) -> list[str]: |
| 185 | 185 | return out |
| 186 | 186 | |
| 187 | 187 | |
| 188 | def _render_delta_md(delta, category: str) -> list[str]: |
| 189 | rule = delta.rule |
| 190 | out = [ |
| 191 | f"### {rule.title}", |
| 192 | "", |
| 193 | f"- **Rule:** `{rule.id}` · **Severity:** {rule.severity}", |
| 194 | f"- **Controls:** {', '.join(rule.controls) or '—'}", |
| 195 | f"- **Change:** {_transition(delta)}", |
| 196 | ] |
| 197 | if delta.new_reason: |
| 198 | out.append(f"- **Now:** {delta.new_reason}") |
| 199 | if category == REGRESSED and rule.remediation: |
| 200 | out.append(f"- **Remediation:** {rule.remediation.strip()}") |
| 201 | out.append("") |
| 202 | if delta.evidence_added: |
| 203 | out.append("**Newly failing rows:**") |
| 204 | out.append("") |
| 205 | out.extend(_md_table(delta.evidence_added)) |
| 206 | out.append("") |
| 207 | if delta.evidence_removed: |
| 208 | out.append("**No longer failing rows:**") |
| 209 | out.append("") |
| 210 | out.extend(_md_table(delta.evidence_removed)) |
| 211 | out.append("") |
| 212 | return out |
| 213 | |
| 214 | |
| 188 | 215 | def _render_md(diff: DiffReport) -> str: |
| 189 | 216 | counts = diff.counts |
| 190 | 217 | out: list[str] = [] |
| @@ -213,27 +240,7 @@ def _render_md(diff: DiffReport) -> str: |
| 213 | 240 | out.append(f"## {_CATEGORY_HEADING[category]}") |
| 214 | 241 | out.append("") |
| 215 | 242 | for delta in rows: |
| 216 | | rule = delta.rule |
| 217 | | out.append(f"### {rule.title}") |
| 218 | | out.append("") |
| 219 | | out.append(f"- **Rule:** `{rule.id}` · **Severity:** {rule.severity}") |
| 220 | | out.append(f"- **Controls:** {', '.join(rule.controls) or '—'}") |
| 221 | | out.append(f"- **Change:** {_transition(delta)}") |
| 222 | | if delta.new_reason: |
| 223 | | out.append(f"- **Now:** {delta.new_reason}") |
| 224 | | if category == REGRESSED and rule.remediation: |
| 225 | | out.append(f"- **Remediation:** {rule.remediation.strip()}") |
| 226 | | out.append("") |
| 227 | | if delta.evidence_added: |
| 228 | | out.append("**Newly failing rows:**") |
| 229 | | out.append("") |
| 230 | | out.extend(_md_table(delta.evidence_added)) |
| 231 | | out.append("") |
| 232 | | if delta.evidence_removed: |
| 233 | | out.append("**No longer failing rows:**") |
| 234 | | out.append("") |
| 235 | | out.extend(_md_table(delta.evidence_removed)) |
| 236 | | out.append("") |
| 243 | out.extend(_render_delta_md(delta, category)) |
| 237 | 244 | |
| 238 | 245 | unchanged = diff.counts[UNCHANGED] |
| 239 | 246 | if unchanged: |
| @@ -275,6 +282,31 @@ table.added caption { color: #c1272d; } table.removed caption { color: #1a7f37; |
| 275 | 282 | ) |
| 276 | 283 | |
| 277 | 284 | |
| 285 | def _render_delta_html(delta, category: str) -> str: |
| 286 | rule = delta.rule |
| 287 | body = [ |
| 288 | f"<h3>{escape(rule.title)}</h3>", |
| 289 | ( |
| 290 | f"<dl><dt>Rule</dt><dd><code>{escape(rule.id)}</code> · " |
| 291 | f"{escape(rule.severity)}</dd>" |
| 292 | ), |
| 293 | f"<dt>Controls</dt><dd>{escape(', '.join(rule.controls) or '—')}</dd>", |
| 294 | f"<dt>Change</dt><dd class='transition'>{escape(_transition(delta))}</dd>", |
| 295 | ] |
| 296 | if delta.new_reason: |
| 297 | body.append(f"<dt>Now</dt><dd>{escape(delta.new_reason)}</dd>") |
| 298 | if category == REGRESSED and rule.remediation: |
| 299 | body.append(f"<dt>Remediation</dt><dd>{escape(rule.remediation.strip())}</dd>") |
| 300 | body.append("</dl>") |
| 301 | if delta.evidence_added: |
| 302 | body.append(_html_table(delta.evidence_added, "Newly failing rows", "added")) |
| 303 | if delta.evidence_removed: |
| 304 | body.append( |
| 305 | _html_table(delta.evidence_removed, "No longer failing rows", "removed") |
| 306 | ) |
| 307 | return f"<div class='delta {category}'>{''.join(body)}</div>" |
| 308 | |
| 309 | |
| 278 | 310 | def _render_html(diff: DiffReport) -> str: |
| 279 | 311 | counts = diff.counts |
| 280 | 312 | parts: list[str] = [] |
| @@ -307,28 +339,7 @@ def _render_html(diff: DiffReport) -> str: |
| 307 | 339 | continue |
| 308 | 340 | parts.append(f"<h2>{escape(_CATEGORY_HEADING[category])}</h2>") |
| 309 | 341 | for delta in rows: |
| 310 | | rule = delta.rule |
| 311 | | body = [ |
| 312 | | f"<h3>{escape(rule.title)}</h3>", |
| 313 | | ( |
| 314 | | f"<dl><dt>Rule</dt><dd><code>{escape(rule.id)}</code> · " |
| 315 | | f"{escape(rule.severity)}</dd>" |
| 316 | | ), |
| 317 | | f"<dt>Controls</dt><dd>{escape(', '.join(rule.controls) or '—')}</dd>", |
| 318 | | f"<dt>Change</dt><dd class='transition'>{escape(_transition(delta))}</dd>", |
| 319 | | ] |
| 320 | | if delta.new_reason: |
| 321 | | body.append(f"<dt>Now</dt><dd>{escape(delta.new_reason)}</dd>") |
| 322 | | if category == REGRESSED and rule.remediation: |
| 323 | | body.append(f"<dt>Remediation</dt><dd>{escape(rule.remediation.strip())}</dd>") |
| 324 | | body.append("</dl>") |
| 325 | | if delta.evidence_added: |
| 326 | | body.append(_html_table(delta.evidence_added, "Newly failing rows", "added")) |
| 327 | | if delta.evidence_removed: |
| 328 | | body.append( |
| 329 | | _html_table(delta.evidence_removed, "No longer failing rows", "removed") |
| 330 | | ) |
| 331 | | parts.append(f"<div class='delta {category}'>{''.join(body)}</div>") |
| 342 | parts.append(_render_delta_html(delta, category)) |
| 332 | 343 | |
| 333 | 344 | if counts[UNCHANGED]: |
| 334 | 345 | parts.append(f"<p class='meta'>{counts[UNCHANGED]} rule(s) unchanged.</p>") |