GHSA PoC Generation Activity — Design Document
Activity key:
vulnetix.ghsapocCode root:scripts/go-processors/internal/aienrich/ghsapoc*.goPrompt:scripts/go-processors/internal/aienrich/prompts/ghsapoc.mdSeed:saas/prisma/migrations/20260514000001_pix_seed_ghsapoc/migration.sqlTest:scripts/go-processors/cmd/ghsapoc-test/main.go—just go-ghsapoc-test prod [GHSA-id]
1. Overview
Purpose: For each newly-ingested GitHub Security Advisory (GHSA) that
carries at least one type='patch' GitHub-commit reference, generate a
Markdown Proof-of-Concept (PoC) that triggers the bug before the patch is
applied. The same PoC also serves as a regression test once the fix lands.
Type: aienrich activity (not a standalone processor) — fires from
(*Enricher).RunBatch per target, gated by record shape. No ECS task, no
EventBridge schedule, no Containerfile target.
Trigger surface: every record committed by the GHSA git processor flows
through aienrich.RunBatch, which calls RunGHSAPoC(ctx, ghsaID, source)
per target with a 180 s per-record budget. Gates inside RunGHSAPoC make the
call a fast no-op for any target that isn’t an eligible GHSA — so the same
hook is dormant on every non-GHSA processor that uses the shared pipeline.
What it reads:
- Read replica:
Pixrow forvulnetix.ghsapoc(provider, model, responseMaxTokens, temperature, topP, systemPromptText) - Read replica:
Exploit(existence check onexploitId='vulnetix-poc-{ghsaID}', source=vulnetix) - Read replica:
CVEMetadata(title),CVEDescription(first EN),CVEProblemType(cweId),CVEAffected(collectionURL, packageName) - Read replica:
CVEMetadataReferences(type='patch'for diff fetch; non-patch types for context) - GitHub REST API:
GET /repos/{org}/{repo}/commits/{sha}withAccept: application/vnd.github.diff(one call per parseable patch ref) - Cloudflare AI Gateway: one chat-completion call to the model configured on
the
Pixrow
What it writes:
PixLog— always, regardless of outcome (success / sentinel / skip / gateway error)Exploit(UPSERT, source=vulnetix, exploitId=vulnetix-poc-{ghsaID}) — only when the model returns a Markdown PoC (not sentinel, not skip)- S3 — markdown PoC at
vulnetix/files/{sha256}/poc-{ghsaID}.mdvias3client.Uploader.Archive
Environment variables:
PIX_INFERENCE_ENABLED=true— gates the entireaienrichpackage (nil enricher = no-op everywhere)CF_AIG_TOKEN(orAI_GATEWAY_TOKEN) — Cloudflare AI Gateway bearerAI_GATEWAY_URL— gateway endpoint baseGITHUB_TOKEN— recommended; raises GitHub API rate limit from 60/h to 5000/hS3_BUCKET_NAME— when unset, the activity still runs and writes the Exploit row, butr2Bucket/r2Keycolumns remain NULL
Pix row (seeded by saas migration 20260514000001_pix_seed_ghsapoc):
| Field | Value |
|---|---|
activityKey | vulnetix.ghsapoc |
provider | cloudflare-ai-gateway |
inferenceModel | @cf/zai-org/glm-5.2 |
responseMaxTokens | 8192 |
temperature | 0.2 |
topP | 0.9 |
systemPromptText | synced from prompts/ghsapoc.md by just pix-prompts-sync prod |
2. Business Rules
Every conditional that decides whether a call is made, how a payload is shaped, what gets persisted, or what gets sent back to the gateway. Listed in evaluation order.
2.1 Activity gate ((*Enricher).RunGHSAPoC, ghsapoc.go)
These checks short-circuit the activity before any Pix load, DB read, or
gateway call. All return nil (no error, no PixLog row).
| # | Condition | Returns | Reason |
|---|---|---|---|
| 1 | e == nil | nil | aienrich disabled globally (PIX_INFERENCE_ENABLED unset / missing gateway credentials) |
| 2 | source != "github" | nil | only the GHSA git processor produces GHSA records under source github |
| 3 | !hasGHSAPrefix(ghsaID) — i.e. id does not start with GHSA- (case-insensitive, ≥ 5 chars) | nil | PYSEC / RUSTSEC / non-GHSA records don’t carry GitHub patch refs in this shape |
| 4 | An Exploit row exists with exploitId='vulnetix-poc-{ghsaID}' AND source='vulnetix' | nil | “new only” idempotency — re-running the GHSA backfill never re-generates an existing PoC. No PixLog row written (normal skip, not a rejection). |
2.2 Pix load (pix.go::loadPixConfig)
| # | Condition | Returns |
|---|---|---|
| 5 | Pix row with activityKey='vulnetix.ghsapoc' AND enabled=true does not exist on the read replica | error — the activity is “configured-off” until a row is seeded |
| 6 | Pix.inferenceModel is empty | error |
2.3 Patch-commit collection (ghsapoc_fetch.go, ghsapoc_context.go)
| # | Condition | Effect |
|---|---|---|
| 7 | CVEMetadataReferences has zero rows with type='patch' for this cveId | activity skipped with PixLog responseText="SKIPPED: no patch-type references", rejected=true |
| 8 | Reference URL does not match ^https?://github\.com/{org}/{repo}/commit/{sha7..40} | reference dropped (regex commitURLRE) |
| 9 | More than maxCommitsPerRecord = 3 parseable patch refs | only the first 3 (ordered by CVEMetadataReferences.createdAt ASC) are considered |
| 10 | GitHub commit fetch returns non-2xx OR an HTTP error | that commit dropped silently with a warn log; activity continues with surviving commits |
| 11 | A single commit’s diff body exceeds maxDiffBytesPerCommit = 12 KiB | the commit is included in patchCommits[] with tooLarge: true and no diff body; the model is instructed in the system prompt to ignore tooLarge entries |
| 12 | The cumulative len(diff) across surviving commits exceeds maxTotalDiffBytes = 32 KiB | later commits flip to tooLarge: true instead of being dropped, preserving their presence as a signal |
| 13 | Zero commits in the final patchCommits[] have a non-empty diff | activity skipped with PixLog responseText="SKIPPED: no usable patch commits (all unfetchable or too large)", rejected=true |
2.4 Context shaping (ghsapoc_context.go)
| # | Condition | Effect |
|---|---|---|
| 14 | CVEMetadata.title starts with the same cveId (e.g. "GHSA-xxx: ...") | leading id+separator stripped via stripCVEPrefix to save tokens |
| 15 | First-source description longer than perDescriptionMaxChars = 1500 | truncated |
| 16 | More than maxReferences = 16 non-patch references | excess dropped after canonical-URL dedup (canonicalizeURL) |
| 17 | A non-patch reference has a title equal to the URL | title omitted (saves tokens — same string carried in url) |
| 18 | Reference type=='patch' | excluded from the references[] context block — patch commits are carried separately under patchCommits[] |
2.5 Gateway call (client.go::callGateway)
| # | Condition | Effect |
|---|---|---|
| 19 | cfg.InferenceModel not prefixed with workers-ai/ | prefix added — every Cloudflare-routed call is normalised to workers-ai/{model} |
| 20 | Hard call ceiling | 5 minute WithTimeout(ctx, 5*time.Minute). Parent-context deadlines (180 s for ghsapoc) still bind, since WithTimeout takes the min |
| 21 | Non-2xx HTTP response | error returned with first 1024 bytes of body |
| 22 | Zero choices[] in response | error returned |
| 23 | choices[0].message.content is empty/null AND choices[0].message.reasoning_content is non-empty | reasoning-model fallback — ResponseText = reasoning_content. Reasoning models return their answer this way — @cf/moonshotai/kimi-k2.6, which this pass ran before glm-5.2, always did |
2.6 Output interpretation (ghsapoc.go, after gateway success)
| # | Condition | Effect |
|---|---|---|
| 24 | responseText, after fence + whitespace stripping, exactly equals INSUFFICIENT_CODE_FOR_POC (isSentinel) | PixLog row written; no Exploit row, no S3 upload. Activity returns nil. |
| 25 | Trimmed responseText is empty | PixLog row written with rejected=true; activity returns an error |
| 26 | Otherwise — model returned Markdown | proceed to S3 + Exploit persistence |
2.7 Persistence (ghsapoc.go::persistGHSAPoCExploit)
| # | Condition | Effect |
|---|---|---|
| 27 | e.s3 == nil (S3 not configured) | upload skipped; r2Bucket / r2Key written as NULL on the Exploit row; bodyContentHash and fileSize still recorded |
| 28 | Uploader.Archive returns false (transient S3 failure after 15 min retry budget) | upload abandoned with a warn log; same NULL r2 fallback |
| 29 | Exploit UPSERT on (exploitId, source) conflict | row updated (mutable fields), idempotent against rare race with concurrent runs |
2.8 PixLog contract
A PixLog row is always written before RunGHSAPoC returns nil/error,
except for gate-rule #4 (existing Exploit row → silent skip, no log
spam). The row carries:
vulnId = ghsaID,vulnSource = "github"pixUuidfrom the loaded Pix row- full
systemPromptText+userPrompt(audit trail) responseText— either the model output, the sentinel literal, an error string prefixed"ERROR: ", or a skip marker prefixed"SKIPPED: "originalResponse— full upstream JSON body (when the gateway call succeeded)promptTokens,completionTokens,totalTokensfromusage{}rejected = truefor parse failures, empty responses, gateway errors, and skips;falsefor successful inference (including sentinel)
2.9 Caching
pixConfig is cached in-process for 5 minutes. Cache invalidation:
- gateway call error →
invalidatePixCache(activityKeyGHSAPoC)so a misconfigured Pix row is re-read on the next attempt
3. Architecture Diagram
Run, processBatch] end subgraph "internal/aienrich/" ENR[enricher.go
RunBatch loop] POC[ghsapoc.go
RunGHSAPoC] FETCH[ghsapoc_fetch.go
parseCommitURL, fetchCommitDiff] CTX[ghsapoc_context.go
gatherGHSAPoCContext] CLIENT[client.go
callGateway] PIX[pix.go
loadPixConfig] LOG[pixlog.go
writePixLog] PROMPTS[prompts/ghsapoc.md
embedded] end subgraph "internal/db/" EXPL[exploit.go
UpsertExploit] REFS[(CVEMetadataReferences)] META[(CVEMetadata + children)] EXTBL[(Exploit)] PIXTBL[(Pix)] PIXLOGTBL[(PixLog)] end subgraph "External" GH[GitHub REST API
commits + diff] CFG[Cloudflare AI Gateway
glm-5.2] S3[(S3
vulnetix/files/.../poc-*.md)] end MAIN --> PIPE PIPE --> ENR ENR --> POC POC --> PIX PIX --> PIXTBL POC --> CTX CTX --> REFS CTX --> META POC --> FETCH FETCH --> GH POC --> CLIENT CLIENT --> CFG CLIENT --> PROMPTS POC --> EXPL EXPL --> EXTBL POC --> LOG LOG --> PIXLOGTBL POC --> S3
4. Data Flow Diagram
5. Decision Tree (per record)
6. Data Mapping
6.1 Inputs assembled into the user-prompt JSON (ghsaPoCContext)
| JSON path | Source table / column | Notes |
|---|---|---|
ghsaId | input parameter | always GHSA-* |
summary | CVEMetadata.title WHERE source='github' | leading CVE-YYYY-NNNN[: ] stripped |
description | CVEDescription.value (first by lang='en' DESC, createdAt DESC) | truncated to 1500 chars |
cwes[] | CVEProblemType.cweId distinct | empty values dropped |
affected[].ecosystem | CVEAffected.collectionURL | up to 10 rows |
affected[].package | CVEAffected.packageName | |
references[].url | CVEMetadataReferences.url WHERE type != 'patch' | canonicalised (lowercased scheme+host, tracking params dropped, trailing / trimmed) |
references[].types[] | CVEMetadataReferences.type collapsed across sources | deduped, empty dropped |
references[].title | CVEMetadataReferences.title | omitted when equal to the URL |
patchCommits[].url | CVEMetadataReferences.url WHERE type='patch' | ordered by createdAt ASC |
patchCommits[].org/repo/sha | parsed from URL via commitURLRE | |
patchCommits[].diff | GitHub API GET /repos/{org}/{repo}/commits/{sha} w/ Accept: application/vnd.github.diff | populated only when ≤ 12 KiB and total < 32 KiB |
patchCommits[].tooLarge | derived | true when oversized or cumulative cap reached |
6.2 Outputs persisted
Exploit row (UPSERT, source=vulnetix)
| Column | Value |
|---|---|
exploitId | vulnetix-poc-{ghsaID} |
source | vulnetix |
title | PoC for {ghsaID} |
originalUrl | https://github.com/advisories/{ghsaID} |
r2Bucket | $S3_BUCKET_NAME (NULL when S3 disabled) |
r2Key | vulnetix/files/{sha256}/poc-{ghsaID}.md (NULL when S3 disabled) |
bodyContentHash | sha256(markdown bytes), lowercase hex |
fileSize | len(markdown bytes) |
cveIds | [ghsaID] as JSON array |
createdAt / updatedAt | time.Now().UnixMilli() (BigInt ms) |
PixLog row (always written, with #4-#27 exceptions)
| Column | Value |
|---|---|
vulnId | ghsaID |
vulnSource | "github" |
pixUuid | Pix.uuid for vulnetix.ghsapoc |
provider | Pix.provider (cloudflare-ai-gateway) |
inferenceModel | Pix.inferenceModel (@cf/zai-org/glm-5.2) |
systemPromptText | full system prompt (DB-sourced, with embedded fallback) |
userPrompt | compact JSON of ghsaPoCContext |
responseText | model markdown, sentinel literal, "ERROR: …", or "SKIPPED: …" |
originalResponse | full upstream JSON (when gateway succeeded) |
promptTokens/completionTokens/totalTokens | from gateway usage{} |
rejected | true for skip/error/parse-fail; false for success and sentinel |
orgUuid | NULL (system task, no tenant) |
S3 object
- Bucket:
$S3_BUCKET_NAME(e.g.vdb-manager-artifacts) - Key:
vulnetix/files/{sha256}/poc-{ghsaID}.md - ContentType:
text/markdown - Body: model output (trimmed of surrounding whitespace)
7. Operational Notes
Idempotency: re-running the GHSA backfill never re-generates an existing PoC. The
Exploitexistence check is the first DB read; replication lag on the read replica can cause at most one re-generation (UpsertExploit on(exploitId, source)is idempotent and the S3 path is content-addressed, so re-uploads are a free no-op).Cost: ~10 000 tokens per successful inference (3 K prompt, 7 K completion) on a 3 KiB Markdown PoC, measured on kimi-k2.6 — the model this pass ran before glm-5.2, which is more verbose, so expect a higher completion count. Skip cases cost zero (no gateway call).
Latency: 60–180 s per record (gateway-bound). Sequential per batch in
RunBatch; ~200 records × 180 s ≈ 10 h worst case per GHSA batch. ECS task duration budget must reflect this when enabling ghsapoc on scheduled runs.Quotas: GitHub API is rate-limited to 60/h unauthenticated, 5000/h with
GITHUB_TOKEN. Cloudflare AI Gateway has its own per-account ceilings; reasoning models like kimi-k2.6 are slower per call which naturally caps the rate.Model swap: change via
just pix-model-set vulnetix.ghsapoc '<model>'. Idempotent; no binary redeploy needed. If the new model is non-reasoning, thereasoning_contentfallback inclient.gois harmless (only fires whencontentis empty).Prompt edit: change
prompts/ghsapoc.md, thenjust pix-prompts-sync prod. The runtime readsPix.systemPromptTextat every cache miss (5 min TTL), so changes propagate within ≤ 5 min.Manual one-shot verification:
just go-ghsapoc-test prod # picks a candidate just go-ghsapoc-test prod GHSA-xxxx-xxxx-xxxx # explicit targetPrints the resulting
PixLogrow +Exploitrow + S3 key.
S3 Persistence
Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.
⚠ Not in the compliance matrix — status needs verification.
Expected paths when implemented:
- Archive:
ghsapoc-activity/files/{sha256}/{filename} - Quarantine:
failed-feeds/ghsapoc-activity/{YYYY-MM-DD}/{reason}/{filename} - Likely reasons: (none documented)