Unit + integration coverage for the session's work: incremental project graph/index, pcode encoder, unreachable/dead-branch, equiv/cross-TU, div-by-zero/sign-conversion/int-truncation, use-after-free, quality-precision, scan-router profile, source TU runner, detector plan, and the eval fixtures.
136 lines
5.4 KiB
C++
136 lines
5.4 KiB
C++
// 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 <clang/Tooling/CompilationDatabase.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <nlohmann/json.hpp>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<std::string> writeDb(const fs::path& dir, const std::vector<fs::path>& sources) {
|
|
nlohmann::json db = nlohmann::json::array();
|
|
std::vector<std::string> 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<fold::Finding> scanFixtures(const std::vector<fs::path>& sources,
|
|
const std::vector<std::string>& 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<fold::Finding>& 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));
|
|
}
|