use secfinding::{Evidence, Finding, FindingBuildError, Location, Severity};
use std::thread;
#[test]
fn test_empty_scanner() {
let err = Finding::builder("", "target", Severity::Info)
.title("title")
.build()
.unwrap_err();
assert_eq!(err, FindingBuildError::EmptyScanner);
}
#[test]
fn test_empty_target() {
let err = Finding::builder("scanner", "", Severity::Info)
.title("title")
.build()
.unwrap_err();
assert_eq!(err, FindingBuildError::EmptyTarget);
}
#[test]
fn test_empty_title() {
let err = Finding::builder("scanner", "target", Severity::Info)
.title("")
.build()
.unwrap_err();
assert_eq!(err, FindingBuildError::EmptyTitle);
}
#[test]
fn test_empty_detail_allowed_but_handled() {
let finding = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.detail("")
.build()
.unwrap();
assert_eq!(finding.detail(), "");
}
#[test]
fn test_null_bytes_in_scanner() {
let res = Finding::builder("scan\0ner", "target", Severity::Info)
.title("title")
.build();
assert!(res.is_err(), "Engine allowed null bytes in scanner name");
}
#[test]
fn test_null_bytes_in_target() {
let res = Finding::builder("scanner", "tar\0get", Severity::Info)
.title("title")
.build();
assert!(res.is_err(), "Engine allowed null bytes in target");
}
#[test]
fn test_null_bytes_in_title() {
let res = Finding::builder("scanner", "target", Severity::Info)
.title("ti\0tle")
.build();
assert!(res.is_err(), "Engine allowed null bytes in title");
}
#[test]
fn test_null_bytes_in_cve() {
let res = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cve("CVE-2024-\x00123")
.build();
assert!(res.is_err(), "Engine allowed null bytes in CVE");
}
#[test]
fn test_max_u32_location_line() {
let loc = Location::new("src/main.rs")
.unwrap()
.line(u32::MAX)
.unwrap();
let finding = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.location(loc)
.build()
.unwrap();
assert_eq!(finding.location().unwrap().line, Some(u32::MAX));
}
#[test]
fn test_max_u32_location_column() {
let loc = Location::new("src/main.rs")
.unwrap()
.column(u32::MAX)
.unwrap();
let finding = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.location(loc)
.build()
.unwrap();
assert_eq!(finding.location().unwrap().column, Some(u32::MAX));
}
#[test]
fn test_max_f64_confidence() {
let res = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.confidence(f64::MAX)
.build();
assert!(
res.is_err() || res.unwrap().confidence() == Some(1.0),
"Confidence was not constrained"
);
}
#[test]
fn test_max_f64_cvss() {
let res = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cvss_score(f64::MAX)
.build();
assert!(
res.is_err() || res.unwrap().cvss_score() == Some(10.0),
"CVSS score was not constrained"
);
}
#[test]
fn test_nan_confidence() {
let err = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.confidence(f64::NAN)
.build()
.unwrap_err();
assert_eq!(err, FindingBuildError::InvalidConfidence);
}
#[test]
fn test_nan_cvss_score() {
let err = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cvss_score(f64::NAN)
.build()
.unwrap_err();
assert_eq!(err, FindingBuildError::InvalidCvssScore);
}
#[test]
fn test_1mb_scanner_input() {
let big_scanner = "a".repeat(1_048_576);
let err = Finding::builder(&big_scanner, "target", Severity::Info)
.title("title")
.build()
.unwrap_err();
assert!(matches!(err, FindingBuildError::FieldTooLong { .. }));
}
#[test]
fn test_1mb_target_input() {
let big_target = "b".repeat(1_048_576);
let err = Finding::builder("scanner", &big_target, Severity::Info)
.title("title")
.build()
.unwrap_err();
assert!(matches!(err, FindingBuildError::FieldTooLong { .. }));
}
#[test]
fn test_1mb_title_input() {
let big_title = "c".repeat(1_048_576);
let result = std::panic::catch_unwind(|| {
Finding::builder("scanner", "target", Severity::Info)
.title(&big_title)
.build()
});
match result {
Ok(res) => assert!(res.is_err(), "Engine allowed 1MB title without error"),
Err(_) => panic!("Engine panicked instead of returning error on 1MB title"),
}
}
#[test]
fn test_1mb_detail_input() {
let big_detail = "d".repeat(1_048_577);
let result = std::panic::catch_unwind(|| {
Finding::builder("scanner", "target", Severity::Info)
.title("title")
.detail(&big_detail)
.build()
});
match result {
Ok(res) => assert!(res.is_err(), "Engine allowed >1MB detail without error"),
Err(_) => panic!("Engine panicked instead of returning error on >1MB detail"),
}
}
#[test]
fn test_concurrent_builders_8_threads() {
let mut handles = vec![];
for i in 0..8 {
handles.push(thread::spawn(move || {
for j in 0..1000 {
let _ = Finding::builder(
format!("scanner-{}", i),
format!("target-{}", j),
Severity::Info,
)
.title("title")
.tag(format!("tag-{}", j))
.build()
.unwrap();
}
}));
}
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn test_malformed_cve_prefix() {
let err = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cve("VCE-2024-1234") .build()
.unwrap_err();
assert!(matches!(err, FindingBuildError::InvalidCveFormat(_)));
}
#[test]
fn test_malformed_cve_length_too_short() {
let err = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cve("CVE-1") .build()
.unwrap_err();
assert!(matches!(err, FindingBuildError::InvalidCveFormat(_)));
}
#[test]
fn test_malformed_cwe_format() {
let err = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cwe("CWE89") .build()
.unwrap_err();
assert!(matches!(err, FindingBuildError::InvalidCweFormat(_)));
}
#[test]
fn test_location_path_traversal() {
let res = Location::new("../../../etc/passwd");
assert!(
res.is_err(),
"Engine allowed path traversal in location file"
);
}
#[test]
fn test_location_absolute_path() {
let res = Location::new("/etc/passwd");
assert!(
res.is_err(),
"Engine allowed absolute path in location file"
);
}
#[test]
fn test_unicode_bom_in_title() {
let title = "\u{FEFF}Title";
let finding = Finding::builder("scanner", "target", Severity::Info)
.title(title)
.build()
.unwrap();
assert!(
!finding.title().starts_with('\u{FEFF}'),
"Engine preserved BOM in title"
);
}
#[test]
fn test_unicode_right_to_left_override() {
let rlo = "\u{202E}";
let res = Finding::builder("scanner", "target", Severity::Info)
.title(format!("{}safe_file.exe\0.txt", rlo))
.build();
assert!(
res.is_err(),
"Engine allowed Right-to-Left Override character"
);
}
#[test]
fn test_unicode_surrogate_replacement() {
let title = "invalid\u{FFFD}char";
let res = Finding::builder("scanner", "target", Severity::Info)
.title(title)
.build();
assert!(res.is_err(), "Engine allowed Unicode replacement character");
}
#[test]
fn test_duplicate_tags_deduplication() {
let finding = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.tag("sqli")
.tag("sqli")
.tag("xss")
.tag("sqli")
.build()
.unwrap();
assert_eq!(finding.tags().len(), 2, "Tags were not deduplicated");
}
#[test]
fn test_duplicate_cves() {
let finding = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.cve("CVE-2024-1234")
.cve("CVE-2024-1234")
.build()
.unwrap();
assert_eq!(finding.cve_ids().len(), 1, "CVEs were not deduplicated");
}
#[test]
fn test_duplicate_matched_values() {
let finding = Finding::builder("scanner", "target", Severity::Info)
.title("title")
.matched_value("needle")
.matched_value("needle")
.build()
.unwrap();
assert_eq!(
finding.matched_values().len(),
1,
"Matched values were not deduplicated"
);
}
#[test]
fn test_scanner_len_exactly_max() {
let max_len = 1024;
let scanner = "a".repeat(max_len);
let finding = Finding::builder(&scanner, "target", Severity::Info)
.title("title")
.build();
assert!(
finding.is_ok(),
"Engine rejected scanner at exact MAX length"
);
}
#[test]
fn test_scanner_len_off_by_one() {
let max_len = 1024;
let scanner = "a".repeat(max_len + 1);
let finding = Finding::builder(&scanner, "target", Severity::Info)
.title("title")
.build();
assert!(
finding.is_err(),
"Engine allowed scanner length off by one (MAX + 1)"
);
}
#[test]
fn test_title_len_exactly_max() {
let max_len = 10240;
let title = "a".repeat(max_len);
let finding = Finding::builder("scanner", "target", Severity::Info)
.title(&title)
.build();
assert!(finding.is_ok(), "Engine rejected title at exact MAX length");
}
#[test]
fn test_title_len_off_by_one() {
let max_len = 10240;
let title = "a".repeat(max_len + 1);
let result = std::panic::catch_unwind(|| {
Finding::builder("scanner", "target", Severity::Info)
.title(&title)
.build()
});
match result {
Ok(res) => assert!(
res.is_err(),
"Engine allowed title length off by one (MAX + 1) without error"
),
Err(_) => panic!("Engine panicked instead of returning error on off by one title"),
}
}
#[test]
fn test_100k_evidence_items() {
let mut builder = Finding::builder("scanner", "target", Severity::Info).title("title");
for _i in 0..100_000 {
builder = builder.evidence(Evidence::http_status(200).unwrap());
}
let result = builder.build();
assert!(
result.is_err(),
"Engine allowed 100,000 evidence items, potential resource exhaustion"
);
}
#[test]
fn test_100k_tags() {
let mut builder = Finding::builder("scanner", "target", Severity::Info).title("title");
for i in 0..100_000 {
builder = builder.tag(format!("tag-{}", i));
}
let result = builder.build();
assert!(
result.is_err(),
"Engine allowed 100,000 tags, potential resource exhaustion"
);
}
#[test]
fn test_json_deserialization_exhaustion() {
let json = format!(
r#"{{
"scanner": "s",
"target": "t",
"severity": "high",
"title": "title",
"tags": [{}]
}}"#,
vec!["\"a\""; 100_000].join(", ")
);
let res: Result<Finding, _> = serde_json::from_str(&json);
assert!(
res.is_err(),
"Engine parsed 100k array JSON without size bounds"
);
}
#[test]
fn test_json_deserialization_negative_line() {
let json = r#"{
"scanner": "s",
"target": "t",
"severity": "high",
"title": "title",
"location": {
"file": "src/main.rs",
"line": -1,
"column": 10
}
}"#;
let res: Result<Finding, _> = serde_json::from_str(json);
assert!(
res.is_err(),
"Engine allowed negative location line in JSON"
);
}
#[test]
fn test_json_deserialization_zero_line() {
let json = r#"{
"scanner": "s",
"target": "t",
"severity": "high",
"title": "title",
"location": {
"file": "src/main.rs",
"line": 0,
"column": 10
}
}"#;
let res: Result<Finding, _> = serde_json::from_str(json);
assert!(res.is_err(), "Engine allowed line 0 in JSON");
}