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
99 lines
3.4 KiB
Go
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
|
|
}
|