mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-19 07:50:43 +01:00
hostagent/commands.go: - Extract execCommandContext as mockable variable hostagent/proxmox_setup.go: - Convert stateFilePath constants to variables (testable) - Extract runCommand and lookPath as mockable functions - Add duplicate comment (minor cleanup needed) notifications/notifications.go: - Add GetQueueStats() method for interface compliance - Used by NotificationMonitor interface updates/manager.go: - Add AddSSEClient, RemoveSSEClient, GetSSECachedStatus methods - Enables interface-based SSE client management pkg/audit/export.go: - Minor testability improvements go.mod/go.sum: - Add stretchr/objx v0.5.2 (test mocking dependency)
258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
package audit
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ExportFormat defines the export file format.
|
|
type ExportFormat string
|
|
|
|
const (
|
|
ExportFormatCSV ExportFormat = "csv"
|
|
ExportFormatJSON ExportFormat = "json"
|
|
)
|
|
|
|
// ExportResult contains export data and metadata.
|
|
type ExportResult struct {
|
|
Data []byte
|
|
ContentType string
|
|
Filename string
|
|
EventCount int
|
|
}
|
|
|
|
// ExportEvent extends Event with verification status for exports.
|
|
type ExportEvent struct {
|
|
ID string `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
EventType string `json:"event_type"`
|
|
User string `json:"user,omitempty"`
|
|
IP string `json:"ip,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Success bool `json:"success"`
|
|
Details string `json:"details,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
SignatureValid *bool `json:"signature_valid,omitempty"`
|
|
}
|
|
|
|
// PersistentLogger defines the interface for loggers that support querying and verification.
|
|
type PersistentLogger interface {
|
|
Query(filter QueryFilter) ([]Event, error)
|
|
VerifySignature(event Event) bool
|
|
}
|
|
|
|
// Exporter provides export functionality for audit logs.
|
|
type Exporter struct {
|
|
logger PersistentLogger
|
|
}
|
|
|
|
// NewExporter creates a new exporter for the given logger.
|
|
func NewExporter(logger PersistentLogger) *Exporter {
|
|
return &Exporter{logger: logger}
|
|
}
|
|
|
|
// Export generates an export in the specified format.
|
|
func (e *Exporter) Export(filter QueryFilter, format ExportFormat, includeVerification bool) (*ExportResult, error) {
|
|
// Remove limit for export (get all matching events)
|
|
filter.Limit = 0
|
|
filter.Offset = 0
|
|
|
|
events, err := e.logger.Query(filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query events for export: %w", err)
|
|
}
|
|
|
|
// Convert to export events
|
|
exportEvents := make([]ExportEvent, len(events))
|
|
for i, event := range events {
|
|
exportEvents[i] = ExportEvent{
|
|
ID: event.ID,
|
|
Timestamp: event.Timestamp,
|
|
EventType: event.EventType,
|
|
User: event.User,
|
|
IP: event.IP,
|
|
Path: event.Path,
|
|
Success: event.Success,
|
|
Details: event.Details,
|
|
Signature: event.Signature,
|
|
}
|
|
|
|
if includeVerification && event.Signature != "" {
|
|
valid := e.logger.VerifySignature(event)
|
|
exportEvents[i].SignatureValid = &valid
|
|
}
|
|
}
|
|
|
|
// Generate timestamp for filename
|
|
timestamp := time.Now().Format("20060102-150405")
|
|
|
|
switch format {
|
|
case ExportFormatCSV:
|
|
return e.exportCSV(exportEvents, timestamp, includeVerification)
|
|
case ExportFormatJSON:
|
|
return e.exportJSON(exportEvents, timestamp)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported export format: %s", format)
|
|
}
|
|
}
|
|
|
|
// exportCSV generates a CSV export.
|
|
func (e *Exporter) exportCSV(events []ExportEvent, timestamp string, includeVerification bool) (*ExportResult, error) {
|
|
var buf bytes.Buffer
|
|
writer := csv.NewWriter(&buf)
|
|
|
|
// Write header
|
|
header := []string{"ID", "Timestamp", "Event Type", "User", "IP", "Path", "Success", "Details", "Signature"}
|
|
if includeVerification {
|
|
header = append(header, "Signature Valid")
|
|
}
|
|
if err := writer.Write(header); err != nil {
|
|
return nil, fmt.Errorf("failed to write CSV header: %w", err)
|
|
}
|
|
|
|
// Write rows
|
|
for _, event := range events {
|
|
success := "false"
|
|
if event.Success {
|
|
success = "true"
|
|
}
|
|
|
|
row := []string{
|
|
event.ID,
|
|
event.Timestamp.Format(time.RFC3339),
|
|
event.EventType,
|
|
event.User,
|
|
event.IP,
|
|
event.Path,
|
|
success,
|
|
event.Details,
|
|
event.Signature,
|
|
}
|
|
|
|
if includeVerification {
|
|
sigValid := ""
|
|
if event.SignatureValid != nil {
|
|
if *event.SignatureValid {
|
|
sigValid = "true"
|
|
} else {
|
|
sigValid = "false"
|
|
}
|
|
}
|
|
row = append(row, sigValid)
|
|
}
|
|
|
|
if err := writer.Write(row); err != nil {
|
|
return nil, fmt.Errorf("failed to write CSV row: %w", err)
|
|
}
|
|
}
|
|
|
|
writer.Flush()
|
|
if err := writer.Error(); err != nil {
|
|
return nil, fmt.Errorf("CSV writer error: %w", err)
|
|
}
|
|
|
|
return &ExportResult{
|
|
Data: buf.Bytes(),
|
|
ContentType: "text/csv; charset=utf-8",
|
|
Filename: fmt.Sprintf("audit-log-%s.csv", timestamp),
|
|
EventCount: len(events),
|
|
}, nil
|
|
}
|
|
|
|
// exportJSON generates a JSON export.
|
|
func (e *Exporter) exportJSON(events []ExportEvent, timestamp string) (*ExportResult, error) {
|
|
// Wrap in an object for better structure
|
|
export := struct {
|
|
ExportedAt time.Time `json:"exported_at"`
|
|
EventCount int `json:"event_count"`
|
|
Events []ExportEvent `json:"events"`
|
|
}{
|
|
ExportedAt: time.Now(),
|
|
EventCount: len(events),
|
|
Events: events,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(export, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal JSON export: %w", err)
|
|
}
|
|
|
|
return &ExportResult{
|
|
Data: data,
|
|
ContentType: "application/json; charset=utf-8",
|
|
Filename: fmt.Sprintf("audit-log-%s.json", timestamp),
|
|
EventCount: len(events),
|
|
}, nil
|
|
}
|
|
|
|
// ExportSummary generates a summary of audit activity.
|
|
type ExportSummary struct {
|
|
TotalEvents int `json:"total_events"`
|
|
SuccessCount int `json:"success_count"`
|
|
FailureCount int `json:"failure_count"`
|
|
EventsByType map[string]int `json:"events_by_type"`
|
|
EventsByUser map[string]int `json:"events_by_user"`
|
|
StartTime *time.Time `json:"start_time,omitempty"`
|
|
EndTime *time.Time `json:"end_time,omitempty"`
|
|
InvalidSigCount int `json:"invalid_signature_count,omitempty"`
|
|
}
|
|
|
|
// GenerateSummary creates a summary of audit events matching the filter.
|
|
func (e *Exporter) GenerateSummary(filter QueryFilter, verifySignatures bool) (*ExportSummary, error) {
|
|
// Remove limit for summary
|
|
filter.Limit = 0
|
|
filter.Offset = 0
|
|
|
|
events, err := e.logger.Query(filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query events for summary: %w", err)
|
|
}
|
|
|
|
summary := &ExportSummary{
|
|
TotalEvents: len(events),
|
|
EventsByType: make(map[string]int),
|
|
EventsByUser: make(map[string]int),
|
|
}
|
|
|
|
var minTime, maxTime *time.Time
|
|
|
|
for _, event := range events {
|
|
if event.Success {
|
|
summary.SuccessCount++
|
|
} else {
|
|
summary.FailureCount++
|
|
}
|
|
|
|
summary.EventsByType[event.EventType]++
|
|
|
|
if event.User != "" {
|
|
summary.EventsByUser[event.User]++
|
|
}
|
|
|
|
// Track time range
|
|
if minTime == nil || event.Timestamp.Before(*minTime) {
|
|
t := event.Timestamp
|
|
minTime = &t
|
|
}
|
|
if maxTime == nil || event.Timestamp.After(*maxTime) {
|
|
t := event.Timestamp
|
|
maxTime = &t
|
|
}
|
|
|
|
// Verify signatures if requested
|
|
if verifySignatures && event.Signature != "" {
|
|
if !e.logger.VerifySignature(event) {
|
|
summary.InvalidSigCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
summary.StartTime = minTime
|
|
summary.EndTime = maxTime
|
|
|
|
return summary, nil
|
|
}
|