package audit import ( "database/sql" "flag" "fmt" "os" "os/user" "github.com/pkg/errors" // empty import as per https://godoc.org/github.com/lib/pq _ "github.com/lib/pq" "github.com/getsops/sops/v3/logging" "github.com/sirupsen/logrus" "go.yaml.in/yaml/v3" ) var log *logrus.Logger func init() { log = logging.NewLogger("AUDIT") confBytes, err := os.ReadFile(configFile) if err != nil { log.WithField("error", err).Debugf("Error reading config") return } var conf config err = yaml.Unmarshal(confBytes, &conf) if err != nil { log.WithField("error", err).Panicf("Error unmarshalling config") } // If we are running test, then don't create auditors. // This is pretty hacky, but doing it The Right Way would require // restructuring SOPS to use dependency injection instead of just using // globals everywhere. if flag.Lookup("test.v") != nil { return } var auditErrors []error for _, pgConf := range conf.Backends.Postgres { auditDb, err := NewPostgresAuditor(pgConf.ConnStr) if err != nil { auditErrors = append(auditErrors, errors.Wrap(err, fmt.Sprintf("connectStr: %s, err", pgConf.ConnStr))) } auditors = append(auditors, auditDb) } if len(auditErrors) > 0 { log.Errorf("connecting to audit database, defined in %s", configFile) for _, err := range auditErrors { log.Error(err) } log.Fatal("one or more audit backends reported errors, exiting") } } // TODO: Make platform agnostic const configFile = "/etc/sops/audit.yaml" type config struct { Backends struct { Postgres []struct { ConnStr string `yaml:"connection_string"` } `yaml:"postgres"` } `yaml:"backends"` } var auditors []Auditor // SubmitEvent handles an event for all auditors func SubmitEvent(event interface{}) { for _, auditor := range auditors { auditor.Handle(event) } } // Register registers a new Auditor in the global auditor list func Register(auditor Auditor) { auditors = append(auditors, auditor) } // Auditor is notified when noteworthy events happen, // for example when a file is encrypted or decrypted. type Auditor interface { // Handle() takes an audit event and attempts to persists it; // how it is persisted and how errors are handled is up to the // implementation of this interface. Handle(event interface{}) } // DecryptEvent contains fields relevant to a decryption event type DecryptEvent struct { File string } // EncryptEvent contains fields relevant to an encryption event type EncryptEvent struct { File string } // RotateEvent contains fields relevant to a key rotation event type RotateEvent struct { File string } // PostgresAuditor is a Postgres SQL DB implementation of the Auditor interface. // It persists the audit event by writing a row to the 'audit_event' table. // Errors with writing to the database will output a log message and the // process will exit with status set to 1 type PostgresAuditor struct { DB *sql.DB } // NewPostgresAuditor is the constructor for a new PostgresAuditor struct // initialized with the given db connection string func NewPostgresAuditor(connStr string) (*PostgresAuditor, error) { db, err := sql.Open("postgres", connStr) pg := &PostgresAuditor{DB: db} if err != nil { return pg, err } var result int err = pg.DB.QueryRow("SELECT 1").Scan(&result) if err != nil { return pg, fmt.Errorf("Pinging audit database failed: %s", err) } else if result != 1 { return pg, fmt.Errorf("Database malfunction: SELECT 1 should return 1, but returned %d", result) } return pg, nil } // Handle persists the audit event by writing a row to the // 'audit_event' postgres table func (p *PostgresAuditor) Handle(event interface{}) { u, err := user.Current() if err != nil { log.Fatalf("Error getting current user for auditing: %s", err) } switch event := event.(type) { case DecryptEvent: // Save the event to the database log.WithField("file", event.File). Debug("Saving decrypt event to database") _, err = p.DB.Exec("INSERT INTO audit_event (action, username, file) VALUES ($1, $2, $3)", "decrypt", u.Username, event.File) if err != nil { log.Fatalf("Failed to insert audit record: %s", err) } case EncryptEvent: // Save the event to the database log.WithField("file", event.File). Debug("Saving encrypt event to database") _, err = p.DB.Exec("INSERT INTO audit_event (action, username, file) VALUES ($1, $2, $3)", "encrypt", u.Username, event.File) if err != nil { log.Fatalf("Failed to insert audit record: %s", err) } case RotateEvent: // Save the event to the database log.WithField("file", event.File). Debug("Saving rotate event to database") _, err = p.DB.Exec("INSERT INTO audit_event (action, username, file) VALUES ($1, $2, $3)", "rotate", u.Username, event.File) if err != nil { log.Fatalf("Failed to insert audit record: %s", err) } default: log.WithField("type", fmt.Sprintf("%T", event)). Info("Received unknown event") } }