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:
Luna 2026-07-24 09:59:09 -07:00
parent 1d1dee7f6e
commit d835386381
No known key found for this signature in database
43 changed files with 3419 additions and 72 deletions

View file

@ -313,6 +313,7 @@
<button data-tab="alerts">Alerts</button>
<button data-tab="posture">Posture</button>
<button data-tab="integrity">Integrity</button>
<button data-tab="sidecar">Sidecar</button>
<button data-tab="rules">Rules</button>
<button data-tab="events">Events</button>
</div>
@ -320,6 +321,7 @@
<section id="alertsTab" hidden></section>
<section id="postureTab" hidden></section>
<section id="integrityTab" hidden></section>
<section id="sidecarTab" hidden></section>
<section id="rulesTab" hidden></section>
<section id="eventsTab" hidden></section>
</main>
@ -333,7 +335,7 @@
<script>
const TOKEN = new URLSearchParams(location.search).get("token") || "";
const HDR = TOKEN ? {Authorization:"Bearer "+TOKEN} : {};
const TABS = new Set(["incident","alerts","posture","integrity","rules","events"]);
const TABS = new Set(["incident","alerts","posture","integrity","sidecar","rules","events"]);
const THEME_OPTIONS = [
["console","Console"],["paper","Paper"],["contrast","Contrast"],
["pride","LGBTQ"],["trans","Trans"],["dracula","Dracula"],
@ -346,7 +348,7 @@ function tabFromHash(){
const name = location.hash.replace(/^#/,"");
return TABS.has(name) ? name : "incident";
}
const state = {incidents:[], alerts:[], posture:null, integrity:null, rules:null, selected:null, incident:null, plan:null, tab:tabFromHash()};
const state = {incidents:[], alerts:[], posture:null, integrity:null, rules:null, sidecar:null, selected:null, incident:null, plan:null, tab:tabFromHash()};
async function api(path){
const r = await fetch(path,{headers:HDR});
@ -398,10 +400,10 @@ function setSettings(open){
async function refresh(){
try{
const [status, incidents, alerts, posture, integrity, rules, events] = await Promise.all([
api("/api/status"), api("/api/incidents"), api("/api/alerts"), api("/api/posture"), api("/api/integrity"), api("/api/rules"), api("/api/events")
const [status, incidents, alerts, posture, integrity, rules, events, sidecar] = await Promise.all([
api("/api/status"), api("/api/incidents"), api("/api/alerts"), api("/api/posture"), api("/api/integrity"), api("/api/rules"), api("/api/events"), loadSidecar()
]);
state.incidents = incidents; state.alerts = alerts; state.posture = posture; state.integrity = integrity; state.rules = rules; state.events = events.events || [];
state.incidents = incidents; state.alerts = alerts; state.posture = posture; state.integrity = integrity; state.rules = rules; state.events = events.events || []; state.sidecar = sidecar;
renderStatus(status); renderLists();
if(!state.selected && incidents.length) await openIncident(incidents[0].id);
else if(state.selected) await openIncident(state.selected, false);
@ -409,6 +411,20 @@ async function refresh(){
}catch(e){ showError(e.message); }
}
async function loadSidecar(){
// The migration evidence tree is optional. A sidecar read failure must not
// hide the Python daemon's authoritative console data or break refresh. Keep
// its requests separate so a future sidecar-only endpoint cannot accidentally
// become a dependency of the production management surface.
try{
const [status, alerts, incidents, events] = await Promise.all([
api("/api/go-sidecar/status"), api("/api/go-sidecar/alerts"),
api("/api/go-sidecar/incidents"), api("/api/go-sidecar/events")
]);
return {status, alerts, incidents, events, error:null};
}catch(e){ return {status:null, alerts:[], incidents:[], events:{events:[]}, error:e.message}; }
}
function renderStatus(st){
document.getElementById("dot").className = "dot "+(st.running && !st.heartbeat_stale ? "up" : "");
let hb = st.heartbeat_age == null ? "no heartbeat" : Math.round(st.heartbeat_age)+"s heartbeat";
@ -473,6 +489,7 @@ async function openAlert(name){
box.hidden = false; document.getElementById("incidentTab").hidden = true;
document.getElementById("postureTab").hidden = true; document.getElementById("rulesTab").hidden = true;
document.getElementById("integrityTab").hidden = true;
document.getElementById("sidecarTab").hidden = true;
document.getElementById("eventsTab").hidden = true;
box.innerHTML='<div class="empty">loading snapshot</div>';
try{
@ -524,11 +541,13 @@ function renderTab(){
document.getElementById("alertsTab").hidden = state.tab !== "alerts";
document.getElementById("postureTab").hidden = state.tab !== "posture";
document.getElementById("integrityTab").hidden = state.tab !== "integrity";
document.getElementById("sidecarTab").hidden = state.tab !== "sidecar";
document.getElementById("rulesTab").hidden = state.tab !== "rules";
document.getElementById("eventsTab").hidden = state.tab !== "events";
if(state.tab === "incident") renderIncident();
if(state.tab === "posture") renderPosture();
if(state.tab === "integrity") renderIntegrity();
if(state.tab === "sidecar") renderSidecar();
if(state.tab === "rules") renderRules();
if(state.tab === "events") renderEvents();
}
@ -678,6 +697,76 @@ function renderIntegrity(){
box.append(grid);
}
function renderSidecar(){
// This is intentionally a read-only ledger, not a second incident workflow.
// Operators should not mistake validation-sidecar evidence for Python's
// production response plans, acknowledgements, or remediation authority.
const box = document.getElementById("sidecarTab");
const source = state.sidecar || {status:null, alerts:[], incidents:[], events:{events:[]}};
box.innerHTML = "";
if(source.error){
box.append(el("div","err","Go sidecar evidence is unavailable: "+source.error));
return;
}
const status = source.status || {};
if(!status.configured){
const empty = el("div","card");
empty.append(el("h2",null,"Go sidecar evidence"));
empty.append(el("div","fine","No migration evidence tree is configured. Set go_sidecar_state_dir to an isolated Go sidecar state directory; this console will only read its retained heartbeat, snapshots, incidents, and event log."));
box.append(empty);
return;
}
const summary = el("div","card");
summary.append(el("h2",null,"Go sidecar evidence"));
const heartbeat = status.heartbeat || {};
summary.append(tags([
"read-only", "state "+(status.available ? "available" : "unavailable"),
"heartbeat "+(heartbeat.status || "unknown"),
"alerts "+(source.alerts||[]).length, "incidents "+(source.incidents||[]).length
]));
summary.append(el("div","fine","Migration evidence is shown separately. Python alert, incident, and response-plan data remain authoritative."));
box.append(summary);
const grid = el("div","integrity-grid");
const health = el("article","state-card "+(heartbeat.status === "fresh" ? "ok" : "review"));
health.append(el("h3",null,"Heartbeat"));
addStateLine(health,"status",heartbeat.status || "unknown");
addStateLine(health,"age",humanAge(heartbeat.age_seconds));
addStateLine(health,"max age",humanAge(heartbeat.max_age_seconds));
addStateLine(health,"state dir",status.state_dir);
grid.append(health);
const evidence = el("article","state-card ok");
evidence.append(el("h3",null,"Retained evidence"));
(source.alerts||[]).slice(0,6).forEach(alert=>{
const row = el("div","path-row");
row.append(sev(alert.severity));
row.append(el("span",null,fmt(alert.time)+" — "+(alert.signatures||[]).join(", ")));
evidence.append(row);
});
if(!(source.alerts||[]).length) evidence.append(el("div","fine","No retained sidecar alert snapshots."));
grid.append(evidence);
const incidents = el("article","state-card ok");
incidents.append(el("h3",null,"Incident index"));
(source.incidents||[]).slice(0,6).forEach(incident=>{
const row = el("div","path-row");
row.append(sev(incident.severity));
row.append(el("span",null,incident.id+" — "+(incident.signatures||[]).join(", ")));
incidents.append(row);
});
if(!(source.incidents||[]).length) incidents.append(el("div","fine","No retained sidecar incidents."));
grid.append(incidents);
box.append(grid);
const events = el("div","card");
events.append(el("h3",null,"Retained sidecar events"));
const pre = el("pre");
const rows = source.events?.events || [];
pre.textContent = rows.length ? rows.map(row=>JSON.stringify(row)).join("\n") : "No retained sidecar events.";
events.append(pre);
box.append(events);
}
function renderRules(){
const box = document.getElementById("rulesTab");
const catalog = state.rules || {rules:[], by_event:{}, by_severity:{}, by_origin:{}};
@ -726,6 +815,7 @@ function showError(msg){
document.getElementById("incidentTab").innerHTML = '<div class="err">'+msg+'</div>';
document.getElementById("postureTab").innerHTML = '<div class="err">'+msg+'</div>';
document.getElementById("integrityTab").innerHTML = '<div class="err">'+msg+'</div>';
document.getElementById("sidecarTab").innerHTML = '<div class="err">'+msg+'</div>';
document.getElementById("rulesTab").innerHTML = '<div class="err">'+msg+'</div>';
document.getElementById("plan").innerHTML = '<div class="err">'+msg+'</div>';
}