- packages/go/audit: AuditEntry 구조체 및 검증 로직 구현 - agent-task: m-policy-history-audit 작업 아카이브 추가 - agent-roadmap: 정책 이력감시 마일스톤 상태 갱신 - README: 프로젝트 구조 문서 업데이트
253 lines
7.7 KiB
Go
253 lines
7.7 KiB
Go
package audit
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestEventValidateRequiresTypeSourceTimestamp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Empty type should return an error
|
|
e := Event{Source: SourceEdge, Timestamp: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
|
|
err := e.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected error for missing event type")
|
|
}
|
|
if got := err.Error(); got != "audit event type is required" {
|
|
t.Fatalf("expected 'audit event type is required', got %q", got)
|
|
}
|
|
|
|
// Empty source should return an error
|
|
e = Event{Type: TypeExecutionRequested, Timestamp: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
|
|
err = e.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected error for missing source")
|
|
}
|
|
if got := err.Error(); got != "audit event source is required" {
|
|
t.Fatalf("expected 'audit event source is required', got %q", got)
|
|
}
|
|
|
|
// Zero timestamp should return an error
|
|
e = Event{Type: TypeExecutionRequested, Source: SourceNode}
|
|
err = e.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected error for zero timestamp")
|
|
}
|
|
if got := err.Error(); got != "audit event timestamp is required" {
|
|
t.Fatalf("expected 'audit event timestamp is required', got %q", got)
|
|
}
|
|
|
|
// All required fields present should pass
|
|
e = Event{
|
|
Type: TypeExecutionCompleted,
|
|
Source: SourceControlPlane,
|
|
Timestamp: time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
if err := e.Validate(); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEventValidateAcceptsStructuredExecutionAuditEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := time.Date(2026, 6, 1, 15, 30, 0, 0, time.UTC)
|
|
e := Event{
|
|
EventID: "evt-001",
|
|
Type: TypeExecutionPolicyEvaluated,
|
|
Source: SourceEdge,
|
|
ActorID: "user-42",
|
|
RequestID: "req-100",
|
|
RunID: "run-200",
|
|
SessionID: "sess-300",
|
|
EdgeID: "edge-1",
|
|
NodeID: "node-5",
|
|
Adapter: "k8s",
|
|
Target: "deploy/web",
|
|
CredentialRef: MetadataCredentialReference,
|
|
PolicyDecision: DecisionAllow,
|
|
PolicyRule: "rule-default-allow",
|
|
Timestamp: ts,
|
|
Status: "passed",
|
|
Metadata: map[string]string{
|
|
"extra": "value",
|
|
},
|
|
}
|
|
if err := e.Validate(); err != nil {
|
|
t.Fatalf("unexpected validation error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEventValidateAcceptsTerminalAuditEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := time.Date(2026, 6, 2, 10, 0, 0, 0, time.UTC)
|
|
e := Event{
|
|
Type: TypeTerminalSessionClosed,
|
|
Source: SourceNode,
|
|
EventID: "evt-term-01",
|
|
NodeID: "node-abc",
|
|
Timestamp: ts,
|
|
}
|
|
if err := e.Validate(); err != nil {
|
|
t.Fatalf("unexpected validation error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEventValidateAcceptsBootstrapAuditEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := time.Date(2026, 6, 3, 8, 0, 0, 0, time.UTC)
|
|
e := Event{
|
|
Type: TypeBootstrapEnrollmentRequested,
|
|
Source: SourceControlPlane,
|
|
EventID: "evt-boot-01",
|
|
Timestamp: ts,
|
|
}
|
|
if err := e.Validate(); err != nil {
|
|
t.Fatalf("unexpected validation error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCopyMetadataDefensivelyCopiesMap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// nil metadata should return nil
|
|
e := Event{
|
|
Type: TypeExecutionRequested,
|
|
Source: SourceEdge,
|
|
Timestamp: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
}
|
|
if got := e.CopyMetadata(); got != nil {
|
|
t.Fatalf("expected nil for nil metadata, got %v", got)
|
|
}
|
|
|
|
// Non-nil metadata should be defensively copied
|
|
original := map[string]string{
|
|
"foo": "bar",
|
|
"key": "val",
|
|
}
|
|
e.Metadata = original
|
|
copied := e.CopyMetadata()
|
|
|
|
if len(copied) != len(original) {
|
|
t.Fatalf("expected copied map length %d, got %d", len(original), len(copied))
|
|
}
|
|
if copied["foo"] != "bar" {
|
|
t.Fatalf("expected copied['foo'] == 'bar', got %q", copied["foo"])
|
|
}
|
|
if copied["key"] != "val" {
|
|
t.Fatalf("expected copied['key'] == 'val', got %q", copied["key"])
|
|
}
|
|
|
|
// Modifying the copy must not affect the original
|
|
copied["foo"] = "changed"
|
|
if original["foo"] != "bar" {
|
|
t.Fatal("original metadata was mutated by changing the copy")
|
|
}
|
|
|
|
// Modifying the original must not affect the copy
|
|
original["key"] = "changed"
|
|
if copied["key"] != "val" {
|
|
t.Fatal("copied metadata was mutated by changing the original")
|
|
}
|
|
}
|
|
|
|
func TestAuditPolicyDefaultsAvoidRawTerminalTranscript(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// MetadataCredentialReference should not contain raw secret text
|
|
if MetadataCredentialReference == "" {
|
|
t.Fatal("MetadataCredentialReference should not be empty")
|
|
}
|
|
if MetadataRedactionMarker == "" {
|
|
t.Fatal("MetadataRedactionMarker should not be empty")
|
|
}
|
|
|
|
// Neither should contain raw terminal transcript or secret content
|
|
if MetadataCredentialReference != "credential_ref" {
|
|
t.Fatalf("expected 'credential_ref', got %q", MetadataCredentialReference)
|
|
}
|
|
if MetadataRedactionMarker != "redaction_marker" {
|
|
t.Fatalf("expected 'redaction_marker', got %q", MetadataRedactionMarker)
|
|
}
|
|
}
|
|
|
|
func TestRequiredEventTypesMatchMilestoneDraft(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expected := []struct {
|
|
name string
|
|
got EventType
|
|
want string
|
|
}{
|
|
{"TypeExecutionRequested", TypeExecutionRequested, "execution.requested"},
|
|
{"TypeExecutionPolicyEvaluated", TypeExecutionPolicyEvaluated, "execution.policy_evaluated"},
|
|
{"TypeExecutionDispatched", TypeExecutionDispatched, "execution.dispatched"},
|
|
{"TypeExecutionStarted", TypeExecutionStarted, "execution.started"},
|
|
{"TypeExecutionCompleted", TypeExecutionCompleted, "execution.completed"},
|
|
{"TypeExecutionFailed", TypeExecutionFailed, "execution.failed"},
|
|
{"TypeExecutionCanceled", TypeExecutionCanceled, "execution.canceled"},
|
|
|
|
{"TypeTerminalSessionRequested", TypeTerminalSessionRequested, "terminal.session_requested"},
|
|
{"TypeTerminalSessionStarted", TypeTerminalSessionStarted, "terminal.session_started"},
|
|
{"TypeTerminalCommandSubmitted", TypeTerminalCommandSubmitted, "terminal.command_submitted"},
|
|
{"TypeTerminalCommandCompleted", TypeTerminalCommandCompleted, "terminal.command_completed"},
|
|
{"TypeTerminalSessionClosed", TypeTerminalSessionClosed, "terminal.session_closed"},
|
|
|
|
{"TypeBootstrapEnrollmentRequested", TypeBootstrapEnrollmentRequested, "bootstrap.enrollment_requested"},
|
|
{"TypeBootstrapCredentialIssued", TypeBootstrapCredentialIssued, "bootstrap.credential_issued"},
|
|
{"TypeBootstrapNodeRegistered", TypeBootstrapNodeRegistered, "bootstrap.node_registered"},
|
|
{"TypeBootstrapEnrollmentFailed", TypeBootstrapEnrollmentFailed, "bootstrap.enrollment_failed"},
|
|
{"TypeBootstrapRevoked", TypeBootstrapRevoked, "bootstrap.revoked"},
|
|
}
|
|
|
|
if len(expected) != 17 {
|
|
t.Fatalf("expected 17 event type constants, got %d", len(expected))
|
|
}
|
|
|
|
seen := map[string]string{}
|
|
for _, tt := range expected {
|
|
if string(tt.got) != tt.want {
|
|
t.Fatalf("%s = %q, want %q", tt.name, tt.got, tt.want)
|
|
}
|
|
if previous, ok := seen[tt.want]; ok {
|
|
t.Fatalf("duplicate event type value %q for %s and %s", tt.want, previous, tt.name)
|
|
}
|
|
seen[tt.want] = tt.name
|
|
}
|
|
}
|
|
|
|
func TestSourceConstants(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expectedSources := []Source{SourceControlPlane, SourceEdge, SourceNode}
|
|
for _, s := range expectedSources {
|
|
if string(s) == "" {
|
|
t.Fatalf("source constant resolved to empty string: %v", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPolicyDecisionConstants(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expectedDecisions := []PolicyDecision{DecisionAllow, DecisionDeny, DecisionWarn}
|
|
for _, d := range expectedDecisions {
|
|
if string(d) == "" {
|
|
t.Fatalf("policy decision constant resolved to empty string: %v", d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRetentionDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if DefaultEdgeLocalRetentionDays != 30 {
|
|
t.Fatalf("expected DefaultEdgeLocalRetentionDays == 30, got %d", DefaultEdgeLocalRetentionDays)
|
|
}
|
|
if DefaultControlPlaneMetadataRetentionDays != 90 {
|
|
t.Fatalf("expected DefaultControlPlaneMetadataRetentionDays == 90, got %d", DefaultControlPlaneMetadataRetentionDays)
|
|
}
|
|
}
|