#include #include #include #include #include "fold/report/finding_query.hpp" #include "fold/report/finding_report_reader.hpp" #include "fold/report/findings_json_report.hpp" #include "fold/report/sarif.hpp" namespace { fold::Finding makeFinding(std::string id, fold::FindingKind kind, fold::Confidence confidence, std::string backend, std::string path, std::string function, std::string message) { fold::Finding finding; finding.id = id; finding.kind = kind; finding.confidence = confidence; finding.evidence.backend = backend; finding.evidence.detector_name = "fixture-detector"; finding.locations.push_back({path, 12, 4, function}); finding.message = message; return finding; } std::vector fixtureFindings() { auto binary = makeFinding("finding-c", fold::FindingKind::HardcodedCredential, fold::Confidence::Heuristic, "binary", "bin/app", "sym.auth", "Embedded API Token"); binary.suppression_reason = "accepted test fixture"; binary.baseline_status = "accepted"; binary.evidence.binary_string = "token=secret"; fold::ExploitContext exploit_context; exploit_context.gadgets.push_back({0x1000, "pop rdi; ret", "reg-control"}); fold::RopChainInfo chain; chain.steps.push_back({"gadget", 0x1000, 0, "pop", 24}); exploit_context.chain = chain; binary.evidence.exploit_context = exploit_context; auto source = makeFinding("finding-a", fold::FindingKind::NullDeref, fold::Confidence::Proved, "source", "src/auth.cpp", "parse_user", "Null pointer reaches user parser"); source.baseline_status = "new"; source.evidence.proof_mode = "bounded-z3"; source.evidence.bail_reasons = {"unsupported-call"}; source.evidence.path_trace = {"entry", "ptr == nullptr", "dereference"}; fold::QualityEvidence quality; quality.stable_anchor = "function-anchor"; quality.representative_id = "source-member"; quality.classification = "product-logic"; quality.member_ids = {"source-member"}; quality.rank_reasons = {"nested-control-flow"}; quality.metrics = {{"family_size", 1.0}}; quality.incomplete = true; quality.uncertainty_reasons = {"comparison-cap"}; source.evidence.quality = quality; auto rust = makeFinding("finding-b", fold::FindingKind::RustPathTraversal, fold::Confidence::Heuristic, "rust", "tools/app/src/main.rs", "read_config", "Untrusted path reaches filesystem API"); auto swift = makeFinding("finding-d", fold::FindingKind::SwiftUnsafeCast, fold::Confidence::Heuristic, "swift", "Sources/App/main.swift", "decodeConfig", "Unchecked Swift cast reaches decoder"); return {binary, source, rust, swift}; } fold::FindingReport readNative(const std::vector& findings) { std::ostringstream encoded; fold::writeFindingsJson(encoded, findings); std::istringstream input(encoded.str()); return fold::readFindingReport(input, "fixture.findings.json"); } fold::FindingReport readSarif(const std::vector& findings) { std::ostringstream encoded; fold::writeSarif(encoded, findings); std::istringstream input(encoded.str()); return fold::readFindingReport(input, "fixture.sarif"); } std::vector ids(const std::vector& findings) { std::vector output; for (const auto& finding : findings) output.push_back(finding.id); return output; } } // namespace TEST_CASE("finding report reader accepts native JSON and SARIF", "[finding-query][reader]") { const auto findings = fixtureFindings(); const auto native = readNative(findings); const auto sarif = readSarif(findings); REQUIRE(native.format == fold::FindingReportFormat::FindingsJson); REQUIRE(sarif.format == fold::FindingReportFormat::Sarif); REQUIRE(native.findings.size() == 4); REQUIRE(sarif.findings.size() == 4); fold::FindingQueryOptions query; query.limit = 10; const auto native_result = fold::queryFindings(native.findings, query); const auto sarif_result = fold::queryFindings(sarif.findings, query); REQUIRE(ids(native_result.findings) == std::vector{"finding-a", "finding-b", "finding-c", "finding-d"}); REQUIRE(ids(sarif_result.findings) == ids(native_result.findings)); REQUIRE(sarif_result.findings[0].evidence.backend == "source"); REQUIRE(sarif_result.findings[0].evidence.proof_mode == "bounded-z3"); REQUIRE(sarif_result.findings[0].evidence.bail_reasons == std::vector{"unsupported-call"}); REQUIRE(sarif_result.findings[0].evidence.quality.has_value()); REQUIRE(sarif_result.findings[0].evidence.quality->stable_anchor == "function-anchor"); REQUIRE(sarif_result.findings[0].evidence.quality->classification == "product-logic"); REQUIRE(sarif_result.findings[0].evidence.quality->metrics.front().name == "family_size"); REQUIRE(sarif_result.findings[0].evidence.quality->incomplete); REQUIRE(sarif_result.findings[1].evidence.backend == "rust"); REQUIRE(sarif_result.findings[2].evidence.backend == "binary"); REQUIRE(sarif_result.findings[2].suppression_reason == "accepted test fixture"); REQUIRE(sarif_result.findings[2].evidence.exploit_context->chain->steps.front().stack_offset == 24); REQUIRE(sarif_result.findings[3].evidence.backend == "swift"); } TEST_CASE("finding report reader preserves unknown wire kinds", "[finding-query][reader]") { std::ostringstream encoded; fold::writeFindingsJson(encoded, {fixtureFindings().front()}); auto document = nlohmann::json::parse(encoded.str()); document["findings"][0]["kind"] = "future-frontend-hazard"; std::istringstream input(document.dump()); const auto report = fold::readFindingReport(input, "future.findings.json"); REQUIRE(report.findings.size() == 1); REQUIRE(report.findings.front().kind == fold::FindingKind::UnknownForeign); REQUIRE(report.findings.front().evidence.correlation_kind == "future-frontend-hazard"); } TEST_CASE("finding queries match literal text only in selected fields", "[finding-query][literal]") { const auto report = readNative(fixtureFindings()); fold::FindingQueryOptions query; query.pattern = "api token"; query.ignore_case = true; query.fields = {fold::FindingQueryField::Message}; auto result = fold::queryFindings(report.findings, query); REQUIRE(result.total_matches == 1); REQUIRE(result.findings.front().id == "finding-c"); query.pattern = "auth.cpp"; REQUIRE(fold::queryFindings(report.findings, query).total_matches == 0); query.fields = {fold::FindingQueryField::Path}; REQUIRE(fold::queryFindings(report.findings, query).findings.front().id == "finding-a"); query.pattern = "PTR == NULLPTR"; query.fields = {fold::FindingQueryField::Evidence}; REQUIRE(fold::queryFindings(report.findings, query).findings.front().id == "finding-a"); query.pattern = "pop rdi; ret"; REQUIRE(fold::queryFindings(report.findings, query).findings.front().id == "finding-c"); } TEST_CASE("finding queries apply typed filters, globbing, and pagination", "[finding-query][filters]") { const auto report = readNative(fixtureFindings()); fold::FindingQueryOptions query; query.backend = "source"; query.confidence = "proved"; query.rule = "fold.null-deref"; query.path_glob = "src/*.cpp"; query.baseline_status = "new"; auto filtered = fold::queryFindings(report.findings, query); REQUIRE(filtered.total_matches == 1); REQUIRE(filtered.findings.front().id == "finding-a"); auto pcode = makeFinding("pcode", fold::FindingKind::NullDeref, fold::Confidence::Proved, "binary-pcode-ssa", "bin/app", "sym.parse", "P-Code null dereference"); query = {}; query.backend = "binary"; REQUIRE(fold::queryFindings({pcode}, query).total_matches == 1); query = {}; query.suppression = fold::FindingSuppressionFilter::Suppressed; REQUIRE(fold::queryFindings(report.findings, query).findings.front().id == "finding-c"); query = {}; query.actionable_only = true; REQUIRE(fold::queryFindings(report.findings, query).total_matches == 3); query = {}; query.offset = 1; query.limit = 1; const auto page = fold::queryFindings(report.findings, query); REQUIRE(page.total_input == 4); REQUIRE(page.total_matches == 4); REQUIRE(page.total_actionable_matches == 3); REQUIRE(page.findings.size() == 1); REQUIRE(page.findings.front().id == "finding-b"); REQUIRE(page.next_offset == 2); REQUIRE(page.has_more); REQUIRE(page.truncated); query.pattern = "does-not-exist"; const auto empty = fold::queryFindings(report.findings, query); REQUIRE(empty.total_matches == 0); REQUIRE(empty.findings.empty()); REQUIRE_FALSE(empty.truncated); } TEST_CASE("finding report reader rejects malformed or unsupported reports", "[finding-query][reader]") { std::istringstream malformed("{not-json"); REQUIRE_THROWS_AS(fold::readFindingReport(malformed, "broken.json"), fold::FindingReportError); std::istringstream unsupported(R"({"output_version":2,"findings":[]})"); REQUIRE_THROWS_AS(fold::readFindingReport(unsupported, "future.json"), fold::FindingReportError); std::istringstream wrong_shape(R"({"output_version":1,"findings":{}})"); REQUIRE_THROWS_AS(fold::readFindingReport(wrong_shape, "wrong.json"), fold::FindingReportError); std::istringstream malformed_sarif(R"({"version":"2.1.0","runs":{}})"); REQUIRE_THROWS_AS(fold::readFindingReport(malformed_sarif, "wrong.sarif"), fold::FindingReportError); std::istringstream trailing(R"({"output_version":1,"findings":[]} trailing)"); REQUIRE_THROWS_AS(fold::readFindingReport(trailing, "trailing.json"), fold::FindingReportError); std::ostringstream valid_sarif; fold::writeSarif(valid_sarif, {fixtureFindings().front()}); auto wrong_message = nlohmann::json::parse(valid_sarif.str()); wrong_message["runs"][0]["results"][0]["message"] = "wrong-shape"; std::istringstream wrong_message_input(wrong_message.dump()); REQUIRE_THROWS_AS(fold::readFindingReport(wrong_message_input, "wrong-message.sarif"), fold::FindingReportError); auto wrong_location = nlohmann::json::parse(valid_sarif.str()); wrong_location["runs"][0]["results"][0]["locations"][0] = nlohmann::json::object(); std::istringstream wrong_location_input(wrong_location.dump()); REQUIRE_THROWS_AS(fold::readFindingReport(wrong_location_input, "wrong-location.sarif"), fold::FindingReportError); }