Files
whetstone_DSL/editor/src/SafetyAuditor.h
Bill ad6c1eb1f4 Complete Sprint 20: Legacy Code Ingestion (steps 439-448, 124/124 tests)
Phase 20a delivers legacy analysis, safety audit with CWE mapping,
modernization suggestions with effort routing, and RPC/MCP tools.
Phase 20b adds multi-file migration planning, API boundary preservation,
target-language test generation, and progressive migration execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:22:15 -07:00

290 lines
11 KiB
C++

#pragma once
// Step 439: Safety Audit via Annotations — structured safety analysis
// with CWE mapping and risk-level classification.
#include <algorithm>
#include <cctype>
#include <string>
#include <vector>
struct SafetyFinding {
std::string category; // e.g. "buffer-overflow", "use-after-free"
std::string annotation; // e.g. "@BoundsCheck(unchecked)"
std::string detail;
std::string cwe; // CWE code, e.g. "CWE-120"
int riskLevel = 0; // 0=info, 1=low, 2=medium, 3=high, 4=critical
};
struct FunctionSafetyFinding {
std::string functionName;
std::vector<SafetyFinding> findings;
int maxRisk = 0;
};
struct SafetyReport {
std::string filePath;
std::string language;
int overallRisk = 0; // max risk across all findings
std::vector<SafetyFinding> findings;
std::vector<FunctionSafetyFinding> functionFindings;
bool hasCategory(const std::string& cat) const {
for (const auto& f : findings)
if (f.category == cat) return true;
return false;
}
bool hasCwe(const std::string& code) const {
for (const auto& f : findings)
if (f.cwe == code) return true;
return false;
}
int countByRisk(int level) const {
int n = 0;
for (const auto& f : findings)
if (f.riskLevel == level) ++n;
return n;
}
};
class SafetyAuditor {
public:
static SafetyReport audit(const std::string& source,
const std::string& language,
const std::string& filePath) {
SafetyReport report;
report.filePath = filePath;
report.language = toLower(language);
detectBufferOverflow(source, report);
detectUseAfterFree(source, report);
detectNullDeref(source, report);
detectRaceCondition(source, report);
detectIntegerOverflow(source, report);
report.overallRisk = 0;
for (const auto& f : report.findings)
report.overallRisk = std::max(report.overallRisk, f.riskLevel);
report.functionFindings = buildFunctionFindings(source, report.findings);
return report;
}
private:
// --- Buffer overflow (CWE-120, CWE-676) ---
static void detectBufferOverflow(const std::string& src, SafetyReport& r) {
// Unchecked array indexing without bounds check
if (hasPattern(src, "gets(")) {
r.findings.push_back({
"buffer-overflow", "@BoundsCheck(unchecked)",
"gets() has no bounds checking — classic buffer overflow",
"CWE-120", 4
});
}
if (hasPattern(src, "strcpy(")) {
r.findings.push_back({
"buffer-overflow", "@BoundsCheck(unchecked)",
"strcpy() does not check destination buffer size",
"CWE-120", 3
});
}
if (hasPattern(src, "sprintf(")) {
r.findings.push_back({
"buffer-overflow", "@BoundsCheck(unchecked)",
"sprintf() does not check buffer bounds",
"CWE-120", 3
});
}
if (hasPattern(src, "strcat(")) {
r.findings.push_back({
"buffer-overflow", "@BoundsCheck(unchecked)",
"strcat() does not check destination capacity",
"CWE-120", 3
});
}
// Array access with no visible bounds check
if (hasUncheckedArrayAccess(src)) {
r.findings.push_back({
"buffer-overflow", "@BoundsCheck(unchecked)",
"array access without visible bounds check",
"CWE-787", 2
});
}
}
// --- Use-after-free (CWE-416) ---
static void detectUseAfterFree(const std::string& src, SafetyReport& r) {
bool hasFree = hasPattern(src, "free(");
bool hasMalloc = hasPattern(src, "malloc(") || hasPattern(src, "calloc(");
bool hasRaii = hasPattern(src, "std::unique_ptr") ||
hasPattern(src, "std::shared_ptr");
if (hasFree && hasMalloc && !hasRaii) {
r.findings.push_back({
"use-after-free", "@Owner(Manual)",
"manual malloc/free without RAII — use-after-free risk",
"CWE-416", 3
});
}
// Dangling pointer after free — simple heuristic: free() followed by usage
if (hasFreeFollowedByDeref(src)) {
r.findings.push_back({
"use-after-free", "@Lifetime(dangling)",
"potential dereference after free()",
"CWE-416", 4
});
}
}
// --- Null dereference (CWE-476) ---
static void detectNullDeref(const std::string& src, SafetyReport& r) {
bool hasNull = hasPattern(src, "NULL") || hasPattern(src, "nullptr");
bool hasDeref = hasPattern(src, "*") || hasPattern(src, "->");
bool hasCheck = hasPattern(src, "!= NULL") || hasPattern(src, "!= nullptr") ||
hasPattern(src, "== NULL") || hasPattern(src, "== nullptr") ||
hasPattern(src, "if (p)") || hasPattern(src, "if(p)");
if (hasNull && hasDeref && !hasCheck) {
r.findings.push_back({
"null-dereference", "@Nullability(nullable)",
"nullable pointer used without null check",
"CWE-476", 3
});
}
// Return value of malloc unchecked
if (hasPattern(src, "malloc(") && !hasPattern(src, "if (") &&
!hasPattern(src, "if(") && !hasPattern(src, "assert(")) {
r.findings.push_back({
"null-dereference", "@Nullability(unchecked-alloc)",
"malloc() return value not checked for NULL",
"CWE-476", 2
});
}
}
// --- Race condition (CWE-362) ---
static void detectRaceCondition(const std::string& src, SafetyReport& r) {
bool hasMutableGlobal = hasGlobalMutableState(src);
bool hasThread = hasPattern(src, "pthread_") || hasPattern(src, "std::thread") ||
hasPattern(src, "CreateThread") || hasPattern(src, "thrd_create");
bool hasSync = hasPattern(src, "mutex") || hasPattern(src, "std::atomic") ||
hasPattern(src, "pthread_mutex") || hasPattern(src, "critical_section") ||
hasPattern(src, "std::lock_guard") || hasPattern(src, "std::unique_lock");
if (hasMutableGlobal && hasThread && !hasSync) {
r.findings.push_back({
"race-condition", "@Sync(none)",
"shared mutable state with threading but no synchronization",
"CWE-362", 3
});
}
if (hasMutableGlobal && !hasSync && !hasThread) {
// Weaker signal — global mutable state without protection
r.findings.push_back({
"race-condition", "@Sync(unprotected-global)",
"global mutable state without synchronization primitives",
"CWE-362", 1
});
}
}
// --- Integer overflow (CWE-190) ---
static void detectIntegerOverflow(const std::string& src, SafetyReport& r) {
bool hasArith = hasPattern(src, " + ") || hasPattern(src, " * ") ||
hasPattern(src, "+=") || hasPattern(src, "*=");
bool hasCheck = hasPattern(src, "INT_MAX") || hasPattern(src, "UINT_MAX") ||
hasPattern(src, "std::numeric_limits") ||
hasPattern(src, "__builtin_add_overflow") ||
hasPattern(src, "__builtin_mul_overflow") ||
hasPattern(src, "SafeInt") || hasPattern(src, "CheckedInt");
bool hasIntType = hasPattern(src, "int ") || hasPattern(src, "size_t ") ||
hasPattern(src, "unsigned ");
if (hasArith && hasIntType && !hasCheck) {
r.findings.push_back({
"integer-overflow", "@Overflow(unchecked)",
"integer arithmetic without overflow check",
"CWE-190", 2
});
}
}
// --- Helpers ---
static bool hasPattern(const std::string& src, const std::string& pat) {
return src.find(pat) != std::string::npos;
}
static std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
return s;
}
static bool hasUncheckedArrayAccess(const std::string& src) {
// Heuristic: `buf[` present but no `if (i <` or `assert` or `size()`
if (src.find('[') == std::string::npos) return false;
// Look for array subscript without bounds checking
bool hasSubscript = false;
for (size_t i = 0; i < src.size(); ++i) {
if (src[i] == '[' && i > 0 && (std::isalnum((unsigned char)src[i-1]) || src[i-1] == '_')) {
hasSubscript = true;
break;
}
}
if (!hasSubscript) return false;
bool hasBoundsCheck = hasPattern(src, ".size()") ||
hasPattern(src, ".length()") ||
hasPattern(src, "< sizeof") ||
hasPattern(src, "assert(");
return !hasBoundsCheck;
}
static bool hasGlobalMutableState(const std::string& src) {
// Heuristic: line starts with a non-const type followed by identifier at file scope
// Simplified: look for `static int`, `static char*`, `int g_`, or global assignments
return hasPattern(src, "static int ") ||
hasPattern(src, "static char ") ||
hasPattern(src, "int g_") ||
hasPattern(src, "volatile ");
}
static bool hasFreeFollowedByDeref(const std::string& src) {
size_t freePos = src.find("free(");
if (freePos == std::string::npos) return false;
// Check for pointer usage after free()
size_t afterFree = src.find(';', freePos);
if (afterFree == std::string::npos) return false;
std::string rest = src.substr(afterFree);
// Look for dereference of the freed pointer
return hasPattern(rest, "->") || hasPattern(rest, "*p");
}
static std::string detectFirstFunctionName(const std::string& source) {
size_t open = source.find('(');
if (open == std::string::npos) return "";
size_t i = open;
while (i > 0 && std::isspace((unsigned char)source[i - 1])) --i;
size_t end = i;
while (i > 0 &&
(std::isalnum((unsigned char)source[i - 1]) || source[i - 1] == '_')) --i;
if (end <= i) return "";
return source.substr(i, end - i);
}
static std::vector<FunctionSafetyFinding> buildFunctionFindings(
const std::string& source,
const std::vector<SafetyFinding>& allFindings) {
std::vector<FunctionSafetyFinding> out;
if (allFindings.empty()) return out;
std::string fn = detectFirstFunctionName(source);
if (fn.empty()) return out;
FunctionSafetyFinding f;
f.functionName = fn;
f.findings = allFindings;
f.maxRisk = 0;
for (const auto& sf : allFindings)
f.maxRisk = std::max(f.maxRisk, sf.riskLevel);
out.push_back(std::move(f));
return out;
}
};