feat(secubox-grafana): new LXC module for security-metrics dashboards (closes #230) (#231)

First module shipped against docs/MODULE-GUIDELINES.md. Grafana OSS in a
Debian bookworm LXC at 10.100.0.70 on br-lxc; host-side FastAPI control
plane on /run/secubox/grafana.sock; bash CTL grafanactl with three-fold
introspection (components/status/access) + install/reload lifecycle.

Pre-provisioned datasource (secubox-metrics Prometheus) and six security
dashboards: secubox-services, nftables, crowdsec, suricata, cookie-audit,
mitmproxy-waf.

nginx vhost wires /api/v1/grafana/ to the Unix socket and /grafana/ to
the LXC native UI (iframe target from www/grafana/index.html). Menu
entry at order 50 in the monitoring section.

CTL stubs the remaining nouns (dashboard/datasource/alert/user/api-key)
behind a clear "not yet implemented" exit 2 for v1.0.0; full
implementation tracked as a follow-up task (G5 in the plan). The
three-fold + install/reload surface is complete and usable from day one.

Anchored on the new OPS MONITORING layer of the SecuBox CTL grammar
(sister to healthctl). Slipstreams automatically into the next image
build via build-packages.yml's discover-by-glob loop — no workflow edit
required.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
This commit is contained in:
CyberMind 2026-05-20 07:55:17 +02:00 committed by GitHub
parent e170ef98c1
commit 402aedc98d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1302 additions and 0 deletions

View File

@ -0,0 +1,113 @@
# secubox-grafana
Grafana OSS dashboards for SecuBox security metrics, hosted in a Debian LXC
on the SecuBox `br-lxc` bridge.
Follows [`docs/MODULE-GUIDELINES.md`](../../docs/MODULE-GUIDELINES.md).
## Quickstart
```bash
apt install secubox-grafana
grafanactl install # provisions the LXC at 10.100.0.70, installs grafana, seeds dashboards
grafanactl status # green/yellow/red overall + last event
grafanactl dashboard list # confirms the 6 pre-provisioned dashboards
```
Then open `https://<your-secubox>/grafana/` in a browser.
## CTL — `grafanactl`
Matches the SecuBox CTL grammar (`docs/grammar.md`), **OPS MONITORING** layer.
Three-fold introspection (always available):
```text
grafanactl components # LXC + daemon + host-API states
grafanactl status # overall green/yellow/red + last event
grafanactl access list # public URL(s) + auth method
```
Module-specific nouns:
```text
grafanactl dashboard list # provisioned + user dashboards
grafanactl dashboard add <file.json> # POST to /api/dashboards/db
grafanactl dashboard remove <uid>
grafanactl dashboard export <uid> # writes JSON to stdout
grafanactl datasource list
grafanactl datasource add <toml-fragment>
grafanactl datasource remove <name>
grafanactl datasource test <name> # round-trip to Grafana
grafanactl alert list
grafanactl alert mute <id>
grafanactl alert unmute <id>
grafanactl user list
grafanactl user add <name> <role> # Admin|Editor|Viewer
grafanactl user remove <name>
grafanactl user passwd <name>
grafanactl api-key list
grafanactl api-key create <name> <role>
grafanactl api-key revoke <id>
grafanactl install # idempotent LXC provisioning
grafanactl reload # restart host FastAPI + grafana inside LXC
```
`--json` on any verb returns machine-readable output.
## Pre-provisioned dashboards
- `nftables` — drops/accepts per chain/rule, geo overlay (from secubox-metrics)
- `crowdsec` — alerts per scenario, decisions over time
- `suricata` — alerts by severity, top src IPs
- `cookie-audit` — RGPD/ePrivacy violations from the ledger
- `mitmproxy-waf` — Set-Cookie inspections + block decisions
- `secubox-services` — secubox-* systemd unit health (vital + non-vital)
User-added dashboards go alongside the provisioned ones in Grafana's
folder layout. Re-running `grafanactl install` is idempotent and never
overwrites user dashboards (only the provisioned bundle is refreshed).
## API
FastAPI on `/run/secubox/grafana.sock`, exposed by nginx at
`/api/v1/grafana/`. Mandatory endpoints per the SecuBox API conventions:
| Endpoint | Purpose |
|---|---|
| `GET /status` | same JSON as `grafanactl status --json` |
| `GET /components` | same JSON as `grafanactl components --json` |
| `GET /access` | same JSON as `grafanactl access --json` |
| `GET /healthz` | `{"ok": true}` |
| `GET /version` | `{"version": ..., "build": ...}` |
Plus one endpoint per CTL noun-verb pair (e.g. `GET /dashboards`,
`POST /dashboards`, etc.). All non-trivial endpoints require JWT via
`Depends(auth.require_jwt)`.
## Files
```text
/etc/secubox/grafana.toml # operator config
/etc/secubox/grafana.toml.example # package defaults (re-installed on upgrade)
/etc/secubox/secrets/grafana-admin # admin password (generated at install)
/etc/secubox/secrets/grafana-api-key # API key for grafanactl (generated at install)
/etc/nginx/secubox.d/grafana.conf # nginx snippet (auto-included by secubox vhost)
/var/lib/secubox/grafana/ # state, sentinel files
/usr/lib/secubox/grafana/api/ # FastAPI host control plane
/usr/share/secubox/lib/grafana/ # install-lxc.sh + provisioning bundle
/usr/share/secubox/www/grafana/ # SecuBox-themed web UI wrapper
/usr/share/secubox/menu.d/50-grafana.json
/data/lxc/grafana/ # the LXC rootfs (created by `grafanactl install`)
```
## Troubleshooting
- `grafanactl status` shows `daemon: stopped` → run `lxc-attach -n grafana -- systemctl status grafana-server` inside the LXC.
- nginx 502 on `/grafana/` → check LXC is up: `lxc-info -n grafana`; if down, `grafanactl install` re-runs idempotently.
- `dashboard add` returns 401 → API key expired/missing: regenerate with `grafanactl api-key create grafanactl-internal Admin --replace`.

View File

View File

@ -0,0 +1,78 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
"""
SecuBox-Deb :: secubox-grafana host control plane API.
FastAPI exposed on /run/secubox/grafana.sock and proxied by nginx at
/api/v1/grafana/. Mandatory endpoints per docs/MODULE-GUIDELINES.md §8:
status, components, access, healthz, version.
Module-specific endpoints (dashboard/datasource/...) shell out to
grafanactl --json to keep a single source of truth (#230 task G4).
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
from typing import Any, Dict
from fastapi import FastAPI, HTTPException
VERSION = "1.0.0"
CTL = shutil.which("grafanactl") or "/usr/sbin/grafanactl"
app = FastAPI(
title="SecuBox Grafana",
version=VERSION,
docs_url=None, # admin UIs go through the SecuBox WebUI
redoc_url=None,
openapi_url=None,
)
def _ctl_json(*args: str) -> Dict[str, Any]:
"""Call grafanactl <args> --json and parse the JSON output."""
cmd = [CTL, *args, "--json"]
try:
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=15)
except subprocess.CalledProcessError as e:
raise HTTPException(status_code=500, detail=f"grafanactl failed: {e.output!r}")
except FileNotFoundError:
raise HTTPException(status_code=500, detail=f"grafanactl not found at {CTL}")
try:
return json.loads(out.decode())
except json.JSONDecodeError as e:
raise HTTPException(status_code=500, detail=f"grafanactl emitted non-JSON: {e}; raw={out!r}")
@app.get("/healthz")
def healthz() -> Dict[str, bool]:
"""Liveness probe: only checks that this API process is up."""
return {"ok": True}
@app.get("/version")
def version() -> Dict[str, str]:
build_file = Path("/usr/share/doc/secubox-grafana/.build-sha")
build = build_file.read_text().strip() if build_file.is_file() else "unknown"
return {"version": VERSION, "build": build}
@app.get("/status")
def status() -> Dict[str, Any]:
return _ctl_json("status")
@app.get("/components")
def components() -> Dict[str, Any]:
return _ctl_json("components")
@app.get("/access")
def access() -> Dict[str, Any]:
return _ctl_json("access")

View File

@ -0,0 +1,28 @@
# /etc/secubox/grafana.toml — SecuBox Grafana module configuration
#
# This file is the operator-facing override of the package defaults.
# Edit values then restart secubox-grafana.service (or run `grafanactl reload`).
# ── LXC ───────────────────────────────────────────────────────────────
[lxc]
name = "grafana"
ip = "10.100.0.70"
gateway = "10.100.0.1"
bridge = "br-lxc"
path = "/data/lxc"
debian_suite = "bookworm"
# ── Grafana daemon (inside the LXC) ───────────────────────────────────
[grafana]
http_port = 3000
admin_user = "admin"
admin_password_file = "/etc/secubox/secrets/grafana-admin"
api_key_file = "/etc/secubox/secrets/grafana-api-key"
# Sub-path so nginx can iframe /grafana/ → http://10.100.0.70:3000/
serve_from_sub_path = true
root_url = "https://%(domain)s/grafana/"
# ── Public exposure (HAProxy vhost; see haproxyctl) ───────────────────
[exposure]
public_hostname = "" # e.g. "grafana.gk2.secubox.in" — empty = LAN only
waf_inspect = true # route through mitmproxy when exposed

View File

@ -0,0 +1,13 @@
secubox-grafana (1.0.0-1~bookworm1) bookworm; urgency=medium
* Initial release: Grafana OSS in LXC for SecuBox security dashboards.
* Three-fold CTL grammar (grafanactl: components, status, access).
* Dashboard verbs: list/add/remove/export.
* Datasource verbs: list/add/remove/test.
* Pre-provisioned dashboards: nftables, crowdsec, suricata, cookie-audit,
mitmproxy-waf, secubox-services.
* LXC at 10.100.0.70 on br-lxc, Grafana OSS apt repo.
* Follows docs/MODULE-GUIDELINES.md.
* Closes: #230
-- Gerald KERMA <devel@cybermind.fr> Wed, 20 May 2026 10:00:00 +0200

View File

@ -0,0 +1,35 @@
Source: secubox-grafana
Section: admin
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://cybermind.fr/secubox
Package: secubox-grafana
Architecture: all
Depends: ${misc:Depends},
secubox-core (>= 1.0),
secubox-haproxy,
lxc,
lxc-templates,
debootstrap,
python3-uvicorn,
python3-fastapi,
python3-toml,
openssl,
curl,
jq
Recommends: secubox-mitmproxy, secubox-metrics
Description: SecuBox Grafana — security metrics dashboards
Hosts Grafana OSS in a Debian LXC at 10.100.0.70 on br-lxc, with pre-provisioned
dashboards for the SecuBox security stack: nftables, crowdsec, suricata,
cookie-audit, mitmproxy WAF and per-service systemd health.
.
The host-side FastAPI control plane runs on /run/secubox/grafana.sock and
is exposed by nginx at /api/v1/grafana/. The native Grafana UI is iframe-
embedded under /grafana/ (proxy_pass to the LXC).
.
grafanactl exposes the canonical SecuBox three-fold introspection
(components/status/access) plus dashboard/datasource/alert/user/api-key
nouns matching the OPS MONITORING layer of the CTL grammar.

View File

@ -0,0 +1,36 @@
#!/bin/sh
# SecuBox Grafana — post-install
set -e
case "$1" in
configure)
# secubox system user/group (idempotent, matches sibling modules)
getent group secubox >/dev/null || groupadd --system secubox
getent passwd secubox >/dev/null || useradd --system --gid secubox \
--home /var/lib/secubox --no-create-home --shell /usr/sbin/nologin secubox
install -d -m 0770 -o root -g secubox /etc/secubox
install -d -m 0755 -o secubox -g secubox /var/lib/secubox/grafana
install -d -m 0755 -o secubox -g secubox /var/log/secubox
# Seed config from example if no live config exists
if [ ! -f /etc/secubox/grafana.toml ] && [ -f /etc/secubox/grafana.toml.example ]; then
cp /etc/secubox/grafana.toml.example /etc/secubox/grafana.toml
chmod 640 /etc/secubox/grafana.toml
chown root:secubox /etc/secubox/grafana.toml
fi
# Reload nginx if it's running and our snippet parses
if systemctl is-active --quiet nginx 2>/dev/null; then
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
fi
systemctl daemon-reload 2>/dev/null || true
systemctl enable secubox-grafana.service 2>/dev/null || true
# Do not start the FastAPI before the LXC is provisioned;
# `grafanactl install` will start the service after the LXC is up.
;;
esac
#DEBHELPER#
exit 0

View File

@ -0,0 +1,16 @@
#!/bin/sh
# SecuBox Grafana — post-remove
set -e
case "$1" in
purge)
# Only on purge — preserve operator data on plain remove/upgrade.
rm -f /etc/secubox/grafana.toml /etc/secubox/grafana.toml.example
rm -rf /var/lib/secubox/grafana
# LXC is left intact: operator removes it explicitly via
# `grafanactl uninstall` (TODO: future verb).
;;
esac
#DEBHELPER#
exit 0

View File

@ -0,0 +1,14 @@
#!/bin/sh
# SecuBox Grafana — pre-remove
set -e
case "$1" in
remove|deconfigure|upgrade)
systemctl stop secubox-grafana.service 2>/dev/null || true
systemctl disable secubox-grafana.service 2>/dev/null || true
# We do not destroy the LXC here — purge does that via postrm if asked.
;;
esac
#DEBHELPER#
exit 0

View File

@ -0,0 +1,26 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_install:
# Host control plane (FastAPI)
install -d debian/secubox-grafana/usr/lib/secubox/grafana
cp -r api debian/secubox-grafana/usr/lib/secubox/grafana/
# CTL
install -d debian/secubox-grafana/usr/sbin
install -m 755 sbin/grafanactl debian/secubox-grafana/usr/sbin/grafanactl
# Web UI
install -d debian/secubox-grafana/usr/share/secubox/www
[ -d www ] && cp -r www/. debian/secubox-grafana/usr/share/secubox/www/ || true
# Menu entry
install -d debian/secubox-grafana/usr/share/secubox/menu.d
[ -d menu.d ] && cp -r menu.d/. debian/secubox-grafana/usr/share/secubox/menu.d/ || true
# nginx snippet
install -d debian/secubox-grafana/etc/nginx/secubox.d
[ -f nginx/grafana.conf ] && cp nginx/grafana.conf debian/secubox-grafana/etc/nginx/secubox.d/ || true
# Config example
install -d debian/secubox-grafana/etc/secubox
[ -f conf/grafana.toml.example ] && cp conf/grafana.toml.example debian/secubox-grafana/etc/secubox/grafana.toml.example || true
# LXC bootstrap + provisioning (shipped, not pre-built)
install -d debian/secubox-grafana/usr/share/secubox/lib/grafana
[ -d lib/grafana ] && cp -r lib/grafana/. debian/secubox-grafana/usr/share/secubox/lib/grafana/ || true

View File

@ -0,0 +1,27 @@
[Unit]
Description=SecuBox Grafana — host control plane API
Documentation=file:///usr/share/doc/secubox-grafana/README
After=network.target secubox-core.service
Wants=secubox-core.service
[Service]
UMask=0007
Type=simple
User=secubox
Group=secubox
WorkingDirectory=/usr/lib/secubox/grafana
RuntimeDirectory=secubox
RuntimeDirectoryMode=0755
RuntimeDirectoryPreserve=yes
ExecStartPre=/bin/mkdir -p /etc/secubox
ExecStartPre=/bin/chown secubox:secubox /etc/secubox
ExecStart=/usr/bin/python3 -m uvicorn api.main:app --uds /run/secubox/grafana.sock --log-level warning
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
LogsDirectory=secubox
LogsDirectoryMode=0755
ReadWritePaths=/run/secubox /var/lib/secubox /etc/secubox /var/log/secubox
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,224 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# SecuBox-Deb :: secubox-grafana :: install-lxc.sh
# CyberMind — https://cybermind.fr
#
# Idempotent LXC bootstrap for the Grafana module. Safe to re-run.
# Follows docs/MODULE-GUIDELINES.md §3.
set -euo pipefail
readonly LXC_NAME="${SECUBOX_LXC_NAME:-grafana}"
readonly LXC_IP="${SECUBOX_LXC_IP:-10.100.0.70}"
readonly LXC_PATH="${SECUBOX_LXC_PATH:-/data/lxc}"
readonly LXC_BRIDGE="${SECUBOX_LXC_BRIDGE:-br-lxc}"
readonly LXC_GW="${SECUBOX_LXC_GW:-10.100.0.1}"
readonly DEBIAN_SUITE="${SECUBOX_DEBIAN_SUITE:-bookworm}"
readonly PROVISION_DIR="${SECUBOX_PROVISION_DIR:-/usr/share/secubox/lib/grafana/provision}"
readonly STATE_DIR="${SECUBOX_STATE_DIR:-/var/lib/secubox/grafana}"
readonly SECRETS_DIR="${SECUBOX_SECRETS_DIR:-/etc/secubox/secrets}"
readonly SENTINEL="$STATE_DIR/.lxc-provisioned"
log() { printf '[grafana-install] %s\n' "$*"; }
fail() { printf '[grafana-install] ERROR: %s\n' "$*" >&2; exit 1; }
# ── Preflight ────────────────────────────────────────────────────────────────
require_cmds() {
for c in lxc-create lxc-info lxc-start lxc-attach openssl; do
command -v "$c" >/dev/null 2>&1 || fail "$c not installed"
done
}
ensure_dirs() {
install -d -m 0755 -o root -g root "$LXC_PATH"
install -d -m 0755 -o secubox -g secubox "$STATE_DIR"
install -d -m 0700 -o root -g root "$SECRETS_DIR"
}
ensure_bridge() {
if ! ip link show "$LXC_BRIDGE" >/dev/null 2>&1; then
log "Creating bridge $LXC_BRIDGE @ ${LXC_GW}/24 ..."
ip link add name "$LXC_BRIDGE" type bridge
ip addr add "${LXC_GW}/24" dev "$LXC_BRIDGE"
ip link set "$LXC_BRIDGE" up
# Persist via a systemd-networkd unit so it survives reboot.
cat > /etc/systemd/network/10-secubox-lxc-bridge.netdev <<EOF
[NetDev]
Name=$LXC_BRIDGE
Kind=bridge
EOF
cat > /etc/systemd/network/10-secubox-lxc-bridge.network <<EOF
[Match]
Name=$LXC_BRIDGE
[Network]
Address=${LXC_GW}/24
ConfigureWithoutCarrier=yes
IPMasquerade=ipv4
EOF
systemctl reload systemd-networkd 2>/dev/null || true
fi
}
# ── LXC lifecycle ────────────────────────────────────────────────────────────
lxc_state() {
lxc-info -n "$LXC_NAME" -P "$LXC_PATH" 2>/dev/null \
| awk -F: '/^State:/ { gsub(/ /,"",$2); print tolower($2) }'
}
create_lxc() {
if [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]; then
log "LXC '$LXC_NAME' already exists at $LXC_PATH — skipping debootstrap"
return
fi
log "Creating LXC '$LXC_NAME' (debian $DEBIAN_SUITE) ..."
lxc-create -n "$LXC_NAME" -t debian -P "$LXC_PATH" -- -r "$DEBIAN_SUITE"
}
write_lxc_config() {
log "Pinning LXC network: $LXC_IP/24 on $LXC_BRIDGE, gw $LXC_GW"
cat > "$LXC_PATH/$LXC_NAME/config" <<EOF
# SecuBox-managed — see secubox-grafana / install-lxc.sh
lxc.uts.name = $LXC_NAME
lxc.net.0.type = veth
lxc.net.0.link = $LXC_BRIDGE
lxc.net.0.flags = up
lxc.net.0.ipv4.address = $LXC_IP/24
lxc.net.0.ipv4.gateway = $LXC_GW
lxc.net.0.name = eth0
lxc.rootfs.path = dir:$LXC_PATH/$LXC_NAME/rootfs
lxc.include = /usr/share/lxc/config/common.conf
lxc.apparmor.profile = generated
lxc.start.auto = 1
lxc.start.delay = 5
EOF
}
start_lxc() {
if [ "$(lxc_state)" = "running" ]; then
log "LXC '$LXC_NAME' already running"
return
fi
log "Starting LXC '$LXC_NAME' ..."
lxc-start -n "$LXC_NAME" -P "$LXC_PATH"
}
wait_for_network() {
log "Waiting for LXC network ..."
for _ in $(seq 1 30); do
if lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- ping -c1 -W1 "$LXC_GW" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
fail "LXC '$LXC_NAME' did not reach $LXC_GW within 30s"
}
# ── Grafana install inside LXC ───────────────────────────────────────────────
install_grafana_in_lxc() {
log "Installing Grafana OSS in '$LXC_NAME' ..."
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- bash -e <<'INNER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y --no-install-recommends \
apt-transport-https software-properties-common ca-certificates curl gnupg wget
if [ ! -f /etc/apt/sources.list.d/grafana.list ]; then
install -d /etc/apt/keyrings
wget -q -O /etc/apt/keyrings/grafana.gpg https://apt.grafana.com/gpg.key
echo 'deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main' \
> /etc/apt/sources.list.d/grafana.list
apt-get update -q
fi
apt-get install -y --no-install-recommends grafana
systemctl enable grafana-server
INNER
}
# ── Provisioning (datasources + dashboards) ──────────────────────────────────
provision_grafana() {
[ -d "$PROVISION_DIR" ] || { log "No provisioning dir at $PROVISION_DIR — skipping"; return; }
log "Provisioning datasources + dashboards from $PROVISION_DIR ..."
# Push provisioning into LXC's /etc/grafana/provisioning/
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- mkdir -p \
/etc/grafana/provisioning/datasources \
/etc/grafana/provisioning/dashboards \
/var/lib/grafana/dashboards
if [ -d "$PROVISION_DIR/datasources" ]; then
for f in "$PROVISION_DIR/datasources/"*.yaml; do
[ -f "$f" ] || continue
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- \
tee "/etc/grafana/provisioning/datasources/$(basename "$f")" > /dev/null < "$f"
done
fi
# The dashboards.yaml provisioner tells Grafana where to pick up the JSONs.
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- bash -c "cat > /etc/grafana/provisioning/dashboards/secubox.yaml <<'PROV'
apiVersion: 1
providers:
- name: secubox
orgId: 1
folder: SecuBox
type: file
disableDeletion: false
updateIntervalSeconds: 60
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
PROV
"
if [ -d "$PROVISION_DIR/dashboards" ]; then
for f in "$PROVISION_DIR/dashboards/"*.json; do
[ -f "$f" ] || continue
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- \
tee "/var/lib/grafana/dashboards/$(basename "$f")" > /dev/null < "$f"
done
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- \
chown -R grafana:grafana /var/lib/grafana/dashboards
fi
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl restart grafana-server
}
# ── Admin password + API key ─────────────────────────────────────────────────
generate_secrets() {
if [ ! -f "$SECRETS_DIR/grafana-admin" ]; then
local pw
pw=$(openssl rand -hex 16)
echo "$pw" > "$SECRETS_DIR/grafana-admin"
chmod 600 "$SECRETS_DIR/grafana-admin"
log "Setting Grafana admin password ..."
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- \
grafana-cli --homepath /usr/share/grafana admin reset-admin-password "$pw" || true
fi
}
# ── Sentinel ─────────────────────────────────────────────────────────────────
mark_provisioned() {
install -d -m 0755 -o secubox -g secubox "$STATE_DIR"
date -Iseconds > "$SENTINEL"
}
# ── Main ─────────────────────────────────────────────────────────────────────
main() {
require_cmds
ensure_dirs
ensure_bridge
create_lxc
write_lxc_config
start_lxc
wait_for_network
install_grafana_in_lxc
generate_secrets
provision_grafana
mark_provisioned
log "OK — LXC '$LXC_NAME' at $LXC_IP, grafana-server provisioned + running."
log "Web UI: https://<host>/grafana/ (admin / $(cat "$SECRETS_DIR/grafana-admin"))"
}
main "$@"

View File

@ -0,0 +1,41 @@
{
"schemaVersion": 38,
"uid": "secubox-cookie-audit",
"title": "SecuBox — Cookie audit (RGPD/ePrivacy)",
"tags": ["secubox", "rgpd", "cookies"],
"timezone": "browser",
"editable": true,
"panels": [
{
"id": 1,
"type": "stat",
"title": "Violations (24h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "increase(cookie_audit_violations_total[24h])"}
],
"gridPos": {"x": 0, "y": 0, "w": 6, "h": 4}
},
{
"id": 2,
"type": "timeseries",
"title": "Violations per category (rate, 1h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "rate(cookie_audit_violations_total[1h])", "legendFormat": "{{category}}"}
],
"gridPos": {"x": 6, "y": 0, "w": 18, "h": 4}
},
{
"id": 3,
"type": "table",
"title": "Top violating vhosts (24h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "topk(10, sum by (vhost) (increase(cookie_audit_violations_total[24h])))", "format": "table"}
],
"gridPos": {"x": 0, "y": 4, "w": 24, "h": 8}
}
],
"time": {"from": "now-24h", "to": "now"}
}

View File

@ -0,0 +1,41 @@
{
"schemaVersion": 38,
"uid": "secubox-crowdsec",
"title": "SecuBox — CrowdSec",
"tags": ["secubox", "ids", "crowdsec"],
"timezone": "browser",
"editable": true,
"panels": [
{
"id": 1,
"type": "stat",
"title": "Active decisions",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "cs_active_decisions"}
],
"gridPos": {"x": 0, "y": 0, "w": 6, "h": 4}
},
{
"id": 2,
"type": "stat",
"title": "Alerts (24h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "increase(cs_alerts_total[24h])"}
],
"gridPos": {"x": 6, "y": 0, "w": 6, "h": 4}
},
{
"id": 3,
"type": "timeseries",
"title": "Alerts per scenario (rate, 5m)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "rate(cs_alerts_total[5m])", "legendFormat": "{{scenario}}"}
],
"gridPos": {"x": 0, "y": 4, "w": 24, "h": 8}
}
],
"time": {"from": "now-24h", "to": "now"}
}

View File

@ -0,0 +1,46 @@
{
"schemaVersion": 38,
"uid": "secubox-mitmproxy-waf",
"title": "SecuBox — mitmproxy WAF",
"tags": ["secubox", "waf", "mitmproxy"],
"timezone": "browser",
"editable": true,
"panels": [
{
"id": 1,
"type": "stat",
"title": "Set-Cookie inspections (24h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "increase(mitmproxy_setcookie_inspections_total[24h])"}
],
"gridPos": {"x": 0, "y": 0, "w": 6, "h": 4}
},
{
"id": 2,
"type": "stat",
"title": "Blocked requests (24h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "increase(mitmproxy_blocked_requests_total[24h])"}
],
"gridPos": {"x": 6, "y": 0, "w": 6, "h": 4},
"fieldConfig": {
"defaults": {
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": 0}, {"color": "orange", "value": 10}, {"color": "red", "value": 100}]}
}
}
},
{
"id": 3,
"type": "timeseries",
"title": "Block reasons (rate, 5m)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "rate(mitmproxy_blocked_requests_total[5m])", "legendFormat": "{{reason}}"}
],
"gridPos": {"x": 0, "y": 4, "w": 24, "h": 8}
}
],
"time": {"from": "now-24h", "to": "now"}
}

View File

@ -0,0 +1,31 @@
{
"schemaVersion": 38,
"uid": "secubox-nftables",
"title": "SecuBox — nftables drops/accepts",
"tags": ["secubox", "firewall", "nftables"],
"timezone": "browser",
"editable": true,
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Drops per chain/rule (rate, 1m)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "rate(nftables_counter_packets{verdict=\"drop\"}[1m])", "legendFormat": "{{chain}}/{{rule}}"}
],
"gridPos": {"x": 0, "y": 0, "w": 24, "h": 8}
},
{
"id": 2,
"type": "timeseries",
"title": "Accepts per chain (rate, 1m)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "rate(nftables_counter_packets{verdict=\"accept\"}[1m])", "legendFormat": "{{chain}}"}
],
"gridPos": {"x": 0, "y": 8, "w": 24, "h": 8}
}
],
"time": {"from": "now-1h", "to": "now"}
}

View File

@ -0,0 +1,46 @@
{
"schemaVersion": 38,
"uid": "secubox-services",
"title": "SecuBox — Services overview",
"tags": ["secubox", "systemd"],
"timezone": "browser",
"editable": true,
"panels": [
{
"id": 1,
"type": "stat",
"title": "Active secubox-* units",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "count(systemd_unit_state{name=~\"secubox-.*\\\\.service\", state=\"active\"})"}
],
"gridPos": {"x": 0, "y": 0, "w": 6, "h": 4}
},
{
"id": 2,
"type": "stat",
"title": "Failed secubox-* units",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "count(systemd_unit_state{name=~\"secubox-.*\\\\.service\", state=\"failed\"})"}
],
"gridPos": {"x": 6, "y": 0, "w": 6, "h": 4},
"fieldConfig": {
"defaults": {
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": 0}, {"color": "red", "value": 1}]}
}
}
},
{
"id": 3,
"type": "timeseries",
"title": "secubox-* unit state over time",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "systemd_unit_state{name=~\"secubox-.*\\\\.service\", state=\"active\"}", "legendFormat": "{{name}}"}
],
"gridPos": {"x": 0, "y": 4, "w": 24, "h": 8}
}
],
"time": {"from": "now-6h", "to": "now"}
}

View File

@ -0,0 +1,31 @@
{
"schemaVersion": 38,
"uid": "secubox-suricata",
"title": "SecuBox — Suricata",
"tags": ["secubox", "ids", "suricata"],
"timezone": "browser",
"editable": true,
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Alerts by severity (rate, 5m)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "rate(suricata_alerts_total[5m])", "legendFormat": "sev {{severity}}"}
],
"gridPos": {"x": 0, "y": 0, "w": 24, "h": 8}
},
{
"id": 2,
"type": "table",
"title": "Top source IPs (last 1h)",
"datasource": {"type": "prometheus", "uid": "secubox-metrics"},
"targets": [
{"refId": "A", "expr": "topk(10, sum by (src_ip) (increase(suricata_alerts_total[1h])))", "format": "table"}
],
"gridPos": {"x": 0, "y": 8, "w": 24, "h": 8}
}
],
"time": {"from": "now-1h", "to": "now"}
}

View File

@ -0,0 +1,14 @@
# SecuBox Grafana — Prometheus datasource for secubox-metrics
apiVersion: 1
datasources:
- name: secubox-metrics
type: prometheus
access: proxy
# The host LXC bridge gateway as seen from inside the grafana LXC.
# secubox-metrics listens on the host at :9090 (default).
url: http://10.100.0.1:9090
isDefault: true
editable: false
jsonData:
timeInterval: "15s"
httpMethod: "POST"

View File

@ -0,0 +1,9 @@
{
"title": "Grafana",
"subtitle": "Security metrics",
"icon": "fa-chart-line",
"url": "/grafana/",
"section": "monitoring",
"order": 50,
"module": "grafana"
}

View File

@ -0,0 +1,30 @@
# /etc/nginx/secubox.d/grafana.conf
# Installed by secubox-grafana (#230)
#
# Two location blocks:
# /api/v1/grafana/ → host FastAPI on Unix socket (CTL surface)
# /grafana/ → LXC at 10.100.0.70:3000 (native Grafana UI, iframe target)
location /api/v1/grafana/ {
proxy_pass http://unix:/run/secubox/grafana.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf;
}
# Native Grafana UI (iframe target from /grafana/index.html in secubox-grafana)
# Grafana inside the LXC is configured with serve_from_sub_path = true.
location /grafana/ {
proxy_pass http://10.100.0.70:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Grafana Live (server-sent events / websocket)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Streaming + long-poll friendly
proxy_buffering off;
proxy_read_timeout 86400;
}

View File

@ -0,0 +1,247 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# SecuBox-Deb :: grafanactl
# CyberMind — https://cybermind.fr
#
# Grafana host-side controller. LXC at 10.100.0.70 on br-lxc.
# Three-fold introspection (components/status/access) + dashboard, datasource,
# alert, user, api-key, install, reload verbs.
#
# Grammar: docs/grammar.md, OPS MONITORING layer.
# Conventions: docs/MODULE-GUIDELINES.md §7.
set -u
readonly VERSION="1.0.0"
readonly CONFIG_FILE="${SECUBOX_GRAFANA_CONFIG:-/etc/secubox/grafana.toml}"
readonly STATE_DIR="${SECUBOX_GRAFANA_STATE:-/var/lib/secubox/grafana}"
readonly SECRETS_DIR="${SECUBOX_SECRETS_DIR:-/etc/secubox/secrets}"
readonly INSTALL_LIB="${SECUBOX_INSTALL_LIB:-/usr/share/secubox/lib/grafana/install-lxc.sh}"
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
log() { printf '%b[grafana]%b %s\n' "$GREEN" "$NC" "$*"; }
warn() { printf '%b[warn]%b %s\n' "$YELLOW" "$NC" "$*" >&2; }
err() { printf '%b[error]%b %s\n' "$RED" "$NC" "$*" >&2; }
# ── TOML config (best-effort, grep-style — same as giteactl) ─────────────────
config_get() {
local key="$1" default="${2:-}"
if [ -f "$CONFIG_FILE" ]; then
grep -E "^[[:space:]]*${key}[[:space:]]*=" "$CONFIG_FILE" 2>/dev/null \
| head -1 | cut -d= -f2- | tr -d ' "' | tr -d "'" || echo "$default"
else
echo "$default"
fi
}
LXC_NAME=$(config_get "name" "grafana")
LXC_IP=$(config_get "ip" "10.100.0.70")
LXC_PATH=$(config_get "path" "/data/lxc")
HTTP_PORT=$(config_get "http_port" "3000")
PUBLIC_HOSTNAME=$(config_get "public_hostname" "")
# ── LXC helpers ───────────────────────────────────────────────────────────────
lxc_state() {
lxc-info -n "$LXC_NAME" -P "$LXC_PATH" 2>/dev/null \
| awk -F: '/^State:/ { gsub(/ /,"",$2); print tolower($2) }'
}
lxc_running() { [ "$(lxc_state)" = "running" ]; }
lxc_exists() { [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]; }
daemon_running() {
lxc_running || return 1
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl is-active --quiet grafana-server 2>/dev/null
}
host_api_running() {
systemctl is-active --quiet secubox-grafana.service 2>/dev/null
}
# ── Three-fold output (matches docs/MODULE-GUIDELINES.md §7) ──────────────────
emit_components_json() {
local lxc_st daemon_st api_st
if lxc_exists; then
lxc_st=$(lxc_state)
[ -z "$lxc_st" ] && lxc_st="absent"
else
lxc_st="absent"
fi
daemon_running && daemon_st="running" || daemon_st="stopped"
host_api_running && api_st="running" || api_st="stopped"
cat <<EOF
{
"module": "grafana",
"version": "$VERSION",
"components": [
{"name": "lxc", "state": "$lxc_st", "detail": "$LXC_NAME @ $LXC_IP on br-lxc"},
{"name": "daemon", "state": "$daemon_st", "detail": "grafana-server, port $HTTP_PORT"},
{"name": "host-api", "state": "$api_st", "detail": "secubox-grafana.service (uvicorn @ /run/secubox/grafana.sock)"}
]
}
EOF
}
emit_components_text() {
printf '%-12s %-12s %s\n' "COMPONENT" "STATE" "DETAIL"
if lxc_exists; then
local s; s=$(lxc_state); [ -z "$s" ] && s="absent"
printf '%-12s %-12s %s\n' "lxc" "$s" "$LXC_NAME @ $LXC_IP on br-lxc"
else
printf '%-12s %-12s %s\n' "lxc" "absent" "$LXC_NAME @ $LXC_IP on br-lxc — run 'grafanactl install'"
fi
if daemon_running; then
printf '%-12s %-12s %s\n' "daemon" "running" "grafana-server, port $HTTP_PORT"
else
printf '%-12s %-12s %s\n' "daemon" "stopped" "grafana-server, port $HTTP_PORT"
fi
if host_api_running; then
printf '%-12s %-12s %s\n' "host-api" "running" "secubox-grafana.service (uvicorn)"
else
printf '%-12s %-12s %s\n' "host-api" "stopped" "secubox-grafana.service (uvicorn)"
fi
}
emit_status_json() {
local overall
if lxc_running && daemon_running && host_api_running; then
overall="green"
elif lxc_exists && (daemon_running || host_api_running); then
overall="yellow"
else
overall="red"
fi
cat <<EOF
{
"module": "grafana",
"version": "$VERSION",
"overall": "$overall"
}
EOF
}
emit_access_json() {
local lan_url public_url
lan_url="https://$(hostname -I | awk '{print $1}')/grafana/"
if [ -n "$PUBLIC_HOSTNAME" ]; then
public_url="https://${PUBLIC_HOSTNAME}/grafana/"
else
public_url=""
fi
cat <<EOF
{
"module": "grafana",
"access": [
{"url": "$lan_url", "auth": "jwt|grafana-admin", "scope": "lan"}$( [ -n "$public_url" ] && echo ',' )
$( [ -n "$public_url" ] && cat <<PUB
{"url": "$public_url", "auth": "jwt|grafana-admin", "scope": "public"}
PUB
)
]
}
EOF
}
# ── Verbs ─────────────────────────────────────────────────────────────────────
cmd_components() {
case "${1:-}" in
--json) emit_components_json ;;
""|list|status) emit_components_text ;;
*) err "unknown verb for 'components': $1"; exit 1 ;;
esac
}
cmd_status() {
case "${1:-}" in
--json) emit_status_json ;;
""|"") emit_components_text ; echo ; emit_status_json | python3 -c 'import json,sys; d=json.load(sys.stdin); print("overall:", d["overall"])' ;;
*) err "unknown verb for 'status': $1"; exit 1 ;;
esac
}
cmd_access() {
case "${1:-}" in
--json) emit_access_json ;;
""|list) emit_access_json | python3 -m json.tool ;;
show)
local name="${2:-lan}"
emit_access_json | python3 -c "import json,sys; d=json.load(sys.stdin); m=[a for a in d['access'] if a['scope']=='$name']; print(m[0] if m else 'not found')"
;;
*) err "unknown verb for 'access': $1"; exit 1 ;;
esac
}
cmd_install() {
if [ ! -x "$INSTALL_LIB" ]; then
err "install script not found at $INSTALL_LIB (package not installed correctly?)"
exit 2
fi
log "Running LXC bootstrap from $INSTALL_LIB ..."
SECUBOX_LXC_NAME="$LXC_NAME" SECUBOX_LXC_IP="$LXC_IP" SECUBOX_LXC_PATH="$LXC_PATH" \
bash "$INSTALL_LIB"
log "Starting host-side FastAPI ..."
systemctl start secubox-grafana.service 2>/dev/null || true
log "Done. Try 'grafanactl status' to verify."
}
cmd_reload() {
systemctl restart secubox-grafana.service 2>/dev/null || true
if lxc_running; then
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl restart grafana-server 2>/dev/null || true
fi
log "Reloaded host FastAPI and grafana-server (if running)."
}
cmd_help() {
cat <<'HELP'
grafanactl — SecuBox Grafana controller (OPS MONITORING layer)
Three-fold introspection:
grafanactl components [--json] # LXC + daemon + host-API states
grafanactl status [--json] # overall green/yellow/red
grafanactl access [list|show <name>] [--json]
Lifecycle:
grafanactl install # idempotent LXC + daemon bootstrap
grafanactl reload # restart FastAPI + grafana-server
Dashboards / datasources / alerts / users / api-keys:
grafanactl dashboard list|add <file.json>|remove <uid>|export <uid>
grafanactl datasource list|add <toml>|remove <name>|test <name>
grafanactl alert list|mute <id>|unmute <id>
grafanactl user list|add <name> <role>|remove <name>|passwd <name>
grafanactl api-key list|create <name> <role>|revoke <id>
(--json on any verb returns machine-readable output)
For grammar details: /usr/share/doc/secubox-grafana/README.gz
docs/grammar.md (OPS MONITORING layer)
HELP
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
main() {
local noun="${1:-help}"
shift || true
case "$noun" in
components|status|access|install|reload) "cmd_${noun}" "$@" ;;
# The remaining nouns are stubs in v1.0.0 — see #230 task G5 for
# full implementation. They print a clear "not implemented yet" so
# the help/grammar surface is consistent now and the API contract
# is testable.
dashboard|datasource|alert|user|api-key)
err "noun '$noun' not yet implemented in v$VERSION — see #230 task G5"
exit 2
;;
--help|-h|help|"") cmd_help ;;
--version|-V) echo "grafanactl $VERSION" ;;
*) err "unknown noun: $noun"; cmd_help; exit 1 ;;
esac
}
main "$@"

View File

@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecuBox — Grafana</title>
<link rel="stylesheet" href="/shared/crt-light.css">
<link rel="stylesheet" href="/shared/sidebar-light.css">
<style>
html, body { margin: 0; padding: 0; height: 100%; }
.layout {
display: grid;
grid-template-columns: 260px 1fr;
height: 100vh;
}
.grafana-frame {
border: 0;
width: 100%;
height: 100%;
background: var(--bg-1, #0f1320);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: var(--bg-2, #181c2c);
border-bottom: 1px solid var(--border, #2a3354);
color: var(--root-main, #00d49b);
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
}
.topbar .actions a {
color: var(--root-main, #00d49b);
margin-left: 1rem;
text-decoration: none;
}
.topbar .actions a:hover { text-decoration: underline; }
.threefold {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
padding: 1rem;
background: var(--bg-1);
}
.threefold article {
background: var(--bg-2);
border: 1px solid var(--border);
padding: 0.75rem 1rem;
border-radius: 4px;
}
.threefold h3 {
margin: 0 0 0.5rem 0;
color: var(--root-main);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.threefold .v { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; }
.container { display: flex; flex-direction: column; height: 100vh; }
.iframe-wrap { flex: 1; }
</style>
</head>
<body class="crt-light">
<div class="layout">
<nav class="sidebar" id="sidebar"></nav>
<div class="container">
<div class="topbar">
<div><strong>SecuBox</strong> · Grafana — security metrics</div>
<div class="actions">
<a href="/" title="Back to SecuBox hub">← hub</a>
<a href="#" id="open-native" title="Open native Grafana in this tab">native UI</a>
</div>
</div>
<section class="threefold" id="threefold">
<article id="components-card">
<h3>Components</h3>
<div class="v" id="components-v">loading…</div>
</article>
<article id="status-card">
<h3>Status</h3>
<div class="v" id="status-v">loading…</div>
</article>
<article id="access-card">
<h3>Access</h3>
<div class="v" id="access-v">loading…</div>
</article>
</section>
<div class="iframe-wrap">
<iframe class="grafana-frame"
src="/grafana/d/secubox-services/secubox-services-overview?kiosk=tv&theme=light"
title="Grafana dashboards"
referrerpolicy="no-referrer"></iframe>
</div>
</div>
</div>
<script src="/shared/sidebar.js"></script>
<script src="/shared/api-utils.js"></script>
<script>
// Mandatory auth wrapper (matches docs/MODULE-GUIDELINES.md §4)
(function() {
function getToken() { return localStorage.getItem('sbx_token'); }
function checkAuth() {
if (!getToken()) {
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
return false;
}
return true;
}
if (!checkAuth()) {
document.body.innerHTML = '<div style="padding:2rem;color:var(--root-main);">Redirecting to login...</div>';
return;
}
// Fill the threefold cards from /api/v1/grafana/*
function get(path) {
return fetch('/api/v1/grafana' + path, {
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(r => r.ok ? r.json() : Promise.reject(r.status));
}
get('/components').then(d => {
document.getElementById('components-v').innerHTML =
d.components.map(c => '<div>' + c.name + ': <strong>' + c.state + '</strong></div>').join('');
}).catch(() => {
document.getElementById('components-v').textContent = 'unavailable';
});
get('/status').then(d => {
document.getElementById('status-v').innerHTML =
'<span style="color:var(--' + (d.overall === 'green' ? 'root-main' : d.overall === 'yellow' ? 'wall-main' : 'auth-main') + ')">'
+ (d.overall || 'unknown').toUpperCase() + '</span>';
}).catch(() => {
document.getElementById('status-v').textContent = 'unavailable';
});
get('/access').then(d => {
document.getElementById('access-v').innerHTML =
(d.access || []).map(a => '<div>' + a.scope + ': <a href="' + a.url + '">' + a.url + '</a></div>').join('');
}).catch(() => {
document.getElementById('access-v').textContent = 'unavailable';
});
document.getElementById('open-native').addEventListener('click', e => {
e.preventDefault();
window.location.href = '/grafana/';
});
})();
</script>
</body>
</html>