Files
AetherForge/agent/client/posture_unix.go

203 lines
7.0 KiB
Go

//go:build !windows
package client
import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"time"
)
func collectPosture() *PostureReport {
r := &PostureReport{AgentServiceOK: boolPtr(true)}
// ── Firewall ───────────────────────────────────────────────────────────────
fwActive := probeUnixFirewall()
r.FirewallDomain = &fwActive
r.FirewallPrivate = &fwActive
// Public profile has no direct Linux equivalent; leave nil.
// ── AV / security daemons ─────────────────────────────────────────────────
avProducts := probeUnixAV()
r.AVProducts = avProducts
avOK := fwActive || len(avProducts) > 0
r.DefenderEnabled = &avOK
// ── SSH ───────────────────────────────────────────────────────────────────
ssh := probeSSH()
r.SSHListening = &ssh
// ── Patch age ─────────────────────────────────────────────────────────────
days, dateStr := probeUnixPatchAge()
if days >= 0 {
r.LastPatchDays = &days
recent := days <= 30
r.PatchRecent = &recent
if dateStr != "" {
r.LastPatch = &dateStr
}
}
// ── Pending updates ───────────────────────────────────────────────────────
pending := probeUnixPendingUpdates()
r.PendingUpdates = &pending
// ── Reboot pending ────────────────────────────────────────────────────────
rp := probeUnixRebootPending()
r.RebootPending = &rp
// ── Elevation ─────────────────────────────────────────────────────────────
elevated := probeUnixElevated()
r.AgentElevated = &elevated
r.PostureScore = computePostureScore(r)
return r
}
// probeUnixFirewall returns true if any host firewall appears active.
func probeUnixFirewall() bool {
if out, _ := exec.Command("ufw", "status").CombinedOutput(); strings.Contains(strings.ToLower(string(out)), "status: active") {
return true
}
if out, _ := exec.Command("systemctl", "is-active", "firewalld").CombinedOutput(); strings.TrimSpace(string(out)) == "active" {
return true
}
if out, err := exec.Command("/bin/sh", "-c", "iptables -L -n 2>/dev/null | wc -l").CombinedOutput(); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n > 8 {
return true
}
}
if out, _ := exec.Command("/bin/sh", "-c", "nft list ruleset 2>/dev/null | wc -l").CombinedOutput(); true {
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n > 3 {
return true
}
}
return false
}
// probeUnixAV returns names of active security daemons.
func probeUnixAV() []string {
type entry struct{ svc, label string }
candidates := []entry{
{"clamav-daemon", "ClamAV"}, {"clamd", "ClamAV"},
{"fail2ban", "Fail2Ban"}, {"rkhunter", "RKHunter"},
{"chkrootkit", "Chkrootkit"}, {"auditd", "auditd"},
}
seen := map[string]bool{}
var found []string
for _, c := range candidates {
out, _ := exec.Command("systemctl", "is-active", c.svc).CombinedOutput()
if strings.TrimSpace(string(out)) == "active" && !seen[c.label] {
seen[c.label] = true
found = append(found, c.label)
}
}
return found
}
// probeUnixPatchAge returns (days since last update, ISO date string).
// Returns (-1, "") when unknown.
func probeUnixPatchAge() (int, string) {
stamps := []string{
"/var/lib/apt/periodic/update-success-stamp",
"/var/cache/apk/lastupdate",
}
for _, path := range stamps {
if info, err := os.Stat(path); err == nil {
d := int(time.Since(info.ModTime()).Hours() / 24)
if d < 0 {
d = 0
}
return d, info.ModTime().Format("2006-01-02")
}
}
// YUM/DNF history
if out, err := exec.Command("/bin/sh", "-c",
`yum history info 2>/dev/null | grep "Begin time" | head -1 | awk '{print $3, $4}'`).CombinedOutput(); err == nil {
line := strings.TrimSpace(string(out))
if line != "" {
if t, err := time.Parse("2006-01-02 15:04", line); err == nil {
return int(time.Since(t).Hours() / 24), t.Format("2006-01-02")
}
}
}
// rpm last installed
if out, err := exec.Command("/bin/sh", "-c", "rpm -qa --last 2>/dev/null | head -1").CombinedOutput(); err == nil {
parts := strings.Fields(string(out))
if len(parts) >= 5 {
dateStr := strings.Join(parts[len(parts)-5:], " ")
if t, err := time.Parse("Mon Jan 2 15:04:05 2006", dateStr); err == nil {
return int(time.Since(t).Hours() / 24), t.Format("2006-01-02")
}
}
}
return -1, ""
}
// probeUnixPendingUpdates returns the count of available package updates.
// Returns -1 if the count cannot be determined.
func probeUnixPendingUpdates() int {
// APT
if out, err := exec.Command("/bin/sh", "-c",
"apt list --upgradable 2>/dev/null | grep -c upgradable").CombinedOutput(); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
return n
}
}
// APT check (outputs "pkg;security" lines)
if out, err := exec.Command("/bin/sh", "-c",
"/usr/lib/update-notifier/apt-check 2>&1 | cut -d';' -f1").CombinedOutput(); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n >= 0 {
return n
}
}
// YUM/DNF
if out, err := exec.Command("/bin/sh", "-c",
"yum check-update --quiet 2>/dev/null | grep -c '^[a-zA-Z]'").CombinedOutput(); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
return n
}
}
// apk (Alpine)
if out, err := exec.Command("/bin/sh", "-c",
"apk version -l '<' 2>/dev/null | wc -l").CombinedOutput(); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
return n
}
}
return -1
}
// probeUnixRebootPending checks kernel and package signals for a required reboot.
func probeUnixRebootPending() bool {
// Debian/Ubuntu explicit flag
if _, err := os.Stat("/var/run/reboot-required"); err == nil {
return true
}
// Kernel update check: compare running vs installed
if out, err := exec.Command("uname", "-r").CombinedOutput(); err == nil {
running := strings.TrimSpace(string(out))
// Check if a newer kernel package exists
if out2, err := exec.Command("/bin/sh", "-c",
fmt.Sprintf("ls /boot/vmlinuz-* 2>/dev/null | grep -v '%s' | wc -l", running)).CombinedOutput(); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(string(out2))); err == nil && n > 0 {
return true
}
}
}
return false
}
// probeUnixElevated returns true when running as root (uid 0).
func probeUnixElevated() bool {
u, err := user.Current()
if err != nil {
return false
}
return u.Uid == "0"
}