From c4e3ecf61f47c6847b61da5527a8c8b66a12c38b Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 11:52:06 -0700 Subject: [PATCH] Add step 602 control attestation registry --- editor/CMakeLists.txt | 9 ++ editor/src/ControlAttestationRegistry.h | 91 ++++++++++++++ editor/tests/step602_test.cpp | 157 ++++++++++++++++++++++++ progress.md | 30 +++++ 4 files changed, 287 insertions(+) create mode 100644 editor/src/ControlAttestationRegistry.h create mode 100644 editor/tests/step602_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 71abe67..e2fc7a0 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4297,4 +4297,13 @@ target_link_libraries(step601_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step602_test tests/step602_test.cpp) +target_include_directories(step602_test PRIVATE src) +target_link_libraries(step602_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/ControlAttestationRegistry.h b/editor/src/ControlAttestationRegistry.h new file mode 100644 index 0000000..353fd1c --- /dev/null +++ b/editor/src/ControlAttestationRegistry.h @@ -0,0 +1,91 @@ +#pragma once +// Step 602: Control Attestation Registry + +#include +#include +#include + +enum class AttestationStatus { + Draft, + Signed, + Expired +}; + +struct ControlAttestation { + std::string attestationId; + std::string controlId; + std::string owner; + int daysUntilExpiry = 0; + AttestationStatus status = AttestationStatus::Draft; + std::string signer; +}; + +class ControlAttestationRegistry { +public: + bool submit(const ControlAttestation& attestation, std::string* error) { + if (!error) return false; + error->clear(); + if (attestation.attestationId.empty()) return fail(error, "attestation_id_missing"); + if (attestation.controlId.empty()) return fail(error, "control_id_missing"); + if (attestation.owner.empty()) return fail(error, "owner_missing"); + if (attestation.daysUntilExpiry < 0) return fail(error, "expiry_days_invalid"); + if (attestation.status == AttestationStatus::Expired) return fail(error, "status_invalid"); + if (items_.count(attestation.attestationId) != 0) return fail(error, "attestation_duplicate"); + items_[attestation.attestationId] = attestation; + order_.push_back(attestation.attestationId); + return true; + } + + bool sign(const std::string& attestationId, + const std::string& signer, + std::string* error) { + if (!error) return false; + error->clear(); + auto it = items_.find(attestationId); + if (it == items_.end()) return fail(error, "attestation_missing"); + if (signer.empty()) return fail(error, "signer_missing"); + if (it->second.status == AttestationStatus::Expired) return fail(error, "attestation_expired"); + it->second.status = AttestationStatus::Signed; + it->second.signer = signer; + return true; + } + + bool expire(const std::string& attestationId, std::string* error) { + if (!error) return false; + error->clear(); + auto it = items_.find(attestationId); + if (it == items_.end()) return fail(error, "attestation_missing"); + it->second.status = AttestationStatus::Expired; + return true; + } + + std::vector byStatus(AttestationStatus status) const { + std::vector out; + for (const auto& id : order_) { + const auto& attestation = items_.at(id); + if (attestation.status == status) out.push_back(attestation); + } + return out; + } + + int expiringWithin(int days) const { + int count = 0; + for (const auto& id : order_) { + const auto& attestation = items_.at(id); + if (attestation.status != AttestationStatus::Expired && + attestation.daysUntilExpiry <= days) { + ++count; + } + } + return count; + } + +private: + std::map items_; + std::vector order_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } +}; diff --git a/editor/tests/step602_test.cpp b/editor/tests/step602_test.cpp new file mode 100644 index 0000000..8fe6bb9 --- /dev/null +++ b/editor/tests/step602_test.cpp @@ -0,0 +1,157 @@ +// Step 602: Control Attestation Registry (12 tests) + +#include "ControlAttestationRegistry.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 {} + +static ControlAttestation attestation(const std::string& id, + const std::string& control, + int days) { + return {id, control, "owner", days, AttestationStatus::Draft, ""}; +} + +void test_submit_success() { + TEST(submit_success); + ControlAttestationRegistry registry; + std::string error; + CHECK(registry.submit(attestation("a1", "ctrl-a", 30), &error), "submit should succeed"); + CHECK(registry.byStatus(AttestationStatus::Draft).size() == 1, "draft count mismatch"); + PASS(); +} + +void test_submit_rejects_missing_attestation_id() { + TEST(submit_rejects_missing_attestation_id); + ControlAttestationRegistry registry; + std::string error; + CHECK(!registry.submit(attestation("", "ctrl-a", 30), &error), "submit should fail"); + CHECK(error == "attestation_id_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_missing_control_id() { + TEST(submit_rejects_missing_control_id); + ControlAttestationRegistry registry; + std::string error; + CHECK(!registry.submit(attestation("a1", "", 30), &error), "submit should fail"); + CHECK(error == "control_id_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_missing_owner() { + TEST(submit_rejects_missing_owner); + ControlAttestationRegistry registry; + std::string error; + auto a = attestation("a1", "ctrl-a", 30); + a.owner.clear(); + CHECK(!registry.submit(a, &error), "submit should fail"); + CHECK(error == "owner_missing", "wrong error"); + PASS(); +} + +void test_submit_rejects_negative_expiry_days() { + TEST(submit_rejects_negative_expiry_days); + ControlAttestationRegistry registry; + std::string error; + CHECK(!registry.submit(attestation("a1", "ctrl-a", -1), &error), "submit should fail"); + CHECK(error == "expiry_days_invalid", "wrong error"); + PASS(); +} + +void test_submit_rejects_expired_status() { + TEST(submit_rejects_expired_status); + ControlAttestationRegistry registry; + std::string error; + auto a = attestation("a1", "ctrl-a", 30); + a.status = AttestationStatus::Expired; + CHECK(!registry.submit(a, &error), "submit should fail"); + CHECK(error == "status_invalid", "wrong error"); + PASS(); +} + +void test_submit_rejects_duplicate() { + TEST(submit_rejects_duplicate); + ControlAttestationRegistry registry; + std::string error; + CHECK(registry.submit(attestation("a1", "ctrl-a", 30), &error), "first submit failed"); + CHECK(!registry.submit(attestation("a1", "ctrl-b", 10), &error), "duplicate should fail"); + CHECK(error == "attestation_duplicate", "wrong error"); + PASS(); +} + +void test_sign_success_sets_signed_status() { + TEST(sign_success_sets_signed_status); + ControlAttestationRegistry registry; + std::string error; + CHECK(registry.submit(attestation("a1", "ctrl-a", 30), &error), "submit failed"); + CHECK(registry.sign("a1", "reviewer", &error), "sign should succeed"); + CHECK(registry.byStatus(AttestationStatus::Signed).size() == 1, "signed count mismatch"); + PASS(); +} + +void test_sign_rejects_missing_attestation() { + TEST(sign_rejects_missing_attestation); + ControlAttestationRegistry registry; + std::string error; + CHECK(!registry.sign("missing", "reviewer", &error), "sign should fail"); + CHECK(error == "attestation_missing", "wrong error"); + PASS(); +} + +void test_sign_rejects_missing_signer() { + TEST(sign_rejects_missing_signer); + ControlAttestationRegistry registry; + std::string error; + CHECK(registry.submit(attestation("a1", "ctrl-a", 30), &error), "submit failed"); + CHECK(!registry.sign("a1", "", &error), "sign should fail"); + CHECK(error == "signer_missing", "wrong error"); + PASS(); +} + +void test_expire_then_sign_fails() { + TEST(expire_then_sign_fails); + ControlAttestationRegistry registry; + std::string error; + CHECK(registry.submit(attestation("a1", "ctrl-a", 30), &error), "submit failed"); + CHECK(registry.expire("a1", &error), "expire failed"); + CHECK(!registry.sign("a1", "reviewer", &error), "sign should fail"); + CHECK(error == "attestation_expired", "wrong error"); + PASS(); +} + +void test_expiring_within_ignores_expired_items() { + TEST(expiring_within_ignores_expired_items); + ControlAttestationRegistry registry; + std::string error; + CHECK(registry.submit(attestation("a1", "ctrl-a", 5), &error), "submit a1 failed"); + CHECK(registry.submit(attestation("a2", "ctrl-a", 20), &error), "submit a2 failed"); + CHECK(registry.submit(attestation("a3", "ctrl-b", 3), &error), "submit a3 failed"); + CHECK(registry.expire("a3", &error), "expire a3 failed"); + CHECK(registry.expiringWithin(10) == 1, "expiring count mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 602: Control Attestation Registry\n"; + + test_submit_success(); // 1 + test_submit_rejects_missing_attestation_id(); // 2 + test_submit_rejects_missing_control_id(); // 3 + test_submit_rejects_missing_owner(); // 4 + test_submit_rejects_negative_expiry_days(); // 5 + test_submit_rejects_expired_status(); // 6 + test_submit_rejects_duplicate(); // 7 + test_sign_success_sets_signed_status(); // 8 + test_sign_rejects_missing_attestation(); // 9 + test_sign_rejects_missing_signer(); // 10 + test_expire_then_sign_fails(); // 11 + test_expiring_within_ignores_expired_items(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 97329db..7750c3f 100644 --- a/progress.md +++ b/progress.md @@ -11605,3 +11605,33 @@ coverage accounting. - `editor/src/ComplianceEvidenceBundle.h` within header-size limit (`57` <= `600`) - `editor/tests/step601_test.cpp` within test-file size guidance (`155` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 602: Control Attestation Registry +**Status:** PASS (12/12 tests) + +Implements a control-attestation registry for tracking attestation lifecycle and +expiry risk for compliance controls. + +**Files added:** +- `editor/src/ControlAttestationRegistry.h` - attestation module: + - attestation submission with validation/duplicate guards + - sign/expire lifecycle transitions with validation + - status filtering and expiry-window risk counting +- `editor/tests/step602_test.cpp` - 12 tests covering: + - submission success/failure behavior + - duplicate/validation and lifecycle transition behavior + - expiry-window counting and expired-item exclusion behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step602_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step602_test step601_test` - PASS +- `./editor/build-native/step602_test` - PASS (12/12) +- `./editor/build-native/step601_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ControlAttestationRegistry.h` within header-size limit (`91` <= `600`) +- `editor/tests/step602_test.cpp` within test-file size guidance (`157` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`