diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2878297..07b9bfe 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3289,4 +3289,13 @@ target_link_libraries(step489_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step490_test tests/step490_test.cpp) +target_include_directories(step490_test PRIVATE src) +target_link_libraries(step490_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) diff --git a/editor/src/ThreatModelIntegration.h b/editor/src/ThreatModelIntegration.h new file mode 100644 index 0000000..6d4d9a7 --- /dev/null +++ b/editor/src/ThreatModelIntegration.h @@ -0,0 +1,126 @@ +#pragma once + +// Step 490: Threat Model Integration +// Models trust boundaries, data flows, and validation diagnostics. + +#include +#include +#include +#include + +struct TrustBoundary { + std::string name; // external-api, database, user-input + std::string level; // high, medium, low +}; + +struct ThreatDataFlow { + std::string fromBoundary; + std::string toBoundary; + bool hasInputValidation = false; + std::string validationKind; // sanitized/raw/escaped +}; + +struct ThreatModelInput { + std::string moduleName; + std::vector boundaries; + std::vector flows; +}; + +struct ThreatDiagnostic { + int code = 0; // E1300 range + std::string message; + std::string severity; + std::string boundaryFrom; + std::string boundaryTo; +}; + +struct ThreatModelOutput { + std::vector diagnostics; + std::vector requiredAnnotations; + std::vector overlayNodes; // for GUI trust overlay + std::vector notes; +}; + +class ThreatModelIntegration { +public: + static ThreatModelOutput analyze(const ThreatModelInput& in) { + ThreatModelOutput out; + out.requiredAnnotations.push_back("@ThreatModel(module=" + in.moduleName + ")"); + + for (const auto& b : in.boundaries) { + out.overlayNodes.push_back(b.name + ":" + b.level); + out.requiredAnnotations.push_back("@TrustBoundary(" + b.name + "," + b.level + ")"); + } + + for (const auto& f : in.flows) { + analyzeFlow(f, out); + } + + if (in.boundaries.empty()) { + out.diagnostics.push_back({1302, "No trust boundaries defined for module", + "medium", "", ""}); + } + if (in.flows.empty()) { + out.notes.push_back("No data flows provided for threat analysis"); + } + out.notes.push_back("Trust boundary overlay data prepared"); + 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(std::tolower(c)); }); + return out; + } + + static bool isExternalBoundary(const std::string& name) { + const std::string n = lower(name); + return n.find("external") != std::string::npos || n.find("user") != std::string::npos; + } + + static void analyzeFlow(const ThreatDataFlow& flow, ThreatModelOutput& out) { + const bool crossing = flow.fromBoundary != flow.toBoundary; + if (crossing) { + out.requiredAnnotations.push_back( + "@DataFlow(" + flow.fromBoundary + "->" + flow.toBoundary + ")"); + } + + const bool highRiskCrossing = crossing && + (isExternalBoundary(flow.fromBoundary) || isExternalBoundary(flow.toBoundary)); + + if (highRiskCrossing && !flow.hasInputValidation) { + out.diagnostics.push_back({ + 1300, + "Missing input validation at trust boundary crossing", + "high", + flow.fromBoundary, + flow.toBoundary + }); + out.requiredAnnotations.push_back("@InputValidation(required)"); + return; + } + + if (flow.hasInputValidation) { + const std::string kind = lower(flow.validationKind); + if (kind.empty() || kind == "raw") { + out.diagnostics.push_back({ + 1301, + "Boundary flow validation exists but remains raw", + "medium", + flow.fromBoundary, + flow.toBoundary + }); + } else if (kind != "sanitized" && kind != "escaped") { + out.diagnostics.push_back({ + 1303, + "Unknown validation kind at trust boundary flow", + "low", + flow.fromBoundary, + flow.toBoundary + }); + } + } + } +}; diff --git a/editor/tests/step490_test.cpp b/editor/tests/step490_test.cpp new file mode 100644 index 0000000..b7c4a71 --- /dev/null +++ b/editor/tests/step490_test.cpp @@ -0,0 +1,167 @@ +// Step 490: Threat Model Integration Tests (12 tests) + +#include "ThreatModelIntegration.h" + +#include +#include + +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 ThreatModelInput baseInput() { + ThreatModelInput in; + in.moduleName = "api"; + in.boundaries = {{"external-api", "high"}, {"database", "medium"}, {"internal", "low"}}; + return in; +} + +void test_threat_model_annotation_emitted_at_module_level() { + TEST(threat_model_annotation_emitted_at_module_level); + auto out = ThreatModelIntegration::analyze(baseInput()); + bool found = false; + for (const auto& a : out.requiredAnnotations) { + if (a.find("@ThreatModel(module=api)") != std::string::npos) found = true; + } + CHECK(found, "missing module threat model annotation"); + PASS(); +} + +void test_trust_boundaries_are_included_in_overlay_data() { + TEST(trust_boundaries_are_included_in_overlay_data); + auto out = ThreatModelIntegration::analyze(baseInput()); + CHECK(out.overlayNodes.size() == 3, "overlay node count mismatch"); + CHECK(out.overlayNodes[0].find("external-api:high") != std::string::npos, "missing boundary node"); + PASS(); +} + +void test_cross_boundary_flow_without_validation_emits_e1300() { + TEST(cross_boundary_flow_without_validation_emits_e1300); + auto in = baseInput(); + in.flows = {{"external-api", "database", false, ""}}; + auto out = ThreatModelIntegration::analyze(in); + CHECK(!out.diagnostics.empty(), "expected diagnostics"); + CHECK(out.diagnostics[0].code == 1300, "expected E1300 diagnostic"); + PASS(); +} + +void test_boundary_flow_with_raw_validation_emits_e1301() { + TEST(boundary_flow_with_raw_validation_emits_e1301); + auto in = baseInput(); + in.flows = {{"external-api", "database", true, "raw"}}; + auto out = ThreatModelIntegration::analyze(in); + CHECK(!out.diagnostics.empty(), "expected diagnostics"); + CHECK(out.diagnostics[0].code == 1301, "expected E1301 diagnostic"); + PASS(); +} + +void test_sanitized_boundary_flow_avoids_validation_diagnostic() { + TEST(sanitized_boundary_flow_avoids_validation_diagnostic); + auto in = baseInput(); + in.flows = {{"external-api", "database", true, "sanitized"}}; + auto out = ThreatModelIntegration::analyze(in); + CHECK(out.diagnostics.empty(), "did not expect diagnostics for sanitized flow"); + PASS(); +} + +void test_unknown_validation_kind_emits_e1303() { + TEST(unknown_validation_kind_emits_e1303); + auto in = baseInput(); + in.flows = {{"external-api", "database", true, "custom"}}; + auto out = ThreatModelIntegration::analyze(in); + CHECK(!out.diagnostics.empty(), "expected diagnostics"); + CHECK(out.diagnostics[0].code == 1303, "expected E1303 diagnostic"); + PASS(); +} + +void test_no_boundaries_emits_e1302() { + TEST(no_boundaries_emits_e1302); + ThreatModelInput in; + in.moduleName = "api"; + in.flows = {{"external-api", "database", false, ""}}; + auto out = ThreatModelIntegration::analyze(in); + bool found = false; + for (const auto& d : out.diagnostics) if (d.code == 1302) found = true; + CHECK(found, "expected E1302 for missing boundaries"); + PASS(); +} + +void test_data_flow_annotation_is_emitted_for_crossing() { + TEST(data_flow_annotation_is_emitted_for_crossing); + auto in = baseInput(); + in.flows = {{"external-api", "database", true, "sanitized"}}; + auto out = ThreatModelIntegration::analyze(in); + bool found = false; + for (const auto& a : out.requiredAnnotations) { + if (a.find("@DataFlow(external-api->database)") != std::string::npos) found = true; + } + CHECK(found, "expected @DataFlow annotation"); + PASS(); +} + +void test_missing_validation_adds_input_validation_required_annotation() { + TEST(missing_validation_adds_input_validation_required_annotation); + auto in = baseInput(); + in.flows = {{"user-input", "internal", false, ""}}; + auto out = ThreatModelIntegration::analyze(in); + bool found = false; + for (const auto& a : out.requiredAnnotations) { + if (a == "@InputValidation(required)") found = true; + } + CHECK(found, "expected @InputValidation(required)"); + PASS(); +} + +void test_internal_same_boundary_flow_does_not_trigger_e1300() { + TEST(internal_same_boundary_flow_does_not_trigger_e1300); + auto in = baseInput(); + in.flows = {{"internal", "internal", false, ""}}; + auto out = ThreatModelIntegration::analyze(in); + for (const auto& d : out.diagnostics) { + CHECK(d.code != 1300, "E1300 should not be emitted for non-crossing flow"); + } + PASS(); +} + +void test_overlay_note_is_present() { + TEST(overlay_note_is_present); + auto out = ThreatModelIntegration::analyze(baseInput()); + CHECK(!out.notes.empty(), "expected notes"); + CHECK(out.notes.back().find("overlay") != std::string::npos, "missing overlay note"); + PASS(); +} + +void test_empty_flows_add_note_but_not_false_positive_diagnostic() { + TEST(empty_flows_add_note_but_not_false_positive_diagnostic); + auto in = baseInput(); + auto out = ThreatModelIntegration::analyze(in); + bool note = false; + for (const auto& n : out.notes) { + if (n.find("No data flows") != std::string::npos) note = true; + } + CHECK(note, "expected no-data-flow note"); + PASS(); +} + +int main() { + std::cout << "Step 490: Threat Model Integration Tests\n"; + + test_threat_model_annotation_emitted_at_module_level(); // 1 + test_trust_boundaries_are_included_in_overlay_data(); // 2 + test_cross_boundary_flow_without_validation_emits_e1300(); // 3 + test_boundary_flow_with_raw_validation_emits_e1301(); // 4 + test_sanitized_boundary_flow_avoids_validation_diagnostic(); // 5 + test_unknown_validation_kind_emits_e1303(); // 6 + test_no_boundaries_emits_e1302(); // 7 + test_data_flow_annotation_is_emitted_for_crossing(); // 8 + test_missing_validation_adds_input_validation_required_annotation(); // 9 + test_internal_same_boundary_flow_does_not_trigger_e1300(); // 10 + test_overlay_note_is_present(); // 11 + test_empty_flows_add_note_but_not_false_positive_diagnostic(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 22733d6..a851fcd 100644 --- a/progress.md +++ b/progress.md @@ -7223,3 +7223,42 @@ post-hoc hardening. - `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` + +### Step 490: Threat Model Integration +**Status:** PASS (12/12 tests) + +Adds threat-model integration primitives that encode trust boundaries and +boundary-crossing data-flow validation requirements, emitting structured +E1300-range diagnostics and overlay-ready graph data. + +**Files added:** +- `editor/src/ThreatModelIntegration.h` — threat model analyzer: + - `TrustBoundary`, `ThreatDataFlow`, `ThreatModelInput` + - `ThreatDiagnostic`, `ThreatModelOutput` + - `analyze(...)` capabilities: + - module-level `@ThreatModel(...)` annotation requirement + - boundary-level `@TrustBoundary(...)` requirements + - cross-boundary `@DataFlow(...)` requirement emission + - missing-validation detection at high-risk crossings (`E1300`) + - raw-validation detection (`E1301`) + - missing-boundary model diagnostic (`E1302`) + - unknown validation kind diagnostic (`E1303`) + - overlay node output for trust-boundary visualization +- `editor/tests/step490_test.cpp` — 12 tests covering: + - annotation emission for module/boundary/flow modeling + - E1300/E1301/E1302/E1303 diagnostic behavior + - sanitized-flow non-error path + - validation-required annotation on missing checks + - non-crossing internal-flow false-positive guard + - overlay and empty-flow note behavior +- `editor/CMakeLists.txt` — `step490_test` target + +**Verification run:** +- `cmake --build editor/build-native --target step490_test step489_test` — PASS +- `./editor/build-native/step490_test` — PASS (12/12) +- `./editor/build-native/step489_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ThreatModelIntegration.h` within header-size limit (`126` <= `600`) +- `editor/tests/step490_test.cpp` within test-file size guidance (`167` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`