Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/store_audit.go
Sharang Parnerkar 9f96061631 refactor(go): split training/store, ucca/rules, ucca_handlers, document_export under 500 LOC
Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC,
ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group
into same-package files, all under the 500-line hard cap. Zero behavior changes,
no renamed exported symbols. Also fixed pre-existing hazard_library split (missing
functions and duplicate UUID keys from a prior session).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 09:29:54 +02:00

384 lines
11 KiB
Go

package iace
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// ============================================================================
// Tech File Section Operations
// ============================================================================
// CreateTechFileSection creates a new section in the technical file
func (s *Store) CreateTechFileSection(ctx context.Context, projectID uuid.UUID, sectionType, title, content string) (*TechFileSection, error) {
tf := &TechFileSection{
ID: uuid.New(),
ProjectID: projectID,
SectionType: sectionType,
Title: title,
Content: content,
Version: 1,
Status: TechFileSectionStatusDraft,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
_, err := s.pool.Exec(ctx, `
INSERT INTO iace_tech_file_sections (
id, project_id, section_type, title, content,
version, status, approved_by, approved_at, metadata,
created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9, $10,
$11, $12
)
`,
tf.ID, tf.ProjectID, tf.SectionType, tf.Title, tf.Content,
tf.Version, string(tf.Status), uuid.Nil, nil, nil,
tf.CreatedAt, tf.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create tech file section: %w", err)
}
return tf, nil
}
// UpdateTechFileSection updates the content of a tech file section and bumps version
func (s *Store) UpdateTechFileSection(ctx context.Context, id uuid.UUID, content string) error {
_, err := s.pool.Exec(ctx, `
UPDATE iace_tech_file_sections SET
content = $2,
version = version + 1,
status = $3,
updated_at = NOW()
WHERE id = $1
`, id, content, string(TechFileSectionStatusDraft))
if err != nil {
return fmt.Errorf("update tech file section: %w", err)
}
return nil
}
// ApproveTechFileSection marks a tech file section as approved
func (s *Store) ApproveTechFileSection(ctx context.Context, id uuid.UUID, approvedBy string) error {
now := time.Now().UTC()
approvedByUUID, err := uuid.Parse(approvedBy)
if err != nil {
return fmt.Errorf("invalid approved_by UUID: %w", err)
}
_, err = s.pool.Exec(ctx, `
UPDATE iace_tech_file_sections SET
status = $2,
approved_by = $3,
approved_at = $4,
updated_at = $4
WHERE id = $1
`, id, string(TechFileSectionStatusApproved), approvedByUUID, now)
if err != nil {
return fmt.Errorf("approve tech file section: %w", err)
}
return nil
}
// ListTechFileSections lists all tech file sections for a project
func (s *Store) ListTechFileSections(ctx context.Context, projectID uuid.UUID) ([]TechFileSection, error) {
rows, err := s.pool.Query(ctx, `
SELECT
id, project_id, section_type, title, content,
version, status, approved_by, approved_at, metadata,
created_at, updated_at
FROM iace_tech_file_sections WHERE project_id = $1
ORDER BY section_type ASC, created_at ASC
`, projectID)
if err != nil {
return nil, fmt.Errorf("list tech file sections: %w", err)
}
defer rows.Close()
var sections []TechFileSection
for rows.Next() {
var tf TechFileSection
var status string
var metadata []byte
err := rows.Scan(
&tf.ID, &tf.ProjectID, &tf.SectionType, &tf.Title, &tf.Content,
&tf.Version, &status, &tf.ApprovedBy, &tf.ApprovedAt, &metadata,
&tf.CreatedAt, &tf.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("list tech file sections scan: %w", err)
}
tf.Status = TechFileSectionStatus(status)
json.Unmarshal(metadata, &tf.Metadata)
sections = append(sections, tf)
}
return sections, nil
}
// ============================================================================
// Monitoring Event Operations
// ============================================================================
// CreateMonitoringEvent creates a new post-market monitoring event
func (s *Store) CreateMonitoringEvent(ctx context.Context, req CreateMonitoringEventRequest) (*MonitoringEvent, error) {
me := &MonitoringEvent{
ID: uuid.New(),
ProjectID: req.ProjectID,
EventType: req.EventType,
Title: req.Title,
Description: req.Description,
Severity: req.Severity,
Status: "open",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
_, err := s.pool.Exec(ctx, `
INSERT INTO iace_monitoring_events (
id, project_id, event_type, title, description,
severity, impact_assessment, status,
resolved_at, resolved_by, metadata,
created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
$9, $10, $11,
$12, $13
)
`,
me.ID, me.ProjectID, string(me.EventType), me.Title, me.Description,
me.Severity, "", me.Status,
nil, uuid.Nil, nil,
me.CreatedAt, me.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create monitoring event: %w", err)
}
return me, nil
}
// ListMonitoringEvents lists all monitoring events for a project
func (s *Store) ListMonitoringEvents(ctx context.Context, projectID uuid.UUID) ([]MonitoringEvent, error) {
rows, err := s.pool.Query(ctx, `
SELECT
id, project_id, event_type, title, description,
severity, impact_assessment, status,
resolved_at, resolved_by, metadata,
created_at, updated_at
FROM iace_monitoring_events WHERE project_id = $1
ORDER BY created_at DESC
`, projectID)
if err != nil {
return nil, fmt.Errorf("list monitoring events: %w", err)
}
defer rows.Close()
var events []MonitoringEvent
for rows.Next() {
var me MonitoringEvent
var eventType string
var metadata []byte
err := rows.Scan(
&me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description,
&me.Severity, &me.ImpactAssessment, &me.Status,
&me.ResolvedAt, &me.ResolvedBy, &metadata,
&me.CreatedAt, &me.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("list monitoring events scan: %w", err)
}
me.EventType = MonitoringEventType(eventType)
json.Unmarshal(metadata, &me.Metadata)
events = append(events, me)
}
return events, nil
}
// UpdateMonitoringEvent updates a monitoring event with a dynamic set of fields
func (s *Store) UpdateMonitoringEvent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*MonitoringEvent, error) {
if len(updates) == 0 {
return s.getMonitoringEvent(ctx, id)
}
query := "UPDATE iace_monitoring_events SET updated_at = NOW()"
args := []interface{}{id}
argIdx := 2
for key, val := range updates {
switch key {
case "title", "description", "severity", "impact_assessment", "status":
query += fmt.Sprintf(", %s = $%d", key, argIdx)
args = append(args, val)
argIdx++
case "event_type":
query += fmt.Sprintf(", event_type = $%d", argIdx)
args = append(args, val)
argIdx++
case "resolved_at":
query += fmt.Sprintf(", resolved_at = $%d", argIdx)
args = append(args, val)
argIdx++
case "resolved_by":
query += fmt.Sprintf(", resolved_by = $%d", argIdx)
args = append(args, val)
argIdx++
case "metadata":
metaJSON, _ := json.Marshal(val)
query += fmt.Sprintf(", metadata = $%d", argIdx)
args = append(args, metaJSON)
argIdx++
}
}
query += " WHERE id = $1"
_, err := s.pool.Exec(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("update monitoring event: %w", err)
}
return s.getMonitoringEvent(ctx, id)
}
// getMonitoringEvent is a helper to fetch a single monitoring event by ID
func (s *Store) getMonitoringEvent(ctx context.Context, id uuid.UUID) (*MonitoringEvent, error) {
var me MonitoringEvent
var eventType string
var metadata []byte
err := s.pool.QueryRow(ctx, `
SELECT
id, project_id, event_type, title, description,
severity, impact_assessment, status,
resolved_at, resolved_by, metadata,
created_at, updated_at
FROM iace_monitoring_events WHERE id = $1
`, id).Scan(
&me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description,
&me.Severity, &me.ImpactAssessment, &me.Status,
&me.ResolvedAt, &me.ResolvedBy, &metadata,
&me.CreatedAt, &me.UpdatedAt,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get monitoring event: %w", err)
}
me.EventType = MonitoringEventType(eventType)
json.Unmarshal(metadata, &me.Metadata)
return &me, nil
}
// ============================================================================
// Audit Trail Operations
// ============================================================================
// AddAuditEntry adds an immutable audit trail entry
func (s *Store) AddAuditEntry(ctx context.Context, projectID uuid.UUID, entityType string, entityID uuid.UUID, action AuditAction, userID string, oldValues, newValues json.RawMessage) error {
id := uuid.New()
now := time.Now().UTC()
userUUID, err := uuid.Parse(userID)
if err != nil {
return fmt.Errorf("invalid user_id UUID: %w", err)
}
// Compute a simple hash for integrity: sha256(entityType + entityID + action + timestamp)
hashInput := fmt.Sprintf("%s:%s:%s:%s:%s", projectID, entityType, entityID, string(action), now.Format(time.RFC3339Nano))
// Use a simple deterministic hash representation
hash := fmt.Sprintf("%x", hashInput)
_, err = s.pool.Exec(ctx, `
INSERT INTO iace_audit_trail (
id, project_id, entity_type, entity_id,
action, user_id, old_values, new_values,
hash, created_at
) VALUES (
$1, $2, $3, $4,
$5, $6, $7, $8,
$9, $10
)
`,
id, projectID, entityType, entityID,
string(action), userUUID, oldValues, newValues,
hash, now,
)
if err != nil {
return fmt.Errorf("add audit entry: %w", err)
}
return nil
}
// ListAuditTrail lists all audit trail entries for a project, newest first
func (s *Store) ListAuditTrail(ctx context.Context, projectID uuid.UUID) ([]AuditTrailEntry, error) {
rows, err := s.pool.Query(ctx, `
SELECT
id, project_id, entity_type, entity_id,
action, user_id, old_values, new_values,
hash, created_at
FROM iace_audit_trail WHERE project_id = $1
ORDER BY created_at DESC
`, projectID)
if err != nil {
return nil, fmt.Errorf("list audit trail: %w", err)
}
defer rows.Close()
var entries []AuditTrailEntry
for rows.Next() {
var e AuditTrailEntry
var action string
err := rows.Scan(
&e.ID, &e.ProjectID, &e.EntityType, &e.EntityID,
&action, &e.UserID, &e.OldValues, &e.NewValues,
&e.Hash, &e.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("list audit trail scan: %w", err)
}
e.Action = AuditAction(action)
entries = append(entries, e)
}
return entries, nil
}
// HasAuditEntryForType checks if an audit trail entry exists for the given entity type within a project.
func (s *Store) HasAuditEntryForType(ctx context.Context, projectID uuid.UUID, entityType string) (bool, error) {
var exists bool
err := s.pool.QueryRow(ctx, `
SELECT EXISTS(
SELECT 1 FROM iace_audit_trail
WHERE project_id = $1 AND entity_type = $2
)
`, projectID, entityType).Scan(&exists)
if err != nil {
return false, fmt.Errorf("has audit entry: %w", err)
}
return exists, nil
}