322 lines
7.2 KiB
Go
322 lines
7.2 KiB
Go
package audit
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// AuditEvent represents an immutable audit log entry
|
||
|
|
type AuditEvent struct {
|
||
|
|
EventID string `json:"event_id"`
|
||
|
|
EventType string `json:"event_type"` // "planner_decision", "judge_verdict", "implementer_change"
|
||
|
|
WorkflowID string `json:"workflow_id"`
|
||
|
|
TaskID string `json:"task_id"`
|
||
|
|
Actor string `json:"actor"` // "planner", "judge", "implementer"
|
||
|
|
Timestamp time.Time `json:"timestamp"`
|
||
|
|
Action string `json:"action"` // Description of what was decided/done
|
||
|
|
Reasoning string `json:"reasoning"` // Why this decision was made
|
||
|
|
Input map[string]interface{} `json:"input,omitempty"`
|
||
|
|
Output map[string]interface{} `json:"output,omitempty"`
|
||
|
|
Status string `json:"status"` // "success", "failure", "pending"
|
||
|
|
Error string `json:"error,omitempty"`
|
||
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// AuditLogger logs immutable audit events
|
||
|
|
type AuditLogger struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
basePath string
|
||
|
|
logFile string
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewAuditLogger creates a new audit logger
|
||
|
|
func NewAuditLogger(basePath string) *AuditLogger {
|
||
|
|
return &AuditLogger{
|
||
|
|
basePath: basePath,
|
||
|
|
logFile: filepath.Join(basePath, "audit", "audit.jsonl"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// LogEvent logs an audit event (immutable append-only)
|
||
|
|
func (al *AuditLogger) LogEvent(event *AuditEvent) error {
|
||
|
|
if event == nil {
|
||
|
|
return fmt.Errorf("event cannot be nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
al.mu.Lock()
|
||
|
|
defer al.mu.Unlock()
|
||
|
|
|
||
|
|
// Set timestamp if not already set
|
||
|
|
if event.Timestamp.IsZero() {
|
||
|
|
event.Timestamp = time.Now()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate event ID if not set
|
||
|
|
if event.EventID == "" {
|
||
|
|
event.EventID = fmt.Sprintf("%s-%d", event.WorkflowID, event.Timestamp.UnixNano())
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create audit directory if it doesn't exist
|
||
|
|
if err := os.MkdirAll(filepath.Dir(al.logFile), 0755); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Marshal to JSON
|
||
|
|
data, err := json.Marshal(event)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Append to file (immutable log)
|
||
|
|
f, err := os.OpenFile(al.logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
_, err = f.Write(append(data, '\n'))
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// LogPlannerDecision logs a planner decision
|
||
|
|
func (al *AuditLogger) LogPlannerDecision(workflowID, taskID string, decision string, reasoning string, metadata map[string]interface{}) error {
|
||
|
|
event := &AuditEvent{
|
||
|
|
EventType: "planner_decision",
|
||
|
|
WorkflowID: workflowID,
|
||
|
|
TaskID: taskID,
|
||
|
|
Actor: "planner",
|
||
|
|
Timestamp: time.Now(),
|
||
|
|
Action: decision,
|
||
|
|
Reasoning: reasoning,
|
||
|
|
Status: "success",
|
||
|
|
Metadata: metadata,
|
||
|
|
}
|
||
|
|
return al.LogEvent(event)
|
||
|
|
}
|
||
|
|
|
||
|
|
// LogJudgeVerdict logs a judge verdict
|
||
|
|
func (al *AuditLogger) LogJudgeVerdict(workflowID, taskID string, verdict string, reasoning string, metadata map[string]interface{}) error {
|
||
|
|
event := &AuditEvent{
|
||
|
|
EventType: "judge_verdict",
|
||
|
|
WorkflowID: workflowID,
|
||
|
|
TaskID: taskID,
|
||
|
|
Actor: "judge",
|
||
|
|
Timestamp: time.Now(),
|
||
|
|
Action: verdict,
|
||
|
|
Reasoning: reasoning,
|
||
|
|
Status: "success",
|
||
|
|
Metadata: metadata,
|
||
|
|
}
|
||
|
|
return al.LogEvent(event)
|
||
|
|
}
|
||
|
|
|
||
|
|
// LogImplementerChange logs an implementer change
|
||
|
|
func (al *AuditLogger) LogImplementerChange(workflowID, taskID string, changeDesc string, filesModified []string, metadata map[string]interface{}) error {
|
||
|
|
output := map[string]interface{}{
|
||
|
|
"files_modified": filesModified,
|
||
|
|
}
|
||
|
|
|
||
|
|
event := &AuditEvent{
|
||
|
|
EventType: "implementer_change",
|
||
|
|
WorkflowID: workflowID,
|
||
|
|
TaskID: taskID,
|
||
|
|
Actor: "implementer",
|
||
|
|
Timestamp: time.Now(),
|
||
|
|
Action: changeDesc,
|
||
|
|
Output: output,
|
||
|
|
Status: "success",
|
||
|
|
Metadata: metadata,
|
||
|
|
}
|
||
|
|
return al.LogEvent(event)
|
||
|
|
}
|
||
|
|
|
||
|
|
// QueryByTask retrieves all events for a specific task
|
||
|
|
func (al *AuditLogger) QueryByTask(taskID string) ([]*AuditEvent, error) {
|
||
|
|
al.mu.Lock()
|
||
|
|
defer al.mu.Unlock()
|
||
|
|
|
||
|
|
data, err := os.ReadFile(al.logFile)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var events []*AuditEvent
|
||
|
|
var inLine []byte
|
||
|
|
|
||
|
|
for _, ch := range data {
|
||
|
|
if ch == '\n' {
|
||
|
|
if len(inLine) > 0 {
|
||
|
|
var event AuditEvent
|
||
|
|
if err := json.Unmarshal(inLine, &event); err == nil {
|
||
|
|
if event.TaskID == taskID {
|
||
|
|
events = append(events, &event)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inLine = nil
|
||
|
|
} else {
|
||
|
|
inLine = append(inLine, ch)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return events, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// QueryByWorkflow retrieves all events for a specific workflow
|
||
|
|
func (al *AuditLogger) QueryByWorkflow(workflowID string) ([]*AuditEvent, error) {
|
||
|
|
al.mu.Lock()
|
||
|
|
defer al.mu.Unlock()
|
||
|
|
|
||
|
|
data, err := os.ReadFile(al.logFile)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var events []*AuditEvent
|
||
|
|
var inLine []byte
|
||
|
|
|
||
|
|
for _, ch := range data {
|
||
|
|
if ch == '\n' {
|
||
|
|
if len(inLine) > 0 {
|
||
|
|
var event AuditEvent
|
||
|
|
if err := json.Unmarshal(inLine, &event); err == nil {
|
||
|
|
if event.WorkflowID == workflowID {
|
||
|
|
events = append(events, &event)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inLine = nil
|
||
|
|
} else {
|
||
|
|
inLine = append(inLine, ch)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return events, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// QueryByActor retrieves all events by a specific actor
|
||
|
|
func (al *AuditLogger) QueryByActor(actor string) ([]*AuditEvent, error) {
|
||
|
|
al.mu.Lock()
|
||
|
|
defer al.mu.Unlock()
|
||
|
|
|
||
|
|
data, err := os.ReadFile(al.logFile)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var events []*AuditEvent
|
||
|
|
var inLine []byte
|
||
|
|
|
||
|
|
for _, ch := range data {
|
||
|
|
if ch == '\n' {
|
||
|
|
if len(inLine) > 0 {
|
||
|
|
var event AuditEvent
|
||
|
|
if err := json.Unmarshal(inLine, &event); err == nil {
|
||
|
|
if event.Actor == actor {
|
||
|
|
events = append(events, &event)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inLine = nil
|
||
|
|
} else {
|
||
|
|
inLine = append(inLine, ch)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return events, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// QueryByTimeRange retrieves events within a time range
|
||
|
|
func (al *AuditLogger) QueryByTimeRange(start, end time.Time) ([]*AuditEvent, error) {
|
||
|
|
al.mu.Lock()
|
||
|
|
defer al.mu.Unlock()
|
||
|
|
|
||
|
|
data, err := os.ReadFile(al.logFile)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var events []*AuditEvent
|
||
|
|
var inLine []byte
|
||
|
|
|
||
|
|
for _, ch := range data {
|
||
|
|
if ch == '\n' {
|
||
|
|
if len(inLine) > 0 {
|
||
|
|
var event AuditEvent
|
||
|
|
if err := json.Unmarshal(inLine, &event); err == nil {
|
||
|
|
if event.Timestamp.After(start) && event.Timestamp.Before(end) {
|
||
|
|
events = append(events, &event)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inLine = nil
|
||
|
|
} else {
|
||
|
|
inLine = append(inLine, ch)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return events, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetAuditTrail retrieves the full audit trail
|
||
|
|
func (al *AuditLogger) GetAuditTrail() ([]*AuditEvent, error) {
|
||
|
|
al.mu.Lock()
|
||
|
|
defer al.mu.Unlock()
|
||
|
|
|
||
|
|
data, err := os.ReadFile(al.logFile)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var events []*AuditEvent
|
||
|
|
var inLine []byte
|
||
|
|
|
||
|
|
for _, ch := range data {
|
||
|
|
if ch == '\n' {
|
||
|
|
if len(inLine) > 0 {
|
||
|
|
var event AuditEvent
|
||
|
|
if err := json.Unmarshal(inLine, &event); err == nil {
|
||
|
|
events = append(events, &event)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inLine = nil
|
||
|
|
} else {
|
||
|
|
inLine = append(inLine, ch)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return events, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetEventCount returns the total number of audit events
|
||
|
|
func (al *AuditLogger) GetEventCount() (int, error) {
|
||
|
|
events, err := al.GetAuditTrail()
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
return len(events), nil
|
||
|
|
}
|