fold/tests/unit/test_confirmation_task.cpp
trvon 2dcbdbc75d
feat: harden Fold quality guard
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
2026-07-13 15:05:35 -04:00

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");
}