Complete Step 489: secure by default generator
This commit is contained in:
@@ -3280,4 +3280,13 @@ target_link_libraries(step488_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step489_test tests/step489_test.cpp)
|
||||
target_include_directories(step489_test PRIVATE src)
|
||||
target_link_libraries(step489_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
202
editor/src/SecureByDefaultGenerator.h
Normal file
202
editor/src/SecureByDefaultGenerator.h
Normal file
@@ -0,0 +1,202 @@
|
||||
#pragma once
|
||||
|
||||
// Step 489: Secure-by-Default Code Generation
|
||||
// Emits secure templates and defaults for common risky operations.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class SecureSnippetKind {
|
||||
SqlQuery,
|
||||
InputHandling,
|
||||
FileOperation,
|
||||
NetworkCall,
|
||||
CryptoOperation
|
||||
};
|
||||
|
||||
struct SecureGenerationRequest {
|
||||
std::string language;
|
||||
SecureSnippetKind kind = SecureSnippetKind::InputHandling;
|
||||
std::string operationName;
|
||||
std::string securityRequirement;
|
||||
};
|
||||
|
||||
struct SecureGenerationOutput {
|
||||
std::string code;
|
||||
std::vector<std::string> annotations;
|
||||
std::vector<std::string> notes;
|
||||
bool requiresHumanReview = false;
|
||||
};
|
||||
|
||||
class SecureByDefaultGenerator {
|
||||
public:
|
||||
static SecureGenerationOutput generate(const SecureGenerationRequest& req) {
|
||||
SecureGenerationOutput out;
|
||||
const std::string lang = lower(req.language);
|
||||
|
||||
out.annotations.push_back("@Risk(security)");
|
||||
out.annotations.push_back("@Review(required, human)");
|
||||
if (!req.securityRequirement.empty()) {
|
||||
out.annotations.push_back("@SecurityRequirement(" + req.securityRequirement + ")");
|
||||
}
|
||||
|
||||
switch (req.kind) {
|
||||
case SecureSnippetKind::SqlQuery:
|
||||
out.code = generateSql(lang, req.operationName);
|
||||
out.notes.push_back("Uses parameterized query placeholders");
|
||||
break;
|
||||
case SecureSnippetKind::InputHandling:
|
||||
out.code = generateInputHandling(lang, req.operationName);
|
||||
out.annotations.push_back("@InputValidation(raw)");
|
||||
out.notes.push_back("Input marked raw until explicit sanitization");
|
||||
break;
|
||||
case SecureSnippetKind::FileOperation:
|
||||
out.code = generateFileOperation(lang, req.operationName);
|
||||
out.notes.push_back("Path traversal checks included");
|
||||
break;
|
||||
case SecureSnippetKind::NetworkCall:
|
||||
out.code = generateNetworkCall(lang, req.operationName);
|
||||
out.notes.push_back("Timeout and TLS verification included");
|
||||
break;
|
||||
case SecureSnippetKind::CryptoOperation:
|
||||
out.code = generateCrypto(lang, req.operationName, out.requiresHumanReview);
|
||||
out.annotations.push_back("@Automatability(human)");
|
||||
out.notes.push_back("Crypto generated with modern algorithm defaults");
|
||||
break;
|
||||
}
|
||||
|
||||
if (contains(lower(out.code), "md5") || contains(lower(out.code), "sha1")) {
|
||||
out.requiresHumanReview = true;
|
||||
out.notes.push_back("Weak crypto keyword detected; escalation required");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string lower(const std::string& s) {
|
||||
std::string out = s;
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool contains(const std::string& text, const std::string& needle) {
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
static std::string opName(const std::string& n, const std::string& fallback) {
|
||||
return n.empty() ? fallback : n;
|
||||
}
|
||||
|
||||
static std::string generateSql(const std::string& lang, const std::string& op) {
|
||||
const std::string f = opName(op, "fetchRecord");
|
||||
if (lang == "python") {
|
||||
return "def " + f + "(conn, user_id):\n"
|
||||
" query = \"SELECT * FROM users WHERE id = %s\"\n"
|
||||
" return conn.execute(query, (user_id,)).fetchone()\n";
|
||||
}
|
||||
if (lang == "typescript" || lang == "javascript") {
|
||||
return "export async function " + f + "(db, userId) {\n"
|
||||
" return db.query(\"SELECT * FROM users WHERE id = ?\", [userId]);\n"
|
||||
"}\n";
|
||||
}
|
||||
if (lang == "rust") {
|
||||
return "pub async fn " + f + "(pool: &DbPool, user_id: i64) -> Result<Row, Error> {\n"
|
||||
" sqlx::query(\"SELECT * FROM users WHERE id = $1\").bind(user_id).fetch_one(pool).await\n"
|
||||
"}\n";
|
||||
}
|
||||
return "STUB: use parameterized query for " + f + "\n";
|
||||
}
|
||||
|
||||
static std::string generateInputHandling(const std::string& lang, const std::string& op) {
|
||||
const std::string f = opName(op, "handleInput");
|
||||
if (lang == "python") {
|
||||
return "def " + f + "(raw_input: str):\n"
|
||||
" # raw until sanitized\n"
|
||||
" return raw_input\n";
|
||||
}
|
||||
if (lang == "typescript" || lang == "javascript") {
|
||||
return "export function " + f + "(rawInput: string) {\n"
|
||||
" // raw until sanitized\n"
|
||||
" return rawInput;\n"
|
||||
"}\n";
|
||||
}
|
||||
if (lang == "rust") {
|
||||
return "pub fn " + f + "(raw_input: &str) -> &str {\n"
|
||||
" raw_input\n"
|
||||
"}\n";
|
||||
}
|
||||
return "STUB: treat input as raw until sanitized in " + f + "\n";
|
||||
}
|
||||
|
||||
static std::string generateFileOperation(const std::string& lang, const std::string& op) {
|
||||
const std::string f = opName(op, "readFileSafe");
|
||||
if (lang == "python") {
|
||||
return "def " + f + "(base_dir, candidate):\n"
|
||||
" import os\n"
|
||||
" resolved = os.path.realpath(os.path.join(base_dir, candidate))\n"
|
||||
" if not resolved.startswith(os.path.realpath(base_dir)):\n"
|
||||
" raise ValueError(\"path traversal blocked\")\n"
|
||||
" with open(resolved, \"r\", encoding=\"utf-8\") as fh:\n"
|
||||
" return fh.read()\n";
|
||||
}
|
||||
if (lang == "typescript" || lang == "javascript") {
|
||||
return "export function " + f + "(baseDir, candidate) {\n"
|
||||
" const path = require(\"path\");\n"
|
||||
" const resolved = path.resolve(baseDir, candidate);\n"
|
||||
" if (!resolved.startsWith(path.resolve(baseDir))) throw new Error(\"path traversal blocked\");\n"
|
||||
" return require(\"fs\").readFileSync(resolved, \"utf8\");\n"
|
||||
"}\n";
|
||||
}
|
||||
return "STUB: enforce canonical path prefix check in " + f + "\n";
|
||||
}
|
||||
|
||||
static std::string generateNetworkCall(const std::string& lang, const std::string& op) {
|
||||
const std::string f = opName(op, "fetchSecure");
|
||||
if (lang == "python") {
|
||||
return "def " + f + "(url):\n"
|
||||
" import requests\n"
|
||||
" return requests.get(url, timeout=5, verify=True)\n";
|
||||
}
|
||||
if (lang == "typescript" || lang == "javascript") {
|
||||
return "export async function " + f + "(url) {\n"
|
||||
" const controller = new AbortController();\n"
|
||||
" setTimeout(() => controller.abort(), 5000);\n"
|
||||
" return fetch(url, { signal: controller.signal });\n"
|
||||
"}\n";
|
||||
}
|
||||
if (lang == "rust") {
|
||||
return "pub async fn " + f + "(url: &str) -> Result<Response, reqwest::Error> {\n"
|
||||
" reqwest::Client::builder().use_rustls_tls().timeout(std::time::Duration::from_secs(5)).build()?.get(url).send().await\n"
|
||||
"}\n";
|
||||
}
|
||||
return "STUB: include timeout and TLS verification in " + f + "\n";
|
||||
}
|
||||
|
||||
static std::string generateCrypto(const std::string& lang, const std::string& op,
|
||||
bool& requiresHumanReview) {
|
||||
requiresHumanReview = true;
|
||||
const std::string f = opName(op, "encryptData");
|
||||
if (lang == "python") {
|
||||
return "def " + f + "(plaintext: bytes, key: bytes):\n"
|
||||
" from cryptography.hazmat.primitives.ciphers.aead import AESGCM\n"
|
||||
" nonce = b\"000000000000\"\n"
|
||||
" return AESGCM(key).encrypt(nonce, plaintext, None)\n";
|
||||
}
|
||||
if (lang == "typescript" || lang == "javascript") {
|
||||
return "export function " + f + "(plaintext, key, iv) {\n"
|
||||
" const crypto = require(\"crypto\");\n"
|
||||
" const cipher = crypto.createCipheriv(\"aes-256-gcm\", key, iv);\n"
|
||||
" return Buffer.concat([cipher.update(plaintext), cipher.final()]);\n"
|
||||
"}\n";
|
||||
}
|
||||
if (lang == "rust") {
|
||||
return "pub fn " + f + "(plaintext: &[u8]) {\n"
|
||||
" // STUB: use AES-GCM via audited crate; keep human review in loop\n"
|
||||
"}\n";
|
||||
}
|
||||
return "STUB: use modern AEAD algorithm (AES-GCM/ChaCha20-Poly1305) in " + f + "\n";
|
||||
}
|
||||
};
|
||||
164
editor/tests/step489_test.cpp
Normal file
164
editor/tests/step489_test.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
// Step 489: Secure-by-Default Code Generation Tests (12 tests)
|
||||
|
||||
#include "SecureByDefaultGenerator.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static SecureGenerationOutput run(const std::string& lang, SecureSnippetKind kind,
|
||||
const std::string& op = "") {
|
||||
SecureGenerationRequest r;
|
||||
r.language = lang;
|
||||
r.kind = kind;
|
||||
r.operationName = op;
|
||||
r.securityRequirement = "default";
|
||||
return SecureByDefaultGenerator::generate(r);
|
||||
}
|
||||
|
||||
void test_sql_generation_uses_parameterized_queries_not_concat() {
|
||||
TEST(sql_generation_uses_parameterized_queries_not_concat);
|
||||
auto out = run("python", SecureSnippetKind::SqlQuery, "getUser");
|
||||
CHECK(out.code.find("%s") != std::string::npos, "expected parameter placeholder");
|
||||
CHECK(out.code.find("+") == std::string::npos, "should not use string concatenation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_input_handling_marks_raw_until_sanitized() {
|
||||
TEST(input_handling_marks_raw_until_sanitized);
|
||||
auto out = run("typescript", SecureSnippetKind::InputHandling, "processInput");
|
||||
bool foundRaw = false;
|
||||
for (const auto& a : out.annotations) {
|
||||
if (a == "@InputValidation(raw)") foundRaw = true;
|
||||
}
|
||||
CHECK(foundRaw, "expected @InputValidation(raw)");
|
||||
CHECK(out.code.find("raw until sanitized") != std::string::npos, "missing raw comment");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_file_operations_include_path_traversal_guard() {
|
||||
TEST(file_operations_include_path_traversal_guard);
|
||||
auto out = run("python", SecureSnippetKind::FileOperation, "readSafe");
|
||||
CHECK(out.code.find("realpath") != std::string::npos, "missing canonicalization");
|
||||
CHECK(out.code.find("path traversal blocked") != std::string::npos, "missing traversal check");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_network_calls_include_timeout_and_tls_verification() {
|
||||
TEST(network_calls_include_timeout_and_tls_verification);
|
||||
auto out = run("python", SecureSnippetKind::NetworkCall, "fetchSecure");
|
||||
CHECK(out.code.find("timeout=5") != std::string::npos, "missing timeout");
|
||||
CHECK(out.code.find("verify=True") != std::string::npos, "missing TLS verify");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_crypto_generation_avoids_md5_and_sha1_defaults() {
|
||||
TEST(crypto_generation_avoids_md5_and_sha1_defaults);
|
||||
auto out = run("python", SecureSnippetKind::CryptoOperation, "encrypt");
|
||||
CHECK(out.code.find("MD5") == std::string::npos, "must not include md5");
|
||||
CHECK(out.code.find("SHA1") == std::string::npos, "must not include sha1");
|
||||
CHECK(out.code.find("AESGCM") != std::string::npos, "expected modern crypto");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_crypto_generation_is_marked_human_review_only() {
|
||||
TEST(crypto_generation_is_marked_human_review_only);
|
||||
auto out = run("rust", SecureSnippetKind::CryptoOperation, "encryptBlob");
|
||||
CHECK(out.requiresHumanReview, "crypto should require human review");
|
||||
bool hasAutom = false;
|
||||
for (const auto& a : out.annotations) if (a == "@Automatability(human)") hasAutom = true;
|
||||
CHECK(hasAutom, "missing @Automatability(human)");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_all_generated_snippets_include_security_risk_and_review_annotations() {
|
||||
TEST(all_generated_snippets_include_security_risk_and_review_annotations);
|
||||
auto a = run("python", SecureSnippetKind::SqlQuery);
|
||||
auto b = run("python", SecureSnippetKind::InputHandling);
|
||||
auto c = run("python", SecureSnippetKind::FileOperation);
|
||||
auto d = run("python", SecureSnippetKind::NetworkCall);
|
||||
auto e = run("python", SecureSnippetKind::CryptoOperation);
|
||||
auto hasBase = [](const SecureGenerationOutput& o) {
|
||||
bool risk = false, review = false;
|
||||
for (const auto& ann : o.annotations) {
|
||||
if (ann == "@Risk(security)") risk = true;
|
||||
if (ann == "@Review(required, human)") review = true;
|
||||
}
|
||||
return risk && review;
|
||||
};
|
||||
CHECK(hasBase(a) && hasBase(b) && hasBase(c) && hasBase(d) && hasBase(e),
|
||||
"missing base security annotations");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_sql_generation_for_rust_uses_bind_pattern() {
|
||||
TEST(sql_generation_for_rust_uses_bind_pattern);
|
||||
auto out = run("rust", SecureSnippetKind::SqlQuery, "fetchOne");
|
||||
CHECK(out.code.find(".bind(") != std::string::npos, "expected bind usage");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_network_generation_for_rust_uses_timeout_and_rustls() {
|
||||
TEST(network_generation_for_rust_uses_timeout_and_rustls);
|
||||
auto out = run("rust", SecureSnippetKind::NetworkCall, "fetch");
|
||||
CHECK(out.code.find("use_rustls_tls") != std::string::npos, "missing rustls");
|
||||
CHECK(out.code.find("from_secs(5)") != std::string::npos, "missing timeout");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_file_operation_for_unknown_lang_falls_back_to_secure_stub() {
|
||||
TEST(file_operation_for_unknown_lang_falls_back_to_secure_stub);
|
||||
auto out = run("lua", SecureSnippetKind::FileOperation, "readSafe");
|
||||
CHECK(out.code.find("canonical path prefix check") != std::string::npos,
|
||||
"missing secure fallback guidance");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_operation_name_is_respected_in_generated_code() {
|
||||
TEST(operation_name_is_respected_in_generated_code);
|
||||
auto out = run("typescript", SecureSnippetKind::InputHandling, "sanitizeLater");
|
||||
CHECK(out.code.find("sanitizeLater") != std::string::npos, "operation name missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_security_requirement_annotation_is_emitted() {
|
||||
TEST(security_requirement_annotation_is_emitted);
|
||||
SecureGenerationRequest r;
|
||||
r.language = "python";
|
||||
r.kind = SecureSnippetKind::SqlQuery;
|
||||
r.operationName = "query";
|
||||
r.securityRequirement = "pci-dss";
|
||||
auto out = SecureByDefaultGenerator::generate(r);
|
||||
bool found = false;
|
||||
for (const auto& a : out.annotations) {
|
||||
if (a == "@SecurityRequirement(pci-dss)") found = true;
|
||||
}
|
||||
CHECK(found, "missing security requirement annotation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 489: Secure-by-Default Code Generation Tests\n";
|
||||
|
||||
test_sql_generation_uses_parameterized_queries_not_concat(); // 1
|
||||
test_input_handling_marks_raw_until_sanitized(); // 2
|
||||
test_file_operations_include_path_traversal_guard(); // 3
|
||||
test_network_calls_include_timeout_and_tls_verification(); // 4
|
||||
test_crypto_generation_avoids_md5_and_sha1_defaults(); // 5
|
||||
test_crypto_generation_is_marked_human_review_only(); // 6
|
||||
test_all_generated_snippets_include_security_risk_and_review_annotations(); // 7
|
||||
test_sql_generation_for_rust_uses_bind_pattern(); // 8
|
||||
test_network_generation_for_rust_uses_timeout_and_rustls(); // 9
|
||||
test_file_operation_for_unknown_lang_falls_back_to_secure_stub(); // 10
|
||||
test_operation_name_is_respected_in_generated_code(); // 11
|
||||
test_security_requirement_annotation_is_emitted(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
44
progress.md
44
progress.md
@@ -7179,3 +7179,47 @@ representations and never silently dropped during transpilation.
|
||||
- `editor/src/SecurityPreservingTranslation.h` within header-size limit (`166` <= `600`)
|
||||
- `editor/tests/step488_test.cpp` within test-file size guidance (`147` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 489: Secure-by-Default Code Generation
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements secure-by-default snippet generation for high-risk operation
|
||||
classes so generated code starts from safe patterns instead of relying on
|
||||
post-hoc hardening.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/SecureByDefaultGenerator.h` — secure generation engine:
|
||||
- `SecureGenerationRequest`, `SecureGenerationOutput`, `SecureSnippetKind`
|
||||
- snippet generators for:
|
||||
- SQL queries (parameterized placeholders / bind APIs)
|
||||
- input handling (`@InputValidation(raw)` baseline)
|
||||
- file operations (path traversal guards)
|
||||
- network calls (timeouts + TLS verification)
|
||||
- crypto operations (modern defaults + mandatory human review)
|
||||
- baseline annotation emission:
|
||||
- `@Risk(security)`
|
||||
- `@Review(required, human)`
|
||||
- optional `@SecurityRequirement(...)`
|
||||
- `@Automatability(human)` for crypto
|
||||
- weak-crypto guard (`md5`/`sha1`) escalation path
|
||||
- `editor/tests/step489_test.cpp` — 12 tests covering:
|
||||
- parameterized SQL enforcement
|
||||
- raw-input annotation semantics
|
||||
- path traversal checks
|
||||
- timeout/TLS requirements
|
||||
- crypto hardening defaults and human-review gating
|
||||
- base security annotation coverage across snippet kinds
|
||||
- language-specific secure path checks (Rust SQL/network)
|
||||
- secure fallback behavior for unknown languages
|
||||
- operation-name propagation and security requirement annotation
|
||||
- `editor/CMakeLists.txt` — `step489_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake --build editor/build-native --target step489_test step488_test` — PASS
|
||||
- `./editor/build-native/step489_test` — PASS (12/12)
|
||||
- `./editor/build-native/step488_test` — PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/SecureByDefaultGenerator.h` within header-size limit (`202` <= `600`)
|
||||
- `editor/tests/step489_test.cpp` within test-file size guidance (`164` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user