- fail closed on MCP input and compile database policy - add shared scan catalogs, finding queries, and tiered gates
270 lines
10 KiB
C++
270 lines
10 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <nlohmann/json.hpp>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
struct TempDir {
|
|
std::filesystem::path path;
|
|
|
|
TempDir()
|
|
: path(std::filesystem::temp_directory_path() /
|
|
("fold-swift-frontend-" +
|
|
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))) {
|
|
std::filesystem::create_directories(path);
|
|
}
|
|
|
|
~TempDir() { std::filesystem::remove_all(path); }
|
|
};
|
|
|
|
struct SwiftScanResult {
|
|
int exit_code = 0;
|
|
nlohmann::json report;
|
|
std::string log;
|
|
};
|
|
|
|
void writeFile(const std::filesystem::path& path, const std::string& content) {
|
|
std::ofstream(path) << content;
|
|
}
|
|
|
|
void writeExecutable(const std::filesystem::path& path, const std::string& content) {
|
|
writeFile(path, content);
|
|
std::filesystem::permissions(path,
|
|
std::filesystem::perms::owner_read |
|
|
std::filesystem::perms::owner_write |
|
|
std::filesystem::perms::owner_exec,
|
|
std::filesystem::perm_options::replace);
|
|
}
|
|
|
|
std::string readFile(const std::filesystem::path& path) {
|
|
std::ifstream in(path);
|
|
std::ostringstream out;
|
|
out << in.rdbuf();
|
|
return out.str();
|
|
}
|
|
|
|
std::string shellQuote(const std::filesystem::path& path) {
|
|
std::string out = "'";
|
|
for (char c : path.string()) {
|
|
if (c == '\'')
|
|
out += "'\\''";
|
|
else
|
|
out += c;
|
|
}
|
|
out += "'";
|
|
return out;
|
|
}
|
|
|
|
std::filesystem::path locateFoldBinary() {
|
|
if (const char* env = std::getenv("FOLD_BINARY")) {
|
|
if (*env && std::filesystem::exists(env))
|
|
return std::filesystem::canonical(env);
|
|
}
|
|
auto cwd = std::filesystem::current_path();
|
|
for (const auto& rel : {"../../src/cli/fold", "../../../src/cli/fold", "../src/cli/fold"}) {
|
|
auto candidate = cwd / rel;
|
|
if (std::filesystem::exists(candidate))
|
|
return std::filesystem::canonical(candidate);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::filesystem::path locateFoldSwiftBinary() {
|
|
if (const char* env = std::getenv("FOLD_SWIFT_BIN")) {
|
|
if (*env && std::filesystem::exists(env))
|
|
return std::filesystem::canonical(env);
|
|
}
|
|
auto cwd = std::filesystem::current_path();
|
|
for (const auto& rel :
|
|
{"../../tools/fold-swift/fold-swift", "../../../tools/fold-swift/fold-swift",
|
|
"../tools/fold-swift/fold-swift"}) {
|
|
auto candidate = cwd / rel;
|
|
if (std::filesystem::exists(candidate))
|
|
return std::filesystem::canonical(candidate);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool swiftcAvailable() { return std::system("command -v swiftc >/dev/null 2>&1") == 0; }
|
|
|
|
std::optional<SwiftScanResult> runSwiftScan(const std::string& source_text,
|
|
const std::string& detectors = {},
|
|
const std::map<std::string, std::string>& env = {}) {
|
|
const auto fold_bin = locateFoldBinary();
|
|
const auto fold_swift_bin = env.contains("FOLD_SWIFT_BIN")
|
|
? std::filesystem::path(env.at("FOLD_SWIFT_BIN"))
|
|
: locateFoldSwiftBinary();
|
|
const bool need_swiftc = !env.contains("FOLD_SWIFT_BIN");
|
|
if (fold_bin.empty() || fold_swift_bin.empty() || (need_swiftc && !swiftcAvailable())) {
|
|
WARN("fold, fold-swift, or swiftc missing; skipping Swift frontend integration test");
|
|
return std::nullopt;
|
|
}
|
|
|
|
TempDir dir;
|
|
const auto source = dir.path / "input.swift";
|
|
writeFile(source, source_text);
|
|
const auto output = dir.path / "report.json";
|
|
const auto log = dir.path / "scan.log";
|
|
|
|
std::string cmd = "FOLD_SWIFT_BIN=" + shellQuote(fold_swift_bin) + " ";
|
|
for (const auto& [key, value] : env)
|
|
if (key != "FOLD_SWIFT_BIN")
|
|
cmd += key + "=" + shellQuote(value) + " ";
|
|
cmd += shellQuote(fold_bin) + " scan --mode=swift ";
|
|
if (!detectors.empty())
|
|
cmd += "--detectors " + detectors + " ";
|
|
cmd += shellQuote(source) + " --format sarif --output " + shellQuote(output) + " > " +
|
|
shellQuote(log) + " 2>&1";
|
|
|
|
SwiftScanResult result;
|
|
result.exit_code = std::system(cmd.c_str());
|
|
result.log = readFile(log);
|
|
if (std::filesystem::exists(output))
|
|
result.report = nlohmann::json::parse(std::ifstream(output));
|
|
return result;
|
|
}
|
|
|
|
const nlohmann::json& scanResults(const SwiftScanResult& scan) {
|
|
return scan.report.at("runs").at(0).at("results");
|
|
}
|
|
|
|
// Swift source that lowers to SIL with an unsafe-pointer load and an
|
|
// unchecked bit cast, exercised by both the structured and fallback paths.
|
|
const char* kUnsafeSwift = R"swift(
|
|
func rawLoad(_ p: UnsafePointer<UInt8>) -> UInt8 { p.pointee }
|
|
class Box {}
|
|
func castIt(_ b: Box) -> UnsafeRawPointer { unsafeBitCast(b, to: UnsafeRawPointer.self) }
|
|
)swift";
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("swift frontend respects detector selection", "[swift][frontend]") {
|
|
auto scan = runSwiftScan(kUnsafeSwift, "div-by-zero");
|
|
if (!scan)
|
|
return;
|
|
|
|
REQUIRE(scan->exit_code == 0); // no selected detector emitted findings
|
|
REQUIRE(scanResults(*scan).empty());
|
|
}
|
|
|
|
TEST_CASE("swift frontend emits Swift-specific rule IDs", "[swift][frontend]") {
|
|
auto scan = runSwiftScan(kUnsafeSwift);
|
|
if (!scan)
|
|
return;
|
|
|
|
REQUIRE(scan->exit_code != 0); // exit 1: actionable findings
|
|
const auto& results = scanResults(*scan);
|
|
REQUIRE(!results.empty());
|
|
|
|
bool saw_load = false;
|
|
bool saw_cast = false;
|
|
for (const auto& result : results) {
|
|
const auto rule = result.at("ruleId").get<std::string>();
|
|
// Regression: Swift findings must never be relabeled as Rust rules
|
|
// (the swift backend previously mapped every kind to Rust*).
|
|
REQUIRE(rule.find("fold.rust-") != 0);
|
|
REQUIRE(result.at("properties").value("backend", std::string{}) == "swift");
|
|
saw_load = saw_load || rule == "fold.swift-unsafe-load";
|
|
saw_cast = saw_cast || rule == "fold.swift-unsafe-cast";
|
|
}
|
|
REQUIRE(saw_load);
|
|
REQUIRE(saw_cast);
|
|
}
|
|
|
|
TEST_CASE("swift frontend honors the swift-security detector group", "[swift][frontend]") {
|
|
auto scan = runSwiftScan(kUnsafeSwift, "swift-security");
|
|
if (!scan)
|
|
return;
|
|
|
|
REQUIRE(scan->exit_code != 0);
|
|
REQUIRE(!scanResults(*scan).empty());
|
|
}
|
|
|
|
TEST_CASE("swift frontend surfaces compiler failures in the log", "[swift][frontend]") {
|
|
auto scan = runSwiftScan("func broken( {\n", {});
|
|
if (!scan)
|
|
return;
|
|
|
|
REQUIRE(scan->exit_code != 0);
|
|
REQUIRE(scan->log.find("swiftc") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("swift frontend fails closed on nonzero fake frontend exit", "[swift][frontend]") {
|
|
TempDir dir;
|
|
const auto fake = dir.path / "fake-fold-swift.sh";
|
|
writeExecutable(fake, "#!/bin/sh\n"
|
|
"printf '%s\\n' '{\"kind\":\"error\",\"data\":{\"message\":\"synthetic "
|
|
"swift frontend failure\"}}'\n"
|
|
"printf '%s\\n' "
|
|
"'{\"kind\":\"function\",\"data\":{\"qualified_name\":\"demo\",\"source_"
|
|
"file\":\"input.swift\",\"start_line\":1,\"end_line\":1}}'\n"
|
|
"exit 9\n");
|
|
|
|
auto scan = runSwiftScan("func demo() {}\n", {}, {{"FOLD_SWIFT_BIN", fake.string()}});
|
|
if (!scan)
|
|
return;
|
|
|
|
REQUIRE(scan->exit_code != 0);
|
|
REQUIRE(scan->log.find("scan failed:") != std::string::npos);
|
|
REQUIRE(scan->log.find("synthetic swift frontend failure") != std::string::npos);
|
|
REQUIRE(scan->log.find("exit status 9") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("swift frontend fails closed on truncated fake frontend output", "[swift][frontend]") {
|
|
TempDir dir;
|
|
const auto fake = dir.path / "fake-fold-swift-truncate.sh";
|
|
writeExecutable(fake, "#!/bin/sh\n"
|
|
"printf '%s\\n' '{\"kind\":\"error\",\"data\":{\"message\":\"synthetic "
|
|
"swift truncation\"}}'\n"
|
|
"i=0\n"
|
|
"while [ \"$i\" -lt 1000 ]; do\n"
|
|
" printf '%s\\n' "
|
|
"'{\"kind\":\"function\",\"data\":{\"qualified_name\":\"demo\",\"source_"
|
|
"file\":\"input.swift\",\"start_line\":1,\"end_line\":1}}'\n"
|
|
" i=$((i + 1))\n"
|
|
"done\n");
|
|
|
|
auto scan = runSwiftScan(
|
|
"func demo() {}\n", {},
|
|
{{"FOLD_SWIFT_BIN", fake.string()}, {"FOLD_FRONTEND_SUBPROCESS_MAX_OUTPUT_BYTES", "128"}});
|
|
if (!scan)
|
|
return;
|
|
|
|
REQUIRE(scan->exit_code != 0);
|
|
REQUIRE(scan->log.find("scan failed:") != std::string::npos);
|
|
REQUIRE(scan->log.find("output truncated") != std::string::npos);
|
|
REQUIRE(scan->log.find("synthetic swift truncation") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("swift frontend does not execute shell metacharacters in paths", "[swift][frontend]") {
|
|
const auto fold_bin = locateFoldBinary();
|
|
const auto fold_swift_bin = locateFoldSwiftBinary();
|
|
if (fold_bin.empty() || fold_swift_bin.empty() || !swiftcAvailable()) {
|
|
WARN("fold, fold-swift, or swiftc missing; skipping Swift frontend integration test");
|
|
return;
|
|
}
|
|
|
|
TempDir dir;
|
|
// A filename that would inject a shell command if any layer of the
|
|
// scan pipeline shelled out unescaped. The sentinel must never appear.
|
|
const auto sentinel = dir.path / "pwned";
|
|
const auto evil = dir.path / ("evil'; touch " + sentinel.string() + "; echo '.swift");
|
|
writeFile(evil, "func f(_ p: UnsafePointer<UInt8>) -> UInt8 { p.pointee }\n");
|
|
const auto output = dir.path / "report.json";
|
|
const auto log = dir.path / "scan.log";
|
|
|
|
std::string cmd = "FOLD_SWIFT_BIN=" + shellQuote(fold_swift_bin) + " " + shellQuote(fold_bin) +
|
|
" scan --mode=swift " + shellQuote(evil) + " --format sarif --output " +
|
|
shellQuote(output) + " > " + shellQuote(log) + " 2>&1";
|
|
std::system(cmd.c_str());
|
|
|
|
REQUIRE(!std::filesystem::exists(sentinel));
|
|
}
|