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)),
)
|
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(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)
|