From eec7b917023b03d982116f403992bf78abae51d4 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 11:45:46 -0700 Subject: [PATCH] Step 600: add security exception review board --- editor/CMakeLists.txt | 9 ++ editor/src/SecurityExceptionReviewBoard.h | 86 +++++++++++++ editor/tests/step600_test.cpp | 146 ++++++++++++++++++++++ progress.md | 32 +++++ 4 files changed, 273 insertions(+) create mode 100644 editor/src/SecurityExceptionReviewBoard.h create mode 100644 editor/tests/step600_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 6561be3..6f02596 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4279,4 +4279,13 @@ target_link_libraries(step599_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step600_test tests/step600_test.cpp) +target_include_directories(step600_test PRIVATE src) +target_link_libraries(step600_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/SecurityExceptionReviewBoard.h b/editor/src/SecurityExceptionReviewBoard.h new file mode 100644 index 0000000..a599908 --- /dev/null +++ b/editor/src/SecurityExceptionReviewBoard.h @@ -0,0 +1,86 @@ +#pragma once +// Step 600: Security Exception Review Board + +#include +#include +#include + +enum class ExceptionDecision { + Pending, + Approved, + Rejected +}; + +struct SecurityExceptionCase { + std::string caseId; + std::string operation; + std::string requester; + std::string justification; + ExceptionDecision decision = ExceptionDecision::Pending; + std::string reviewer; +}; + +class SecurityExceptionReviewBoard { +public: + bool submit(const std::string& caseId, + const std::string& operation, + const std::string& requester, + const std::string& justification, + std::string* error) { + if (!error) return false; + error->clear(); + if (caseId.empty()) return fail(error, "case_id_missing"); + if (operation.empty()) return fail(error, "operation_missing"); + if (requester.empty()) return fail(error, "requester_missing"); + if (justification.empty()) return fail(error, "justification_missing"); + if (cases_.count(caseId) != 0) return fail(error, "case_duplicate"); + cases_[caseId] = {caseId, operation, requester, justification, ExceptionDecision::Pending, ""}; + order_.push_back(caseId); + return true; + } + + bool decide(const std::string& caseId, + ExceptionDecision decision, + const std::string& reviewer, + std::string* error) { + if (!error) return false; + error->clear(); + auto it = cases_.find(caseId); + if (it == cases_.end()) return fail(error, "case_missing"); + if (reviewer.empty()) return fail(error, "reviewer_missing"); + if (decision == ExceptionDecision::Pending) return fail(error, "decision_invalid"); + it->second.decision = decision; + it->second.reviewer = reviewer; + return true; + } + + std::vector pending() const { + std::vector out; + for (const auto& id : order_) { + const auto& c = cases_.at(id); + if (c.decision == ExceptionDecision::Pending) out.push_back(c); + } + return out; + } + + int approvedCount() const { + int n = 0; + for (const auto& id : order_) if (cases_.at(id).decision == ExceptionDecision::Approved) ++n; + return n; + } + + int rejectedCount() const { + int n = 0; + for (const auto& id : order_) if (cases_.at(id).decision == ExceptionDecision::Rejected) ++n; + return n; + } + +private: + std::map cases_; + std::vector order_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } +}; diff --git a/editor/tests/step600_test.cpp b/editor/tests/step600_test.cpp new file mode 100644 index 0000000..4dc47fd --- /dev/null +++ b/editor/tests/step600_test.cpp @@ -0,0 +1,146 @@ +// Step 600: Security Exception Review Board (12 tests) + +#include "SecurityExceptionReviewBoard.h" + +#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 {} + +void test_submit_success() { + TEST(submit_success); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "rm -rf /tmp/x", "alice", "urgent rollback", &error), "submit should succeed"); + CHECK(b.pending().size() == 1, "pending size mismatch"); + PASS(); +} + +void test_submit_rejects_missing_case_id() { + TEST(submit_rejects_missing_case_id); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(!b.submit("", "op", "alice", "why", &error), "submit should fail"); + CHECK(error == "case_id_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_missing_operation() { + TEST(submit_rejects_missing_operation); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(!b.submit("c1", "", "alice", "why", &error), "submit should fail"); + CHECK(error == "operation_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_missing_requester() { + TEST(submit_rejects_missing_requester); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(!b.submit("c1", "op", "", "why", &error), "submit should fail"); + CHECK(error == "requester_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_missing_justification() { + TEST(submit_rejects_missing_justification); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(!b.submit("c1", "op", "alice", "", &error), "submit should fail"); + CHECK(error == "justification_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_duplicate_case() { + TEST(submit_rejects_duplicate_case); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "op", "alice", "why", &error), "submit failed"); + CHECK(!b.submit("c1", "op", "alice", "why", &error), "duplicate should fail"); + CHECK(error == "case_duplicate", "wrong error"); + PASS(); +} + +void test_decide_approve_success() { + TEST(decide_approve_success); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "op", "alice", "why", &error), "submit failed"); + CHECK(b.decide("c1", ExceptionDecision::Approved, "sec-admin", &error), "decide should succeed"); + CHECK(b.approvedCount() == 1, "approved count mismatch"); + PASS(); +} + +void test_decide_reject_success() { + TEST(decide_reject_success); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "op", "alice", "why", &error), "submit failed"); + CHECK(b.decide("c1", ExceptionDecision::Rejected, "sec-admin", &error), "decide should succeed"); + CHECK(b.rejectedCount() == 1, "rejected count mismatch"); + PASS(); +} + +void test_decide_fails_for_missing_case() { + TEST(decide_fails_for_missing_case); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(!b.decide("missing", ExceptionDecision::Approved, "sec-admin", &error), "decide should fail"); + CHECK(error == "case_missing", "wrong error"); + PASS(); +} + +void test_decide_fails_for_missing_reviewer() { + TEST(decide_fails_for_missing_reviewer); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "op", "alice", "why", &error), "submit failed"); + CHECK(!b.decide("c1", ExceptionDecision::Approved, "", &error), "decide should fail"); + CHECK(error == "reviewer_missing", "wrong error"); + PASS(); +} + +void test_decide_fails_for_pending_decision() { + TEST(decide_fails_for_pending_decision); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "op", "alice", "why", &error), "submit failed"); + CHECK(!b.decide("c1", ExceptionDecision::Pending, "sec-admin", &error), "decide should fail"); + CHECK(error == "decision_invalid", "wrong error"); + PASS(); +} + +void test_pending_filters_closed_cases() { + TEST(pending_filters_closed_cases); + SecurityExceptionReviewBoard b; + std::string error; + CHECK(b.submit("c1", "op", "alice", "why", &error), "submit c1 failed"); + CHECK(b.submit("c2", "op", "bob", "why", &error), "submit c2 failed"); + CHECK(b.decide("c1", ExceptionDecision::Approved, "sec-admin", &error), "decide failed"); + CHECK(b.pending().size() == 1, "one pending expected"); + PASS(); +} + +int main() { + std::cout << "Step 600: Security Exception Review Board\n"; + + test_submit_success(); // 1 + test_submit_rejects_missing_case_id(); // 2 + test_submit_rejects_missing_operation(); // 3 + test_submit_rejects_missing_requester(); // 4 + test_submit_rejects_missing_justification(); // 5 + test_submit_rejects_duplicate_case(); // 6 + test_decide_approve_success(); // 7 + test_decide_reject_success(); // 8 + test_decide_fails_for_missing_case(); // 9 + test_decide_fails_for_missing_reviewer(); // 10 + test_decide_fails_for_pending_decision(); // 11 + test_pending_filters_closed_cases(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 181816e..ec8d829 100644 --- a/progress.md +++ b/progress.md @@ -11542,3 +11542,35 @@ completion tracking, and close-readiness validation. - `editor/src/IncidentResponseRunbookEngine.h` within header-size limit (`106` <= `600`) - `editor/tests/step599_test.cpp` within test-file size guidance (`179` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 600: Security Exception Review Board +**Status:** PASS (12/12 tests) + +Implements security exception case intake and decision flow for guarded +operations requiring explicit reviewer adjudication. + +**Files added:** +- `editor/src/SecurityExceptionReviewBoard.h` - exception review module: + - case submission with validation and duplicate guards + - approve/reject decision lifecycle with reviewer attribution + - pending-case filtering + - approved/rejected count helpers +- `editor/tests/step600_test.cpp` - 12 tests covering: + - submission success/failure behavior + - decision success/failure behavior + - reviewer/decision validation behavior + - pending/approved/rejected state behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step600_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step600_test step599_test` - PASS +- `./editor/build-native/step600_test` - PASS (12/12) +- `./editor/build-native/step599_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/SecurityExceptionReviewBoard.h` within header-size limit (`86` <= `600`) +- `editor/tests/step600_test.cpp` within test-file size guidance (`146` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`