Skip to content

klyrek-report

Status: live on PyPI — pip install klyrek-report

Renders a ScanResult into Markdown, JSON, or self-contained HTML, computes summary statistics, and looks up remediation guidance for known finding titles.

from klyrek_report.markdown import render_markdown
from klyrek_report.json_export import render_json       # render_json(result, indent=2)
from klyrek_report.html import render_html
from klyrek_report.summary import summarize
from klyrek_report.remediation import remediation_for

summary = summarize(result)
tip = remediation_for("CORS wildcard origin combined with allowed credentials")

Real summarize() output from a full sandbox scan:

total_endpoints: 19
total_technologies: 27
total_findings: 136
findings_by_severity: {'critical': 3, 'high': 1, 'medium': 51, 'low': 25, 'info': 56}
endpoints_by_source: {'crawler': 12, 'crawler-form': 4, 'crawler-script': 1, 'js-endpoint': 2}
technologies_by_category: {'framework': 13, 'hosting': 14}

Real remediation_for() output:

"Never combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true;
 return a specific, validated origin instead."

"Set the HttpOnly attribute on every session cookie to block JavaScript access."

remediation_for matches against a fixed set of known finding-title hints and returns None if nothing matches — it's curated guidance for common findings, not a general-purpose knowledge base.

API reference

klyrek_report.summary

Compute summary statistics from a ScanResult for use across report formats.

summarize

summarize(result: ScanResult) -> ReportSummary

Compute endpoint/technology/finding counts, including a per-severity breakdown.

Source code in klyrek_report\summary.py
def summarize(result: ScanResult) -> ReportSummary:
    """Compute endpoint/technology/finding counts, including a per-severity breakdown."""
    severity_counts = Counter(f.severity.value for f in result.findings)
    findings_by_severity = {
        severity.value: severity_counts[severity.value]
        for severity in SEVERITY_ORDER
        if severity_counts.get(severity.value)
    }

    return ReportSummary(
        total_endpoints=len(result.endpoints),
        total_technologies=len(result.technologies),
        total_findings=len(result.findings),
        findings_by_severity=findings_by_severity,
        endpoints_by_source=dict(Counter(e.source for e in result.endpoints)),
        technologies_by_category=dict(Counter(t.category for t in result.technologies)),
    )

klyrek_report.remediation

Generic remediation guidance keyed to the finding titles Klyrek's own detectors produce.

This is a fixed, curated lookup over the vocabulary of finding titles klyrek-headers, klyrek-js, and klyrek-assets actually generate — not an open-ended CWE/CVE knowledge base.

remediation_for

remediation_for(title: str) -> str | None

Return generic remediation guidance for a finding title, if we have a match.

Source code in klyrek_report\remediation.py
def remediation_for(title: str) -> str | None:
    """Return generic remediation guidance for a finding title, if we have a match."""
    lowered = title.lower()
    for hint, guidance in _REMEDIATION_BY_HINT:
        if hint.lower() in lowered:
            return guidance
    return None

klyrek_report.markdown

Render a ScanResult as a Markdown report.

render_markdown

render_markdown(result: ScanResult) -> str

Render a full Markdown report: executive summary, findings, technologies, endpoints.

Source code in klyrek_report\markdown.py
def render_markdown(result: ScanResult) -> str:
    """Render a full Markdown report: executive summary, findings, technologies, endpoints."""
    summary = summarize(result)
    lines: list[str] = [f"# Klyrek Scan Report: {result.target.base_url}", ""]

    lines.append(f"- Scan started: {result.started_at.isoformat()}")
    if result.finished_at:
        lines.append(f"- Scan finished: {result.finished_at.isoformat()}")
    lines.append(f"- Endpoints discovered: {summary.total_endpoints}")
    lines.append(f"- Technologies identified: {summary.total_technologies}")
    lines.append(f"- Findings: {summary.total_findings}")
    lines.append("")

    lines.append("## Executive Summary")
    lines.append("")
    if summary.findings_by_severity:
        for severity, count in summary.findings_by_severity.items():
            lines.append(f"- **{severity.upper()}**: {count}")
    else:
        lines.append("No findings were raised during this scan.")
    lines.append("")

    lines.append("## Findings")
    lines.append("")
    grouped = _group_by_severity(result.findings)
    if not result.findings:
        lines.append("_No findings._")
    for severity in SEVERITY_ORDER:
        findings = grouped.get(severity, [])
        if not findings:
            continue
        lines.append(f"### {severity.value.upper()}")
        lines.append("")
        for finding in findings:
            lines.append(f"#### {finding.title}")
            if finding.module:
                lines.append(f"*Module: {finding.module}*")
            if finding.endpoint:
                lines.append(f"*Endpoint: `{finding.endpoint.method} {finding.endpoint.url}`*")
            lines.append("")
            if finding.description:
                lines.append(finding.description)
                lines.append("")
            if finding.evidence:
                lines.append("**Evidence:**")
                lines.extend(f"- `{item}`" for item in finding.evidence)
                lines.append("")
            guidance = remediation_for(finding.title)
            if guidance:
                lines.append(f"**Remediation:** {guidance}")
                lines.append("")

    lines.append("## Technologies")
    lines.append("")
    if result.technologies:
        lines.append("| Category | Name | Version |")
        lines.append("|---|---|---|")
        for tech in result.technologies:
            lines.append(f"| {tech.category} | {tech.name} | {tech.version or '-'} |")
    else:
        lines.append("_No technologies identified._")
    lines.append("")

    lines.append("## Endpoints")
    lines.append("")
    if result.endpoints:
        lines.append("| Method | URL | Source | Status |")
        lines.append("|---|---|---|---|")
        for endpoint in result.endpoints:
            status = endpoint.status_code or "-"
            lines.append(f"| {endpoint.method} | {endpoint.url} | {endpoint.source} | {status} |")
    else:
        lines.append("_No endpoints discovered._")
    lines.append("")

    return "\n".join(lines)

klyrek_report.json_export

Render a ScanResult as JSON.

render_json

render_json(result: ScanResult, indent: int = 2) -> str

Serialize a full ScanResult (target, endpoints, technologies, findings) to JSON.

Source code in klyrek_report\json_export.py
def render_json(result: ScanResult, indent: int = 2) -> str:
    """Serialize a full ScanResult (target, endpoints, technologies, findings) to JSON."""
    return json.dumps(result, default=_default, indent=indent)

klyrek_report.html

Render a ScanResult as a simple, self-contained HTML report.

Every piece of target-derived content (finding titles, evidence, endpoint URLs, tech names) is HTML-escaped before embedding. That content ultimately came from the site being scanned, not from Klyrek itself, so it must never be trusted to render as raw HTML in a report someone opens in a browser.

render_html

render_html(result: ScanResult) -> str

Render a full, self-contained HTML report.

Source code in klyrek_report\html.py
def render_html(result: ScanResult) -> str:
    """Render a full, self-contained HTML report."""
    summary = summarize(result)
    target_url = escape(result.target.base_url)

    parts: list[str] = [
        "<!doctype html><html><head><meta charset='utf-8'>",
        f"<title>Klyrek Scan Report: {target_url}</title>",
        f"<style>{_STYLE}</style></head><body>",
        f"<h1>Klyrek Scan Report: {target_url}</h1>",
        "<h2>Executive Summary</h2>",
        "<ul>",
        f"<li>Endpoints discovered: {summary.total_endpoints}</li>",
        f"<li>Technologies identified: {summary.total_technologies}</li>",
        f"<li>Findings: {summary.total_findings}</li>",
        "</ul>",
    ]

    if summary.findings_by_severity:
        parts.append("<ul>")
        for severity, count in summary.findings_by_severity.items():
            parts.append(f"<li><strong>{escape(severity.upper())}</strong>: {count}</li>")
        parts.append("</ul>")

    parts.append("<h2>Findings</h2>")
    grouped = _group_by_severity(result.findings)
    if not result.findings:
        parts.append("<p><em>No findings.</em></p>")
    for severity in SEVERITY_ORDER:
        findings = grouped.get(severity, [])
        if not findings:
            continue
        parts.append(f"<h3>{severity.value.upper()}</h3>")
        parts.extend(_render_finding(finding) for finding in findings)

    parts.append("<h2>Technologies</h2>")
    if result.technologies:
        parts.append("<table><tr><th>Category</th><th>Name</th><th>Version</th></tr>")
        for tech in result.technologies:
            parts.append(
                f"<tr><td>{escape(tech.category)}</td><td>{escape(tech.name)}</td>"
                f"<td>{escape(tech.version or '-')}</td></tr>"
            )
        parts.append("</table>")
    else:
        parts.append("<p><em>No technologies identified.</em></p>")

    parts.append("<h2>Endpoints</h2>")
    if result.endpoints:
        parts.append("<table><tr><th>Method</th><th>URL</th><th>Source</th><th>Status</th></tr>")
        for endpoint in result.endpoints:
            status = endpoint.status_code or "-"
            parts.append(
                f"<tr><td>{escape(endpoint.method)}</td><td>{escape(endpoint.url)}</td>"
                f"<td>{escape(endpoint.source)}</td><td>{status}</td></tr>"
            )
        parts.append("</table>")
    else:
        parts.append("<p><em>No endpoints discovered.</em></p>")

    parts.append("</body></html>")
    return "\n".join(parts)