// Unit tests for cross-detector finding deduplication. #include #include #include #include "fold/report/filter.hpp" #include "fold/report/finding.hpp" #include "fold/report/sarif.hpp" using fold::Confidence; using fold::Finding; using fold::FindingEvidence; using fold::FindingKind; using fold::Location; namespace { Finding makeFinding(FindingKind kind, const std::string& file, int line, int col, const std::string& detector, Confidence confidence) { Finding f; f.kind = kind; Location loc; loc.file = file; loc.line = line; loc.column = col; f.locations.push_back(loc); f.evidence.detector_name = detector; f.confidence = confidence; f.id = std::string(fold::findingKindName(kind)) + ":" + file + ":" + std::to_string(line) + ":" + std::to_string(col); return f; } } // namespace TEST_CASE("crossDetectorDedup preserves independent kinds at the same location", "[filter][dedup]") { std::vector findings; // Same location, two detectors findings.push_back( makeFinding(FindingKind::NullDeref, "foo.cpp", 42, 10, "null-deref", Confidence::Proved)); findings.push_back(makeFinding(FindingKind::DivByZero, "foo.cpp", 42, 10, "div-by-zero", Confidence::Heuristic)); fold::FilterOptions opts; opts.exclude_third_party = false; auto result = fold::filterAndDedup(findings, opts); REQUIRE(result.size() == 2); REQUIRE(result[0].kind == FindingKind::DivByZero); REQUIRE(result[1].kind == FindingKind::NullDeref); } TEST_CASE("crossDetectorDedup keeps distinct locations separate", "[filter][dedup]") { std::vector findings; findings.push_back( makeFinding(FindingKind::NullDeref, "foo.cpp", 42, 10, "null-deref", Confidence::Proved)); findings.push_back(makeFinding(FindingKind::NullDeref, "foo.cpp", 100, 5, "null-deref", Confidence::Heuristic)); fold::FilterOptions opts; opts.exclude_third_party = false; auto result = fold::filterAndDedup(findings, opts); // Different locations should stay separate. REQUIRE(result.size() == 2); } TEST_CASE("crossDetectorDedup handles empty input", "[filter][dedup]") { std::vector findings; fold::FilterOptions opts; auto result = fold::filterAndDedup(findings, opts); REQUIRE(result.empty()); } TEST_CASE("crossDetectorDedup annotates correlation kind when merging", "[filter][dedup]") { std::vector findings; findings.push_back(makeFinding(FindingKind::IntOverflow, "bar.cpp", 7, 3, "range-analysis", Confidence::Heuristic)); findings.push_back(makeFinding(FindingKind::IntOverflow, "bar.cpp", 7, 3, "int-overflow-z3", Confidence::Proved)); fold::FilterOptions opts; opts.exclude_third_party = false; auto result = fold::filterAndDedup(findings, opts); REQUIRE(result.size() == 1); // Higher-confidence (Proved) wins. REQUIRE(result[0].kind == FindingKind::IntOverflow); REQUIRE(result[0].confidence == Confidence::Proved); REQUIRE(result[0].evidence.correlation_kind.find("cross-detector") != std::string::npos); } TEST_CASE("filterAndDedup drops locationless findings", "[filter][droploc]") { std::vector findings; // Valid finding. findings.push_back( makeFinding(FindingKind::NullDeref, "foo.cpp", 42, 10, "null-deref", Confidence::Proved)); // Empty file. findings.push_back( makeFinding(FindingKind::NullDeref, "", 42, 10, "null-deref", Confidence::Proved)); // Line 0. findings.push_back( makeFinding(FindingKind::NullDeref, "bar.cpp", 0, 10, "null-deref", Confidence::Proved)); // No locations at all. { Finding f; f.kind = FindingKind::DivByZero; f.confidence = Confidence::Heuristic; f.id = "div0:nowhere"; findings.push_back(f); } fold::FilterOptions opts; opts.exclude_third_party = false; auto result = fold::filterAndDedup(findings, opts); REQUIRE(result.size() == 1); REQUIRE(result[0].locations.front().file == "foo.cpp"); } TEST_CASE("whole-program reachability identities survive without fabricated lines", "[filter][production-reachability][sarif]") { Finding finding; finding.id = "fold:production-reachability:dead"; finding.kind = FindingKind::ProductionReachability; finding.confidence = Confidence::Proved; finding.locations.push_back({.file = "app.cpp", .function = "dead"}); finding.evidence.quality = fold::QualityEvidence{ .representative_id = "usr:dead", .classification = "unreachable-production", }; finding.evidence.reachability = fold::ReachabilityEvidence{ .classification = "unreachable-production", .default_result = "unsat", .free_configuration_result = "unsat", .closed_world = true, .removal_candidate = true, }; fold::FilterOptions opts; opts.exclude_third_party = false; Finding second = finding; second.id = "fold:production-reachability:test-only"; second.evidence.quality->representative_id = "usr:test-only"; second.evidence.quality->classification = "test-only"; second.evidence.reachability->classification = "test-only"; second.evidence.reachability->removal_candidate = false; const auto filtered = fold::filterAndDedup({finding, second}, opts); REQUIRE(filtered.size() == 2); CHECK(filtered.front().locations.front().line == 0); std::ostringstream output; fold::writeSarif(output, {finding}); const auto sarif = nlohmann::json::parse(output.str()); const auto& result = sarif["runs"][0]["results"][0]; CHECK_FALSE(result.contains("locations")); CHECK(result["properties"]["reachabilityEvidence"]["classification"] == "unreachable-production"); CHECK(result["properties"]["reachabilityEvidence"]["removal_candidate"] == true); } TEST_CASE("runtime-only concrete crashes survive filtering and SARIF reporting", "[filter][fuzz-crash][sarif]") { Finding crash; crash.id = "fuzz-crash:SEGV:worker:crash-deadbeef"; crash.kind = FindingKind::FuzzCrash; crash.confidence = Confidence::ConcreteWitness; crash.message = "Fuzzer observed an unpredicted SEGV"; crash.evidence.detector_name = "fuzz-run"; crash.evidence.correlation_kind = "runtime-only"; crash.evidence.fuzz_witness = fold::FuzzWitness{"/tmp/crash-deadbeef", "SEGV", "", 0}; fold::FilterOptions opts; opts.exclude_third_party = false; const auto filtered = fold::filterAndDedup({crash}, opts); REQUIRE(filtered.size() == 1); REQUIRE(filtered[0].locations.empty()); std::ostringstream output; fold::writeSarif(output, {crash}); const auto sarif = nlohmann::json::parse(output.str()); const auto& result = sarif["runs"][0]["results"][0]; REQUIRE_FALSE(result.contains("locations")); REQUIRE(result["properties"]["correlationKind"] == "runtime-only"); REQUIRE(result["properties"]["fuzzWitness"]["seedPath"] == "/tmp/crash-deadbeef"); REQUIRE(result["properties"]["fuzzWitness"]["sanitizerKind"] == "SEGV"); }