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
This commit is contained in:
parent
1d1dee7f6e
commit
d835386381
43 changed files with 3419 additions and 72 deletions
99
go-agent/internal/fim/pkgverify.go
Normal file
99
go-agent/internal/fim/pkgverify.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue