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
|
|
@ -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>';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue