Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ ADMIN_USER_IDS=your-admin-user-id
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-specific-password
SMTP_PASS=your-app-specific-password
# ZeroClaw webhook integration — generate with: python -c "import secrets; print(secrets.token_hex(32))"
ZEROCLAW_WEBHOOK_SECRET=your-zeroclaw-shared-secret
276 changes: 276 additions & 0 deletions docs/zeroclaw-integration-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# ZeroClaw × LPI Platform — Integration Strategy Document

**Author:** Daksh Garg
**Date:** June 27, 2026
**Branch:** `daksh-zeroclaw-strategy`
**Status:** Ready for team review

---

## Section A — Integration Overview

### What is ZeroClaw?

ZeroClaw is an automated security scanning platform built on FastAPI that runs multiple audit pipelines against code repositories:
- **Syntax Auditor** — static code pattern scanning
- **Architecture Auditor** — structural and design analysis
- **pip-audit** — Python dependency vulnerability detection
- **detect-secrets** — hardcoded credential detection

Its AI engine (ZeroClaw Labs Agent Runtime via OpenRouter) enriches raw findings with STRIDE threat classification, OWASP alignment, reasoning chains, and auto-generated patch code.

### How the Integration Works (Plain English)

ZeroClaw scans a repository and emits structured security events. LPI receives those events via a dedicated webhook endpoint, normalizes them into LPI's existing `Signal` schema, and stores them in `activity_signals`. From there, they surface in the team velocity metrics dashboard (`GET /api/v1/metrics/team`) and can inform recommendations. The UNIQUE constraint on `signal_id` prevents duplicate alerts from being processed twice — no app-layer lock or Redis needed.

This makes ZeroClaw LPI's **security intelligence stream** — sitting alongside GitHub and Boardy as a first-class signal source.

---

## Section B — Architecture

```
ZeroClaw Platform
│ POST (webhook push on scan.completed / vulnerability.detected)
│ OR GET (polling on scan status)
LPI Webhook Receiver
POST /api/v1/signals/zeroclaw
├── HMAC-SHA256 signature verification (or service API key header)
├── Payload normalization layer (zeroclaw_normalizer.py)
└── Maps ZeroClaw payload → LPI Signal schema
store.insert_signal()
└── activity_signals table
stream="zeroclaw", source="zeroclaw_webhook"
├──▶ create_notification_if_new() (dedup by signal_id UNIQUE constraint)
│ └── notifications table
└──▶ GET /api/v1/metrics/team (surfaces in velocity/security dashboard)
└── security findings visible in per_user_velocity + team_summary
```

### New environment variables required

```
ZEROCLAW_WEBHOOK_SECRET=<shared HMAC secret — generated and shared with ZeroClaw>
OPENROUTER_API_KEY=sk-or-v1-... (ZeroClaw's AI enrichment — their side only)
```

---

## Section C — API / Webhook Contract

### ZeroClaw Events

ZeroClaw emits four primary event types:

| ZeroClaw Event | LPI `event_type` | Trigger |
|---|---|---|
| `scan.started` | `zeroclaw_scan_started` | Scan pipeline begins |
| `scan.completed` | `zeroclaw_scan_completed` | All scanners finished |
| `scan.failed` | `zeroclaw_scan_failed` | Pipeline error |
| `vulnerability.detected` | `vulnerability_detected` | Finding found |

### ZeroClaw Payload Schema (from their `schema.json` contract)

```json
{
"event_type": "vulnerability.detected",
"scanner_name": "string",
"timestamp": "2026-06-27T10:00:00Z",
"vulnerabilities": [
{
"id": "PATTERN-0001",
"severity": "HIGH",
"file_path": "src/lpi/routers/goals.py",
"line_number": 42,
"description": "XSS risk: dangerouslySetInnerHTML at line 42",
"status": "active"
}
]
}
```

### Mapping to LPI `Signal` Schema

| ZeroClaw Field | LPI Signal Field | Notes |
|---|---|---|
| `event_type` | `event_type` | Direct map |
| `scanner_name` | `stream` | e.g. `"zeroclaw"` |
| `timestamp` | `timestamp` | Parse to UTC datetime |
| Full payload | `payload` | Store as JSONB |
| Derived from payload | `id` | `uuid4()` generated by LPI |
| From JWT / service key | `user_id` | Resolved from auth context |
| Hardcoded | `source` | `"zeroclaw_webhook"` |

### LPI Signal produced

```json
{
"id": "<uuid4>",
"user_id": "<resolved from auth>",
"stream": "zeroclaw",
"event_type": "vulnerability_detected",
"source": "zeroclaw_webhook",
"payload": {
"scanner_name": "pattern_scanner",
"severity": "HIGH",
"findings_count": 2,
"vulnerabilities": [ ... ]
},
"timestamp": "2026-06-27T10:00:00Z"
}
```

---

## Section D — New Code Required

| File | Action | Description |
|---|---|---|
| `src/lpi/routers/signals.py` | Modify | Add `POST /api/v1/signals/zeroclaw` route |
| `src/lpi/utils/zeroclaw_auth.py` | Create | HMAC-SHA256 signature verification middleware |
| `src/lpi/utils/zeroclaw_normalizer.py` | Create | Maps raw ZeroClaw payload → LPI Signal schema |
| `src/lpi/config.py` | Modify | Add `zeroclaw_webhook_secret: str = ""` to Settings |
| `supabase/migrations/20260627000002_zeroclaw_signals.sql` | Create | Add `zeroclaw_scan_started`, `zeroclaw_scan_completed`, `zeroclaw_scan_failed`, `vulnerability_detected` to `user_activity_logs` CHECK constraint if needed |
| `.env.example` | Modify | Add `ZEROCLAW_WEBHOOK_SECRET=` |

---

## Section E — Authentication & Security

### ZeroClaw's preferred auth options (from their response)
1. **HMAC-SHA256 shared secret** — ZeroClaw signs every payload with a shared secret. LPI verifies the signature on receipt.
2. **Service-to-service API key** — A dedicated key included in the request header.

### Recommended approach: HMAC-SHA256

This matches the pattern ZeroClaw prefers and is more secure than a plain API key (payload integrity is verified, not just identity).

### How LPI verifies (exact pseudocode)

```python
# src/lpi/utils/zeroclaw_auth.py
import hashlib
import hmac
from fastapi import HTTPException, Request
from lpi.config import settings

async def verify_zeroclaw_signature(request: Request) -> bytes:
"""Verify HMAC-SHA256 signature on incoming ZeroClaw webhook."""
signature_header = request.headers.get("X-ZeroClaw-Signature")
if not signature_header:
raise HTTPException(status_code=401, detail="Missing ZeroClaw signature.")

body = await request.body()
secret = settings.zeroclaw_webhook_secret.encode()
expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
actual = signature_header.removeprefix("sha256=")

if not hmac.compare_digest(expected, actual):
raise HTTPException(status_code=401, detail="Invalid ZeroClaw signature.")

return body
```

### Secret storage
- Stored as `ZEROCLAW_WEBHOOK_SECRET` in `.env` (never committed)
- Added to `Settings` class in `config.py` as `zeroclaw_webhook_secret: str = ""`
- Same pattern as `SUPABASE_JWT_SECRET` and `BOARDY_WEBHOOK_SECRET`

### On auth failure
- Return `401 Unauthorized`
- Do not process or store the payload
- Log the attempt via `logger.warning("ZeroClaw signature verification failed from %s", ip)`

---

## Section F — Error Handling & Reliability

### Malformed payload
- Wrap normalizer in `try/except`
- On failure: return `400 Bad Request` with detail message, log the raw body for debugging
- Do not crash — ZeroClaw should not retry on 4xx

### Retry behavior (from ZeroClaw)
- ZeroClaw prefers a **webhook push model** but can support polling as fallback
- No formal retry queue confirmed yet — see Open Questions
- LPI should return `200 OK` immediately to acknowledge receipt, then process async if needed

### Idempotency
- `signal_id` UNIQUE constraint already in `activity_signals` (from migration `20260625000000`)
- If ZeroClaw replays a `scan.completed` event, the second `store.insert_signal()` call will raise a DB constraint error → caught → silently skipped
- Notifications table also has `UNIQUE(signal_id)` — dedup handled at DB level, zero app-layer lock needed

### Response format ZeroClaw expects
```json
{ "status": "success" }
```
Return `200 OK` with this body. Any non-2xx triggers ZeroClaw's retry logic.

---

## Section G — Testing Plan

| # | Test | Method | Expected Result |
|---|---|---|---|
| 1 | Valid webhook receipt | `POST /api/v1/signals/zeroclaw` with correct HMAC signature + sample payload | Signal stored in `activity_signals`, notification created, `200 OK` returned |
| 2 | Duplicate event replay | POST same payload twice (same `id` in vulnerabilities) | Second insert silently rejected by UNIQUE constraint, `200 OK` still returned |
| 3 | Invalid signature | POST with wrong HMAC header | `401 Unauthorized`, nothing stored, attempt logged |
| 4 | Unknown event type | POST with `event_type: "scan.unknown"` | Graceful skip, logged as warning, `200 OK` returned |
| 5 | Missing signature header | POST with no `X-ZeroClaw-Signature` header | `401 Unauthorized` |
| 6 | Malformed payload | POST with invalid JSON | `400 Bad Request`, raw body logged |
| 7 | Sandbox end-to-end | Trigger real scan in ZeroClaw sandbox | Signal appears in `GET /api/v1/signals/?stream=zeroclaw` within seconds, surfaces in `GET /api/v1/metrics/team` |

---

## Section H — Timeline Estimate

| Task | Owner (TBD) | Estimate |
|---|---|---|
| DB migration (action check update) | Aryan | 1 hour |
| `zeroclaw_auth.py` — HMAC verification | Daksh | 2 hours |
| `zeroclaw_normalizer.py` — payload mapping | Daksh | 2 hours |
| `POST /api/v1/signals/zeroclaw` route | Adil | 2 hours |
| Unit tests for auth + normalizer | Daksh | 2 hours |
| Integration / E2E test with sandbox | Jaivardhan | 3 hours |
| Frontend: "Run Security Scan" button + results UI | Jahanvi | 1–2 days |
| Deploy + smoke test in staging | Aryan + Danial | 2 hours |
| **Total** | | **~3 days** |

---

## Section I — Open Questions

The following must be resolved before implementation begins. Each is a specific blocker.

| # | Question | Why it blocks us |
|---|---|---|
| 1 | **Platform metadata fields:** Does ZeroClaw need us to inject workspace IDs or stream session keys into the payload schema? | Without this we cannot finalize the normalizer — unknown fields would be silently dropped |
| 2 | **Delivery mechanism preference:** Webhook push or polling? ZeroClaw said they naturally expose a polling API but asked for our preference | Determines whether we build a receiver endpoint (webhook) or a scheduled pull job |
| 3 | **Auth pattern final decision:** HMAC-SHA256 or dedicated service API key? | Determines which auth middleware to build |
| 4 | **Peak request volume:** How many scan events/hour does LPI expect to receive? | Required to configure FastAPI worker limits and set rate-limit thresholds |
| 5 | **Event replay on failure:** If our webhook returns 5xx, does ZeroClaw retry? How many times, with what backoff? | Without this we cannot design our reliability guarantees |
| 6 | **Sandbox access:** Do we have credentials for ZeroClaw's test environment? | Without a sandbox we cannot run the end-to-end test in Section G |

---

## Submission Checklist

- [x] All eight sections complete
- [x] ZeroClaw event → LPI signal mapping table filled in
- [x] Sample payload JSON included for `vulnerability.detected`
- [x] Auth section includes exact HMAC verification pseudocode
- [x] Testing plan has 7 test cases with expected outcomes
- [x] Timeline section has task-level estimates with owners
- [x] Open questions phrased as actionable blockers
- [ ] Sandbox credentials received from ZeroClaw team
- [ ] Final open questions answered by ZeroClaw contact

---

*Document ready for submission to group — June 27, 2026*
3 changes: 3 additions & 0 deletions src/lpi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ class Settings(BaseSettings):
github_client_id: str = ""
github_client_secret: str = ""
admin_user_ids: str = ""
# ── Notification service (SMTP) ───────────────────────────────────────────
smtp_server: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_pass: str = ""
# ── ZeroClaw webhook integration ──────────────────────────────────────────
zeroclaw_webhook_secret: str = ""

@property
def admin_ids_list(self) -> list[str]:
Expand Down
Loading
Loading