Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions core/audit/snapshot_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package audit

import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions core/controller/reconciliation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions core/controller/reconciliation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
55 changes: 55 additions & 0 deletions core/estate/assignment_json_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion core/estate/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down