Add step 602 control attestation registry
This commit is contained in:
@@ -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)
|
||||
|
||||
91
editor/src/ControlAttestationRegistry.h
Normal file
91
editor/src/ControlAttestationRegistry.h
Normal file
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
// Step 602: Control Attestation Registry
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<ControlAttestation> byStatus(AttestationStatus status) const {
|
||||
std::vector<ControlAttestation> 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<std::string, ControlAttestation> items_;
|
||||
std::vector<std::string> order_;
|
||||
|
||||
static bool fail(std::string* error, const char* code) {
|
||||
*error = code;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
157
editor/tests/step602_test.cpp
Normal file
157
editor/tests/step602_test.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// Step 602: Control Attestation Registry (12 tests)
|
||||
|
||||
#include "ControlAttestationRegistry.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
30
progress.md
30
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`
|
||||
|
||||
Reference in New Issue
Block a user