chore: sync from fn-registry agent
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Audit struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const auditSchema = `
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER NOT NULL,
|
||||
request_id TEXT NOT NULL,
|
||||
capability TEXT NOT NULL,
|
||||
args_hash TEXT NOT NULL,
|
||||
exit_code INTEGER NOT NULL,
|
||||
prev_hash TEXT NOT NULL,
|
||||
this_hash TEXT NOT NULL UNIQUE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS audit_log_ts ON audit_log(ts);
|
||||
CREATE INDEX IF NOT EXISTS audit_log_request_id ON audit_log(request_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_shell_eval (
|
||||
audit_id INTEGER PRIMARY KEY,
|
||||
cmd TEXT NOT NULL,
|
||||
cwd TEXT,
|
||||
shell TEXT NOT NULL,
|
||||
stdout_b64 TEXT,
|
||||
stderr_b64 TEXT,
|
||||
FOREIGN KEY (audit_id) REFERENCES audit_log(id)
|
||||
);
|
||||
`
|
||||
|
||||
func OpenAudit(path string) (*Audit, error) {
|
||||
db, err := sql.Open("sqlite", path+"?_pragma=journal_mode(wal)&_pragma=foreign_keys(on)")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open audit db %s: %w", path, err)
|
||||
}
|
||||
if _, err := db.Exec(auditSchema); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("apply audit schema: %w", err)
|
||||
}
|
||||
return &Audit{db: db}, nil
|
||||
}
|
||||
|
||||
func (a *Audit) Close() error { return a.db.Close() }
|
||||
|
||||
// Append registra una invocacion. Devuelve this_hash.
|
||||
// args raw → SHA256 → args_hash. Hash chain SHA256(prev || canonical).
|
||||
func (a *Audit) Append(requestID, capability string, args any, exitCode int) (string, error) {
|
||||
argsBytes, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal args: %w", err)
|
||||
}
|
||||
argsHash := sha256hex(argsBytes)
|
||||
|
||||
var prevHash string
|
||||
_ = a.db.QueryRow("SELECT this_hash FROM audit_log ORDER BY id DESC LIMIT 1").Scan(&prevHash)
|
||||
|
||||
ts := time.Now().Unix()
|
||||
canonical := fmt.Sprintf("%s|%d|%s|%s|%s|%d", prevHash, ts, requestID, capability, argsHash, exitCode)
|
||||
thisHash := sha256hex([]byte(canonical))
|
||||
|
||||
_, err = a.db.Exec(
|
||||
`INSERT INTO audit_log (ts, request_id, capability, args_hash, exit_code, prev_hash, this_hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
ts, requestID, capability, argsHash, exitCode, prevHash, thisHash,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("insert audit: %w", err)
|
||||
}
|
||||
return thisHash, nil
|
||||
}
|
||||
|
||||
func sha256hex(b []byte) string {
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ShellEvalRecord is the verbose audit payload for shell.eval invocations.
|
||||
// stdout / stderr larger than 4KB are gzip+base64 compressed in the DB.
|
||||
type ShellEvalRecord struct {
|
||||
Cmd string
|
||||
CWD string
|
||||
Shell string
|
||||
Stdout string
|
||||
Stderr string
|
||||
}
|
||||
|
||||
// AppendVerbose registra una invocacion + payload cleartext (cmd/cwd/shell + stdout/stderr)
|
||||
// para forense. Devuelve this_hash. Linkado via audit_log.id en audit_shell_eval.
|
||||
func (a *Audit) AppendVerbose(requestID, capability string, args any, exitCode int, rec ShellEvalRecord) (string, error) {
|
||||
argsBytes, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal args: %w", err)
|
||||
}
|
||||
argsHash := sha256hex(argsBytes)
|
||||
|
||||
var prevHash string
|
||||
_ = a.db.QueryRow("SELECT this_hash FROM audit_log ORDER BY id DESC LIMIT 1").Scan(&prevHash)
|
||||
|
||||
ts := time.Now().Unix()
|
||||
canonical := fmt.Sprintf("%s|%d|%s|%s|%s|%d", prevHash, ts, requestID, capability, argsHash, exitCode)
|
||||
thisHash := sha256hex([]byte(canonical))
|
||||
|
||||
tx, err := a.db.Begin()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
res, err := tx.Exec(
|
||||
`INSERT INTO audit_log (ts, request_id, capability, args_hash, exit_code, prev_hash, this_hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
ts, requestID, capability, argsHash, exitCode, prevHash, thisHash,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("insert audit_log: %w", err)
|
||||
}
|
||||
auditID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lastinsertid: %w", err)
|
||||
}
|
||||
|
||||
stdoutEnc, err := encodeMaybeGzip(rec.Stdout)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode stdout: %w", err)
|
||||
}
|
||||
stderrEnc, err := encodeMaybeGzip(rec.Stderr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode stderr: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO audit_shell_eval (audit_id, cmd, cwd, shell, stdout_b64, stderr_b64)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
auditID, rec.Cmd, rec.CWD, rec.Shell, stdoutEnc, stderrEnc,
|
||||
); err != nil {
|
||||
return "", fmt.Errorf("insert audit_shell_eval: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", fmt.Errorf("commit tx: %w", err)
|
||||
}
|
||||
return thisHash, nil
|
||||
}
|
||||
|
||||
// encodeMaybeGzip: si len(s) <= 4KB devuelve base64(plain); si supera, gzip+base64.
|
||||
// Prefijo "gz:" para distinguir. Vacio devuelve "".
|
||||
func encodeMaybeGzip(s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", nil
|
||||
}
|
||||
const threshold = 4096
|
||||
if len(s) <= threshold {
|
||||
return "plain:" + base64.StdEncoding.EncodeToString([]byte(s)), nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
if _, err := zw.Write([]byte(s)); err != nil {
|
||||
_ = zw.Close()
|
||||
return "", err
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "gz:" + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
Reference in New Issue
Block a user