- 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
189 lines
7.2 KiB
C++
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");
|
|
}
|