docs: rewrite dashy-soc-board.md with real verified evidence

Live-verified every specific claim directly against the real Node-RED
function source (not just trusted from the KB): the 104,443-char
function size, all 5 Wazuh finding categories, Vault's live PKI cert
endpoints, the real DNS Security Signals implementation, the
fleetAudit global pattern, the 12-app Slack channel map, the
acknowledge key format and localStorage key name, and the Mustache
triple-brace URL gotcha. Replaced both mermaid diagrams: the flat
data-sources fan-in became a real specifics table (redundant diagram
cut per the diagramming standing process), and the acknowledge-flow
diagram was rebuilt as a real Nextcloud diagram. Stamped both source
KB notes.
This commit is contained in:
thesabear committed 2026-08-28 09:58:49 +03:00
1 parent 8781ce7c11
commit 9368f26856
2 files changed
+35 -27

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Do not edit this file with editors other than draw.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" style="background: transparent; background-color: transparent; color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1142px" height="177px" viewBox="0 0 1142 177" content="&lt;mxfile host=&quot;embed.diagrams.net&quot; scale=&quot;1&quot; border=&quot;0&quot;&gt;&#10; &lt;diagram name=&quot;Acknowledge Mechanism&quot; id=&quot;ack-mech-1&quot;&gt;&#10; &lt;mxGraphModel dx=&quot;1414&quot; dy=&quot;791&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;1100&quot; pageHeight=&quot;380&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&#10; &lt;root&gt;&#10; &lt;mxCell id=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;1&quot; parent=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;finding&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;Finding&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size: 10px;&amp;quot;&amp;gt;stable content-derived key&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;60&quot; width=&quot;170&quot; x=&quot;20&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;ack&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#666666;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;User acknowledges&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;60&quot; width=&quot;170&quot; x=&quot;240&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;storage&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;Browser localStorage&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size: 10px;&amp;quot;&amp;gt;flat map, per-browser&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;60&quot; width=&quot;170&quot; x=&quot;460&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;recompute&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;Live re-derive&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size: 10px;&amp;quot;&amp;gt;badges, issue strip, gauges&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;60&quot; width=&quot;180&quot; x=&quot;680&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;upstream&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontColor=#333333;fontSize=12;fontStyle=1;shadow=1;&quot; value=&quot;Wazuh / Slack / any&amp;lt;br&amp;gt;&amp;lt;font style=&amp;quot;font-size: 10px;&amp;quot;&amp;gt;upstream source&amp;lt;/font&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#10; &lt;mxGeometry height=&quot;60&quot; width=&quot;180&quot; x=&quot;970&quot; y=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;e1&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;finding&quot; style=&quot;html=1;endArrow=block;strokeColor=#666666;strokeWidth=2;rounded=1;&quot; target=&quot;ack&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;e2&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;ack&quot; style=&quot;html=1;endArrow=block;strokeColor=#666666;strokeWidth=2;rounded=1;&quot; target=&quot;storage&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;e3&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;storage&quot; style=&quot;html=1;endArrow=block;strokeColor=#666666;strokeWidth=2;rounded=1;&quot; target=&quot;recompute&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;e4&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;recompute&quot; style=&quot;html=1;labelBackgroundColor=#1C1D1E;endArrow=block;dashed=1;strokeColor=#b85450;fontColor=#b85450;strokeWidth=2;rounded=1;fontSize=12;&quot; target=&quot;upstream&quot; value=&quot;&amp;lt;span style=&amp;quot;background-color: light-dark(rgb(255, 255, 255), rgb(28, 29, 30));&amp;quot;&amp;gt;never tLine truncated

After

Width:  |  Height:  |  Size: 207 KiB

+31 -27
View File
@@ -5,26 +5,28 @@
## Fourteen data sources, one board
```mermaid
flowchart TD
subgraph Sources["Data sources"]
direction LR
Wazuh["Wazuh indexer<br/>+ compliance API"]
Loki["Loki<br/>24h error counts"]
Prom["Prometheus<br/>CPU/RAM/disk"]
Slack["Slack channels<br/>per app + per host"]
Orch["Kestra / n8n APIs<br/>execution health"]
Audit["Fleet config-drift<br/>audit feed"]
Hyper["Proxmox API<br/>VM/LXC tree"]
Vault["Vault<br/>seal state, lease counts"]
SSO["Authentik<br/>login/failure stats"]
DNS["Technitium<br/>query stats, blocklist"]
Backup["Backup server<br/>job age/status"]
FW["pfSense<br/>state table, top rules"]
end
This board didn't start from nothing — it absorbed several formerly-standalone Dashy pages (a Dependency Tiers page, a Wazuh overview, an Authentik page, Kestra/n8n execution health) as per-host expandable sub-sections instead. There's no separate Wazuh-only or Dependency-Tiers-only page anymore; both live only inside this one board now. A representative slice of what each source actually contributes, confirmed by reading the real function's source, not paraphrased from what it's assumed to do:
Sources --> Function["One Node-RED function<br/>~104KB source"]
Function --> Board["SOC Board<br/>client-rendered"]
| Source | What it actually contributes |
|---|---|
| Wazuh indexer | 7-day findings, categorized: ClamAV, vulnerabilities, rootkit/anomaly, authentication, other |
| Vault | Seal state, active lease/token counts, KV engine counts, token TTL distribution, and root/intermediate CA cert expiry — parsed live from Vault's own `pki/cert/ca` and `pki_int/cert/ca` endpoints on every load, not a cached value |
| Technitium DNS | Query stats, top clients/domains, blocklist state, plus a dedicated "DNS Security Signals" section: forwarder failures (read from the Loki-shipped query log), query-log ingestion lag, next blocklist update time |
| Fleet config-drift audit | *Not* computed by this board's own function — read from Node-RED's shared `fleetAudit` global, populated by a separate ingestion process, covering 4 finding types: untracked stacks, missing declared directories, containers stuck in `Created`, and TZ drift |
| Slack | 12 app-specific channels (the *arr suite, Jellyfin/Emby/Seerr, Kopia, Authentik, Gitea, Netbox, Grafana) mapped to their owning host by a plain lookup table, plus each host's own alert channel |
| Proxmox `cluster/resources` API | The two hypervisor nodes' own CPU/RAM/disk (no Prometheus coverage on bare metal) plus the full running VM/LXC tree per node |
This is the real function, not a claimed size — one single Node-RED function, not several smaller ones stitched together:
```
$ python3 -c "
import json
data = json.load(open('flows.json'))
for n in data:
if n.get('type') == 'function' and n.get('name') == 'Build SOC Board HTML':
print(n['name'], '->', len(n['func']), 'chars')
"
Build SOC Board HTML -> 104443 chars
```
## Why one function instead of native Dashy widgets
@@ -40,16 +42,18 @@ A top row shows a 6-tier infrastructure dependency ladder (mirroring the trust-b
## Acknowledge mechanism: client-side, no backend
```mermaid
flowchart LR
Finding["Finding<br/>stable content-derived key"] --> Ack["User acknowledges"]
Ack --> Storage["Browser localStorage<br/>flat map, per-browser"]
Storage --> Recompute["Live re-derive:<br/>badges, issue strip, gauges"]
Recompute -.never touches.-> Upstream["Wazuh / Slack / any<br/>upstream source"]
![A finding's stable key (hostId::group::title) gets acknowledged into the browser's localStorage as a flat map, which live re-derives the host tile badge, the Active Issues strip, and any open detail panel's gauges — and never touches Wazuh, Slack, or any other upstream source](../assets/dashy-soc-board-acknowledge.svg)
This is the real key format and storage, confirmed directly against the function source, not a description of the design:
```
const key = `${hostId}::${group}::${it.title}`;
...
localStorage.getItem("socAckedV2")
```
> [!IMPORTANT]
> Acknowledging a finding is a **display filter only** — it never writes back to Wazuh, Slack, or any upstream source described elsewhere in this repo, is scoped per-browser rather than fleet-wide, and a full data set for every host is always kept in memory so the recompute has everything it needs regardless of which panel happens to be open. A reload in a different browser shows the finding again. This was a deliberate tradeoff favoring simplicity — no backend state to build, secure, or back up — over persistence, made because the board's job is triage visibility, not an audit trail of who acknowledged what (that's what Wazuh's own alert log in [`docs/security-monitoring.md`](security-monitoring.md) is for).
> Acknowledging a finding is a **display filter only** — it never writes back to Wazuh, Slack, or any upstream source described elsewhere in this repo, is scoped per-browser rather than fleet-wide, and a full data set for every host is always kept in memory (in inert template elements, not just whichever panel is currently open) so the recompute has everything it needs regardless of which panel happens to be open. A reload in a different browser shows the finding again. This was a deliberate tradeoff favoring simplicity — no backend state to build, secure, or back up — over persistence, made because the board's job is triage visibility, not an audit trail of who acknowledged what (that's what Wazuh's own alert log in [`docs/security-monitoring.md`](security-monitoring.md) is for).
## Real integration gotchas hit building this
@@ -58,7 +62,7 @@ flowchart LR
> - Embedding via `fetch` + `innerHTML` means injected `<script>` tags never execute — each one has to be explicitly re-created on every refresh to keep interactive elements working.
> - A background poll that could clobber an open detail panel needs to check "is a panel open" **both** before *and* after the fetch, since the aggregation endpoint itself can take tens of seconds to respond given fourteen upstream calls.
> - Gitea's own package-list API hard-caps results per page regardless of a higher requested limit — real pagination has to be implemented, not assumed away, or the registry data from [`docs/registry-and-build.md`](registry-and-build.md) silently truncates.
> - Node-RED's Mustache templating scoped to the wrong object silently produces an empty string instead of erroring — an easy, quiet failure mode that looks like an upstream API problem until you check the template scoping.
> - Node-RED's Mustache templating in a core node's URL field is scoped to `msg` directly, not `msg.msg` — the correct reference is `{{{url}}}`, not `{{{msg.url}}}`, and it needs the triple-brace form specifically for values containing characters like `&` that the double-brace form would HTML-escape. Get either wrong and the request silently goes to an empty or mangled URL instead of erroring.
> - Replacing a bind-mounted config file's content doesn't get picked up by a plain container restart — the container keeps watching the old inode; a full recreate is required, the same gotcha that shows up in [`docs/registry-and-build.md`](registry-and-build.md)'s redeploy path.
## Widget design: shape follows data, not one template everywhere