From 80e9808a0c356bf6361cf0160e19d54195770339 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 8 Sep 2026 21:39:10 +0500 Subject: [PATCH] fix(audit): omit false archived so historical snapshots verify Assignment gained a required archived JSON key, so prune remashaled historical signed payloads with archived:false and failed the digest check. Record the audit error string in reconciliation findings. Signed-off-by: rldyourmnd Co-authored-by: Cursor --- core/audit/snapshot_test.go | 31 +++++++++++++++ core/controller/reconciliation.go | 7 +++- core/controller/reconciliation_test.go | 12 ++++++ core/estate/assignment_json_test.go | 55 ++++++++++++++++++++++++++ core/estate/types.go | 2 +- 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 core/estate/assignment_json_test.go diff --git a/core/audit/snapshot_test.go b/core/audit/snapshot_test.go index 931efcb..5f13dc2 100644 --- a/core/audit/snapshot_test.go +++ b/core/audit/snapshot_test.go @@ -1,6 +1,7 @@ package audit import ( + "bytes" "context" "crypto/ed25519" "crypto/rand" @@ -78,6 +79,36 @@ func TestRecorderPrunesOnlyExpiredVerifiedSnapshots(t *testing.T) { } } +func TestRecorderPruneAcceptsSnapshotOmittingFalseArchived(t *testing.T) { + recorder, publicKey := auditRecorder(t) + createdAt := time.Date(2026, 7, 11, 0, 0, 0, 0, time.UTC) + snapshotID, _, err := recorder.Record( + context.Background(), "estate_01KX7PNHB7DFRJ36HK7G12E6PF", + "reconciliation_01KX7BV07RHD6KRA4Z4J0KCHGS", auditResult(), createdAt, + ) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(recorder.Directory, snapshotID+".json") + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(raw, []byte(`"archived"`)) { + t.Fatalf("false archived leaked into signed audit payload: %s", raw) + } + if _, err := loadSnapshot(path, publicKey); err != nil { + t.Fatal(err) + } + if _, _, err := recorder.Record( + context.Background(), "estate_01KX7PNHB7DFRJ36HK7G12E6PF", + "reconciliation_01KX7BV07RHD6KRA4Z4J0KCHGT", auditResult(), + createdAt.Add(time.Minute), + ); err != nil { + t.Fatal(err) + } +} + func auditRecorder(t *testing.T) (*Recorder, string) { t.Helper() publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) diff --git a/core/controller/reconciliation.go b/core/controller/reconciliation.go index 8a274dc..8726651 100644 --- a/core/controller/reconciliation.go +++ b/core/controller/reconciliation.go @@ -123,8 +123,11 @@ func (runner ReconciliationRunner) Run(ctx context.Context) (ReconciliationRunRe if auditErr != nil { result.Findings = append(result.Findings, domain.Finding{ Code: "GDS_AUDIT_SNAPSHOT_FAILED", Severity: domain.SeverityHigh, - Message: "Signed reconciliation audit snapshot could not be created.", - Evidence: map[string]any{"error_type": fmt.Sprintf("%T", auditErr)}, + Message: "Signed reconciliation audit snapshot could not be created.", + Evidence: map[string]any{ + "error_type": fmt.Sprintf("%T", auditErr), + "error": auditErr.Error(), + }, }) status = "failed" for _, installation := range result.Installations { diff --git a/core/controller/reconciliation_test.go b/core/controller/reconciliation_test.go index 83ac945..890877c 100644 --- a/core/controller/reconciliation_test.go +++ b/core/controller/reconciliation_test.go @@ -127,6 +127,18 @@ func TestReconciliationRunnerCannotSucceedWithoutSignedAudit(t *testing.T) { !reconcileResultHasFinding(run.Result, "GDS_AUDIT_SNAPSHOT_FAILED") { t.Fatalf("run=%+v err=%v", run, err) } + if got := auditFindingEvidence(run.Result, "error"); got != "private signing detail" { + t.Fatalf("audit finding error=%q", got) + } +} + +func auditFindingEvidence(result reconciler.Result, key string) any { + for _, finding := range result.Findings { + if finding.Code == "GDS_AUDIT_SNAPSHOT_FAILED" { + return finding.Evidence[key] + } + } + return nil } func reconcileResultHasFinding(result reconciler.Result, code string) bool { diff --git a/core/estate/assignment_json_test.go b/core/estate/assignment_json_test.go new file mode 100644 index 0000000..9ff47de --- /dev/null +++ b/core/estate/assignment_json_test.go @@ -0,0 +1,55 @@ +package estate + +import ( + "bytes" + "encoding/json" + "testing" +) + +func TestAssignmentJSONOmitsFalseArchived(t *testing.T) { + t.Parallel() + raw, err := json.Marshal(Assignment{ + ProviderID: 1, Owner: "example-user", Name: "example", + IdentityState: "unassigned", ManagementMode: "observe-only", + }) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(raw, []byte(`"archived"`)) { + t.Fatalf("false archived must omit the key so historical audit payloads remashal: %s", raw) + } +} + +func TestAssignmentJSONIncludesTrueArchived(t *testing.T) { + t.Parallel() + raw, err := json.Marshal(Assignment{ + ProviderID: 1, Owner: "example-user", Name: "retired", Archived: true, + IdentityState: "unassigned", ManagementMode: "observe-only", + }) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(raw, []byte(`"archived":true`)) { + t.Fatalf("true archived must remain visible: %s", raw) + } +} + +func TestHistoricalAssignmentRemarshalsWithoutArchivedKey(t *testing.T) { + t.Parallel() + historical := []byte( + `{"provider_id":1,"owner":"example-user","name":"example",` + + `"identity_state":"unassigned","management_mode":"observe-only",` + + `"portfolios":null,"policy_profiles":null,"rollout_ring":""}`, + ) + var assignment Assignment + if err := json.Unmarshal(historical, &assignment); err != nil { + t.Fatal(err) + } + fresh, err := json.Marshal(assignment) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(historical, fresh) { + t.Fatalf("historical assignment remarshal changed payload\n historical=%s\n fresh=%s", historical, fresh) + } +} diff --git a/core/estate/types.go b/core/estate/types.go index 0f4a34f..8f51ca4 100644 --- a/core/estate/types.go +++ b/core/estate/types.go @@ -170,7 +170,7 @@ type Assignment struct { ProviderID int64 `json:"provider_id"` Owner string `json:"owner"` Name string `json:"name"` - Archived bool `json:"archived"` + Archived bool `json:"archived,omitempty"` // false must omit: audit prune remashals historical snapshots OwnerID string `json:"owner_id,omitempty"` InstallationID string `json:"installation_id,omitempty"` IdentityState string `json:"identity_state"`