diff --git a/docs/GO_PORT_HANDOFF.md b/docs/GO_PORT_HANDOFF.md index a019a15..011dc12 100644 --- a/docs/GO_PORT_HANDOFF.md +++ b/docs/GO_PORT_HANDOFF.md @@ -1,8 +1,8 @@ # Go Port Handoff -Saved: 2026-07-22T02:48:00-07:00 +Saved: 2026-07-22T03:00:00-07:00 Branch: `main` -Base commit: `6355fb4` (`feat(go): expose retained incident views`) +Base commit: `88d7a6e` (`feat(go): enrich snapshots with package ownership`) Status: implemented and green, but uncommitted ## Worktree warning @@ -12,8 +12,8 @@ continuations. Do not reset, clean, or broadly restage it. - The validation-sidecar, incident-persistence, bounded-enrichment, local- assurance, asynchronous-enrichment, and incident-reader tranches are signed - commits `f85c2e8`, `538d1d9`, `fd2bbba`, `1ab4add`, `eff31b3`, and - `6355fb4`. The package-ownership enrichment slice is uncommitted. + commits `f85c2e8`, `538d1d9`, `fd2bbba`, `1ab4add`, `eff31b3`, `6355fb4`, + and `88d7a6e`. The integrity-anchor enrichment slice is uncommitted. - At this checkpoint, `git status --short` has 20 entries with untracked directories collapsed. - The Python GUI files and tests are separate pre-existing work. Preserve them @@ -106,7 +106,10 @@ static binary. - The asynchronous worker now resolves pacman/dpkg/rpm package-owner strings with a two-second bound and cache; ownership lookup never holds a detector lock or blocks alert capture. -- Richer integrity anchors and notification fan-out are not yet ported. +- The worker now reports FIM-baseline, package-DB-anchor, and Go hash-chain + presence/age. It explicitly marks package verification and rootcheck disabled + until their actual engines are ported. +- Richer integrity engines and notification fan-out are not yet ported. - `--incidents-list` and `--incident-show ` now read the sidecar's isolated state without starting the agent. The latter returns `enodia.incident.view.v1` with the incident, available snapshots, and time-ordered timeline. @@ -154,8 +157,9 @@ the suite still exits successfully. ## Resume here -1. Add bounded integrity-anchor collectors to the existing asynchronous worker - without destructive response behavior. +1. Port the actual FIM/package-verification/rootcheck engines behind explicit, + bounded acceptance gates; do not treat enrichment anchor status as detection + parity. 2. Add a read-only management API consumer for isolated Go snapshot/event state while keeping Python authoritative. 3. Continue broader Phase 3 rule metadata/state parity before considering any diff --git a/go-agent/README.md b/go-agent/README.md index 14362c2..8be2bd8 100644 --- a/go-agent/README.md +++ b/go-agent/README.md @@ -119,7 +119,10 @@ from already-captured data without new I/O. A 16-job asynchronous worker then adds streamed SHA-256 values for regular executables up to 8 MiB, file metadata, and pacman/dpkg/rpm package-owner strings with a two-second query bound; saturation leaves fields unknown rather than delaying alerts. Richer integrity -anchors and notifications remain future parity work. Snapshot text/JSON +engines and notifications remain future parity work. The worker does report +truthful FIM-baseline, package-DB-anchor, and Go hash-chain presence/age while +explicitly marking unported package verification and rootcheck as disabled. +Snapshot text/JSON revisions append synchronized `enodia.hash_chain.v1` records to a private local `hash-chain.jsonl`. diff --git a/go-agent/internal/enrich/enrich.go b/go-agent/internal/enrich/enrich.go index 35f447c..0a8a4ca 100644 --- a/go-agent/internal/enrich/enrich.go +++ b/go-agent/internal/enrich/enrich.go @@ -10,10 +10,12 @@ import ( "encoding/hex" "io" "os" + "path/filepath" "regexp" "sort" "strings" "syscall" + "time" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/netutil" @@ -45,6 +47,7 @@ type Process struct { type SlowResult struct { Processes map[int]map[string]any Files map[string]map[string]any + Integrity map[string]any } func CollectSlow(processes []Process, paths []string, owner ...func(string) string) SlowResult { @@ -68,6 +71,31 @@ func CollectSlow(processes []Process, paths []string, owner ...func(string) stri return result } +// IntegrityAnchors reports durable state presence without running FIM, package +// verification, or rootcheck on the alert path. Those engines stay disabled +// until their own Go parity tranches land. +func IntegrityAnchors(directory string) map[string]any { + return map[string]any{ + "fim_baseline": pathStatus(filepath.Join(directory, "fim-baseline.json")), + "pkgdb_anchor": pathStatus(filepath.Join(directory, "pkgdb-anchor.json")), + "package_verify": map[string]any{"enabled": false, "sample": 0}, + "rootcheck": map[string]any{"enabled": false, "interval": 0}, + "go_assurance": pathStatus(filepath.Join(directory, "hash-chain.jsonl")), + } +} + +func pathStatus(path string) map[string]any { + info, err := os.Stat(path) + if err != nil { + return map[string]any{"path": path, "status": "missing", "age": nil} + } + age := int64(time.Since(info.ModTime()).Seconds()) + if age < 0 { + age = 0 + } + return map[string]any{"path": path, "status": "present", "age": age} +} + func boundedHash(path string) any { info, err := os.Stat(path) if err != nil || !info.Mode().IsRegular() || info.Size() > MaxHashBytes { diff --git a/go-agent/internal/enrich/enrich_test.go b/go-agent/internal/enrich/enrich_test.go index df285e2..cf4161e 100644 --- a/go-agent/internal/enrich/enrich_test.go +++ b/go-agent/internal/enrich/enrich_test.go @@ -74,3 +74,19 @@ func TestCollectSlowAddsInjectedPackageOwnership(t *testing.T) { t.Fatalf("result=%#v", result) } } + +func TestIntegrityAnchorsAreTruthfulAndReadOnly(t *testing.T) { + directory := t.TempDir() + anchors := IntegrityAnchors(directory) + if anchors["fim_baseline"].(map[string]any)["status"] != "missing" || + anchors["rootcheck"].(map[string]any)["enabled"] != false { + t.Fatalf("anchors=%#v", anchors) + } + if err := os.WriteFile(filepath.Join(directory, "hash-chain.jsonl"), []byte("{}\n"), 0o600); err != nil { + t.Fatal(err) + } + anchors = IntegrityAnchors(directory) + if anchors["go_assurance"].(map[string]any)["status"] != "present" { + t.Fatalf("anchors=%#v", anchors) + } +} diff --git a/go-agent/internal/snapshot/store.go b/go-agent/internal/snapshot/store.go index 2f7513a..d4200de 100644 --- a/go-agent/internal/snapshot/store.go +++ b/go-agent/internal/snapshot/store.go @@ -291,6 +291,7 @@ func (s *Store) applySlowEnrichment(job slowJob) { processes := enrichmentProcesses(report.Processes) paths := enrichmentPaths(report.Enrichment) updates := enrich.CollectSlow(processes, paths, provenance.PackageOwner) + updates.Integrity = enrich.IntegrityAnchors(s.Dir) // All slow I/O has completed. Take the lock only to merge into the newest // same-second report, so a concurrent alert capture never loses evidence. @@ -361,6 +362,9 @@ func mergeSlow(report *Report, updates enrich.SlowResult) { } } } + if updates.Integrity != nil { + report.Enrichment["integrity"] = updates.Integrity + } } func nullToNil(value any) any { diff --git a/go-agent/internal/snapshot/store_test.go b/go-agent/internal/snapshot/store_test.go index 40d3ab9..4442ae6 100644 --- a/go-agent/internal/snapshot/store_test.go +++ b/go-agent/internal/snapshot/store_test.go @@ -171,6 +171,11 @@ func TestSlowEnrichmentAddsBoundedHashAndFileMetadata(t *testing.T) { if file["exists"] != true || file["mode"] != float64(0o600) { t.Fatalf("file enrichment=%#v", file) } + integrity := report.Enrichment["integrity"].(map[string]any) + if integrity["go_assurance"].(map[string]any)["status"] != "present" || + integrity["rootcheck"].(map[string]any)["enabled"] != false { + t.Fatalf("integrity enrichment=%#v", integrity) + } logRaw, err := os.ReadFile(filepath.Join(store.Dir, "alert-20260722-101112.log")) if err != nil || !strings.Contains(string(logRaw), "ENODIA SENTINEL ALERT") { t.Fatalf("slow log err=%v text=%q", err, logRaw)