fold/tests/unit/test_cross_detector_dedup.cpp
trvon 86b2f14a43
feat(analysis): harden project-wide scans
- persist TU-owned graphs and guarded configuration evidence
- add closed-world production reachability classification
- calibrate detector precision and reviewed self-scan policy
- terminate poisoned workers instead of longjmp recovery
2026-07-30 18:18:04 -04:00

189 lines
7.2 KiB
C++

// Unit tests for cross-detector finding deduplication.
#include <catch2/catch_test_macros.hpp>
#include <nlohmann/json.hpp>
#include <sstream>
#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<Finding> 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<Finding> 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<Finding> 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<Finding> 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<Finding> 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");
}