go-crypt/trust/audit.go
Claude 7407b89b8d
refactor(ax): AX RFC-025 compliance sweep pass 1
Remove banned imports (fmt, strings, os, errors, path/filepath) across all
production and test files, replace with core.* primitives, coreio.ReadStream,
and coreerr.E. Upgrade dappco.re/go/core v0.5.0 → v0.7.0 for core.PathBase
and core.Is. Fix isRepoScoped to exclude pr.* capabilities (enforcement is at
the forge layer, not the policy engine). Add Good/Bad/Ugly test coverage to
all packages missing the mandatory three-category naming convention.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 08:48:56 +01:00

160 lines
3.6 KiB
Go

package trust
import (
"encoding/json"
goio "io"
"iter"
"sync"
"time"
coreerr "dappco.re/go/core/log"
)
// AuditEntry records a single policy evaluation for compliance.
type AuditEntry struct {
// Timestamp is when the evaluation occurred.
Timestamp time.Time `json:"timestamp"`
// Agent is the name of the agent being evaluated.
Agent string `json:"agent"`
// Cap is the capability that was evaluated.
Cap Capability `json:"capability"`
// Repo is the repo context (empty if not repo-scoped).
Repo string `json:"repo,omitempty"`
// Decision is the evaluation outcome.
Decision Decision `json:"decision"`
// Reason is the human-readable reason for the decision.
Reason string `json:"reason"`
}
// MarshalJSON implements custom JSON encoding for Decision.
func (d Decision) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
// UnmarshalJSON implements custom JSON decoding for Decision.
func (d *Decision) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch s {
case "deny":
*d = Deny
case "allow":
*d = Allow
case "needs_approval":
*d = NeedsApproval
default:
return coreerr.E("trust.Decision.UnmarshalJSON", "unknown decision: "+s, nil)
}
return nil
}
// AuditLog is an append-only log of policy evaluations.
type AuditLog struct {
mu sync.Mutex
entries []AuditEntry
writer goio.Writer
}
// NewAuditLog creates an in-memory audit log. If a writer is provided,
// each entry is also written as a JSON line to that writer (append-only).
//
// auditLog := trust.NewAuditLog(os.Stdout)
// err := auditLog.Record(result, "host-uk/core")
func NewAuditLog(w goio.Writer) *AuditLog {
return &AuditLog{
writer: w,
}
}
// Record appends an evaluation result to the audit log.
func (l *AuditLog) Record(result EvalResult, repo string) error {
entry := AuditEntry{
Timestamp: time.Now(),
Agent: result.Agent,
Cap: result.Cap,
Repo: repo,
Decision: result.Decision,
Reason: result.Reason,
}
l.mu.Lock()
defer l.mu.Unlock()
l.entries = append(l.entries, entry)
if l.writer != nil {
data, err := json.Marshal(entry)
if err != nil {
return coreerr.E("trust.AuditLog.Record", "marshal failed", err)
}
data = append(data, '\n')
if _, err := l.writer.Write(data); err != nil {
return coreerr.E("trust.AuditLog.Record", "write failed", err)
}
}
return nil
}
// Entries returns a snapshot of all audit entries.
func (l *AuditLog) Entries() []AuditEntry {
l.mu.Lock()
defer l.mu.Unlock()
out := make([]AuditEntry, len(l.entries))
copy(out, l.entries)
return out
}
// EntriesSeq returns an iterator over all audit entries.
func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] {
return func(yield func(AuditEntry) bool) {
l.mu.Lock()
defer l.mu.Unlock()
for _, e := range l.entries {
if !yield(e) {
return
}
}
}
}
// Len returns the number of entries in the log.
func (l *AuditLog) Len() int {
l.mu.Lock()
defer l.mu.Unlock()
return len(l.entries)
}
// EntriesFor returns all audit entries for a specific agent.
func (l *AuditLog) EntriesFor(agent string) []AuditEntry {
l.mu.Lock()
defer l.mu.Unlock()
var out []AuditEntry
for _, e := range l.entries {
if e.Agent == agent {
out = append(out, e)
}
}
return out
}
// EntriesForSeq returns an iterator over audit entries for a specific agent.
func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] {
return func(yield func(AuditEntry) bool) {
l.mu.Lock()
defer l.mu.Unlock()
for _, e := range l.entries {
if e.Agent == agent {
if !yield(e) {
return
}
}
}
}
}