mod common;
use common::{finding, finding_full};
use secreport::Format;
use secfinding::{Evidence, Finding, Severity};
#[test]
fn markdown_header_contains_tool_and_target() {
let f = finding("T", Severity::Info);
let out = secreport::render(&[f], Format::Markdown, "MyTool").unwrap();
assert!(out.starts_with("# MyTool Security Report"));
assert!(out.contains("https://example.com"));
}
#[test]
fn markdown_zero_findings_shows_unknown_target() {
let out = secreport::render(&[] as &[Finding], Format::Markdown, "tool").unwrap();
assert!(out.starts_with("# "));
assert!(out.contains("0 findings"));
assert!(out.contains("unknown"));
}
#[test]
fn markdown_risk_summary_present() {
let f = finding("T", Severity::High);
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains("## Risk Summary"));
assert!(out.contains("| Severity | Count |"));
assert!(out.contains("| HIGH | 1 |"));
}
#[test]
fn markdown_sections_omitted_when_empty() {
let f = finding("T", Severity::Info);
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(!out.contains("## Critical Findings"));
assert!(!out.contains("## High Findings"));
assert!(!out.contains("## Medium Findings"));
assert!(!out.contains("## Low Findings"));
assert!(out.contains("## Informational"));
}
#[test]
fn markdown_all_severity_sections_present() {
let findings = vec![
finding("C", Severity::Critical),
finding("H", Severity::High),
finding("M", Severity::Medium),
finding("L", Severity::Low),
finding("I", Severity::Info),
];
let out = secreport::render(&findings, Format::Markdown, "tool").unwrap();
assert!(out.contains("## Critical Findings"));
assert!(out.contains("## High Findings"));
assert!(out.contains("## Medium Findings"));
assert!(out.contains("## Low Findings"));
assert!(out.contains("## Informational"));
}
#[test]
fn markdown_per_finding_fields() {
let f = finding_full("The Title", Severity::Medium);
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains("### The Title"));
assert!(out.contains("**Target:** `https://example.com`"));
assert!(out.contains(r"**Scanner:** test\-scanner"));
assert!(out.contains("**Category:** Vulnerability"));
assert!(out.contains("`web`"));
assert!(out.contains("`xss`"));
assert!(out.contains("**CWE:** CWE\\-79"));
assert!(out.contains("**CVE:** CVE\\-2024\\-12345"));
assert!(out.contains("Detailed description of the issue."));
assert!(out.contains("curl -X GET https://example.com/poc"));
}
#[test]
fn markdown_escapes_special_chars() {
let f = Finding::new("scan*ner", "t", Severity::High, "`code`", "a | b").unwrap();
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains(r"\`code\`"));
assert!(out.contains(r"a \| b"));
assert!(out.contains(r"scan\*ner"));
}
#[test]
fn markdown_escapes_links() {
let f = Finding::new(
"s",
"t",
Severity::High,
"[click](javascript:alert(1))",
"",
)
.unwrap();
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(!out.contains("[click](javascript:alert(1))"));
assert!(out.contains(r"\[click\]"));
}
#[test]
fn markdown_escapes_html_tags() {
let f = Finding::new("s", "t", Severity::High, "<script>alert(1)</script>", "").unwrap();
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains(r"\<script\>alert\(1\)\</script\>"));
}
#[test]
fn markdown_tool_name_injection_blocked() {
let f = finding("T", Severity::Low);
let out = secreport::render(&[f], Format::Markdown, "evil](/)").unwrap();
assert!(!out.contains("[evil](/)]("));
assert!(out.contains("https://github.com/santh-io/"));
}
#[test]
fn markdown_exploit_hint_fence_adaptive() {
let f = Finding::builder("s", "t", Severity::High)
.title("T")
.exploit_hint("echo ok\n```\nmalicious\n```\nend")
.build()
.unwrap();
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains("````bash") || out.contains("`````bash"));
assert!(out.contains("malicious"));
}
#[test]
fn markdown_exploit_hint_six_backticks() {
let f = Finding::builder("s", "t", Severity::High)
.title("T")
.exploit_hint("``````")
.build()
.unwrap();
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains("```````bash") || out.contains("````````bash"));
}
#[test]
fn markdown_footer_link_percent_encoded() {
let f = finding("T", Severity::Low);
let out = secreport::render(&[f], Format::Markdown, "tool name").unwrap();
assert!(out.contains("https://github.com/santh-io/tool%20name"));
}
#[test]
fn markdown_unicode_preserved() {
let f = Finding::new("s", "t", Severity::Medium, "日本語 🚨", "詳細").unwrap();
let out = secreport::render(&[f], Format::Markdown, "ツール").unwrap();
assert!(out.contains("日本語 🚨"));
assert!(out.contains("詳細"));
assert!(out.contains("ツール"));
}
#[test]
fn markdown_empty_detail_omitted() {
let f = Finding::new("s", "t", Severity::High, "T", "").unwrap();
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(out.contains("### T"));
}
#[test]
fn markdown_no_exploit_hint_when_missing() {
let f = finding("T", Severity::High);
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(!out.contains("Exploit / PoC"));
}
#[test]
fn markdown_no_cwe_cve_when_missing() {
let f = finding("T", Severity::High);
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(!out.contains("**CWE:**"));
assert!(!out.contains("**CVE:**"));
}
#[test]
fn markdown_no_tags_when_missing() {
let f = finding("T", Severity::High);
let out = secreport::render(&[f], Format::Markdown, "tool").unwrap();
assert!(!out.contains("**Tags:**"));
}
#[test]
fn markdown_multiple_findings_same_severity_grouped() {
let findings = vec![
finding("A", Severity::High),
finding("B", Severity::High),
finding("C", Severity::High),
];
let out = secreport::render(&findings, Format::Markdown, "tool").unwrap();
let group_start = out.find("## High Findings").unwrap();
let group = &out[group_start..];
assert!(group.contains("### A"));
assert!(group.contains("### B"));
assert!(group.contains("### C"));
}
#[test]
fn markdown_risk_summary_counts_exact() {
let findings = vec![
Finding::new("s", "t", Severity::Critical, "C1", "").unwrap(),
Finding::new("s", "t", Severity::Critical, "C2", "").unwrap(),
Finding::new("s", "t", Severity::High, "H1", "").unwrap(),
Finding::new("s", "t", Severity::Medium, "M1", "").unwrap(),
Finding::new("s", "t", Severity::Medium, "M2", "").unwrap(),
Finding::new("s", "t", Severity::Medium, "M3", "").unwrap(),
Finding::new("s", "t", Severity::Low, "L1", "").unwrap(),
Finding::new("s", "t", Severity::Info, "I1", "").unwrap(),
Finding::new("s", "t", Severity::Info, "I2", "").unwrap(),
];
let out = secreport::render(&findings, Format::Markdown, "tool").unwrap();
assert!(out.contains("| CRIT | 2 |"));
assert!(out.contains("| HIGH | 1 |"));
assert!(out.contains("| MED | 3 |"));
assert!(out.contains("| LOW | 1 |"));
assert!(out.contains("| INFO | 2 |"));
}
#[test]
fn markdown_large_number_of_findings() {
let findings: Vec<Finding> = (0..5_000)
.map(|i| finding(&format!("Finding-{}", i), Severity::Medium))
.collect();
let out = secreport::render(&findings, Format::Markdown, "tool").unwrap();
assert!(out.contains("5\u{2009}000 findings") || out.contains("5000 findings"));
assert!(out.contains("## Medium Findings"));
}