package main import ( "context" "encoding/json" "strconv" "strings" "time" ) // BusUnits are the system bus's units as the journal knows them: dbus-broker's own name, and the // alias every implementation answers to. var BusUnits = []string{"dbus-broker.service", "dbus.service"} // JournalLines is the most journal entries one read takes. const JournalLines = 2000 // Denial is one policy denial as the bus logged it: the header of the message it refused, never its // body, which the bus does not log either. type Denial struct { At string `json:"at"` Action string `json:"action,omitempty"` Type string `json:"type,omitempty"` Sender string `json:"sender,omitempty"` Destination string `json:"destination,omitempty"` Path string `json:"path,omitempty"` Interface string `json:"interface,omitempty"` Member string `json:"member,omitempty"` Policy string `json:"policy,omitempty"` } // key is what makes two denials the same example: who was refused what, ignoring the sender's unique // name, which differs at every connection. func (d Denial) key() string { return d.Action + "|" + d.Type + "|" + d.Destination + "|" + d.Interface + "|" + d.Member } // journalEntry is the part of a journal entry the module reads. MESSAGE is read only to recognise a // denial; it is never answered or published. type journalEntry struct { Cursor string `json:"__CURSOR"` Realtime string `json:"__REALTIME_TIMESTAMP"` Message any `json:"MESSAGE"` Action string `json:"DBUS_BROKER_TRANSMIT_ACTION"` Type string `json:"DBUS_BROKER_MESSAGE_TYPE"` Sender string `json:"DBUS_BROKER_SENDER_UNIQUE_NAME"` Destination string `json:"DBUS_BROKER_MESSAGE_DESTINATION"` Path string `json:"DBUS_BROKER_MESSAGE_PATH"` Interface string `json:"DBUS_BROKER_MESSAGE_INTERFACE"` Member string `json:"DBUS_BROKER_MESSAGE_MEMBER"` Policy string `json:"DBUS_BROKER_POLICY_TYPE"` } // IsDenial is whether a bus's log line is a policy denial: dbus-broker's "A security policy denied", // or the reference daemon's "Rejected send message". func IsDenial(message string) bool { return strings.Contains(message, "security policy denied") || strings.Contains(message, "Rejected send message") || strings.Contains(message, "Rejected receive message") } // ParseDenials reads journalctl's JSON lines and answers the denials among them and the last cursor. func ParseDenials(out string) ([]Denial, string) { var got []Denial cursor := "" for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if line == "" || line[0] != '{' { continue } var e journalEntry if json.Unmarshal([]byte(line), &e) != nil { continue } if e.Cursor != "" { cursor = e.Cursor } msg, _ := e.Message.(string) // a binary MESSAGE comes as an array of bytes and is no denial if !IsDenial(msg) { continue } at := "" if us, err := strconv.ParseInt(e.Realtime, 10, 64); err == nil { at = time.UnixMicro(us).UTC().Format(time.RFC3339) } got = append(got, Denial{At: at, Action: e.Action, Type: e.Type, Sender: e.Sender, Destination: e.Destination, Path: e.Path, Interface: e.Interface, Member: e.Member, Policy: e.Policy}) } return got, cursor } func journalArgs() []string { args := []string{"--no-pager", "-o", "json", "-n", strconv.Itoa(JournalLines)} for _, u := range BusUnits { args = append(args, "-u", u) } return args } // Denials is the watcher's Journal on this machine: journalctl as the operator's account, which // reads the system journal through its group. func (m *Machine) Denials(ctx context.Context, after string, since time.Time) ([]Denial, string, error) { args := journalArgs() if after != "" { args = append(args, "--after-cursor", after) } else { args = append(args, "--since", "@"+strconv.FormatInt(since.Unix(), 10)) } out, err := m.Run(ctx, "journalctl", args...) if err != nil { return nil, "", err } d, cursor := ParseDenials(out) return d, cursor, nil }