Midterm Update: Building a Zero-Trust Triage Path for BLT-NetGuardian
Google Summer of Code 2026 · OWASP BLT · BLT-NetGuardian
Author: Preetham Poojari · July 2026
Archived on the GSoC work-product page: midterm.html
The problem we're solving
Security scanners produce findings. Triage teams need to review them, understand the evidence, and turn the important ones into tracked issues — without leaking secrets along the way. In a zero-trust model, that means every finding should arrive signed, be stored encrypted, and only be decrypted on authorized view, with a full audit trail.
At the midterm checkpoint, BLT-NetGuardian now runs that path end-to-end: from signed ingest through encrypted storage, server-side decrypt, triage, and conversion into a BLT issue.
What shipped by midterm
1. Signed ingestion (ztr-finding-1)
Findings enter through POST /api/ingest as ztr-finding-1 envelopes:
- HMAC-SHA256 signature over canonical JSON
- Body digest (
X-BLT-Body-Digest) so the wire payload cannot be tampered with - Nonce + clock skew checks for replay protection
- Support for both plaintext mode (dev) and
payload_ciphertext(production-shaped)
Ingest rejects bad signatures, digest mismatches, expired timestamps, and replayed nonces with structured error codes — not generic 500s.
2. Evidence encryption and decrypt-on-view
This was the biggest midterm gap to close. Evidence payloads can now be stored AES-256-GCM encrypted at rest. On GET /api/findings/{id}:
- The server decrypts only for an authorized org token
- Sensitive keys (
password,token, etc.) are redacted before the response leaves the server - Access is logged as
decrypt_view(plaintext views log asview_detail)
The triage UI shows a badge when evidence was encrypted at rest and successfully decrypted — so analysts can see the cryptographic state, not just the text.
3. Triage dashboard (live, same-origin)
The SPA at /triage.html is fully wired to the backend:
| Capability | Status |
|---|---|
| Org-scoped findings list | ✅ |
| Filters (severity, status, CVE, date range) | ✅ |
| Priority triage queue (open critical/high, no BLT issue) | ✅ |
| Risk-ranked severity sort | ✅ |
| Detail panel (evidence / risk / status tabs) | ✅ |
| Status update via PATCH | ✅ |
| Convert to Issue via BLT-API | ✅ |
| CSV export with redaction | ✅ |
| BLT-API health probe in UI | ✅ |
Nothing on the dashboard is decorative — connection status, badges, and actions all reflect real API state.
4. BLT-API integration
"Convert to Issue" calls the real BLT-API client. On success, the returned issue ID is stored on the finding and status moves to converted. The operation is idempotent: converting again returns the existing issue instead of creating duplicates.
5. Tests and reproducibility
- 186 tests passing (ingest verification, crypto roundtrip, encrypted ingest → decrypt e2e, findings API, PATCH, filters, sort)
- Local dev is one command:
python local_dev/serve.py→ openhttp://localhost:8787/triage.htmlwith tokentriage-token - Optional BLT-API stub on port 8788 for live convert-to-issue demos
- Live-ingest helper:
python local_dev/send_finding.pyposts a fresh signed+encrypted finding through the real pipeline (useful for screen recordings)
Architecture (midterm slice)
flowchart LR
Scanner[Scanner / Agent] -->|signed ztr-finding-1| Ingest["POST /api/ingest"]
Ingest --> D1[(D1 / SQLite)]
D1 --> List["GET /api/findings"]
D1 --> Detail["GET /api/findings/{id}"]
Detail -->|decrypt + redact + audit| UI[Triage SPA]
UI -->|PATCH status| D1
UI -->|POST convert-to-issue| BLT[BLT-API]
BLT --> D1
UI -->|GET export.csv| D1
Stack: Cloudflare Python Worker (BLTWorker) + D1-compatible store + static SPA in public/. Locally, serve.py bridges the same worker logic to an in-memory SQLite stand-in so the full flow is testable without a Cloudflare deploy.
Security invariants we enforce today:
- Org isolation on every findings route (Bearer token →
org_id) - No plaintext secrets in API responses or CSV export
- Decrypt events are auditable (
decrypt_viewin access logs) - Ingest never stores raw ciphertext as readable JSON without the wrapper format
Midterm demo flow
The checkpoint from the proposal is:
signed ingestion → Finding in DB → triage list with filters → server-side decrypt/view evidence → Convert to Issue with CVE autopopulated
That flow is demonstrable today:
- Ingest — seed data or
send_finding.pyposts a signed envelope; server returns201 created - List — triage UI loads org-scoped findings with filters and sort
- Detail — open the encrypted finding (
semgrep.python.sql-injection); see redacted payload + decrypt badge + audit log - Convert — click Convert to Issue; BLT-API returns an issue ID; finding status →
converted - Export — CSV download with no plaintext secrets
A shot-by-shot recording script lives in the repo: the midterm demo script (local working notes).
Code delivery
Work is in MR !12 on owasp-blt/blt-netguardian (branch gsoc/evidence-encryption), including:
- AES-256-GCM evidence encryption + decrypt-on-view
- Wording fix for ciphertext ingest validation
- Findings API hardening (risk-ranked severity sort, deduplicated row mapper, PATCH 400 on bad JSON)
What we intentionally deferred
Honest scope boundaries for the second half of GSoC:
| Item | Why deferred |
|---|---|
| Cloudflare deployment (Worker + persistent D1) | Final deliverable; local path is reproducible for midterm |
| Workers WebCrypto adapter | Local/dev uses Python cryptography; port for production runtime |
| GitHub OAuth / PKCE | Org Bearer token auth works for midterm; OAuth is a larger auth slice |
| Flutter desktop client | Server contracts are stable; client can integrate later |
| Verified events webhook | Week 11 milestone |
| Detection MVP (Semgrep + HTTP checks) | Week 7–9 milestone |
| PDF export | Week 12, timeboxed |
These are schedule choices, not unknowns — the midterm slice is the trust + triage backbone; the second half adds scanners, events, deployment, and polish.
Lessons learned
- Sort by meaning, not alphabet. Severity is a category. Lexical sort put
mediumabovecritical. We fixed it with an explicit risk rank in SQL — a small bug that would have confused every analyst using "Severity (high first)."
- One source of truth for API shapes. The finding object was duplicated in three places. Consolidating to
finding_row_to_item()prevented detail and PATCH from drifting apart.
- Demo contrast beats demo decoration. Seeding one encrypted and two plaintext findings lets you show decrypt-on-view is real (badge +
decrypt_viewaudit) instead of claiming it in slides.
- Same-origin local dev matters. Serving the SPA from
serve.py(notfile://or a separate Live Server) avoids CORS/auth confusion and matches how the Worker will servepublic/in production.
What's next (Weeks 7–12)
- Merge MR !12 and deploy to Cloudflare with persistent D1
- Detection pack: Semgrep rules + HTTP checks feeding the ingest pipeline
- GitHub OAuth for triage sessions
- Verified events for Rewards / RepoTrust downstream
- Flutter client against the stable ingest contract
- Pilot with a first org and metrics (time-to-triage, FP/FN on fixtures)
Try it yourself
git clone https://gitlab.com/owasp-blt/blt-netguardian.git
cd blt-netguardian
git checkout gsoc/evidence-encryption
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
# Terminal A
.venv/bin/python local_dev/blt_api_stub.py
# Terminal B
.venv/bin/python local_dev/serve.py
Open http://localhost:8787/triage.html · token: triage-token
Post a live finding:
.venv/bin/python local_dev/send_finding.py
Refresh the UI — a new zap.ssrf critical finding should appear, encrypted at rest.
Links
- Repo: gitlab.com/owasp-blt/blt-netguardian
- Midterm MR: !12 — Evidence encryption + findings API hardening
- Production (future): netguardian.owaspblt.org
Questions or feedback welcome on the MR or OWASP BLT channels.
