mod common;
use common::{finding, finding_full};
use secreport::{Format, emit, render, render_any};
use secfinding::{Evidence, Finding, FindingKind, Reportable, Severity};
#[test]
fn pipeline_json_end_to_end() {
let findings = vec![
finding_full("SQLi", Severity::Critical),
finding_full("XSS", Severity::High),
finding_full("Info", Severity::Info),
];
let out = render(&findings, Format::Json, "pipeline-scanner").unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
assert_eq!(parsed.len(), 3);
assert_eq!(parsed[0]["severity"], "critical");
assert_eq!(parsed[1]["severity"], "high");
assert_eq!(parsed[2]["severity"], "info");
}
#[test]
fn pipeline_sarif_end_to_end() {
let findings = vec![
finding("A", Severity::Critical),
finding("B", Severity::High),
finding("C", Severity::Medium),
];
let out = render(&findings, Format::Sarif, "pipeline-scanner").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(parsed["version"], "2.1.0");
let results = parsed["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn pipeline_markdown_end_to_end() {
let findings = vec![
finding("Critical Finding", Severity::Critical),
finding("High Finding", Severity::High),
];
let out = render(&findings, Format::Markdown, "pipeline-scanner").unwrap();
assert!(out.contains(r"# pipeline\-scanner Security Report"));
assert!(out.contains("## Critical Findings"));
assert!(out.contains("## High Findings"));
assert!(out.contains("### Critical Finding"));
assert!(out.contains("### High Finding"));
}
#[test]
fn pipeline_text_end_to_end() {
let findings = vec![
finding("F1", Severity::Critical),
finding("F2", Severity::High),
finding("F3", Severity::Medium),
];
let out = render(&findings, Format::Text, "pipeline-scanner").unwrap();
assert!(out.contains("CRIT"));
assert!(out.contains("HIGH"));
assert!(out.contains("MED"));
assert!(out.contains("Total: \x1b[1m3\x1b[0m findings"));
}
#[test]
fn pipeline_jsonl_end_to_end() {
let findings: Vec<Finding> = (0..100)
.map(|i| finding(&format!("Finding-{}", i), Severity::Info))
.collect();
let out = render(&findings, Format::Jsonl, "pipeline-scanner").unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 100);
for line in lines {
let _: serde_json::Value = serde_json::from_str(line).unwrap();
}
}
#[test]
fn pipeline_render_any_with_custom_type() {
struct CustomFinding {
scanner: &'static str,
target: &'static str,
title: &'static str,
severity: Severity,
detail: &'static str,
}
impl Reportable for CustomFinding {
fn scanner(&self) -> &str { self.scanner }
fn target(&self) -> &str { self.target }
fn title(&self) -> &str { self.title }
fn severity(&self) -> Severity { self.severity }
fn detail(&self) -> &str { self.detail }
}
let findings = vec![
CustomFinding {
scanner: "custom",
target: "https://custom.example.com",
title: "Custom Issue",
severity: Severity::High,
detail: "Custom detail",
},
];
for format in [
Format::Text,
Format::Json,
Format::Jsonl,
Format::Sarif,
Format::Markdown,
] {
let out = render_any(&findings, format, "custom-tool").unwrap();
assert!(!out.is_empty(), "{:?} produced empty output", format);
}
}
#[test]
fn pipeline_emit_to_vec() {
let mut buf = Vec::new();
emit("hello world", &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "hello world");
}
#[test]
fn pipeline_emit_with_unicode() {
let mut buf = Vec::new();
emit("日本語 🎉", &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "日本語 🎉");
}
#[test]
fn pipeline_all_formats_empty_findings() {
let empty: Vec<Finding> = vec![];
for format in [
Format::Text,
Format::Json,
Format::Jsonl,
Format::Sarif,
Format::Markdown,
] {
let out = render(&empty, format, "tool").unwrap();
if format == Format::Jsonl {
assert_eq!(out, "");
} else {
assert!(!out.is_empty());
}
}
}
#[test]
fn pipeline_all_formats_single_finding() {
let f = finding("Single", Severity::High);
for format in [
Format::Text,
Format::Json,
Format::Jsonl,
Format::Sarif,
Format::Markdown,
] {
let out = render(&[f.clone()], format, "tool").unwrap();
assert!(!out.is_empty());
}
}
#[test]
fn pipeline_mixed_severity_counts_accurate() {
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 md = render(&findings, Format::Markdown, "tool").unwrap();
assert!(md.contains("| CRIT | 2 |"));
assert!(md.contains("| HIGH | 1 |"));
assert!(md.contains("| MED | 3 |"));
assert!(md.contains("| LOW | 1 |"));
assert!(md.contains("| INFO | 2 |"));
let txt = render(&findings, Format::Text, "tool").unwrap();
assert!(txt.contains(" 2 critical"));
assert!(txt.contains(" 1 high"));
assert!(txt.contains(" 3 medium"));
assert!(txt.contains(" 1 low"));
assert!(txt.contains(" 2 info"));
}
#[test]
fn pipeline_format_from_str_loose_all_variants() {
assert_eq!(Format::from_str_loose("text"), Some(Format::Text));
assert_eq!(Format::from_str_loose("json"), Some(Format::Json));
assert_eq!(Format::from_str_loose("jsonl"), Some(Format::Jsonl));
assert_eq!(Format::from_str_loose("sarif"), Some(Format::Sarif));
assert_eq!(Format::from_str_loose("markdown"), Some(Format::Markdown));
assert_eq!(Format::from_str_loose("md"), Some(Format::Markdown));
}
#[test]
fn pipeline_format_from_str_loose_case_insensitive() {
assert_eq!(Format::from_str_loose("TEXT"), Some(Format::Text));
assert_eq!(Format::from_str_loose("Json"), Some(Format::Json));
assert_eq!(Format::from_str_loose("JSONL"), Some(Format::Jsonl));
assert_eq!(Format::from_str_loose("SarIf"), Some(Format::Sarif));
assert_eq!(Format::from_str_loose("MarkDown"), Some(Format::Markdown));
assert_eq!(Format::from_str_loose("MD"), Some(Format::Markdown));
}
#[test]
fn pipeline_format_from_str_loose_unknown_returns_none() {
assert_eq!(Format::from_str_loose(""), None);
assert_eq!(Format::from_str_loose("xml"), None);
assert_eq!(Format::from_str_loose("jsonlines"), None);
assert_eq!(Format::from_str_loose("sarif2.1"), None);
}
#[test]
fn pipeline_format_display() {
assert_eq!(Format::Text.to_string(), "text");
assert_eq!(Format::Json.to_string(), "json");
assert_eq!(Format::Jsonl.to_string(), "jsonl");
assert_eq!(Format::Sarif.to_string(), "sarif");
assert_eq!(Format::Markdown.to_string(), "markdown");
}
#[test]
fn pipeline_generic_finding_builder_defaults() {
let gf = secreport::models::GenericFinding::builder("scanner-a", "https://target.com", Severity::High).build();
assert_eq!(gf.scanner, "scanner-a");
assert_eq!(gf.target, "https://target.com");
assert_eq!(gf.severity, Severity::High);
assert_eq!(gf.title, "");
assert_eq!(gf.detail, "");
assert!(gf.cwe_ids.is_empty());
assert!(gf.cve_ids.is_empty());
assert!(gf.tags.is_empty());
assert_eq!(gf.confidence, None);
assert_eq!(gf.rule_id, "");
assert_eq!(gf.sarif_level, Severity::High.sarif_level());
assert_eq!(gf.exploit_hint, None);
assert!(gf.evidence.is_empty());
assert_eq!(gf.kind, FindingKind::Unclassified);
}
#[test]
fn pipeline_generic_finding_builder_chains_all_fields() {
let cwe_ids: Vec<std::sync::Arc<str>> = vec!["CWE-79".into(), "CWE-89".into()];
let cve_ids: Vec<std::sync::Arc<str>> = vec!["CVE-2024-1".into()];
let tags: Vec<std::sync::Arc<str>> = vec!["xss".into(), "web".into()];
let evidence = vec![Evidence::http_status(500).unwrap()];
let gf = secreport::models::GenericFinding::builder("sc", "tgt", Severity::Critical)
.title("XSS")
.detail("Reflected XSS")
.cwe_ids(&cwe_ids)
.cve_ids(&cve_ids)
.tags(&tags)
.confidence(Some(0.95))
.rule_id("RULE-001")
.sarif_level("error")
.exploit_hint(Some("curl ..."))
.evidence(&evidence)
.kind(FindingKind::Vulnerability)
.build();
assert_eq!(gf.scanner, "sc");
assert_eq!(gf.target, "tgt");
assert_eq!(gf.severity, Severity::Critical);
assert_eq!(gf.title, "XSS");
assert_eq!(gf.detail, "Reflected XSS");
assert_eq!(gf.cwe_ids, &cwe_ids[..]);
assert_eq!(gf.cve_ids, &cve_ids[..]);
assert_eq!(gf.tags, &tags[..]);
assert_eq!(gf.confidence, Some(0.95));
assert_eq!(gf.rule_id, "RULE-001");
assert_eq!(gf.sarif_level, "error");
assert_eq!(gf.exploit_hint, Some("curl ..."));
assert_eq!(gf.evidence, &evidence[..]);
assert_eq!(gf.kind, FindingKind::Vulnerability);
}