151 lines
3.9 KiB
Go
151 lines
3.9 KiB
Go
// Package audit implementa el logging por niveles y el registro de auditoría
|
|
// (agent.log) del agente.
|
|
//
|
|
// Seguridad: todo texto pasa por un redactor que sustituye los secretos
|
|
// registrados (p.ej. API_KEY) por su versión enmascarada. Es una defensa en
|
|
// profundidad: el código no debería intentar loguear secretos, pero si un
|
|
// mensaje de error los incluye por accidente, no llegan al disco.
|
|
package audit
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Level es la severidad de un mensaje.
|
|
type Level int
|
|
|
|
const (
|
|
Debug Level = iota
|
|
Info
|
|
Warn
|
|
Error
|
|
)
|
|
|
|
func (l Level) String() string {
|
|
switch l {
|
|
case Debug:
|
|
return "DEBUG"
|
|
case Info:
|
|
return "INFO"
|
|
case Warn:
|
|
return "WARN"
|
|
default:
|
|
return "ERROR"
|
|
}
|
|
}
|
|
|
|
// Logger escribe mensajes en la consola y en el archivo de auditoría.
|
|
//
|
|
// Consola: WARN y ERROR siempre; DEBUG e INFO solo con --verbose.
|
|
// Archivo: INFO en adelante siempre; DEBUG solo con --verbose.
|
|
type Logger struct {
|
|
mu sync.Mutex
|
|
console io.Writer
|
|
file *os.File
|
|
verbose bool
|
|
replacer *strings.Replacer
|
|
secrets []string
|
|
now func() time.Time
|
|
}
|
|
|
|
// New crea un Logger. Si path está vacío no se escribe archivo de auditoría.
|
|
// El archivo se crea con permisos 0600 porque puede contener rutas y
|
|
// comandos sensibles del sistema administrado.
|
|
func New(console io.Writer, path string, verbose bool) (*Logger, error) {
|
|
l := &Logger{console: console, verbose: verbose, now: time.Now}
|
|
if path != "" {
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open audit log %s: %w", path, err)
|
|
}
|
|
l.file = f
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// AddSecret registra un valor que nunca debe aparecer en claro.
|
|
// Se ignoran valores muy cortos para no destrozar texto normal.
|
|
func (l *Logger) AddSecret(secret, masked string) {
|
|
if len(secret) < 4 {
|
|
return
|
|
}
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.secrets = append(l.secrets, secret, masked)
|
|
l.replacer = strings.NewReplacer(l.secrets...)
|
|
}
|
|
|
|
// Redact aplica el redactor a un texto arbitrario.
|
|
func (l *Logger) Redact(s string) string {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.redactLocked(s)
|
|
}
|
|
|
|
func (l *Logger) redactLocked(s string) string {
|
|
if l.replacer == nil {
|
|
return s
|
|
}
|
|
return l.replacer.Replace(s)
|
|
}
|
|
|
|
// Debugf, Infof, Warnf y Errorf registran mensajes por nivel.
|
|
func (l *Logger) Debugf(format string, args ...any) { l.log(Debug, format, args...) }
|
|
func (l *Logger) Infof(format string, args ...any) { l.log(Info, format, args...) }
|
|
func (l *Logger) Warnf(format string, args ...any) { l.log(Warn, format, args...) }
|
|
func (l *Logger) Errorf(format string, args ...any) { l.log(Error, format, args...) }
|
|
|
|
func (l *Logger) log(level Level, format string, args ...any) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
msg := l.redactLocked(fmt.Sprintf(format, args...))
|
|
ts := l.now().Format(time.RFC3339)
|
|
|
|
if level >= Warn || l.verbose {
|
|
fmt.Fprintf(l.console, "%s %s\n", level, msg)
|
|
}
|
|
if l.file != nil && (level >= Info || l.verbose) {
|
|
fmt.Fprintf(l.file, "%s %s %s\n", ts, level, msg)
|
|
}
|
|
}
|
|
|
|
// Action registra una entrada de auditoría estructurada en agent.log.
|
|
// fields se escribe como key="value" en el orden recibido (pares clave/valor).
|
|
//
|
|
// Ejemplo:
|
|
//
|
|
// 2026-09-27T10:35:17-06:00 ACTION task="01-nginx.md" action="llm_chat" result="ok"
|
|
func (l *Logger) Action(fields ...string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if l.file == nil {
|
|
return
|
|
}
|
|
var b strings.Builder
|
|
b.WriteString(l.now().Format(time.RFC3339))
|
|
b.WriteString(" ACTION")
|
|
for i := 0; i+1 < len(fields); i += 2 {
|
|
fmt.Fprintf(&b, " %s=%q", fields[i], l.redactLocked(fields[i+1]))
|
|
}
|
|
b.WriteByte('\n')
|
|
l.file.WriteString(b.String())
|
|
}
|
|
|
|
// Close cierra el archivo de auditoría tras sincronizarlo a disco.
|
|
func (l *Logger) Close() error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if l.file == nil {
|
|
return nil
|
|
}
|
|
l.file.Sync()
|
|
err := l.file.Close()
|
|
l.file = nil
|
|
return err
|
|
}
|