178 lines
4.1 KiB
Go
178 lines
4.1 KiB
Go
package logs
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/coreos/go-systemd/v22/sdjournal"
|
|
)
|
|
|
|
const defaultLimit = 500
|
|
|
|
type Collector struct {
|
|
matches []string
|
|
}
|
|
|
|
func NewCollector() *Collector {
|
|
return &Collector{matches: defaultMatches()}
|
|
}
|
|
|
|
func (c *Collector) Collect(ctx context.Context, cursor string, limit int) ([]Event, string, error) {
|
|
if limit <= 0 {
|
|
limit = defaultLimit
|
|
}
|
|
j, err := sdjournal.NewJournal()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer j.Close()
|
|
|
|
for i, match := range c.matches {
|
|
if i > 0 {
|
|
if err := j.AddDisjunction(); err != nil {
|
|
return nil, "", err
|
|
}
|
|
}
|
|
if err := j.AddMatch(match); err != nil {
|
|
return nil, "", err
|
|
}
|
|
}
|
|
|
|
if cursor != "" {
|
|
if err := j.SeekCursor(cursor); err == nil {
|
|
_, _ = j.Next()
|
|
}
|
|
} else {
|
|
_ = j.SeekTail()
|
|
_, _ = j.Next()
|
|
}
|
|
|
|
var events []Event
|
|
var nextCursor string
|
|
for len(events) < limit {
|
|
select {
|
|
case <-ctx.Done():
|
|
return events, nextCursor, ctx.Err()
|
|
default:
|
|
}
|
|
n, err := j.Next()
|
|
if err != nil {
|
|
return events, nextCursor, err
|
|
}
|
|
if n == 0 {
|
|
break
|
|
}
|
|
entry, err := j.GetEntry()
|
|
if err != nil {
|
|
return events, nextCursor, err
|
|
}
|
|
event := fromEntry(entry)
|
|
events = append(events, event)
|
|
nextCursor = entry.Cursor
|
|
}
|
|
|
|
return events, nextCursor, nil
|
|
}
|
|
|
|
func defaultMatches() []string {
|
|
return []string{
|
|
"_SYSTEMD_UNIT=sshd.service",
|
|
"_SYSTEMD_UNIT=sudo.service",
|
|
"_SYSTEMD_UNIT=systemd-networkd.service",
|
|
"_SYSTEMD_UNIT=NetworkManager.service",
|
|
"_SYSTEMD_UNIT=systemd-logind.service",
|
|
"_TRANSPORT=kernel",
|
|
}
|
|
}
|
|
|
|
func fromEntry(entry *sdjournal.JournalEntry) Event {
|
|
ts := time.Unix(0, int64(entry.RealtimeTimestamp)*int64(time.Microsecond))
|
|
event := NewEvent(ts)
|
|
fields := entry.Fields
|
|
unit := fields["_SYSTEMD_UNIT"]
|
|
message := fields["MESSAGE"]
|
|
identifier := fields["SYSLOG_IDENTIFIER"]
|
|
|
|
event.Unit = unit
|
|
event.Message = message
|
|
event.Priority = fields["PRIORITY"]
|
|
event.Hostname = fields["_HOSTNAME"]
|
|
event.Fields = fields
|
|
|
|
event.Category = categorize(unit, identifier, fields)
|
|
event.EventType, event.Username, event.SourceIP, event.SessionID = parseMessage(event.Category, message)
|
|
if event.EventType == "" {
|
|
event.EventType = defaultEventType(event.Category)
|
|
}
|
|
return event
|
|
}
|
|
|
|
func categorize(unit string, identifier string, fields map[string]string) string {
|
|
switch {
|
|
case unit == "sshd.service" || identifier == "sshd":
|
|
return "access"
|
|
case unit == "sudo.service" || identifier == "sudo":
|
|
return "auth"
|
|
case unit == "systemd-networkd.service" || identifier == "NetworkManager":
|
|
return "network"
|
|
case fields["_TRANSPORT"] == "kernel":
|
|
return "system"
|
|
default:
|
|
return "system"
|
|
}
|
|
}
|
|
|
|
func defaultEventType(category string) string {
|
|
switch category {
|
|
case "access":
|
|
return "ssh"
|
|
case "auth":
|
|
return "auth"
|
|
case "network":
|
|
return "network"
|
|
default:
|
|
return "system"
|
|
}
|
|
}
|
|
|
|
func parseMessage(category string, msg string) (eventType string, username string, sourceIP string, sessionID string) {
|
|
if msg == "" {
|
|
return "", "", "", ""
|
|
}
|
|
lower := strings.ToLower(msg)
|
|
if category == "access" {
|
|
switch {
|
|
case strings.Contains(lower, "accepted"):
|
|
eventType = "ssh.login.success"
|
|
username = extractBetween(msg, "for ", " from")
|
|
sourceIP = extractBetween(msg, "from ", " port")
|
|
case strings.Contains(lower, "failed password"):
|
|
eventType = "ssh.login.fail"
|
|
username = extractBetween(msg, "for ", " from")
|
|
sourceIP = extractBetween(msg, "from ", " port")
|
|
case strings.Contains(lower, "session opened"):
|
|
eventType = "ssh.session.open"
|
|
username = extractBetween(msg, "for user ", " by")
|
|
case strings.Contains(lower, "session closed"):
|
|
eventType = "ssh.session.close"
|
|
username = extractBetween(msg, "for user ", " by")
|
|
}
|
|
}
|
|
return eventType, strings.TrimSpace(username), strings.TrimSpace(sourceIP), strings.TrimSpace(sessionID)
|
|
}
|
|
|
|
func extractBetween(msg string, start string, end string) string {
|
|
startIdx := strings.Index(msg, start)
|
|
if startIdx == -1 {
|
|
return ""
|
|
}
|
|
startIdx += len(start)
|
|
rest := msg[startIdx:]
|
|
endIdx := strings.Index(rest, end)
|
|
if endIdx == -1 {
|
|
return strings.TrimSpace(rest)
|
|
}
|
|
return strings.TrimSpace(rest[:endIdx])
|
|
}
|