fix: race conditions in lock pop, CSV formula injection, serialize_finding None leak, consolidate plans, update docs
This commit is contained in:
35
AGENTS.md
35
AGENTS.md
@@ -26,7 +26,7 @@ For local development without Docker:
|
||||
make install dev
|
||||
export $(cat .env | xargs)
|
||||
python -m guarddog_nexus.main
|
||||
make test # 135 tests
|
||||
make test # 137 tests (101 unit + 36 e2e)
|
||||
make lint # ruff
|
||||
make format # ruff format + fix
|
||||
```
|
||||
@@ -56,12 +56,12 @@ guarddog_nexus/
|
||||
├── web/ # Static assets
|
||||
│ ├── templates/ # Jinja2 templates
|
||||
│ └── static/ # CSS, JS
|
||||
├── schemas.py # Pydantic models + serialize_finding helper
|
||||
├── config.py # env-var configuration dataclass
|
||||
├── constants.py # all magic strings/limits
|
||||
├── schemas.py # Pydantic models + serialize_finding() helper
|
||||
├── config.py # env-var configuration dataclass + _env_int()
|
||||
├── constants.py # all magic strings/limits + SUPPORTED_ECOSYSTEMS
|
||||
├── i18n.py # RU/EN translation dictionaries
|
||||
├── logging_setup.py # JSON logging + syslog
|
||||
└── main.py # FastAPI app, middleware, lifepan
|
||||
└── main.py # FastAPI app, middleware, lifespan, /health/dependencies
|
||||
```
|
||||
|
||||
**Data flow:**
|
||||
@@ -70,7 +70,7 @@ guarddog_nexus/
|
||||
3. `harvester.py` downloads file (async via `asyncio.to_thread`), validates URL against `NEXUS_ALLOWED_HOSTS` (SSRF protection), computes SHA256, deduplicates
|
||||
4. `scanner.py` runs `guarddog <ecosystem> scan <file> --output-format json`
|
||||
5. Findings stored in SQLite (`scans` + `findings` tables)
|
||||
6. If `LLM_ENABLED=1` and `LLM_AUTO_ANALYZE=1`, `llm.py` sends each finding to the configured model with retry logic. `finding.report` state machine: `None` → `{"status": "analyzing"}` → `{verdict, summary, analysis, severity_rating}` or `None` on failure. LLM response validated with defaults for missing fields.
|
||||
6. If `LLM_ENABLED=1` and `LLM_AUTO_ANALYZE=1`, `llm.py` sends findings to the configured model in parallel via `asyncio.gather` (respects `LLM_MAX_CONCURRENT_ANALYSES`). Retry logic with exponential backoff (2s, 4s, 8s, max 3 attempts). `finding.report` state machine: `None` → `{"status": "analyzing"}` → `{verdict, summary, analysis, severity_rating}` or `None` on failure. LLM response validated via `_validate_report()` which applies defaults for missing fields (`verdict→unknown`, `severity_rating→unknown`, etc.).
|
||||
|
||||
---
|
||||
|
||||
@@ -81,10 +81,11 @@ guarddog_nexus/
|
||||
- **Line length:** 100 (ruff)
|
||||
- **Lint:** `ruff check guarddog_nexus tests` (E/F/I/W rules)
|
||||
- **Format:** `ruff format guarddog_nexus tests`
|
||||
- **Tests:** `pytest -v` (135 tests, pytest-asyncio auto mode)
|
||||
- **Tests:** `pytest -v` (137 tests: 101 unit + 36 e2e, pytest-asyncio auto mode)
|
||||
- **Commits:** Russian descriptions, prefix convention: `feat:`, `fix:`, `refactor:`, `docs:`, `ui:`
|
||||
- **No comments** in code unless explicitly requested
|
||||
- **Async I/O:** file reads/writes wrapped in `asyncio.to_thread()` — never raw `open()` in async context
|
||||
- **Config validation:** `_env_int` logs a warning on invalid values instead of crashing
|
||||
|
||||
---
|
||||
|
||||
@@ -98,10 +99,16 @@ All via environment variables, defined in `config.py`. Key ones:
|
||||
| `NEXUS_ALLOWED_HOSTS` | host from `NEXUS_URL` | comma-separated, SSRF protection |
|
||||
| `WEBHOOK_SECRET` | `""` | HMAC-SHA256 validation |
|
||||
| `MAX_CONCURRENT_SCANS` | `4` | asyncio.Semaphore for guarddog processes |
|
||||
| `SCAN_TIMEOUT_SECONDS` | `300` | per-package scan timeout |
|
||||
| `GUARDDOG_BINARY` | `guarddog` | path to GuardDog CLI |
|
||||
| `NEXUS_DOWNLOAD_TIMEOUT_SECONDS` | `120` | download timeout from Nexus |
|
||||
| `NEXUS_API_TIMEOUT_SECONDS` | `30` | Nexus REST API timeout |
|
||||
| `LLM_ENABLED` | `0` | `1` to enable analysis |
|
||||
| `LLM_AUTO_ANALYZE` | `0` | `1` to auto-trigger after scan; `0` = manual via UI button |
|
||||
| `LLM_API_KEY` | `""` | OpenAI-compatible key |
|
||||
| `LLM_API_BASE` | `https://api.openai.com/v1` | OpenAI-compatible base URL |
|
||||
| `LLM_MODEL` | `gpt-4o-mini` | |
|
||||
| `LLM_TIMEOUT_SECONDS` | `30` | LLM request timeout |
|
||||
| `LLM_MAX_CONCURRENT_ANALYSES` | `2` | Semaphore for LLM calls |
|
||||
| `DATABASE_PATH` | `data/guarddog.db` | |
|
||||
|
||||
@@ -127,13 +134,13 @@ Full list in `config.py`.
|
||||
| Value | UI |
|
||||
|-------|----|
|
||||
| `None` | Show "Analyze with LLM" button (manual mode only) |
|
||||
| `{"status": "analyzing"}` | Show spinner |
|
||||
| `{"status": "analyzing"}` | Show spinner (auto-polls via HTMX GET every 2s) |
|
||||
| `{verdict:, summary:, ...}` | Show report + "Retry" link |
|
||||
|
||||
**Auto mode** (`LLM_AUTO_ANALYZE=1`): analysis runs immediately after scan; button hidden.
|
||||
**Manual mode** (`LLM_AUTO_ANALYZE=0`): user clicks button; button visible for each finding.
|
||||
|
||||
Per-finding `asyncio.Lock` in `web.py` prevents concurrent analysis of the same finding. Retry passes `?retry=1` to bypass the idempotency guard.
|
||||
Per-finding `asyncio.Lock` in `web.py` prevents concurrent analysis of the same finding. Retry passes `?retry=1` to bypass the idempotency guard. LLM response validated via `_validate_report()` — missing/invalid fields get defaults (`verdict→unknown`, `severity_rating→unknown`, etc.). Retry with exponential backoff: 2s, 4s, 8s (max 3 attempts). Reports can also be unwrapped from markdown code fences (```json ... ```).
|
||||
|
||||
---
|
||||
|
||||
@@ -164,7 +171,7 @@ docker compose down -v # stop + destroy volumes (make docker-destroy)
|
||||
docker compose logs -f # tail logs
|
||||
```
|
||||
|
||||
The Dockerfile uses `uv pip install . --system` to install the package and all dependencies from `pyproject.toml`. GuardDog is installed as a separate `uv pip install` step.
|
||||
The Dockerfile uses `uv pip install . --system` to install the package and all dependencies from `pyproject.toml`. GuardDog is installed as a separate `uv pip install --system "guarddog>=2.10.0"` step. A `.dockerignore` excludes cache dirs, tests, and examples. Docker HEALTHCHECK at `/health` runs every 30 seconds.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,7 +181,7 @@ The Dockerfile uses `uv pip install . --system` to install the package and all d
|
||||
- Tests use in-memory SQLite (`:memory:`)
|
||||
- `conftest.py` sets up `os.environ` before importing the app
|
||||
- Mock `guarddog` output via fixtures — no real CLI execution
|
||||
- 135 tests covering: API, webhooks, harvester, scanner, web UI, i18n, metrics, LLM analysis, e2e flows
|
||||
- 137 tests covering: API, webhooks, harvester, scanner, web UI, i18n, metrics, LLM analysis, e2e flows
|
||||
- E2E tests in `tests/e2e/` cover full webhook-to-scan pipeline, API filtering/pagination, LLM analysis, and error handling
|
||||
|
||||
When adding features:
|
||||
@@ -218,8 +225,14 @@ curl -X POST http://localhost:8080/webhooks/nexus \
|
||||
|
||||
- **AI-generated code:** all code in this repository was generated by an AI assistant (Claude). Review carefully before production use.
|
||||
- **No Nexus Pro required:** the system works with Nexus OSS. Webhooks can be triggered manually or via community plugins.
|
||||
- **Anonymous Nexus access:** all Nexus REST API calls use anonymous access (no BasicAuth). Ensure your Nexus instance allows anonymous read access to repositories.
|
||||
- **GuardDog deadlocks:** GuardDog is CPU-intensive. Use `MAX_CONCURRENT_SCANS` to avoid resource exhaustion.
|
||||
- **LLM may be slow:** increase `LLM_TIMEOUT_SECONDS` for large models. Set `LLM_MAX_CONCURRENT_ANALYSES` to limit parallel requests.
|
||||
- **Security headers:** `SecurityHeadersMiddleware` sets X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, and Permissions-Policy on all responses.
|
||||
- **Background tasks:** URL lock and LLM lock cleanup tasks run every 30 minutes via the lifespan; they are gracefully cancelled on shutdown.
|
||||
- **`serialize_finding()` helper** in `schemas.py` prevents `**f.data` field injection in API responses by extracting only known fields.
|
||||
- **`SUPPORTED_ECOSYSTEMS`** constant in `constants.py` defines the accepted ecosystem set (`pypi`, `go`, `npm`).
|
||||
- **`/_health/dependencies`** endpoint checks database connectivity and Nexus API reachability.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user