Skip to content

v1.3.30 -- Scenario descriptions (deferred from v1.3.25)

A small, focused cosmetic-enrichment release. The Scenarios management UI shipped in v1.3.25 with names only; v1.3.30 adds the hub-catalogue description as a hover tooltip on each scenario name. Operators no longer need to context-switch to cscli scenarios inspect <name> or the upstream hub website to remember what a given scenario actually detects.

What it does

Hovering over a scenario name in the Scenarios tab now shows a one-line description from CrowdSec's hub catalogue. A small glyph next to the name signals the tooltip is available. When a scenario has no description (rare; the hub catalogue currently covers 100% of the 779 entries), the name renders alone with no glyph -- no visual noise.

Why this took five releases to ship

v1.3.25's planning notes flagged "scenario descriptions" as deferred to a future release because the source file -- /etc/crowdsec/hub/.index.json -- has structural variance risk and access constraints. Investigating in v1.3.30:

  1. Structural risk: not real. The file is stable JSON, 775 KB, with scenarios as a top-level map keyed by canonical name. Each entry has description, path, version, versions, optionally references. No surprises in CrowdSec v1.7.7.
  2. Access constraint: real. The file is mode 0600 owned by root:root inside the crowdsec_config volume. The panel runs as nobody (uid 65534) per the v1.0 hardening. The existing v1.3.25 /crowdsec-state read-only mount exposes the file but doesn't grant read access to the panel user. v1.3.25's scenarios.Reader works only because it reads symlink targets via Readlink (path-string only, never opens file content); reading .index.json content is a fundamentally different access pattern.

What ships

Reverse-sentinel pattern (new)

Existing argos sentinels are panel -> crowdsec: the panel writes /data/shared/argos-*.{txt,yaml} and setup-appsec.sh consumes on next run. v1.3.30 introduces the inverse direction: crowdsec -> panel.

setup-appsec.sh::emit_scenarios_index (running as root inside the crowdsec container, can read 0600 hub files) parses .index.json with jq, slims it to a {canonical_name: description} map, and writes /shared/argos-scenarios-index.json. The shared volume's default umask gives the file mode 0644, so the panel-as- nobody can read it via /data/shared/argos-scenarios- index.json.

The slimmed file is ~115 KB (vs 775 KB for the original catalogue); the panel reads it on demand with mtime-based cache invalidation so a fresh setup-appsec.sh run is reflected on the very next API request.

Backend

  • backend/internal/security/scenarios/descriptions.go: DescriptionsLoader with Get(canonicalName) string and Len() int. Nil-safe (Get on nil receiver returns ""). 7 unit tests covering missing file, valid lookup, mtime reload, malformed-file resilience, nil receiver, Reader integration, and the loader-nil-empty path.
  • Scenario struct gains Description string (json tag description,omitempty). Reader.Read() enriches each scenario with Descriptions.Get(canonicalName) when the loader is bound.
  • scenarios.New() now binds the default descriptions loader (/data/shared/argos-scenarios-index.json) automatically. The handler layer needs no wiring change.

Crowdsec-side script

  • crowdsec/setup-appsec.sh::emit_scenarios_index: ~30 lines of bash. apk add --no-cache jq (idempotent; ~1.2s when already cached). jq slimmer:
.scenarios
  | with_entries(select(.value.description != null
                        and .value.description != ""))
  | with_entries(.value = .value.description)

Atomic-write via tempfile + rename. cmp-based no-op detection so runs that produce identical output don't rewrite the file.

Frontend

  • frontend/src/api/client.ts: SecurityScenarioItem.description?: string
  • frontend/src/pages/Security.tsx: scenario name cell carries a native title= attribute when description is present, plus a small glyph for discoverability. The glyph is absent when description is empty.

Smoke

scripts/smoke/scenario-descriptions.sh -- 5-step EFFECT verification:

  1. Run setup-appsec.sh; assert /shared/argos-scenarios- index.json is valid JSON.
  2. GET /api/security/scenarios -> >= 90% of installed scenarios carry a description (configurable via COVERAGE_PCT env var).
  3. crowdsecurity/CVE-2017-9841 description contains the substring CVE-2017-9841 (configurable via KNOWN_SCENARIO + KNOWN_SUBSTRING).
  4. Rename the index file aside; assert the API still returns scenarios (graceful degrade -- no 500).
  5. Restore + touch the file; assert subsequent request picks it up (mtime invalidation).

Smoke gate (5/5 PASS on prod stack)

[1/5] /shared/argos-scenarios-index.json: 115433 bytes        OK
[2/5] 54 / 54 scenarios have description (100%)               OK
[3/5] CVE-2017-9841 description: "Detect CVE-2017-9841 exploits"  OK
[4/5] graceful degrade: 54 scenarios returned without index   OK
[5/5] restore + mtime touch -> description recovers           OK

(54 here is the count of installed scenarios on this stack; the upstream hub catalogue has 779, all with descriptions.)

Files changed

  • crowdsec/setup-appsec.sh (emit_scenarios_index function + one call from apply_panel_sentinels)
  • backend/internal/security/scenarios/scenarios.go (Scenario.Description, Reader.Descriptions field, default binding in New())
  • backend/internal/security/scenarios/descriptions.go (new)
  • backend/internal/security/scenarios/descriptions_test.go (new, 7 unit tests)
  • frontend/src/api/client.ts (description field on type)
  • frontend/src/pages/Security.tsx (tooltip + ⓘ glyph)
  • backend/cmd/argos/main.go (argosVersion bump)
  • frontend/package.json (version bump)
  • scripts/smoke/scenario-descriptions.sh (new)
  • docs/release-notes/v1.3.30.md (this file)
  • CHANGELOG.md, mkdocs.yml

Upgrade

cd ~/argos-edge
git pull
make sync-prod                 # picks up the new setup-appsec.sh
                               # + scripts/smoke/* + Caddyfile
                               # change (none in v1.3.30)
docker compose -f /path/to/argos-prod/docker-compose.yml \
    restart crowdsec           # bind-mount inode refresh -- the
                               # v1.3.29 lesson; needed for the
                               # new emit_scenarios_index
                               # function to run inside the
                               # container

Then rebuild + redeploy the panel for the version-string bump + the new scenarios.DescriptionsLoader code:

cd ~/argos-prod
docker build -f backend/Dockerfile -t argos-prod-argos:v1.3.30 .
# update docker-compose.override.yml: image: argos-prod-argos:v1.3.30
docker compose up -d --force-recreate --no-deps argos

The first docker exec argos-prod-crowdsec /setup-appsec.sh after the deploy creates /shared/argos-scenarios-index.json (takes ~1.5s including the apk-add jq). Subsequent runs detect the file is unchanged and no-op.

Not changed

  • All v1.3.29 backend / frontend / migration code unchanged.
  • LAPI WAL mode (v1.3.28) untouched.
  • Drift detector (v1.3.27) untouched.
  • True detect mode (v1.3.29) untouched.
  • Migration 031 still latest; no schema change in v1.3.30.

Reverse-sentinel pattern: documented for future use

The pattern v1.3.30 introduces is reusable any time the panel needs to surface CrowdSec internal state that lives in 0600 root-owned files inside the crowdsec_config volume. Future candidates:

  • Per-scenario hub version (visible in cscli scenarios list but not directly in the panel).
  • Acquisition source listing (lives in /etc/crowdsec/acquis*).
  • Bouncer registration state (/etc/crowdsec/local_api_*).

Each becomes a small jq slimmer in setup-appsec.sh plus a small reader in backend/internal/security/scenarios/ (or a sibling package). No new mounts, no privilege escalation.