commit 9aaf3d32d025137b57560d63c697416fceece35e Author: Viktor Barzin Date: Tue Oct 14 19:21:36 2025 +0000 initial - add implementation for simple crowdsec app to list and delete non-CAPI decisions diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4a1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..916586b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# ---- Stage 1: Build dependencies ---- +FROM python:3.12-slim AS builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends build-essential + +# Copy requirement files +COPY requirements.txt . + +# Install to a local folder +RUN pip install --prefix=/install -r requirements.txt + +# ---- Stage 2: Runtime ---- +FROM python:3.12-slim + +WORKDIR /app + +# Copy installed packages +COPY --from=builder /install /usr/local + +# Copy app source +COPY . . + +# Create non-root user +RUN useradd -m fastapiuser && chown -R fastapiuser /app +USER fastapiuser + +# Expose FastAPI port +EXPOSE 8000 + +# Start app with uvicorn +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..0ee0da2 --- /dev/null +++ b/app/api.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter, HTTPException +from app.crowdsec_api import list_decisions, delete_decision + +router = APIRouter() + + +@router.get("/decisions") +async def get_decisions(): + try: + decisions = await list_decisions() + return {"decisions": decisions} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/decisions/{ip}") +async def remove_decision(ip: str): + try: + await delete_decision(ip) + return {"status": "deleted", "ip": ip} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/crowdsec_api.py b/app/crowdsec_api.py new file mode 100644 index 0000000..547e53a --- /dev/null +++ b/app/crowdsec_api.py @@ -0,0 +1,47 @@ +import httpx +from typing import List, Dict +import os + +API_URL = os.environ["CS_API_URL"] # e.g http://127.0.0.1:8080/v1 +API_KEY = os.environ["CS_API_KEY"] # cscli bouncers add +MACHINE_ID = os.environ["CS_MACHINE_ID"] +PASSWORD = os.environ["CS_MACHINE_PASSWORD"] + + +async def list_decisions() -> List[Dict]: + """ + Get all current decisions from CrowdSec API. + """ + async with httpx.AsyncClient() as client: + resp = await client.get( + f"{API_URL}/decisions", + headers={ + "X-Api-Key": API_KEY, + "Content-Type": "application/json", + }, + ) + resp.raise_for_status() + decisions = resp.json() or [] + decisions = [d for d in decisions if d["origin"] != "CAPI"] + return decisions + + +async def delete_decision(ip: str) -> None: + """ + Delete a decision by IP. + """ + async with httpx.AsyncClient() as client: + login = await client.post( + f"{API_URL}/watchers/login", + json={ + "machine_id": MACHINE_ID, + "password": PASSWORD, + }, + headers={"Content-Type": "application/json"}, + ) + response = login.json() + token = response["token"] + resp = await client.delete( + f"{API_URL}/decisions?ip={ip}", headers={"Authorization": f"Bearer {token}"} + ) + resp.raise_for_status() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..d177258 --- /dev/null +++ b/app/main.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from app.api import router as api_router +from app.crowdsec_api import list_decisions + +app = FastAPI(title="CrowdSec Web UI") + +# Include API +app.include_router(api_router, prefix="/api") + +# Templates +templates = Jinja2Templates(directory="app/templates") + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + decisions = await list_decisions() + return templates.TemplateResponse( + "index.html", {"request": request, "decisions": decisions} + ) diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..2497b22 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,44 @@ + + + + + CrowdSec Web UI + + + + +

CrowdSec Decisions

+ + + + + + + + + + + {% for d in decisions %} + + + + + + + + + + {% endfor %} +
IPScopeTypeExpirationSourceScenarioActions
{{ d.value }}{{ d.scope }}{{ d.type }}{{ d.duration }}{{ d.origin }}{{ d.scenario }}
+ + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9c07d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +jinja2 +httpx +python-multipart