wireguard-ui/audit/audit.go

200 lines
5.0 KiB
Go

package audit
import (
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/labstack/gommon/log"
)
// Logger handles audit log operations
type Logger struct {
db *sql.DB
}
// Entry represents a single audit log entry
type Entry struct {
Actor string
Action string
ResourceType string
ResourceID string
Details interface{}
IPAddress string
}
// LogEntry represents a stored audit log entry
type LogEntry struct {
ID int `json:"id"`
Timestamp time.Time `json:"timestamp"`
Actor string `json:"actor"`
Action string `json:"action"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
Details string `json:"details"`
IPAddress string `json:"ip_address"`
}
// NewLogger creates a new audit logger
func NewLogger(db *sql.DB) *Logger {
return &Logger{db: db}
}
// Log records an audit log entry
func (l *Logger) Log(entry Entry) {
if l == nil || l.db == nil {
return
}
details := "{}"
if entry.Details != nil {
if b, err := json.Marshal(entry.Details); err == nil {
details = string(b)
}
}
_, err := l.db.Exec(
`INSERT INTO audit_logs (actor, action, resource_type, resource_id, details, ip_address)
VALUES (?, ?, ?, ?, ?, ?)`,
entry.Actor, entry.Action, entry.ResourceType, entry.ResourceID, details, entry.IPAddress,
)
if err != nil {
log.Errorf("Failed to write audit log: %v", err)
}
}
// LogWithUser records an audit log entry with explicit username
func (l *Logger) LogWithUser(username, action, resourceType, resourceID, ipAddress string, details interface{}) {
l.Log(Entry{
Actor: username,
Action: action,
ResourceType: resourceType,
ResourceID: resourceID,
Details: details,
IPAddress: ipAddress,
})
}
// Query returns audit log entries with optional filtering
const maxPerPage = 200
const maxExportRows = 100000
func buildWhereClause(from, to, actor, action, search string) (string, []interface{}) {
where := "WHERE 1=1"
args := make([]interface{}, 0)
if from != "" {
where += " AND timestamp >= ?"
args = append(args, from)
}
if to != "" {
where += " AND timestamp <= ?"
args = append(args, to)
}
if actor != "" {
where += " AND actor = ?"
args = append(args, actor)
}
if search != "" {
where += " AND (resource_id LIKE ? OR details LIKE ? OR actor LIKE ?)"
q := "%" + search + "%"
args = append(args, q, q, q)
}
if action != "" {
where += " AND action = ?"
args = append(args, action)
}
return where, args
}
func scanLogEntries(rows *sql.Rows) ([]LogEntry, error) {
var entries []LogEntry
for rows.Next() {
var e LogEntry
if err := rows.Scan(&e.ID, &e.Timestamp, &e.Actor, &e.Action, &e.ResourceType, &e.ResourceID, &e.Details, &e.IPAddress); err != nil {
return nil, err
}
entries = append(entries, e)
}
return entries, rows.Err()
}
// Query returns audit log entries with optional filtering
func (l *Logger) Query(from, to, actor, action, search string, page, perPage int) ([]LogEntry, int, error) {
if perPage <= 0 {
perPage = 50
}
if perPage > maxPerPage {
perPage = maxPerPage
}
if page <= 0 {
page = 1
}
where, args := buildWhereClause(from, to, actor, action, search)
var total int
if err := l.db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM audit_logs %s", where), args...).Scan(&total); err != nil {
return nil, 0, err
}
offset := (page - 1) * perPage
query := fmt.Sprintf("SELECT id, timestamp, actor, action, resource_type, resource_id, details, ip_address FROM audit_logs %s ORDER BY timestamp DESC LIMIT ? OFFSET ?", where)
args = append(args, perPage, offset)
rows, err := l.db.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
entries, err := scanLogEntries(rows)
return entries, total, err
}
// QueryAll returns audit log entries matching filters (for export), capped at maxExportRows
func (l *Logger) QueryAll(from, to, actor, action, search string) ([]LogEntry, error) {
where, args := buildWhereClause(from, to, actor, action, search)
query := fmt.Sprintf("SELECT id, timestamp, actor, action, resource_type, resource_id, details, ip_address FROM audit_logs %s ORDER BY timestamp DESC LIMIT ?", where)
args = append(args, maxExportRows)
rows, err := l.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return scanLogEntries(rows)
}
// DistinctFilters returns distinct actors and actions for filter dropdowns
func (l *Logger) DistinctFilters() ([]string, []string, error) {
actors, err := l.distinctColumn("actor")
if err != nil {
return nil, nil, err
}
actions, err := l.distinctColumn("action")
if err != nil {
return nil, nil, err
}
return actors, actions, nil
}
func (l *Logger) distinctColumn(column string) ([]string, error) {
rows, err := l.db.Query("SELECT DISTINCT " + column + " FROM audit_logs ORDER BY " + column)
if err != nil {
return nil, err
}
defer rows.Close()
var values []string
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
return nil, err
}
values = append(values, v)
}
return values, rows.Err()
}