enodia-sentinal/go-agent/internal/fim/pkgverify.go
Luna d835386381
feat(go): port FIM/pkgdb/rootcheck integrity engines and add go-sidecar dashboard consumer
Adds sidecar-only, --state-dir-gated FIM, package-DB-anchor, and rootcheck
engines to the Go migration sidecar, wired into agent.Sweep/Initialize
behind the existing baseline lifecycle. Adds a read-only, schema-checked
Python dashboard consumer (go_sidecar_state_dir, /api/go-sidecar/*, Sidecar
tab) that never starts, stops, or mutates the Go sidecar's state.

Also fixes drift found while reconciling this work: enodia_sentinel/web.py
had reinvented local schema constants instead of using the canonical
enodia_sentinel/schemas.py catalog (now registers enodia.go.sidecar.v1
there and reuses ALERT_SNAPSHOT_V1/INCIDENT_V1); docs/RULES.md had drifted
from what `rules docs` actually generates for SIDs 100069-100078 (ruleops.py
was missing metadata for four SIDs and had stale drill text for four more),
now back in sync with a regression test pinning them together.

Updates CLAUDE.md, README.md, go-agent/README.md, and docs/{ROADMAP,
GO_PORT_HANDOFF,SURICATA_ASSIMILATION,THREAT_MODEL,OPERATIONS,SCHEMAS,
COMMAND_REFERENCE}.md to reflect the landed work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQivSKBqQJsayz1xcYqWzZ
2026-07-24 09:59:09 -07:00

99 lines
3.4 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
package fim
import (
"context"
"os/exec"
"strings"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
)
// PackageFinding is one meaningful package-manager integrity discrepancy.
// Package verification is intentionally separate from the FIM baseline: it
// relies on distribution-owned hashes for files that Sentinel should not
// silently re-baseline after an upgrade.
type PackageFinding struct {
Package string `json:"package"`
Path string `json:"path"`
Reason string `json:"reason"`
}
// ParsePacmanVerify mirrors Python's parse_pacman_verify. pacman emits both
// routine summaries and diagnostics to stdout/stderr; only content, mode, or
// ownership mismatches are security findings. A timestamp-only difference is
// deliberately ignored because a harmless touch must not page an operator.
func ParsePacmanVerify(output string) []PackageFinding {
findings := make([]PackageFinding, 0)
for _, raw := range strings.Split(output, "\n") {
line := strings.TrimSpace(raw)
open, close := strings.LastIndex(line, "("), strings.LastIndex(line, ")")
if open < 0 || close <= open {
continue
}
reason := strings.TrimSpace(line[open+1 : close])
lower := strings.ToLower(reason)
if !containsAny(lower, "checksum", "sha", "size", "permission", "ownership", "uid", "gid") {
continue
}
head := strings.TrimSpace(line[:open])
packageName, path, ok := strings.Cut(head, ": /")
if !ok {
continue
}
packageName = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(packageName), "warning:"))
path = "/" + strings.TrimSpace(path)
if packageName == "" || path == "/" {
continue
}
findings = append(findings, PackageFinding{Package: packageName, Path: path, Reason: reason})
}
return findings
}
func containsAny(value string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(value, needle) {
return true
}
}
return false
}
// PackageAlert converts a package-owned integrity discrepancy to the same
// critical Sentinel alert as Python. The path is the stable cooldown key, so
// multiple pacman diagnostics for one path do not cause repeated pages.
func PackageAlert(finding PackageFinding) model.Alert {
return model.Alert{
SID: SIDPackage, Severity: "CRITICAL", Signature: "fim_pkg_modified",
Classtype: "integrity-violation", Key: "fim:pkg:" + finding.Path,
Detail: "package file altered (" + finding.Package + ": " + finding.Reason + "): " + finding.Path,
PIDs: []int{},
}
}
// VerifyAlerts executes an injected verification command and turns any
// parseable output into alerts. A missing package manager or a timeout fails
// open; a non-zero exit with diagnostics is still parsed because pacman uses
// that exit status to report changed files.
func VerifyAlerts(run func() (string, error)) []model.Alert {
alerts := make([]model.Alert, 0)
if run == nil {
return alerts
}
output, _ := run()
for _, finding := range ParsePacmanVerify(output) {
alerts = append(alerts, PackageAlert(finding))
}
return alerts
}
// PacmanVerify runs pacman without a shell and joins stdout/stderr because the
// tool may use either stream for discrepancies. The caller supplies a bounded
// context; this function has no unbounded host operation of its own.
func PacmanVerify(ctx context.Context) (string, error) {
command := exec.CommandContext(ctx, "pacman", "-Qkk")
output, err := command.CombinedOutput()
return string(output), err
}