{"id":47745808,"url":"https://github.com/theduffman85/crowdsec-web-ui","last_synced_at":"2026-06-06T00:01:50.895Z","repository":{"id":328056129,"uuid":"1113342653","full_name":"TheDuffman85/crowdsec-web-ui","owner":"TheDuffman85","description":"A modern, responsive web interface for managing CrowdSec alerts and decisions. ","archived":false,"fork":false,"pushed_at":"2026-05-07T15:41:32.000Z","size":5322,"stargazers_count":261,"open_issues_count":0,"forks_count":10,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-07T17:29:15.160Z","etag":null,"topics":["crowdsec","crowdsec-lapi","crowdsec-manager","dashboard","docker-container","gotify","mqtt","notifications","ntfy","self-hosted"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TheDuffman85.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-09T21:07:39.000Z","updated_at":"2026-05-07T15:37:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/TheDuffman85/crowdsec-web-ui","commit_stats":null,"previous_names":["theduffman85/crowdsec-web-ui"],"tags_count":27,"template":false,"template_full_name":null,"purl":"pkg:github/TheDuffman85/crowdsec-web-ui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheDuffman85%2Fcrowdsec-web-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheDuffman85%2Fcrowdsec-web-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheDuffman85%2Fcrowdsec-web-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheDuffman85%2Fcrowdsec-web-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TheDuffman85","download_url":"https://codeload.github.com/TheDuffman85/crowdsec-web-ui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheDuffman85%2Fcrowdsec-web-ui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33157229,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-17T09:28:26.183Z","status":"ssl_error","status_checked_at":"2026-05-17T09:27:52.702Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crowdsec","crowdsec-lapi","crowdsec-manager","dashboard","docker-container","gotify","mqtt","notifications","ntfy","self-hosted"],"created_at":"2026-04-03T01:00:21.843Z","updated_at":"2026-05-17T22:01:33.888Z","avatar_url":"https://github.com/TheDuffman85.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"client/public/logo.svg\" alt=\"CrowdSec Web UI Logo\" width=\"400\" /\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n  [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TheDuffman85/crowdsec-web-ui/release.yml?style=flat-square\u0026logo=github\u0026label=build)](https://github.com/TheDuffman85/crowdsec-web-ui/actions/workflows/release.yml)\n  [![Trivy Scan](https://img.shields.io/github/actions/workflow/status/TheDuffman85/crowdsec-web-ui/trivy-scan.yml?style=flat-square\u0026logo=aqua\u0026label=security)](https://github.com/TheDuffman85/crowdsec-web-ui/actions/workflows/trivy-scan.yml)\n  [![GitHub License](https://img.shields.io/github/license/TheDuffman85/crowdsec-web-ui?style=flat-square\u0026logo=github)](https://github.com/TheDuffman85/crowdsec-web-ui/blob/main/LICENSE)\n  [![GitHub last commit](https://img.shields.io/github/last-commit/TheDuffman85/crowdsec-web-ui?style=flat-square\u0026logo=github)](https://github.com/TheDuffman85/crowdsec-web-ui/commits/main)\n  [![Latest Container](https://img.shields.io/badge/ghcr.io-latest-blue?style=flat-square\u0026logo=github)](https://github.com/users/TheDuffman85/packages/container/package/crowdsec-web-ui)\n\n\u003c/div\u003e\n\n# CrowdSec Web UI\n\nA modern, responsive web interface for managing [CrowdSec](https://crowdsec.net/) alerts and decisions. Built with **React**, **Vite**, **Node.js**, and **Tailwind CSS**.\n\n\u003e [!IMPORTANT]\n\u003e **Improved Performance \u0026 Better Scale**: Recent backend and caching improvements significantly reduce resource pressure and improve responsiveness across the application. CrowdSec Web UI now also supports larger-scale deployments more reliably, including environments with multiple machines and high alert or decision volumes.\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://react.dev/\"\u003e\u003cimg src=\"https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge\u0026logo=react\u0026logoColor=%2361DAFB\" alt=\"React\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://vite.dev/\"\u003e\u003cimg src=\"https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge\u0026logo=vite\u0026logoColor=white\" alt=\"Vite\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://tailwindcss.com/\"\u003e\u003cimg src=\"https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge\u0026logo=tailwind-css\u0026logoColor=white\" alt=\"Tailwind CSS\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://nodejs.org/\"\u003e\u003cimg src=\"https://img.shields.io/badge/node.js-%23339933.svg?style=for-the-badge\u0026logo=node.js\u0026logoColor=white\" alt=\"Node.js\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.docker.com/\"\u003e\u003cimg src=\"https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge\u0026logo=docker\u0026logoColor=white\" alt=\"Docker\" /\u003e\u003c/a\u003e\n\u003c/div\u003e\n\n## Features\n\n### Dashboard\nHigh-level overview of total alerts and live active decisions. Statistics and top lists with dynamic filtering, including simulation-mode visibility when enabled.\n\n\u003ca href=\"screenshots/dashboard.png\"\u003e\n  \u003cimg src=\"screenshots/dashboard.png\" alt=\"Dashboard\" width=\"50%\"\u003e\n\u003c/a\u003e\n\n### Alerts Management\nView detailed logs of security events, including clear simulation-mode labeling, broad free-text filtering, and optional advanced search syntax.\n\n\u003ca href=\"screenshots/alerts.png\"\u003e\n  \u003cimg src=\"screenshots/alerts.png\" alt=\"Alerts\" width=\"50%\"\u003e\n\u003c/a\u003e\n\n### Alert Details\nDetailed modal view showing attacker IP, AS information, location with map, and triggered events breakdown.\n\n\u003ca href=\"screenshots/alert_details.png\"\u003e\n  \u003cimg src=\"screenshots/alert_details.png\" alt=\"Alert Details\" width=\"50%\"\u003e\n\u003c/a\u003e\n\n### Decisions Management\nView and manage active bans/decisions. Supports filtering by status (active/expired), simulation mode, hiding duplicate decisions, and the same unified search syntax used on Alerts.\n\n\u003ca href=\"screenshots/decisions.png\"\u003e\n  \u003cimg src=\"screenshots/decisions.png\" alt=\"Decisions\" width=\"50%\"\u003e\n\u003c/a\u003e\n\n### Manual Actions\nBan IPs directly from the UI with custom duration and reason.\n\n\u003ca href=\"screenshots/add_decision.png\"\u003e\n  \u003cimg src=\"screenshots/add_decision.png\" alt=\"Add Decision\" width=\"50%\"\u003e\n\u003c/a\u003e\n\n### Update Notifications\nAutomatically detects new container images on GitHub Container Registry (GHCR). A badge appears in the sidebar when an update is available for your current tag.\n\n### Notification Center\nCreate notification rules for alert spikes, alert thresholds, recent CVE activity, and application updates, then deliver them to one or more outbound destinations such as Email, Gotify, MQTT, ntfy, or Webhooks.\n\n### Unified Search\n-   **Free-text first**: The Alerts and Decisions search bars still support normal free-text queries.\n-   **Advanced syntax**: Power users can refine searches with quoted phrases, `field:value`, `AND`, `OR`, `NOT`, unary `-`, and parentheses.\n-   **Inline field search**: Mix free text and fielded terms in the same query, for example `country:germany ssh`.\n-   **Built-in help**: Use the `Info` button next to each search bar to open the page-specific syntax reference and examples.\n\n### Modern UI\n-   **Dark/Light Mode**: Full support for both themes.\n-   **Responsive**: Optimized for mobile and desktop.\n-   **Real-time**: Fast interactions using modern React technology.\n\n\u003e [!CAUTION]\n\u003e **Security Notice**: This application **does not provide any built-in authentication mechanism**. It is NOT intended to be exposed publicly without protection. We strongly recommend deploying this application behind a reverse proxy with an Identity Provider (IdP) such as [Authentik](https://goauthentik.io/), [Authelia](https://www.authelia.com/), or [Keycloak](https://www.keycloak.org/) to handle authentication and authorization.\n\n## Architecture\n\n-   **Client**: React (Vite) + Tailwind CSS. Located in `client/`.\n-   **Server**: Node.js (Hono). Acts as an intelligent caching layer for CrowdSec Local API (LAPI) with delta updates and optimized chunked historical data sync for improved performance and larger-scale deployments.\n-   **Build Output**: The root build emits the frontend to `dist/client` and the compiled server to `dist/server`.\n-   **Database**: SQLite (`better-sqlite3`). Persists alerts and decisions locally in `/app/data/crowdsec.db` to reduce memory usage and support historical data.\n-   **Security**: The application runs as a non-root user (`node`) inside the container and communicates with CrowdSec via HTTP/LAPI. It uses **Machine Authentication** to obtain a JWT for full access (read/write), either via watcher `User/Password` or agent **mTLS**.\n\n## Prerequisites\n\n-   **CrowdSec**: A running CrowdSec instance.\n-   **Authentication**: Configure exactly one CrowdSec LAPI auth mode for this web UI:\n\n    1.  **Watcher password auth**\n        Generate a secure password:\n        ```bash\n        openssl rand -hex 32\n        ```\n        Create the machine:\n        ```bash\n        docker exec crowdsec cscli machines add crowdsec-web-ui --password \u003cgenerated_password\u003e -f /dev/null\n        ```\n\n    2.  **Agent mTLS auth**\n        Configure CrowdSec LAPI TLS auth and generate an agent client certificate/key pair for this Web UI as described in the [CrowdSec TLS authentication docs](https://docs.crowdsec.net/docs/local_api/tls_auth/).\n\n\u003e [!NOTE]\n\u003e The `-f /dev/null` flag is crucial. It tells `cscli` **not** to overwrite the existing credentials file of the CrowdSec container. We only want to register the machine in the database, not change the container's local config.\n\n\u003e [!IMPORTANT]\n\u003e Choose exactly one auth mode:\n\u003e - Password auth: `CROWDSEC_USER` + `CROWDSEC_PASSWORD`\n\u003e - mTLS auth: `CROWDSEC_TLS_CERT_PATH` + `CROWDSEC_TLS_KEY_PATH` with optional `CROWDSEC_TLS_CA_CERT_PATH`\n\u003e\n\u003e Do not set both modes at the same time. The container will fail fast on mixed or partial auth configuration.\n\n## Run with Docker (Recommended)\n\nThe examples below intentionally use only the required environment variables. Optional knobs are documented in [Environment Variables](#environment-variables).\n\n1.  **Build the image**:\n    ```bash\n    docker build -t crowdsec-web-ui .\n    ```\n\n    You can optionally specify `DOCKER_IMAGE_REF` to override the default image reference used for checking updates (useful for forks or private registries):\n    ```bash\n    docker build --build-arg DOCKER_IMAGE_REF=my-registry/my-image -t crowdsec-web-ui .\n    ```\n\n\u003e [!NOTE]\n\u003e Current Docker images are based on Node.js rather than Bun, so the previous Bun/AVX-specific x64 runtime limitation no longer applies.\n\n2.  **Run the container**:\n    Provide the CrowdSec LAPI URL and one supported auth mode.\n\n    ```bash\n    docker run -d \\\n      --name crowdsec_web_ui \\\n      -p 3000:3000 \\\n      -e CROWDSEC_URL=http://\u003ccrowdsec-host\u003e:8080 \\\n      -e CROWDSEC_USER=crowdsec-web-ui \\\n      -e CROWDSEC_PASSWORD=\u003cyour-secure-password\u003e \\\n      -v $(pwd)/data:/app/data \\\n      --network your_crowdsec_network \\\n      crowdsec-web-ui\n    ```\n\u003e [!NOTE]\n\u003e Ensure the container is on the same Docker network as CrowdSec so it can reach the URL.\n\n### Docker Compose Example\n\n```yaml\nservices:\n  crowdsec-web-ui:\n    image: ghcr.io/theduffman85/crowdsec-web-ui:latest\n    container_name: crowdsec_web_ui\n    ports:\n      - \"3000:3000\"\n    environment:\n      - CROWDSEC_URL=http://crowdsec:8080\n      - CROWDSEC_USER=crowdsec-web-ui\n      - CROWDSEC_PASSWORD=\u003cgenerated_password\u003e\n    volumes:\n      - ./data:/app/data\n    restart: unless-stopped\n```\n\nThe repository also ships a minimal [`docker-compose.yml`](docker-compose.yml) that builds the image locally and reads the same runtime inputs from `.env`.\n\n### Docker Compose Example (mTLS Authentication)\n\n```yaml\nservices:\n  crowdsec-web-ui:\n    image: ghcr.io/theduffman85/crowdsec-web-ui:latest\n    container_name: crowdsec_web_ui\n    ports:\n      - \"3000:3000\"\n    environment:\n      - CROWDSEC_URL=https://crowdsec:8080\n      - CROWDSEC_TLS_CERT_PATH=/certs/agent.pem\n      - CROWDSEC_TLS_KEY_PATH=/certs/agent-key.pem\n      # Optional when CrowdSec LAPI uses a private or self-signed CA\n      # - CROWDSEC_TLS_CA_CERT_PATH=/certs/ca.pem\n    volumes:\n      - ./data:/app/data\n      - /path/on/host/agent.pem:/certs/agent.pem:ro\n      - /path/on/host/agent-key.pem:/certs/agent-key.pem:ro\n      # - /path/on/host/ca.pem:/certs/ca.pem:ro\n    restart: unless-stopped\n```\n\n## Environment Variables\n\n### CrowdSec Connection and Authentication\n\nChoose exactly one auth mode: password auth or mTLS auth.\n\n| Variable | Default | Required | Description |\n| --- | --- | --- | --- |\n| `CROWDSEC_URL` | `http://crowdsec:8080` | Usually | CrowdSec LAPI base URL. Use `https://...` when TLS is enabled. |\n| `CROWDSEC_USER` | none | Password auth only | CrowdSec machine/user name for watcher-password login. Must be set together with `CROWDSEC_PASSWORD`. |\n| `CROWDSEC_PASSWORD` | none | Password auth only | CrowdSec watcher password. Must be set together with `CROWDSEC_USER`. |\n| `CROWDSEC_TLS_CERT_PATH` | none | mTLS only | Path inside the container or host process to the client certificate used for CrowdSec mTLS auth. |\n| `CROWDSEC_TLS_KEY_PATH` | none | mTLS only | Path to the client private key used for CrowdSec mTLS auth. |\n| `CROWDSEC_TLS_CA_CERT_PATH` | none | No | Optional CA bundle used to verify the CrowdSec LAPI server certificate during mTLS connections. |\n\n### Runtime Settings\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `PORT` | `3000` | HTTP listen port. If you change this in Docker, also update port mappings and the container health check to match. |\n| `BASE_PATH` | empty | Serve the UI under a path prefix such as `/crowdsec`. Start with `/` and omit the trailing slash. |\n| `DB_DIR` | `/app/data` | Directory that stores the SQLite database and other persisted app data. If you change it, update your volume mounts too. |\n| `CROWDSEC_LOOKBACK_PERIOD` | `168h` | Alert/history retention window used for sync and cleanup. Accepts values like `12h`, `7d`, or `30m`. |\n| `CROWDSEC_REFRESH_INTERVAL` | `30s` | Normal background refresh interval. Accepts `0`, `manual`, `5s`, `30s`, `1m`, `5m`, or other `s`/`m`/`h`/`d` values. |\n| `CROWDSEC_IDLE_REFRESH_INTERVAL` | `5m` | Refresh interval used when the app considers itself idle. |\n| `CROWDSEC_IDLE_THRESHOLD` | `2m` | Inactivity period before the app switches to idle refresh behavior. |\n| `CROWDSEC_FULL_REFRESH_INTERVAL` | `5m` | Interval for full cache refreshes while active. |\n| `CROWDSEC_LAPI_REQUEST_TIMEOUT` | `30s` | Timeout for individual CrowdSec LAPI requests. Increase this for high-latency or very large CrowdSec datasets. |\n| `CROWDSEC_ALERT_SYNC_CHUNK` | `6h` | Window size used when syncing historical and active-decision alerts from LAPI. Smaller chunks reduce per-request payload size. |\n| `CROWDSEC_ALERT_SYNC_MIN_CHUNK` | `15m` | Smallest window size used when retrying timed-out alert sync windows. |\n| `CROWDSEC_BOOTSTRAP_RETRY_DELAY` | `30s` | Delay between background retries when initial CrowdSec bootstrap fails. |\n| `CROWDSEC_BOOTSTRAP_RETRY_ENABLED` | `true` | Enables background bootstrap retry after startup or login failures. |\n| `CROWDSEC_SIMULATIONS_ENABLED` | `false` | Include simulation-mode alerts and decisions from CrowdSec and expose the related UI indicators. |\n| `CROWDSEC_ALERT_INCLUDE_ORIGINS` | empty | Comma-separated list of exact origins to include when syncing alerts. |\n| `CROWDSEC_ALERT_EXCLUDE_ORIGINS` | empty | Comma-separated list of exact origins to drop after alert results are merged. |\n| `CROWDSEC_ALERT_INCLUDE_CAPI` | `false` | Add the Central API / community-blocklist alert feed. |\n| `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY` | `false` | Keep alerts whose effective origin is empty when using explicit include filters. |\n| `CROWDSEC_ALERT_EXCLUDE_ORIGIN_EMPTY` | `false` | Drop alerts whose effective origin is empty. |\n| `NOTIFICATION_SECRET_KEY` | auto-generated and persisted | Optional fixed encryption key for saved notification secrets. If unset, the app generates one and stores it in app metadata. |\n| `NOTIFICATION_ALLOW_PRIVATE_ADDRESSES` | `true` | Allow notification destinations on private, loopback, and link-local addresses. Set to `false` to block them. |\n| `NOTIFICATION_DEBUG_PAYLOADS` | `false` | When enabled, failed notification deliveries log a truncated rendered request body for troubleshooting. Use carefully because payloads may contain sensitive data. |\n| `NODE_EXTRA_CA_CERTS` | none | Optional Node.js trust bundle for HTTPS connections, useful when using password auth against a private or self-signed CrowdSec CA. |\n\n### Build and Image Metadata\n\nThese values are mainly relevant when building your own image or local production bundle.\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `DOCKER_IMAGE_REF` | `theduffman85/crowdsec-web-ui` | Image reference used by the built-in update checker. Accepts `owner/repo` or registry-prefixed forms such as `ghcr.io/owner/repo`. |\n| `VITE_VERSION` | `0.0.0` | Version label shown in the UI and used for update-check comparisons. |\n| `VITE_BRANCH` | `main` | Branch label shown in the UI. `dev` enables dev-build update comparisons. |\n| `VITE_COMMIT_HASH` | empty | Commit hash displayed in the sidebar and used for build metadata/update logic. |\n| `VITE_BUILD_DATE` | auto-generated at build time | Build timestamp shown in the UI. |\n| `VITE_REPO_URL` | `https://github.com/TheDuffman85/crowdsec-web-ui` | Repository URL used for release and commit links in the UI. |\n\n### Development and Test Only\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `BACKEND_URL` | `http://localhost:3000` | Vite dev-server proxy target for `/api` during local frontend development. |\n| `CROWDSEC_MTLS_IMAGE` | `crowdsecurity/crowdsec:latest` | Override image used by `pnpm run test:mtls:crowdsec`. |\n| `CROWDSEC_MTLS_KEEP` | `0` | Set to `1` to keep the disposable CrowdSec test container after the mTLS smoke test. |\n| `CROWDSEC_MTLS_CONTAINER` | auto-generated | Override the disposable container name used by the mTLS smoke test. |\n\n\u003e [!NOTE]\n\u003e `scripts/ensure-native-deps.mjs` also honors standard Node/npm cache variables such as `COREPACK_HOME`, `XDG_CACHE_HOME`, `PREBUILD_INSTALL_CACHE`, `npm_config_cache`, `npm_config_devdir`, and `npm_config_nodedir`. Those are generic toolchain settings rather than project-specific configuration, so they are not required for normal setup.\n\n## Deployment Notes\n\n### Trusted IPs for Delete Operations (Optional)\n\nBy default, CrowdSec may restrict certain write operations such as deleting alerts to trusted IP addresses. If you encounter `403 Forbidden` errors when trying to delete alerts, add the Web UI network or IP range to CrowdSec's trusted IPs list.\n\n**Docker Setup**: Add the Web UI container's network to the CrowdSec configuration in `/etc/crowdsec/config.yaml` or via environment variable:\n\n```yaml\napi:\n  server:\n    trusted_ips:\n      - 127.0.0.1\n      - ::1\n      - 172.16.0.0/12  # Docker default bridge network\n```\n\nOr using `TRUSTED_IPS` environment variable on the CrowdSec container:\n\n```bash\nTRUSTED_IPS=\"127.0.0.1,::1,172.16.0.0/12\"\n```\n\nSee the [CrowdSec documentation](https://docs.crowdsec.net/docs/local_api/intro/) for more details on LAPI configuration.\n\n### Using CrowdSec Web UI with a Local or Custom Certificate\n\nIf your CrowdSec Local API (LAPI) uses HTTPS with a self-signed certificate or an internal Certificate Authority (CA), the Web UI container may not trust it by default. This can result in errors like:\n\n```\nLogin failed: unable to get local issuer certificate\n```\n\n#### Solution: Mount the CA Certificate and Use NODE_EXTRA_CA_CERTS\n\nYou can mount your CA certificate into the container and instruct Node.js to trust it using the `NODE_EXTRA_CA_CERTS` environment variable.\n\n#### Example Docker Compose\n\n```yaml\nservices:\n  crowdsec-web-ui:\n    image: ghcr.io/theduffman85/crowdsec-web-ui:latest\n    container_name: crowdsec_web_ui\n    ports:\n      - \"3000:3000\"\n    environment:\n      - CROWDSEC_URL=https://crowdsec:8080\n      - CROWDSEC_USER=crowdsec-web-ui\n      - CROWDSEC_PASSWORD=\u003cgenerated_password\u003e\n      - NODE_EXTRA_CA_CERTS=/certs/root_ca.crt\n    volumes:\n      - ./data:/app/data\n      - /path/on/host/root_ca.crt:/certs/root_ca.crt:ro\n    restart: unless-stopped\n```\n\n#### Notes\n\n- Replace `/path/on/host/root_ca.crt` with the path to your local CA certificate.\n- The `:ro` ensures the certificate is mounted read-only.\n- This method avoids rebuilding the container image.\n- Works for self-signed certificates as well as private CA certificates.\n- `NODE_EXTRA_CA_CERTS` is a general runtime trust mechanism. When using the new mTLS auth mode, prefer `CROWDSEC_TLS_CA_CERT_PATH` as the explicit CrowdSec LAPI trust input for the Web UI client connection.\n\n### Reverse Proxy with Base Path\n\nIf you need to serve the Web UI at a non-root URL path (e.g., `https://example.com/crowdsec/` instead of `https://example.com/`), use the `BASE_PATH` environment variable.\n\n#### Docker Compose Example\n\n```yaml\nservices:\n  crowdsec-web-ui:\n    image: ghcr.io/theduffman85/crowdsec-web-ui:latest\n    container_name: crowdsec_web_ui\n    ports:\n      - \"3000:3000\"\n    environment:\n      - CROWDSEC_URL=http://crowdsec:8080\n      - CROWDSEC_USER=crowdsec-web-ui\n      - CROWDSEC_PASSWORD=\u003cgenerated_password\u003e\n      - BASE_PATH=/crowdsec\n    volumes:\n      - ./data:/app/data\n    restart: unless-stopped\n```\n\n#### Nginx Reverse Proxy Example\n\n```nginx\nlocation /crowdsec/ {\n    proxy_pass http://localhost:3000/crowdsec/;\n    proxy_http_version 1.1;\n    proxy_set_header Host $host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n}\n```\n\n#### Notes\n\n- The `BASE_PATH` must start with a `/` (e.g., `/crowdsec`, not `crowdsec`)\n- Do not include a trailing slash (use `/crowdsec`, not `/crowdsec/`)\n- When `BASE_PATH` is set, accessing the root URL (`/`) will redirect to the base path\n- All API calls, assets, and navigation will automatically use the configured base path\n\n### Health Check\n\nThe Docker image includes a built-in `HEALTHCHECK` that verifies the web server is responding. Docker will automatically mark the container as `healthy` or `unhealthy`.\n\nStartup is non-blocking: if CrowdSec LAPI is temporarily unavailable, the Web UI stays up and continues retrying cache/bootstrap initialization in the background. This means the container can become `healthy` before the initial CrowdSec sync has completed.\n\n**Endpoint:** `GET /api/health` (no authentication required)\n\n```bash\ncurl http://localhost:3000/api/health\n# {\"status\":\"ok\"}\n```\n\nThe health check runs every 30 seconds with a 10-second start period to allow for initialization. You can check the container's health status with:\n\n```bash\ndocker inspect --format='{{.State.Health.Status}}' crowdsec_web_ui\n```\n\nIf you use `BASE_PATH`, the health check still targets `localhost:3000/api/health` directly inside the container, so no additional configuration is needed. If you change `PORT`, update the health check command in your deployment to match.\n\n## Runtime Behavior\n\n### Simulation Mode Visibility\n\nCrowdSec can run scenarios in **simulation mode**, where alerts and decisions are generated but no live remediation is applied. The Web UI can display those entries separately from real remediations.\n\n- `CROWDSEC_SIMULATIONS_ENABLED=false` by default.\n- When enabled, the UI shows simulation badges, simulation filters, and separate simulation counts on the dashboard.\n- When left unset or set to `false`, the UI hides simulated alerts/decisions and the backend stops requesting simulated data from the CrowdSec LAPI.\n\n### Table Column Visibility\n\nThe Alerts and Decisions tables include a Columns button that lets you choose which data columns are visible. Desktop and mobile layouts are saved separately in the application database and apply globally to the Web UI.\n\n- `ID`, `Machine`, and `Origin` are hidden by default.\n- The app automatically uses the saved desktop or mobile column layout for the current screen size.\n- Machine values prefer `machine_alias` and fall back to `machine_id`.\n- Alerts with decisions from more than one origin display `Mixed` when the Origin column is visible.\n- Hidden columns remain searchable with the advanced search syntax, including `id:`, `machine:`, and `origin:`.\n\n### Search Syntax\n\nThe Alerts and Decisions pages use a single search box that supports both normal free-text search and optional advanced syntax.\n\n- Plain words keep working as free-text search, for example `ssh hetzner`\n- Quoted phrases match exact text, for example `\"nginx bf\"`\n- Fielded search uses `field:value`, for example `country:germany` or `status:active`\n- Date filtering uses the `date` field with ISO dates or timestamps, for example `date\u003e=2026-03-24` or `date\u003c2026-03-25T12:00:00Z`\n- Exact field checks use `=` and `\u003c\u003e`, for example `country=DE` or `sim\u003c\u003esimulated`\n- Boolean operators `AND`, `OR`, and `NOT` are supported\n- Unary `-` can be used as shorthand for negation, for example `-sim:simulated`\n- Parentheses can group expressions, for example `country:(germany OR france)`\n\nExamples:\n\n- Alerts: `country:germany ssh`\n- Alerts: `date\u003e=2026-03-24 AND date\u003c2026-03-25`\n- Alerts: `country:(germany OR france) AND -sim:simulated`\n- Decisions: `status:active AND action:ban`\n- Decisions: `date\u003e=2026-03-24 AND action:ban`\n- Decisions: `alert:123 OR ip:\"192.168.5.0/24\"`\n\nNotes:\n\n- A field name by itself, such as `country`, is treated as normal free text unless it is followed by `:`\n- Ordered comparisons such as `\u003c`, `\u003e`, `\u003c=`, `\u003e=`, and `=\u003e` are supported for the `date` field\n- If you want to search for literal operator words like `AND`, `OR`, or `NOT`, wrap them in double quotes\n- Use the `Info` button beside the search field to see the supported fields and examples for the current page\n\n### Alert Source Filtering\n\nSome CrowdSec setups ingest very large volumes of alerts and decisions from external automation, imported blocklists, or community feeds. In those cases, you may want the Web UI to focus on specific synced alerts instead of caching everything exposed by the LAPI.\n\nThe recommended configuration is:\n\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS`: comma-separated list of exact origins to include when syncing alerts\n- `CROWDSEC_ALERT_EXCLUDE_ORIGINS`: comma-separated list of exact origins that cause a synced alert to be dropped\n- `CROWDSEC_ALERT_INCLUDE_CAPI`: set to `true` to include Central API / community blocklist alerts\n- `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY`: set to `true` to also include alerts whose effective origin is empty when using explicit include filters\n- `CROWDSEC_ALERT_EXCLUDE_ORIGIN_EMPTY`: set to `true` to drop alerts whose effective origin is empty\n\n```yaml\nenvironment:\n  - CROWDSEC_ALERT_INCLUDE_ORIGINS=crowdsec,cscli-import\n  - CROWDSEC_ALERT_EXCLUDE_ORIGINS=cscli\n  - CROWDSEC_ALERT_INCLUDE_CAPI=true\n  - CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY=true\n  - CROWDSEC_ALERT_EXCLUDE_ORIGIN_EMPTY=false\n```\n\nBehavior:\n\n- if no alert source vars are set, the Web UI keeps the current default and fetches the normal non-CAPI/non-lists alert feed\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS` limits upstream queries to alerts matching the origins you list\n- `CROWDSEC_ALERT_INCLUDE_CAPI=true` adds the dedicated CAPI/community-blocklist query on top of the normal non-CAPI/non-lists feed, unless you also enable explicit include filtering with `CROWDSEC_ALERT_INCLUDE_ORIGINS` and/or `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY`\n- `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY=true` adds an extra unfiltered non-CAPI query lane so explicit include filters can also keep alerts whose effective origin stays empty after local evaluation\n- `CROWDSEC_ALERT_EXCLUDE_ORIGIN_EMPTY=true` drops alerts whose effective origin stays empty after local evaluation\n- `CROWDSEC_ALERT_EXCLUDE_ORIGINS` removes matching alerts after the result sets are merged; if an alert contains any excluded origin, the whole alert is dropped\n- these origin checks are based on the alert's associated decision origins when present, with CrowdSec blocklist/list source scopes used as a fallback for alerts without decisions\n\nCommon origins in CrowdSec include:\n\n- `crowdsec` for alerts carrying decisions created by the security engine\n- `cscli` for alerts created by manual `cscli decisions add`\n- `cscli-import` for alerts created by `cscli decisions import`\n- `lists` for imported list feeds\n- `CAPI` for Central API / community blocklist alerts\n\nExamples:\n\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS=crowdsec` keeps only security-engine alerts\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS=lists` fetches only list-based alerts\n- `CROWDSEC_ALERT_INCLUDE_CAPI=true` keeps the default non-CAPI feed and adds CAPI/community-blocklist alerts\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS=CAPI` fetches only CAPI/community-blocklist alerts\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS=crowdsec` with `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY=true` keeps both `crowdsec` alerts and alerts without an origin\n- `CROWDSEC_ALERT_INCLUDE_ORIGINS=cscli` with `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY=true` keeps both `cscli` alerts and alerts without an origin\n- `CROWDSEC_ALERT_EXCLUDE_ORIGIN_EMPTY=true` removes alerts without an effective origin from the synced cache\n- `CROWDSEC_ALERT_EXCLUDE_ORIGINS=cscli,lists` removes manual `cscli` alerts and imported list alerts from the local synced cache view\n\nNotes:\n\n- include filters are applied upstream where possible, which is usually the biggest performance win\n- `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY` is local-only because CrowdSec LAPI does not expose an upstream \"missing origin\" filter\n- `CROWDSEC_ALERT_INCLUDE_ORIGIN_EMPTY` is mainly intended as an additive option alongside `CROWDSEC_ALERT_INCLUDE_ORIGINS` and/or `CROWDSEC_ALERT_INCLUDE_CAPI`\n- `CROWDSEC_ALERT_EXCLUDE_ORIGIN_EMPTY` is also local-only because CrowdSec LAPI does not expose an upstream \"missing origin\" filter\n- generic excludes are applied locally after fetch because CrowdSec LAPI does not expose a general alert-origin exclude filter\n- because the local decisions view is built from synced alerts, these settings also affect which imported decisions appear in the UI\n\n## Notifications\n\nThe **Notifications** page lets you define rules that watch the locally cached CrowdSec data and create notification events when a condition matches. Every notification is also stored in-app, where you can review delivery status and mark items as read.\n\n### Rules\n\nEach rule has:\n\n-   a name\n-   a severity: `info`, `warning`, or `critical`\n-   incident-based deduplication so the same condition only fires once until it clears and reappears\n-   one or more destination channels\n\nAlert-based rules can also use optional filters for scenario text, target text, and whether simulated alerts should be included.\n\nAvailable rule types:\n\n-   `Alert Spike`: compares the current window with the previous window and triggers when the percentage increase and minimum alert count are exceeded\n-   `Alert Threshold`: triggers when the number of matching alerts in the configured time window reaches the threshold\n-   `Recent CVE`: extracts CVE IDs from matching alerts and checks publication age before notifying\n-   `Application Update`: uses the built-in update check and triggers when a newer CrowdSec Web UI version is available\n\n\u003e [!NOTE]\n\u003e The `Recent CVE` rule queries the NVD API to determine when a CVE was published. If outbound access to `services.nvd.nist.gov` is blocked, recent-CVE notifications may be skipped.\n\n### Destinations\n\nYou can create multiple destinations and attach the same rule to several of them.\n\nShared behavior:\n\n-   destinations can be enabled or disabled independently\n-   secrets are masked when you reopen a saved destination\n-   destinations with saved secrets are encrypted at rest using `NOTIFICATION_SECRET_KEY`, or an auto-generated key persisted in app metadata when the env var is unset\n-   **Send Test** validates a saved destination without waiting for a real rule to fire\n-   delivery results are stored with each notification as `delivered` or `failed`\n-   private, loopback, and link-local outbound destinations are allowed by default and can be blocked explicitly\n\nSupported destination types:\n\n#### Email\n\nSMTP delivery with:\n\n-   `SMTP Host`\n-   `SMTP Port`\n-   `SMTP Security`: `Plain SMTP`, `STARTTLS`, or `SMTPS / Implicit TLS`\n-   optional `SMTP User` and `SMTP Password`\n-   `From Address`\n-   one or more comma-separated `To Address(es)`\n-   `Importance`: `auto`, `normal`, or `important`\n-   optional `Allow insecure TLS` for trusted self-signed SMTP endpoints\n\nWhen email importance is set to `auto`, it follows the rule severity:\n\n-   `info` -\u003e `normal`\n-   `warning` -\u003e `important`\n-   `critical` -\u003e `important`\n\n#### Gotify\n\nGotify delivery with:\n\n-   `Gotify URL`\n-   `App Token`\n-   `Priority`: `auto` or an explicit integer\n\nWhen Gotify priority is set to `auto`, it follows the rule severity:\n\n-   `info` -\u003e `5`\n-   `warning` -\u003e `7`\n-   `critical` -\u003e `10`\n\n#### ntfy\n\nntfy delivery with:\n\n-   `Server URL`\n-   `Topic`\n-   optional `Access Token`\n-   `Priority`: `auto`, `min`, `low`, `default`, `high`, or `urgent`\n\nWhen ntfy priority is set to `auto`, it follows the rule severity:\n\n-   `info` -\u003e `default`\n-   `warning` -\u003e `high`\n-   `critical` -\u003e `urgent`\n\n#### MQTT\n\nMQTT delivery is generic publish-only notification output. It does **not** include Home Assistant discovery, entity sync, or command handling.\n\nMQTT settings:\n\n-   `Broker URL`\n-   optional `Username` and `Password`\n-   optional `Client ID`\n-   `QoS`: `0` or `1`\n-   `Keepalive`\n-   `Connect Timeout`\n-   `Topic`\n-   `Retain MQTT payloads`\n\nEach notification publishes a JSON payload to the configured topic containing:\n\n-   `title`\n-   `message`\n-   `severity`\n-   `metadata`\n-   `sent_at`\n-   `channel_id`\n-   `channel_name`\n-   `channel_type`\n-   `rule_id`\n-   `rule_name`\n-   `rule_type`\n\nFor test sends, rule fields use a synthetic context: `rule_id` is `test`, `rule_name` is `Test notification`, and `rule_type` is `test`.\n\n#### Webhook\n\nWebhook delivery supports custom integrations such as automation tools, internal APIs, chat bridges, and other HTTP endpoints.\n\nWebhook settings:\n\n-   HTTP method: `POST`, `PUT`, or `PATCH`\n-   target `URL`\n-   optional query parameters\n-   optional custom headers\n-   authentication: none, bearer token, or basic auth\n-   body mode: `JSON`, `Text`, or `Form`\n-   request timeout\n-   retry attempts and retry delay\n-   optional `Allow insecure TLS` for trusted self-signed HTTPS endpoints\n\nWebhook templates support simple dotted variables rooted at `event.*`. The body and templated fields can reference values such as:\n\n-   `{{event.title}}`\n-   `{{event.titleJson}}`\n-   `{{event.message}}`\n-   `{{event.messageJson}}`\n-   `{{event.severity}}`\n-   `{{event.severityJson}}`\n-   `{{event.metadata}}`\n-   `{{event.metadataJson}}`\n-   `{{event.sent_at}}`\n-   `{{event.sent_atJson}}`\n-   `{{event.channel_name}}`\n-   `{{event.channel_nameJson}}`\n-   `{{event.rule_id}}`\n-   `{{event.rule_idJson}}`\n-   `{{event.rule_idOrUnknown}}`\n-   `{{event.rule_idOrUnknownJson}}`\n-   `{{event.rule_name}}`\n-   `{{event.rule_nameJson}}`\n-   `{{event.rule_nameOrUnknown}}`\n-   `{{event.rule_nameOrUnknownJson}}`\n-   `{{event.rule_type}}`\n-   `{{event.rule_typeJson}}`\n-   `{{event.rule_typeOrUnknown}}`\n-   `{{event.rule_typeOrUnknownJson}}`\n\nUse the `*Json` variables when placing values inside JSON templates without surrounding quotes. Nullable rule fields also provide `OrUnknown` aliases for destinations that reject JSON `null` values.\n\nFailed webhook deliveries include the HTTP status and a truncated response body in the delivery error and server warning logs. Set `NOTIFICATION_DEBUG_PAYLOADS=true` to also include a truncated rendered request body in warning logs; sensitive form fields are redacted, but JSON/text bodies may still contain secrets.\n\n### Notification Security Controls\n\n-   `NOTIFICATION_SECRET_KEY`: optional override for the notification encryption key. If unset, the backend auto-generates one on first start and persists it in application metadata so encrypted destinations continue working across restarts.\n-   `NOTIFICATION_ALLOW_PRIVATE_ADDRESSES=true` by default. Set it to `false` if you want to block private, loopback, and link-local destinations.\n-   `NOTIFICATION_DEBUG_PAYLOADS=false` by default. Set it to `true` only while troubleshooting failed deliveries, then turn it back off.\n\n### Current Scope\n\nThe notification system currently supports:\n\n-   in-app notification history\n-   rule-based outbound delivery\n-   Email, Gotify, MQTT, ntfy, and Webhook destinations\n\nIt currently does **not** include:\n\n-   Telegram destinations\n-   Home Assistant MQTT discovery\n-   MQTT entity state publishing or inbound commands\n\n### Run with Helm\n\nA Helm chart for deploying `crowdsec-web-ui` on Kubernetes is available (maintained by the zekker6):\n[https://github.com/zekker6/helm-charts/tree/main/charts/apps/crowdsec-web-ui](https://github.com/zekker6/helm-charts/tree/main/charts/apps/crowdsec-web-ui)\n\n## Persistence \u0026 Alert History\n\nAll data is stored in SQLite under `/app/data`. To persist data across container restarts, mount the `/app/data` directory rather than only the `crowdsec.db` file, because SQLite also uses `crowdsec.db-wal` and `crowdsec.db-shm` sidecar files.\n\n**Docker Run:**\nAdd `-v $(pwd)/data:/app/data` to your command.\n\n**Docker Compose:**\nAdd the volume mapping:\n```yaml\nvolumes:\n  - ./data:/app/data\n```\n\n### How It Works\n\nThe Web UI maintains its own local history of alerts and decisions. Data fetched from the CrowdSec LAPI is stored in the local database and preserved across restarts, while successful full refreshes reconcile the local cache with LAPI so alerts deleted outside the UI are removed locally too.\n\n- Alerts are kept for the duration of `CROWDSEC_LOOKBACK_PERIOD` (default: 7 days), then automatically cleaned up.\n- On restart, existing data is reused and new data from LAPI is merged in, then successful full sync windows prune alerts no longer returned by LAPI.\n- Large active-decision sets are synced in `CROWDSEC_ALERT_SYNC_CHUNK` windows. If a window times out, it is retried in smaller windows down to `CROWDSEC_ALERT_SYNC_MIN_CHUNK`.\n- If LAPI is unavailable during startup, the Web UI keeps retrying bootstrap in the background using `CROWDSEC_BOOTSTRAP_RETRY_DELAY` until it can initialize automatically.\n- If some sync windows fail but others succeed, the UI serves the imported cache and marks sync as partial while background retries continue.\n- To force a full cache reset, use the `POST /api/cache/clear` endpoint.\n\n## Local Development\n\n1.  **Install Dependencies**:\n    You need Node.js `24.15.0` and pnpm `10.33.0` installed locally.\n    ```bash\n    pnpm install\n    ```\n\n2.  **Configuration**:\n    Create a `.env` file in the root directory with your CrowdSec credentials:\n    ```bash\n    CROWDSEC_URL=http://localhost:8080\n    CROWDSEC_USER=crowdsec-web-ui\n    CROWDSEC_PASSWORD=\u003cyour-secure-password\u003e\n    CROWDSEC_SIMULATIONS_ENABLED=true\n    CROWDSEC_REFRESH_INTERVAL=30s\n    CROWDSEC_LAPI_REQUEST_TIMEOUT=30s\n    CROWDSEC_ALERT_SYNC_CHUNK=6h\n    CROWDSEC_ALERT_SYNC_MIN_CHUNK=15m\n    CROWDSEC_BOOTSTRAP_RETRY_DELAY=30s\n    CROWDSEC_BOOTSTRAP_RETRY_ENABLED=true\n    # Optional: Base path for reverse proxy deployments\n    # BASE_PATH=/crowdsec\n    ```\n\n    Or use mTLS instead of `CROWDSEC_USER`/`CROWDSEC_PASSWORD`:\n    ```bash\n    CROWDSEC_URL=https://localhost:8080\n    CROWDSEC_TLS_CERT_PATH=/path/to/agent.pem\n    CROWDSEC_TLS_KEY_PATH=/path/to/agent-key.pem\n    # Optional when using a private CA or self-signed CrowdSec LAPI certificate\n    CROWDSEC_TLS_CA_CERT_PATH=/path/to/ca.pem\n    CROWDSEC_SIMULATIONS_ENABLED=true\n    CROWDSEC_REFRESH_INTERVAL=30s\n    ```\n\n3.  **Start the Application**:\n    You can either use the root pnpm scripts directly or the helper script `run.sh`.\n\n    **Development Mode with pnpm**:\n    Starts both server (port 3000) and client (port 5173).\n    ```bash\n    pnpm run dev\n    ```\n\n    **Production Build with pnpm**:\n    Builds the client and compiled server output.\n    ```bash\n    pnpm run build\n    ```\n\n    **Production Start with pnpm**:\n    Starts the compiled server from `dist/server`. This is the same startup contract used by the Docker image via `pnpm start`.\n    ```bash\n    pnpm start\n    ```\n\n    **Development Mode with helper script**:\n    Starts both server (port 3000) and client (port 5173).\n    ```bash\n    ./run.sh dev\n    ```\n\n    **Production Mode with helper script**:\n    Builds the application and starts the server (port 3000).\n    ```bash\n    ./run.sh\n    ```\n\n4.  **CrowdSec mTLS smoke test**:\n    Starts a disposable CrowdSec LAPI container, generates temporary server/client certificates, enables LAPI client certificate verification, logs in through the Web UI LAPI client, and confirms CrowdSec registered the TLS machine.\n    ```bash\n    pnpm run test:mtls:crowdsec\n    ```\n\n    Optional overrides:\n    ```bash\n    CROWDSEC_MTLS_IMAGE=crowdsecurity/crowdsec:latest pnpm run test:mtls:crowdsec\n    CROWDSEC_MTLS_KEEP=1 pnpm run test:mtls:crowdsec\n    CROWDSEC_MTLS_CONTAINER=my-crowdsec-test pnpm run test:mtls:crowdsec\n    ```\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=TheDuffman85/crowdsec-web-ui\u0026type=Date)](https://star-history.com/#TheDuffman85/crowdsec-web-ui\u0026Date)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheduffman85%2Fcrowdsec-web-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftheduffman85%2Fcrowdsec-web-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheduffman85%2Fcrowdsec-web-ui/lists"}