diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 564e8e7..fc8dcf5 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -194,52 +194,24 @@ protected page. Runs natively on the host (systemd service \`security-dashboard\ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly. ## Tabs +The nav only ever shows tabs for things actually present on this box — no +tab for a service you haven't installed. **Security Log** and **Extensions** +are always there (they only need Asterisk itself, detected once at install +time). **Asterisk Admin**, **PSTN Trunk**, and **CrowdSec** each check their +own live install state on every page load and hide their own nav button +entirely if not found, so this one page/URL scales from a bare LAN Asterisk +box (just those first two tabs) up to a full droplet with a trunk and +CrowdSec, without ever showing a tab for something that isn't set up. + - **Security Log** — parses \`$ASTERISK_LOG_DIR/full\` for SIP auth failures (wrong password, unknown extension, etc.) with timestamp/account/remote IP, - filterable per column (each header has its own text filter, live as you type). -- **CrowdSec** — current bans (\`cscli decisions list\`), a delete/unban button - per entry, carrier/ASN + country columns, and management of the ASN-exempt - Asterisk brute-force scenarios (see \`services/crowdsec.sh\`'s "Exempt - specific carrier ASNs" option) without SSHing in: - - **Currently-exempt ASNs** are listed with carrier name (resolved from - current bans, falling back to alert history for ASNs with no active ban - right now) regardless of when they were added. - - **Unwhitelist** removes an ASN from the exemption list — future Asterisk - auth failures from it are evaluated normally again. - - **Unwhitelist + Ban** does that *and* immediately bans (24h) every IP - CrowdSec has ever recorded for that ASN, for accidental-whitelist cases - where you don't want to wait for it to misbehave again. -- **PSTN Trunk** — a **Groups** card at the top (always available, whether - or not a PSTN trunk has ever been installed) lets you name a set of - extensions and bulk-enable/disable messaging for all of them at once — - a management convenience only, not a runtime concept: applying an action - just writes the same per-extension \`pstn-permissions.conf\` key each - member's own checkbox would, and membership changes never retroactively - affect anything already applied. Below that, the rest of the tab detects - whether \`services/pstn-trunk.sh\`'s dialplan is - actually installed (\`pstn-trunk-dialplan.conf\` present) and shows a - clear "not installed" message instead of the calling-permissions editor - if not, so it never shows real-looking-but-unenforced defaults. When - installed: the outbound/inbound concurrent-call caps, and every known - extension's permission tier (internal / restricted / full) and, for - restricted, its approved numbers — all editable live, no Asterisk - restart, no reinstall. Also manages personal-number assignments (DID -> - owner extension), additive to the shared trunk DID. Writes directly to - \`pstn-limits.conf\` / \`pstn-permissions.conf\` / \`pstn-personal-dids.conf\`, - which the dialplan reads fresh on every call. The spend-cap kill-switch - and international-calling allow-list are deliberately **not** managed - here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more - security-sensitive than what this tab already exposes. An "Internal SIP - messaging" card at the **bottom** of the tab (a checkbox chip per known - extension, independent of PSTN calling entirely — no cost, no carrier, no - DID, no dependency on a PSTN trunk being installed) is always available - regardless of any of the above. + sortable per column (click a header to sort, click again to reverse). - **Asterisk Admin** — an embedded, lazy-loaded iframe of the real Asterisk web admin (only fetched the first time you open the tab), plus an - "open in a new tab" fallback link that's always there regardless. Only - shows up once an Asterisk install is detected. If a local Caddy install is - found for both this dashboard and the Asterisk admin's own domain, install - automatically patches the admin's Caddy site block from + "open in a new tab" fallback link that's always there regardless. Its nav + button only appears once an Asterisk install is detected. If a local Caddy + install is found for both this dashboard and the Asterisk admin's own + domain, install automatically patches the admin's Caddy site block from `X-Frame-Options` to a `Content-Security-Policy: frame-ancestors` entry naming only this dashboard's domain, so the browser actually allows the frame — every other site is still refused framing exactly as before. This @@ -248,6 +220,43 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly. confirmed against Authelia's own portal-framing behavior on a live install) — if the tab shows a blank frame, use the fallback link and check this service's own log output from install time for a manual one-line fix. +- **Extensions** — always available, independent of any PSTN trunk. A + **Groups** card lets you name a set of extensions and bulk-enable/disable + messaging for all of them at once — a management convenience only, not a + runtime concept: applying an action just writes the same per-extension + \`pstn-permissions.conf\` key each member's own checkbox would, and + membership changes never retroactively affect anything already applied. + An **Internal SIP messaging** card (a checkbox chip per known extension, + independent of PSTN calling entirely — no cost, no carrier, no DID, no + dependency on a PSTN trunk being installed) sits below it. +- **PSTN Trunk** — its nav button only appears once + \`services/pstn-trunk.sh\`'s dialplan is actually installed + (\`pstn-trunk-dialplan.conf\` present), so it never shows a + real-looking-but-unenforced editor. When present: the outbound/inbound + concurrent-call caps, and every known extension's permission tier + (internal / restricted / full) and, for restricted, its approved numbers — + all editable live, no Asterisk restart, no reinstall, sortable per column. + Also manages personal-number assignments (DID -> owner extension or + group), additive to the shared trunk DID. Writes directly to + \`pstn-limits.conf\` / \`pstn-permissions.conf\` / \`pstn-personal-dids.conf\`, + which the dialplan reads fresh on every call. The spend-cap kill-switch + and international-calling allow-list are deliberately **not** managed + here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more + security-sensitive than what this tab already exposes. +- **CrowdSec** — its nav button only appears once \`cscli\` is detected on + this host. Current bans (\`cscli decisions list\`), a delete/unban button + per entry, carrier/ASN + country columns (sortable per column), and + management of the ASN-exempt Asterisk brute-force scenarios (see + \`services/crowdsec.sh\`'s "Exempt specific carrier ASNs" option) without + SSHing in: + - **Currently-exempt ASNs** are listed with carrier name (resolved from + current bans, falling back to alert history for ASNs with no active ban + right now) regardless of when they were added. + - **Unwhitelist** removes an ASN from the exemption list — future Asterisk + auth failures from it are evaluated normally again. + - **Unwhitelist + Ban** does that *and* immediately bans (24h) every IP + CrowdSec has ever recorded for that ASN, for accidental-whitelist cases + where you don't want to wait for it to misbehave again. ## Manage \`\`\` @@ -814,6 +823,14 @@ def run_sudo(args, timeout=15): return False, "", str(e) +def crowdsec_installed(): + """True if cscli is actually present on this host — mirrors + pstn_installed()'s approach of checking for the real thing rather than a + stored flag, so the CrowdSec tab tracks live state without needing this + dashboard reinstalled after CrowdSec is added or removed.""" + return os.path.isfile("/usr/bin/cscli") + + def get_decisions(): ok, out, err = run_sudo(["/usr/bin/cscli", "decisions", "list", "-o", "json"]) if not ok or not out.strip(): @@ -1524,9 +1541,10 @@ INDEX_HTML = """

Security Dashboard

@@ -1566,7 +1584,7 @@ INDEX_HTML = """
-