// End-to-end SAST coverage for the security detectors: run the scan runner over // the committed source fixtures and assert proved findings on the vulnerable // cases AND a clean negative on a guarded case (no false positive). // // Note: null_guarded.cpp is intentionally NOT used as a negative — its own // comment documents that the current first-cut null detector flags it (a known // FP a later milestone will fix), so div_guarded.cpp is the true-negative case. #include #include #include #include #include #include #include #include #include #include "fold/cli/scan_options.hpp" #include "fold/cli/scan_runner.hpp" #include "fold/frontend/comp_db.hpp" #include "fold/report/finding.hpp" namespace { namespace fs = std::filesystem; // Locate tests/fixtures/security — via env (set by meson) or a path probe. fs::path securityFixturesDir() { if (const char* env = std::getenv("FOLD_SECURITY_FIXTURES"); env != nullptr && *env != '\0') return fs::path(env); for (const fs::path& cand : {fs::path("../fixtures/security"), fs::path("tests/fixtures/security"), fs::current_path() / "../fixtures/security"}) if (fs::exists(cand / "div_const_zero.cpp")) return cand; return fs::path("../fixtures/security"); } struct TempDir { fs::path path; TempDir() : path(fs::temp_directory_path() / ("fold-sast-e2e-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))) { fs::create_directories(path); } ~TempDir() { fs::remove_all(path); } }; // Write a compile_commands.json (in `dir`) referencing the given absolute source // paths, and return them for the shard runner. std::vector writeDb(const fs::path& dir, const std::vector& sources) { nlohmann::json db = nlohmann::json::array(); std::vector files; for (const auto& src : sources) { db.push_back({{"directory", dir.string()}, {"command", "clang++ -std=c++20 -c " + src.string()}, {"file", src.string()}}); files.push_back(src.string()); } std::ofstream(dir / "compile_commands.json") << db.dump(); return files; } std::vector scanFixtures(const std::vector& sources, const std::vector& detectors) { TempDir dir; auto files = writeDb(dir.path, sources); std::string err; auto db = fold::loadCompilationDatabase(dir.path.string(), err); REQUIRE(db); fold::cli::ScanOptions opts; opts.compile_db = dir.path.string(); opts.detectors = detectors; return fold::cli::runDetectorsOnShard(*db, opts, files, /*include_clone_clustering=*/false) .findings; } bool hasKind(const std::vector& fs, fold::FindingKind k) { for (const auto& f : fs) if (f.kind == k) return true; return false; } } // namespace TEST_CASE("sast e2e: security detectors prove div-by-zero and null-deref", "[e2e][sast][security]") { const auto dir = securityFixturesDir(); REQUIRE(fs::exists(dir / "div_const_zero.cpp")); const auto findings = scanFixtures({dir / "div_const_zero.cpp", dir / "null_unconditional.cpp"}, {"div-by-zero", "null-deref"}); REQUIRE(hasKind(findings, fold::FindingKind::DivByZero)); // x / 0 REQUIRE(hasKind(findings, fold::FindingKind::NullDeref)); // *(int*)nullptr } TEST_CASE("sast e2e: full security group preserves composite observer findings", "[e2e][sast][security][composite]") { const auto dir = securityFixturesDir(); REQUIRE(fs::exists(dir / "null_unconditional.cpp")); // The security group currently installs more observers than the original // fixed composite-run reserve. Growing that vector after observer creation // used to invalidate their findings/stats references and silently lose the // null-deref query. const auto findings = scanFixtures({dir / "null_unconditional.cpp"}, {"security"}); REQUIRE(hasKind(findings, fold::FindingKind::NullDeref)); } TEST_CASE("sast e2e: a guarded divisor is not flagged (no false positive)", "[e2e][sast][security]") { const auto dir = securityFixturesDir(); REQUIRE(fs::exists(dir / "div_guarded.cpp")); const auto findings = scanFixtures({dir / "div_guarded.cpp"}, {"div-by-zero"}); REQUIRE_FALSE(hasKind(findings, fold::FindingKind::DivByZero)); // path predicate rules out y==0 } TEST_CASE("sast e2e: sysconf page-size wrappers are not flagged as div-by-zero", "[e2e][sast][security][sysconf]") { const auto dir = securityFixturesDir(); REQUIRE(fs::exists(dir / "sysconf_pagesize_safe.cpp")); const auto findings = scanFixtures({dir / "sysconf_pagesize_safe.cpp"}, {"div-by-zero"}); REQUIRE_FALSE(hasKind(findings, fold::FindingKind::DivByZero)); } TEST_CASE("sast e2e: strtol endptr writes are not flagged as null-deref", "[e2e][sast][security][strtol]") { const auto dir = securityFixturesDir(); REQUIRE(fs::exists(dir / "strtol_endptr_safe.cpp")); const auto findings = scanFixtures({dir / "strtol_endptr_safe.cpp"}, {"null-deref"}); REQUIRE_FALSE(hasKind(findings, fold::FindingKind::NullDeref)); }