diff --git a/src/md_to_html.py b/src/md_to_html.py index bb87a8d..008607d 100644 --- a/src/md_to_html.py +++ b/src/md_to_html.py @@ -392,11 +392,30 @@ def find_latest_report(reports_dir): return candidates[-1] +def neutralize_raw_html(md_text): + """Defense-in-depth: strip any live HTML out of the Markdown source + before parsing, regardless of whether the report generator already + escaped it. + + The generator scripts (ad_audit.py / Invoke-ADAudit.ps1) escape AD data + before writing it into table cells, but this converter shouldn't have to + trust that: a report could have been generated by an older version, by + a third-party tool, or hand-edited. Neither of our reports intentionally + emits raw HTML, so it's safe to normalize the whole source first -- + unescape any existing entities, then re-escape uniformly. That round + trip keeps already-escaped content single-escaped instead of doubling + up (which naive blanket-escaping would do to freshly generated reports). + """ + unescaped = html.unescape(md_text) + return unescaped.replace("&", "&").replace("<", "<").replace(">", ">") + + def convert(md_path, out_path): with open(md_path, "r", encoding="utf-8") as f: md_text = f.read() - body_html = markdown.markdown(md_text, extensions=["tables", "fenced_code", "toc"]) + safe_md_text = neutralize_raw_html(md_text) + body_html = markdown.markdown(safe_md_text, extensions=["tables", "fenced_code", "toc"]) body_html = badge_findings_table(body_html) body_html = wrap_timestamps(body_html) toc_html = build_toc(body_html)