github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/audit/choria.go (about) 1 // Copyright (c) 2020-2023, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 // Package audit is a auditing system that's compatible with the 6 // one found in the mcollective-choria Ruby project, log lines will 7 // be identical and can be put in the same file as the ruby one 8 package audit 9 10 import ( 11 "encoding/json" 12 "fmt" 13 "io/fs" 14 "os" 15 "os/user" 16 "strconv" 17 "sync" 18 "time" 19 20 log "github.com/sirupsen/logrus" 21 22 "github.com/choria-io/go-choria/config" 23 "github.com/choria-io/go-choria/protocol" 24 ) 25 26 var mu = &sync.Mutex{} 27 28 // Message is the format of a Choria audit log 29 type Message struct { 30 TimeStamp string `json:"timestamp"` 31 RequestID string `json:"request_id"` 32 RequestTime int64 `json:"request_time"` 33 CallerID string `json:"caller"` 34 Sender string `json:"sender"` 35 Agent string `json:"agent"` 36 Action string `json:"action"` 37 Data json.RawMessage `json:"data"` 38 } 39 40 // Request writes a audit log to a configured log 41 func Request(request protocol.Request, agent string, action string, data json.RawMessage, cfg *config.Config) bool { 42 if !cfg.RPCAudit { 43 return false 44 } 45 46 logfile := cfg.Choria.RPCAuditLogfile 47 logfileGroup := cfg.Choria.RPCAuditLogfileGroup 48 49 logfileMode, err := strconv.ParseUint(cfg.Choria.RPCAuditLogFileMode, 0, 32) 50 if err != nil { 51 log.Errorf("Failed to parse plugin.rpcaudit.logfile.mode: %v", err) 52 return false 53 } 54 55 if logfile == "" { 56 log.Warnf("Choria RPC Auditing is enabled but no logfile is configured, skipping") 57 return false 58 } 59 60 amsg := Message{ 61 TimeStamp: time.Now().UTC().Format("2006-01-02T15:04:05.000000-0700"), 62 RequestID: request.RequestID(), 63 RequestTime: request.Time().UTC().Unix(), 64 CallerID: request.CallerID(), 65 Sender: request.SenderID(), 66 Agent: agent, 67 Action: action, 68 Data: data, 69 } 70 71 j, err := json.Marshal(amsg) 72 if err != nil { 73 log.Warnf("Auditing is not functional because the auditing data could not be represented as JSON: %s", err) 74 return false 75 } 76 77 mu.Lock() 78 defer mu.Unlock() 79 80 f, err := createAuditLog(logfile, logfileGroup, uint32(logfileMode)) 81 if err != nil { 82 log.Warnf("Auditing is not functional because opening the logfile '%s' failed: %s", logfile, err) 83 return false 84 } 85 defer f.Close() 86 87 _, err = f.WriteString(fmt.Sprintf("%s\n", string(j))) 88 if err != nil { 89 log.Warnf("Auditing is not functional because writing to logfile '%s' failed: %s", logfile, err) 90 return false 91 } 92 93 return true 94 } 95 96 func createAuditLog(logfile string, logfileGroup string, logfileMode uint32) (*os.File, error) { 97 f, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, fs.FileMode(logfileMode)) 98 if err != nil { 99 return f, err 100 } 101 102 if logfileGroup == "" { 103 return f, nil 104 } 105 106 grp, err := user.LookupGroup(logfileGroup) 107 if err != nil { 108 f.Close() 109 return f, err 110 } 111 112 gid, err := strconv.Atoi(grp.Gid) 113 if err != nil { 114 f.Close() 115 return f, err 116 } 117 118 err = os.Chown(logfile, os.Getuid(), gid) 119 if err != nil { 120 f.Close() 121 return f, err 122 } 123 124 return f, nil 125 }