mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 11:12:29 +00:00
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:
parent
e170ef98c1
commit
402aedc98d
113
packages/secubox-grafana/README.md
Normal file
113
packages/secubox-grafana/README.md
Normal 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`.
|
||||
0
packages/secubox-grafana/api/__init__.py
Normal file
0
packages/secubox-grafana/api/__init__.py
Normal file
78
packages/secubox-grafana/api/main.py
Normal file
78
packages/secubox-grafana/api/main.py
Normal 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")
|
||||
28
packages/secubox-grafana/conf/grafana.toml.example
Normal file
28
packages/secubox-grafana/conf/grafana.toml.example
Normal 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
|
||||
13
packages/secubox-grafana/debian/changelog
Normal file
13
packages/secubox-grafana/debian/changelog
Normal 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
|
||||
35
packages/secubox-grafana/debian/control
Normal file
35
packages/secubox-grafana/debian/control
Normal 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.
|
||||
36
packages/secubox-grafana/debian/postinst
Executable file
36
packages/secubox-grafana/debian/postinst
Executable 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
|
||||
16
packages/secubox-grafana/debian/postrm
Executable file
16
packages/secubox-grafana/debian/postrm
Executable 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
|
||||
14
packages/secubox-grafana/debian/prerm
Executable file
14
packages/secubox-grafana/debian/prerm
Executable 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
|
||||
26
packages/secubox-grafana/debian/rules
Executable file
26
packages/secubox-grafana/debian/rules
Executable 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
|
||||
27
packages/secubox-grafana/debian/secubox-grafana.service
Normal file
27
packages/secubox-grafana/debian/secubox-grafana.service
Normal 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
|
||||
224
packages/secubox-grafana/lib/grafana/install-lxc.sh
Executable file
224
packages/secubox-grafana/lib/grafana/install-lxc.sh
Executable 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 "$@"
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
|
@ -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"
|
||||
9
packages/secubox-grafana/menu.d/50-grafana.json
Normal file
9
packages/secubox-grafana/menu.d/50-grafana.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"title": "Grafana",
|
||||
"subtitle": "Security metrics",
|
||||
"icon": "fa-chart-line",
|
||||
"url": "/grafana/",
|
||||
"section": "monitoring",
|
||||
"order": 50,
|
||||
"module": "grafana"
|
||||
}
|
||||
30
packages/secubox-grafana/nginx/grafana.conf
Normal file
30
packages/secubox-grafana/nginx/grafana.conf
Normal 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;
|
||||
}
|
||||
247
packages/secubox-grafana/sbin/grafanactl
Executable file
247
packages/secubox-grafana/sbin/grafanactl
Executable 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 "$@"
|
||||
156
packages/secubox-grafana/www/grafana/index.html
Normal file
156
packages/secubox-grafana/www/grafana/index.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user