- Enhance audit checkpoint system - Update KMS provider and tenant key management - Refine configuration constants - Improve TUI config handling
180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
// Package audit provides tamper-evident audit logging with hash chaining
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// DBCheckpointManager stores chain state in a PostgreSQL database for external tamper detection.
|
|
// A root attacker who modifies the local log file cannot also silently modify a remote Postgres instance
|
|
// (assuming separate credentials and network controls).
|
|
type DBCheckpointManager struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewDBCheckpointManager creates a new database checkpoint manager
|
|
func NewDBCheckpointManager(db *sql.DB) *DBCheckpointManager {
|
|
return &DBCheckpointManager{db: db}
|
|
}
|
|
|
|
// Checkpoint stores current chain state in the database
|
|
func (dcm *DBCheckpointManager) Checkpoint(seq uint64, hash, fileName string) error {
|
|
fileHash, err := sha256File(fileName)
|
|
if err != nil {
|
|
return fmt.Errorf("hash file for checkpoint: %w", err)
|
|
}
|
|
|
|
_, err = dcm.db.Exec(
|
|
`INSERT INTO audit_chain_checkpoints
|
|
(last_seq, last_hash, file_name, file_hash, checkpoint_time)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
seq, hash, filepath.Base(fileName), fileHash, time.Now().UTC(),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("insert checkpoint: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VerifyAgainstDB verifies local file against the latest database checkpoint.
|
|
// This should be run from a separate host, not the app process itself.
|
|
func (dcm *DBCheckpointManager) VerifyAgainstDB(filePath string) error {
|
|
var dbSeq uint64
|
|
var dbHash string
|
|
err := dcm.db.QueryRow(
|
|
`SELECT last_seq, last_hash
|
|
FROM audit_chain_checkpoints
|
|
WHERE file_name = $1
|
|
ORDER BY checkpoint_time DESC
|
|
LIMIT 1`,
|
|
filepath.Base(filePath),
|
|
).Scan(&dbSeq, &dbHash)
|
|
if err != nil {
|
|
return fmt.Errorf("db checkpoint lookup: %w", err)
|
|
}
|
|
|
|
localSeq, localHash, err := getLastEventFromFile(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if uint64(localSeq) != dbSeq || localHash != dbHash {
|
|
return fmt.Errorf(
|
|
"TAMPERING DETECTED: local(seq=%d hash=%s) vs db(seq=%d hash=%s)",
|
|
localSeq, localHash, dbSeq, dbHash,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyAllFiles checks all known audit files against their latest checkpoints
|
|
func (dcm *DBCheckpointManager) VerifyAllFiles() ([]VerificationResult, error) {
|
|
rows, err := dcm.db.Query(
|
|
`SELECT DISTINCT ON (file_name) file_name, last_seq, last_hash
|
|
FROM audit_chain_checkpoints
|
|
ORDER BY file_name, checkpoint_time DESC`,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query checkpoints: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []VerificationResult
|
|
for rows.Next() {
|
|
var fileName string
|
|
var dbSeq uint64
|
|
var dbHash string
|
|
if err := rows.Scan(&fileName, &dbSeq, &dbHash); err != nil {
|
|
continue
|
|
}
|
|
|
|
result := VerificationResult{
|
|
Timestamp: time.Now().UTC(),
|
|
Valid: true,
|
|
}
|
|
|
|
localSeq, localHash, err := getLastEventFromFile(fileName)
|
|
if err != nil {
|
|
result.Valid = false
|
|
result.Error = fmt.Sprintf("read local file: %v", err)
|
|
} else if uint64(localSeq) != dbSeq || localHash != dbHash {
|
|
result.Valid = false
|
|
result.FirstTampered = localSeq
|
|
result.Error = fmt.Sprintf(
|
|
"TAMPERING DETECTED: local(seq=%d hash=%s) vs db(seq=%d hash=%s)",
|
|
localSeq, localHash, dbSeq, dbHash,
|
|
)
|
|
result.ChainRootHash = localHash
|
|
}
|
|
|
|
results = append(results, result)
|
|
}
|
|
|
|
return results, rows.Err()
|
|
}
|
|
|
|
// InitializeSchema creates the required database tables and permissions
|
|
func (dcm *DBCheckpointManager) InitializeSchema() error {
|
|
schema := `
|
|
CREATE TABLE IF NOT EXISTS audit_chain_checkpoints (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
checkpoint_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seq BIGINT NOT NULL,
|
|
last_hash TEXT NOT NULL,
|
|
file_name TEXT NOT NULL,
|
|
file_hash TEXT NOT NULL,
|
|
metadata JSONB
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_checkpoints_file_time
|
|
ON audit_chain_checkpoints(file_name, checkpoint_time DESC);
|
|
`
|
|
_, err := dcm.db.Exec(schema)
|
|
return err
|
|
}
|
|
|
|
// RestrictWriterPermissions revokes UPDATE and DELETE permissions from the audit_writer role.
|
|
// This makes the table effectively append-only for the writer user.
|
|
func (dcm *DBCheckpointManager) RestrictWriterPermissions(writerRole string) error {
|
|
_, err := dcm.db.Exec(
|
|
fmt.Sprintf("REVOKE UPDATE, DELETE ON audit_chain_checkpoints FROM %s", writerRole),
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ContinuousVerification runs verification at regular intervals and reports issues.
|
|
// This should be run as a background goroutine or separate process.
|
|
func (dcm *DBCheckpointManager) ContinuousVerification(
|
|
ctx context.Context,
|
|
interval time.Duration,
|
|
filePaths []string,
|
|
alerter AlertManager,
|
|
) {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
for _, filePath := range filePaths {
|
|
if err := dcm.VerifyAgainstDB(filePath); err != nil {
|
|
if alerter != nil {
|
|
_ = alerter.Alert(ctx, TamperAlert{
|
|
DetectedAt: time.Now().UTC(),
|
|
Severity: "critical",
|
|
Description: fmt.Sprintf("Database checkpoint verification failed for %s", filePath),
|
|
FilePath: filePath,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|