diff --git a/README.md b/README.md index 330445a..e17ac3b 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,59 @@ # Endpoint Monitor -A typed FastAPI service that stores endpoint monitors in process-local memory and performs secure, on-demand HTTP checks. +A typed FastAPI service that stores endpoint monitors in memory and runs secure, on-demand HTTP checks. There is no authentication; do not expose it directly to untrusted networks. -## Quick start +## Layout -Requires Python 3.12. +* `docs/SERVICE_DESIGN.md` — complete API/status/security contract +* `app/models.py`, `app/store.py` — typed resources and locked state +* `app/security.py`, `app/checker.py` — DNS/redirect SSRF policy and checks +* `app/main.py` — API and operational routes +* `tests/` — mocked API, checker, race, and redaction tests +* `docs/VERIFICATION.md` — reproducible validation record + +## Local development + +Python 3.12 is required. ```sh python -m venv .venv . .venv/bin/activate pip install -e '.[dev]' -make check -uvicorn app.main:app --reload --workers 1 +ruff format --check . +ruff check . +mypy app +pytest +uvicorn app.main:app --reload ``` Create and check a monitor: ```sh -curl -sS -X POST http://localhost:8000/v1/monitors \ +curl -sS -X POST http://localhost:8000/api/v1/monitors \ -H 'content-type: application/json' \ - -d '{"name":"example","url":"https://example.com/health?token=do-not-log"}' -curl -sS -X POST http://localhost:8000/v1/monitors/MONITOR_UUID/checks -curl -sS http://localhost:8000/v1/monitors/MONITOR_UUID/status + -d '{"name":"Example","url":"https://example.com/health?token=not-logged"}' +curl -sS -X POST http://localhost:8000/api/v1/monitors/MONITOR_UUID/check +curl -sS http://localhost:8000/api/v1/monitors/MONITOR_UUID/status +curl -sS http://localhost:8000/healthz ``` -Interactive OpenAPI documentation is at `/docs`. The full route/status/error contract and SSRF model are in [docs/service-design.md](docs/service-design.md). +OpenAPI is at `/docs` and `/openapi.json`. ## Configuration -All values are startup-validated and use environment prefix `MONITOR_`: - -| Variable | Default | Constraint | -|---|---:|---| -| `MONITOR_APP_NAME` | endpoint-monitor | string | -| `MONITOR_LOG_LEVEL` | INFO | logging level | -| `MONITOR_CHECK_TIMEOUT_SECONDS` | 5 | >0, <=30 | -| `MONITOR_MAX_REDIRECTS` | 5 | 0–10 | -| `MONITOR_MAX_RESPONSE_BYTES` | 1000000 | 1–10000000 | - -Checks resolve every hop and block any hostname with a non-public answer. Redirects are manual and bounded; total elapsed timeout and response size are bounded. Query and fragment data and URL credentials are redacted from structured logs. URL credentials are rejected. Application checks cannot create a perfect DNS-rebinding boundary; use deny-by-default egress networking or a hardened outbound proxy in sensitive deployments. - -## Tests and quality - -```sh -make lint # Ruff formatting check and lint -make type # strict mypy -make test # pytest API/unit suite -make check # all three -``` - -Tests are directly inspectable under `tests/`: CRUD/operations and API mapping (`test_api.py`), lock concurrency and stale compare-and-set updates (`test_store.py`), timeouts/statuses/DNS and redirect SSRF/log redaction (`test_checker.py`), and environment validation (`test_config.py`). Outbound requests use `httpx.MockTransport`; tests do not access the network. +All settings use the `MONITOR_` prefix: `APP_NAME`, `LOG_LEVEL`, `REQUEST_TIMEOUT_SECONDS` (default 5, max 30), `MAX_REDIRECTS` (default 5, max 10), and `MAX_RESPONSE_BYTES` (default 65536, max 1048576). Invalid values fail startup. ## Container ```sh -docker build -t endpoint-monitor:local . -docker run --rm -p 8000:8000 endpoint-monitor:local +docker build -t endpoint-monitor . +docker run --rm -p 8000:8000 endpoint-monitor # or docker compose up --build ``` -The image runs as a non-root user, has a liveness healthcheck, and deliberately starts one Uvicorn worker. - -## Layout - -```text -app/ service implementation - checker.py bounded checks, DNS/redirect SSRF policy - config.py validated environment settings - logging.py structured JSON and URL redaction - main.py FastAPI routes and lifecycle - models.py API schemas and status semantics - store.py lock-protected in-memory state and CAS updates -tests/ mocked unit and API tests -docs/service-design.md contract and architecture -docs/verification.md execution record and reproducible commands -Dockerfile, compose.yaml, pyproject.toml, Makefile -``` +The image uses an unprivileged user and one Uvicorn worker. One worker is mandatory because data is process-local. ## Important limitations -All monitors disappear on restart. State is not shared between processes, hosts, or Uvicorn workers, so run exactly one worker. There is no authentication; place the service behind an authenticated gateway if exposed. For durable or horizontally scaled use, replace `MonitorStore` with a transactional shared datastore and coordinate check ownership. +State is neither persistent nor shared: restart loses all monitors and multiple workers have divergent data. DNS checks occur before each hop, but the default HTTP stack resolves again while connecting. Use an egress proxy/firewall in hostile production environments to close that DNS-rebinding timing window. Logs redact URL credentials, query, and fragment; avoid secrets in path segments.