Autonomous AI security auditing agent for GitHub repositories.
Drop a GitHub URL and watch the AI clone the repo, run static analysis, and reason through vulnerabilities in real time — streaming every thought to your browser as it works.
🔗 Live demo: https://devaudit.nathnaeltesfaw.dev
| Repository | Report | Notes |
|---|---|---|
| mitmproxy/mitmproxy | View report | 17 confirmed findings, incl. a high-severity GitHub Actions shell-injection issue |
| sherlock-project/sherlock | View report | 56 raw scanner hits triaged down to 0 real findings |
| OWASP/WebGoat | View report | 188 raw Semgrep hits reviewed, 13 confirmed findings |
- Clones the target repo (shallow clone, latest commit only)
- Scans with three static analysis tools in parallel:
- Reasons with Claude (claude-sonnet-5 with adaptive thinking) — evaluates each finding, filters false positives, assigns severity, writes fix recommendations
- Streams every token of AI reasoning live to the browser over WebSocket
- Generates a shareable public report URL with full findings grouped by severity
Browser
│
├── Next.js (port 3000)
│ └── WebSocket client → live thought log + findings panel
│
└── Express API (port 3001)
├── POST /api/audits → enqueue job
├── GET /api/audits/:id → poll status + events
├── GET /api/reports/:slug → shareable report
└── WebSocket /ws → push events to browser
↑
BullMQ + Redis (job queue)
↓
Worker Process
├── git clone
├── Semgrep + Bandit + Gitleaks (parallel)
├── Claude API (streaming)
└── PostgreSQL (persist events + findings)
| Layer | Tech |
|---|---|
| Frontend | Next.js 14, Tailwind CSS, WebSocket |
| API | Express, BullMQ, WebSocket (ws) |
| Agent worker | Node.js, Anthropic SDK (claude-sonnet-5) |
| Static analysis | Semgrep, Bandit, Gitleaks |
| Queue | BullMQ + Redis |
| Database | PostgreSQL |
| Deployment | Docker Compose, AWS EC2, Cloudflare Tunnel (HTTPS, no exposed ports) |
Prerequisites: Docker + Docker Compose
git clone https://github.com/Nathnael45/DevAudit.git
cd DevAudit
cp .env.example .env
# Fill in ANTHROPIC_API_KEY, JWT_SECRET, INTERNAL_SECRET, and POSTGRES_PASSWORD in .env
docker-compose up --buildOpen http://localhost:3000.
Generate a secret (for JWT_SECRET and INTERNAL_SECRET):
openssl rand -base64 32Postgres and Redis are not published to the host — only api/worker need to reach
them, over the docker network. If you run api/worker/web natively via the root
npm run dev script instead of the full compose stack, add a git-ignored
docker-compose.override.yml that republishes 5432/6379 for your machine.
Upgrading an existing deployment (verified against a real local volume that predated these changes — both steps were needed, not just the first one people usually remember):
- The
auditstable gained anowner_token_hashcolumn (used to authorize audit cancel/delete without requiring an account).docker-entrypoint-initdb.donly runs on a fresh volume, so it won't apply automatically:It also gained adocker compose exec postgres psql -U devaudit -d devaudit \ -c "ALTER TABLE audits ADD COLUMN IF NOT EXISTS owner_token_hash TEXT;"
timingscolumn (per-phase clone/scan/AI durations, used to back up the performance numbers below with real measurements instead of config constants):docker compose exec postgres psql -U devaudit -d devaudit \ -c "ALTER TABLE audits ADD COLUMN IF NOT EXISTS timings JSONB;"
- If your volume predates
POSTGRES_PASSWORDbeing required (i.e. it was initialized with the old hardcodeddevaudit/devauditcredential), Postgres keeps that password on disk — setting a newPOSTGRES_PASSWORDin.envalone won't rotate it, andapi/workerwill fail to connect. Update it to match:docker compose exec -e PGPASSWORD=devaudit postgres psql -U devaudit -d devaudit \ -c "ALTER USER devaudit WITH PASSWORD '<value of POSTGRES_PASSWORD in .env>';" docker compose restart api worker
- CORS is now restricted to
WEB_URLinstead of allowing any origin. If.envdoesn't setWEB_URLto wherever the web app is actually reachable (e.g.https://your-domain.com), it defaults tohttp://localhost:3000— meaning the real frontend will suddenly get CORS errors calling the API until you set it correctly and rundocker compose up -d api. - The frontend no longer hardcodes
${hostname}:3001— it now readsNEXT_PUBLIC_API_URL/NEXT_PUBLIC_WS_URL, baked in at build time (not read at container runtime, sodocker compose up -dalone won't pick up a change — needs--build). Check.envon the server before redeploying: if either var is set to something stale (e.g. a leftoverYOUR_EC2_IPplaceholder), that broken value gets permanently baked into the bundle. Leaving both blank is almost always what you want — it preserves the automatic same-host detection that lets the demo survive an IP change without a rebuild.
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key |
JWT_SECRET |
Random secret for JWT signing |
INTERNAL_SECRET |
Random secret shared by api/worker, required on POST /internal/broadcast |
POSTGRES_PASSWORD |
Postgres password (also used to build DATABASE_URL) |
DATABASE_URL |
PostgreSQL connection string |
REDIS_URL |
Redis connection string |
NEXT_PUBLIC_API_URL |
API base URL (browser-facing) |
NEXT_PUBLIC_WS_URL |
WebSocket base URL (browser-facing) |
INTERNAL_API_URL |
API base URL (server-side, uses Docker hostname) |
WEB_URL |
The only origin the API's CORS policy allows — must match wherever web is actually served |
Observable agent loop — the core differentiator. Rather than returning a finished report, every step of the AI's reasoning streams live to the browser. Users watch the agent think ("Semgrep found 3 issues → analyzing for false positives → confirmed SQL injection at line 47").
HTTP broadcast over persistent WebSocket — the worker pushes events to the API via HTTP POST (/internal/broadcast) rather than maintaining a persistent WebSocket connection, which proved unreliable across Docker networks. The API then fans out to browser clients.
Batched streaming — Claude token deltas are buffered (300ms or 200 chars) before broadcasting to avoid flooding the HTTP broadcast endpoint with hundreds of tiny requests per second.
Shallow clone — git clone --depth 1 keeps clone times under 10 seconds for most repos and avoids storing full git history on disk.
Ownership without accounts — most audits are started anonymously, but people still need to cancel or delete the ones they kicked off. Each audit gets a random owner token at creation time (returned once, held client-side); cancel/delete require that token or a matching authenticated user_id. GET /api/audits/recent and shareable reports stay public, but only the creator can mutate their own audit.
Shared secret on the internal broadcast route — the worker pushes events to the browser via POST /internal/broadcast on the API, but the API's port is reachable from outside the docker network too (the browser talks to it directly). That route requires an INTERNAL_SECRET header rather than trusting network placement alone.
Per-phase timing, not just config constants — each audit records clone/scan/AI durations (audits.timings), with each of the three scanners timed individually even though they run concurrently. That's what makes it possible to state an honest parallelism payoff (parallel wall-clock vs. the sum of the three durations run serially) instead of just describing the setting (concurrency: 3) and assuming it helped.
Flat subdomains, not nested ones — the API is served from devaudit-api.nathnaeltesfaw.dev rather than the more obvious api.devaudit.nathnaeltesfaw.dev. Cloudflare's free Universal SSL certificate only covers the bare domain plus one level of wildcard (*.nathnaeltesfaw.dev); a second nested level has no matching certificate at the edge and fails the TLS handshake before the request ever reaches the tunnel. Keeping both hostnames one level deep avoids paying for Advanced Certificate Manager just for a demo.