Preserve source, binary, and runtime evidence boundaries while making precision and performance regressions reproducible. - decompose analysis orchestration into bounded modules - add differential, metamorphic, organic, and Yams change-time gates - fix detector, P-Code, fuzz sanitizer, and report identity precision
199 lines
9.2 KiB
C++
199 lines
9.2 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <stdexcept>
|
|
|
|
#include "fold/confirm/confirmation_task.hpp"
|
|
|
|
using namespace fold;
|
|
using namespace fold::confirm;
|
|
|
|
TEST_CASE("confirmation task JSON round-trips source targets", "[confirmation]") {
|
|
FindingConfirmationTask task;
|
|
task.version = "1";
|
|
task.task_id = "confirm:bounded-symbolic:oob:file.cc:42:7";
|
|
task.finding_id = "oob-access:file.cc:42:7";
|
|
task.hazard = FindingKind::OOBAccess;
|
|
task.backend = ConfirmationBackend::Source;
|
|
task.lane = ConfirmationLane::Clang;
|
|
task.adapter = ConfirmationAdapter::BoundedSymbolic;
|
|
task.target.artifact = "/repo/file.cc";
|
|
task.target.function = "demo::parse";
|
|
task.target.source_location = Location{"/repo/file.cc", 42, 7, "demo::parse"};
|
|
task.input_channel = InputChannel::Harness;
|
|
task.controlled_values = {"idx", "len"};
|
|
task.path_predicate = "idx >= len";
|
|
task.expected_violation = "out-of-bounds access";
|
|
task.artifacts.push_back({"graphml", "witness/file.graphml", "", "application/xml"});
|
|
task.fact_provenance =
|
|
ir::FactProvenance{"source",
|
|
"clang",
|
|
"/repo/file.cc",
|
|
"hash",
|
|
ir::makeFunctionId("clang", "/repo/file.cc", "demo::parse"),
|
|
"",
|
|
"",
|
|
""};
|
|
|
|
const auto j = confirmationTaskToJson(task);
|
|
REQUIRE(j[json_keys::kHazard] == "oob-access");
|
|
REQUIRE(j[json_keys::kTarget][json_keys::kSourceLocation][json_keys::kLine] == 42);
|
|
|
|
const auto parsed = confirmationTaskFromJson(j);
|
|
REQUIRE(parsed.task_id == task.task_id);
|
|
REQUIRE(parsed.finding_id == task.finding_id);
|
|
REQUIRE(parsed.hazard == FindingKind::OOBAccess);
|
|
REQUIRE(parsed.backend == ConfirmationBackend::Source);
|
|
REQUIRE(parsed.lane == ConfirmationLane::Clang);
|
|
REQUIRE(parsed.adapter == ConfirmationAdapter::BoundedSymbolic);
|
|
REQUIRE(parsed.target.source_location.has_value());
|
|
REQUIRE(parsed.target.source_location->file == "/repo/file.cc");
|
|
REQUIRE(parsed.target.source_location->line == 42);
|
|
REQUIRE(parsed.input_channel == InputChannel::Harness);
|
|
REQUIRE(parsed.controlled_values.size() == 2);
|
|
REQUIRE(parsed.artifacts.size() == 1);
|
|
REQUIRE(parsed.artifacts.front().media_type == "application/xml");
|
|
REQUIRE(parsed.fact_provenance.has_value());
|
|
REQUIRE(parsed.fact_provenance->lane == "clang");
|
|
}
|
|
|
|
TEST_CASE("confirmation task JSON round-trips binary targets", "[confirmation]") {
|
|
FindingConfirmationTask task;
|
|
task.task_id = "confirm:angr:binary:401000";
|
|
task.finding_id = "binary-oob:app:0x401000";
|
|
task.hazard = FindingKind::OOBAccess;
|
|
task.backend = ConfirmationBackend::Binary;
|
|
task.lane = ConfirmationLane::GhidraPcode;
|
|
task.adapter = ConfirmationAdapter::Angr;
|
|
task.target.artifact = "/tmp/app";
|
|
task.target.function = "sym.vuln";
|
|
task.target.binary_address = 0x401000;
|
|
task.input_channel = InputChannel::Stdin;
|
|
task.partial = true;
|
|
|
|
const auto parsed = confirmationTaskFromJson(confirmationTaskToJson(task));
|
|
REQUIRE(parsed.backend == ConfirmationBackend::Binary);
|
|
REQUIRE(parsed.lane == ConfirmationLane::GhidraPcode);
|
|
REQUIRE(parsed.adapter == ConfirmationAdapter::Angr);
|
|
REQUIRE(parsed.target.binary_address.has_value());
|
|
REQUIRE(*parsed.target.binary_address == 0x401000);
|
|
REQUIRE(!parsed.target.source_location.has_value());
|
|
REQUIRE(parsed.input_channel == InputChannel::Stdin);
|
|
REQUIRE(parsed.partial);
|
|
}
|
|
|
|
TEST_CASE("confirmation result JSON round-trips ConcreteWitness evidence", "[confirmation]") {
|
|
FindingConfirmationResult result;
|
|
result.task_id = "confirm:libfuzzer-asan:oob:file.cc:42:7";
|
|
result.finding_id = "oob-access:file.cc:42:7";
|
|
result.status = ConfirmationStatus::Confirmed;
|
|
result.adapter = ConfirmationAdapter::LibFuzzerAsan;
|
|
result.promoted_confidence = Confidence::ConcreteWitness;
|
|
result.progress.reached = true;
|
|
result.progress.triggered = true;
|
|
result.progress.detected = true;
|
|
result.progress.replayed = true;
|
|
result.progress.time_to_detected_ms = 17;
|
|
result.message = "ASan observed heap-buffer-overflow at predicted sink";
|
|
result.fuzz_witness = FuzzWitness{"run/crash-1", "heap-buffer-overflow", "/repo/file.cc", 42};
|
|
result.artifacts.push_back({"seed", "run/crash-1", "abc123", "application/octet-stream"});
|
|
result.diagnostics.push_back("matched by finding id");
|
|
|
|
const auto j = confirmationResultToJson(result);
|
|
REQUIRE(j[json_keys::kStatus] == "confirmed");
|
|
REQUIRE(j[json_keys::kPromotedConfidence] == "concrete-witness");
|
|
REQUIRE(j[json_keys::kFuzzWitness][json_keys::kAsanKind] == "heap-buffer-overflow");
|
|
|
|
const auto parsed = confirmationResultFromJson(j);
|
|
REQUIRE(parsed.status == ConfirmationStatus::Confirmed);
|
|
REQUIRE(parsed.adapter == ConfirmationAdapter::LibFuzzerAsan);
|
|
REQUIRE(parsed.promoted_confidence.has_value());
|
|
REQUIRE(*parsed.promoted_confidence == Confidence::ConcreteWitness);
|
|
REQUIRE(parsed.progress.replayed);
|
|
REQUIRE(parsed.progress.time_to_detected_ms == 17);
|
|
REQUIRE(parsed.fuzz_witness.has_value());
|
|
REQUIRE(parsed.fuzz_witness->seed_path == "run/crash-1");
|
|
REQUIRE(parsed.artifacts.size() == 1);
|
|
REQUIRE(parsed.diagnostics.size() == 1);
|
|
}
|
|
|
|
TEST_CASE("confirmation progress rejects impossible stage ordering", "[confirmation]") {
|
|
FindingConfirmationResult result;
|
|
result.progress.detected = true;
|
|
REQUIRE_THROWS_AS(confirmationResultToJson(result), std::invalid_argument);
|
|
|
|
auto valid = confirmationResultToJson(FindingConfirmationResult{});
|
|
valid[json_keys::kProgress][json_keys::kReplayed] = true;
|
|
REQUIRE_THROWS_AS(confirmationResultFromJson(valid), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE("confirmation parsers reject unknown enum strings", "[confirmation]") {
|
|
auto j = confirmationTaskToJson(FindingConfirmationTask{});
|
|
j[json_keys::kAdapter] = "not-an-adapter";
|
|
REQUIRE_THROWS_AS(confirmationTaskFromJson(j), std::invalid_argument);
|
|
|
|
auto r = confirmationResultToJson(FindingConfirmationResult{});
|
|
r[json_keys::kStatus] = "maybe";
|
|
REQUIRE_THROWS_AS(confirmationResultFromJson(r), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE("confirmation JSON omits absent optional result fields", "[confirmation]") {
|
|
FindingConfirmationResult result;
|
|
result.task_id = "task";
|
|
result.status = ConfirmationStatus::Unsupported;
|
|
|
|
const auto j = confirmationResultToJson(result);
|
|
REQUIRE(!j.contains(json_keys::kPromotedConfidence));
|
|
REQUIRE(!j.contains(json_keys::kFuzzWitness));
|
|
|
|
const auto parsed = confirmationResultFromJson(j);
|
|
REQUIRE(parsed.status == ConfirmationStatus::Unsupported);
|
|
REQUIRE(!parsed.promoted_confidence.has_value());
|
|
REQUIRE(!parsed.fuzz_witness.has_value());
|
|
}
|
|
|
|
TEST_CASE("confirmation helpers map source findings into bounded-symbolic tasks",
|
|
"[confirmation]") {
|
|
Finding finding;
|
|
finding.id = "null-deref:/repo/main.cpp:3:20";
|
|
finding.kind = FindingKind::NullDeref;
|
|
finding.confidence = Confidence::Proved;
|
|
finding.message = "null pointer dereference";
|
|
finding.locations.push_back(Location{"/repo/main.cpp", 3, 20, "f"});
|
|
finding.evidence.variable_name = "p";
|
|
finding.evidence.path_trace = {"branch_taken: /repo/main.cpp:3"};
|
|
|
|
auto task = confirmationTaskForFinding(finding, ConfirmationAdapter::BoundedSymbolic,
|
|
ConfirmationBackend::Source, ConfirmationLane::Clang,
|
|
InputChannel::Harness);
|
|
REQUIRE(task.finding_id == finding.id);
|
|
REQUIRE(task.hazard == FindingKind::NullDeref);
|
|
REQUIRE(task.target.source_location.has_value());
|
|
REQUIRE(task.target.source_location->function == "f");
|
|
REQUIRE(task.controlled_values == std::vector<std::string>{"p"});
|
|
REQUIRE(task.path_predicate.find("branch_taken") != std::string::npos);
|
|
|
|
auto result = confirmationResultForFinding(finding, task, ConfirmationStatus::Confirmed);
|
|
REQUIRE(result.task_id == task.task_id);
|
|
REQUIRE(result.finding_id == finding.id);
|
|
REQUIRE(result.status == ConfirmationStatus::Confirmed);
|
|
REQUIRE(result.adapter == ConfirmationAdapter::BoundedSymbolic);
|
|
REQUIRE(!result.promoted_confidence.has_value());
|
|
}
|
|
|
|
TEST_CASE("confirmation helpers preserve ConcreteWitness promotion evidence", "[confirmation]") {
|
|
Finding finding;
|
|
finding.id = "oob-access:/repo/main.cpp:5:9";
|
|
finding.kind = FindingKind::OOBAccess;
|
|
finding.confidence = Confidence::ConcreteWitness;
|
|
finding.evidence.fuzz_witness =
|
|
FuzzWitness{"crash.bin", "stack-buffer-overflow", "/repo/main.cpp", 5};
|
|
|
|
auto task = confirmationTaskForFinding(finding, ConfirmationAdapter::LibFuzzerAsan,
|
|
ConfirmationBackend::Source, ConfirmationLane::Clang,
|
|
InputChannel::Harness);
|
|
auto result = confirmationResultForFinding(finding, task, ConfirmationStatus::Confirmed,
|
|
"matched ASan crash");
|
|
REQUIRE(result.promoted_confidence.has_value());
|
|
REQUIRE(*result.promoted_confidence == Confidence::ConcreteWitness);
|
|
REQUIRE(result.fuzz_witness.has_value());
|
|
REQUIRE(result.fuzz_witness->asan_kind == "stack-buffer-overflow");
|
|
}
|