feat(go): report integrity anchor context
This commit is contained in:
parent
88d7a6e3b8
commit
a25c73ab1a
6 changed files with 68 additions and 8 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
# Go Port Handoff
|
# Go Port Handoff
|
||||||
|
|
||||||
Saved: 2026-07-22T02:48:00-07:00
|
Saved: 2026-07-22T03:00:00-07:00
|
||||||
Branch: `main`
|
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
|
Status: implemented and green, but uncommitted
|
||||||
|
|
||||||
## Worktree warning
|
## Worktree warning
|
||||||
|
|
@ -12,8 +12,8 @@ continuations. Do not reset, clean, or broadly restage it.
|
||||||
|
|
||||||
- The validation-sidecar, incident-persistence, bounded-enrichment, local-
|
- The validation-sidecar, incident-persistence, bounded-enrichment, local-
|
||||||
assurance, asynchronous-enrichment, and incident-reader tranches are signed
|
assurance, asynchronous-enrichment, and incident-reader tranches are signed
|
||||||
commits `f85c2e8`, `538d1d9`, `fd2bbba`, `1ab4add`, `eff31b3`, and
|
commits `f85c2e8`, `538d1d9`, `fd2bbba`, `1ab4add`, `eff31b3`, `6355fb4`,
|
||||||
`6355fb4`. The package-ownership enrichment slice is uncommitted.
|
and `88d7a6e`. The integrity-anchor enrichment slice is uncommitted.
|
||||||
- At this checkpoint, `git status --short` has 20 entries with untracked
|
- At this checkpoint, `git status --short` has 20 entries with untracked
|
||||||
directories collapsed.
|
directories collapsed.
|
||||||
- The Python GUI files and tests are separate pre-existing work. Preserve them
|
- 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
|
- The asynchronous worker now resolves pacman/dpkg/rpm package-owner strings
|
||||||
with a two-second bound and cache; ownership lookup never holds a detector
|
with a two-second bound and cache; ownership lookup never holds a detector
|
||||||
lock or blocks alert capture.
|
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 <id>` now read the sidecar's isolated
|
- `--incidents-list` and `--incident-show <id>` now read the sidecar's isolated
|
||||||
state without starting the agent. The latter returns `enodia.incident.view.v1`
|
state without starting the agent. The latter returns `enodia.incident.view.v1`
|
||||||
with the incident, available snapshots, and time-ordered timeline.
|
with the incident, available snapshots, and time-ordered timeline.
|
||||||
|
|
@ -154,8 +157,9 @@ the suite still exits successfully.
|
||||||
|
|
||||||
## Resume here
|
## Resume here
|
||||||
|
|
||||||
1. Add bounded integrity-anchor collectors to the existing asynchronous worker
|
1. Port the actual FIM/package-verification/rootcheck engines behind explicit,
|
||||||
without destructive response behavior.
|
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
|
2. Add a read-only management API consumer for isolated Go snapshot/event state
|
||||||
while keeping Python authoritative.
|
while keeping Python authoritative.
|
||||||
3. Continue broader Phase 3 rule metadata/state parity before considering any
|
3. Continue broader Phase 3 rule metadata/state parity before considering any
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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;
|
and pacman/dpkg/rpm package-owner strings with a two-second query bound;
|
||||||
saturation leaves fields unknown rather than delaying alerts. Richer integrity
|
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`
|
revisions append synchronized `enodia.hash_chain.v1`
|
||||||
records to a private local `hash-chain.jsonl`.
|
records to a private local `hash-chain.jsonl`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
|
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
|
||||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/netutil"
|
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/netutil"
|
||||||
|
|
@ -45,6 +47,7 @@ type Process struct {
|
||||||
type SlowResult struct {
|
type SlowResult struct {
|
||||||
Processes map[int]map[string]any
|
Processes map[int]map[string]any
|
||||||
Files map[string]map[string]any
|
Files map[string]map[string]any
|
||||||
|
Integrity map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
func CollectSlow(processes []Process, paths []string, owner ...func(string) string) SlowResult {
|
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
|
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 {
|
func boundedHash(path string) any {
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil || !info.Mode().IsRegular() || info.Size() > MaxHashBytes {
|
if err != nil || !info.Mode().IsRegular() || info.Size() > MaxHashBytes {
|
||||||
|
|
|
||||||
|
|
@ -74,3 +74,19 @@ func TestCollectSlowAddsInjectedPackageOwnership(t *testing.T) {
|
||||||
t.Fatalf("result=%#v", result)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ func (s *Store) applySlowEnrichment(job slowJob) {
|
||||||
processes := enrichmentProcesses(report.Processes)
|
processes := enrichmentProcesses(report.Processes)
|
||||||
paths := enrichmentPaths(report.Enrichment)
|
paths := enrichmentPaths(report.Enrichment)
|
||||||
updates := enrich.CollectSlow(processes, paths, provenance.PackageOwner)
|
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
|
// 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.
|
// 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 {
|
func nullToNil(value any) any {
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,11 @@ func TestSlowEnrichmentAddsBoundedHashAndFileMetadata(t *testing.T) {
|
||||||
if file["exists"] != true || file["mode"] != float64(0o600) {
|
if file["exists"] != true || file["mode"] != float64(0o600) {
|
||||||
t.Fatalf("file enrichment=%#v", file)
|
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"))
|
logRaw, err := os.ReadFile(filepath.Join(store.Dir, "alert-20260722-101112.log"))
|
||||||
if err != nil || !strings.Contains(string(logRaw), "ENODIA SENTINEL ALERT") {
|
if err != nil || !strings.Contains(string(logRaw), "ENODIA SENTINEL ALERT") {
|
||||||
t.Fatalf("slow log err=%v text=%q", err, logRaw)
|
t.Fatalf("slow log err=%v text=%q", err, logRaw)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue