244 lines
11 KiB
C++
244 lines
11 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<fold::Finding> 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<fold::Finding>& 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<fold::Finding>& findings) {
|
|
std::ostringstream encoded;
|
|
fold::writeSarif(encoded, findings);
|
|
std::istringstream input(encoded.str());
|
|
return fold::readFindingReport(input, "fixture.sarif");
|
|
}
|
|
|
|
std::vector<std::string> ids(const std::vector<fold::Finding>& findings) {
|
|
std::vector<std::string> 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<std::string>{"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<std::string>{"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);
|
|
}
|