Initial linux agent and api functionality for enrolling servers
This commit is contained in:
177
agent/internal/logs/collector.go
Normal file
177
agent/internal/logs/collector.go
Normal file
@@ -0,0 +1,177 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user