GHSA PoC Generation Activity — Design Document

Activity key: vulnetix.ghsapoc Code root: scripts/go-processors/internal/aienrich/ghsapoc*.go Prompt: scripts/go-processors/internal/aienrich/prompts/ghsapoc.md Seed: saas/prisma/migrations/20260514000001_pix_seed_ghsapoc/migration.sql Test: scripts/go-processors/cmd/ghsapoc-test/main.gojust 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: Pix row for vulnetix.ghsapoc (provider, model, responseMaxTokens, temperature, topP, systemPromptText)
  • Read replica: Exploit (existence check on exploitId='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} with Accept: application/vnd.github.diff (one call per parseable patch ref)
  • Cloudflare AI Gateway: one chat-completion call to the model configured on the Pix row

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}.md via s3client.Uploader.Archive

Environment variables:

  • PIX_INFERENCE_ENABLED=true — gates the entire aienrich package (nil enricher = no-op everywhere)
  • CF_AIG_TOKEN (or AI_GATEWAY_TOKEN) — Cloudflare AI Gateway bearer
  • AI_GATEWAY_URL — gateway endpoint base
  • GITHUB_TOKEN — recommended; raises GitHub API rate limit from 60/h to 5000/h
  • S3_BUCKET_NAME — when unset, the activity still runs and writes the Exploit row, but r2Bucket / r2Key columns remain NULL

Pix row (seeded by saas migration 20260514000001_pix_seed_ghsapoc):

FieldValue
activityKeyvulnetix.ghsapoc
providercloudflare-ai-gateway
inferenceModel@cf/zai-org/glm-5.2
responseMaxTokens8192
temperature0.2
topP0.9
systemPromptTextsynced 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).

#ConditionReturnsReason
1e == nilnilaienrich disabled globally (PIX_INFERENCE_ENABLED unset / missing gateway credentials)
2source != "github"nilonly 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)nilPYSEC / RUSTSEC / non-GHSA records don’t carry GitHub patch refs in this shape
4An 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)

#ConditionReturns
5Pix row with activityKey='vulnetix.ghsapoc' AND enabled=true does not exist on the read replicaerror — the activity is “configured-off” until a row is seeded
6Pix.inferenceModel is emptyerror

2.3 Patch-commit collection (ghsapoc_fetch.go, ghsapoc_context.go)

#ConditionEffect
7CVEMetadataReferences has zero rows with type='patch' for this cveIdactivity skipped with PixLog responseText="SKIPPED: no patch-type references", rejected=true
8Reference URL does not match ^https?://github\.com/{org}/{repo}/commit/{sha7..40}reference dropped (regex commitURLRE)
9More than maxCommitsPerRecord = 3 parseable patch refsonly the first 3 (ordered by CVEMetadataReferences.createdAt ASC) are considered
10GitHub commit fetch returns non-2xx OR an HTTP errorthat commit dropped silently with a warn log; activity continues with surviving commits
11A single commit’s diff body exceeds maxDiffBytesPerCommit = 12 KiBthe commit is included in patchCommits[] with tooLarge: true and no diff body; the model is instructed in the system prompt to ignore tooLarge entries
12The cumulative len(diff) across surviving commits exceeds maxTotalDiffBytes = 32 KiBlater commits flip to tooLarge: true instead of being dropped, preserving their presence as a signal
13Zero commits in the final patchCommits[] have a non-empty diffactivity skipped with PixLog responseText="SKIPPED: no usable patch commits (all unfetchable or too large)", rejected=true

2.4 Context shaping (ghsapoc_context.go)

#ConditionEffect
14CVEMetadata.title starts with the same cveId (e.g. "GHSA-xxx: ...")leading id+separator stripped via stripCVEPrefix to save tokens
15First-source description longer than perDescriptionMaxChars = 1500truncated
16More than maxReferences = 16 non-patch referencesexcess dropped after canonical-URL dedup (canonicalizeURL)
17A non-patch reference has a title equal to the URLtitle omitted (saves tokens — same string carried in url)
18Reference type=='patch'excluded from the references[] context block — patch commits are carried separately under patchCommits[]

2.5 Gateway call (client.go::callGateway)

#ConditionEffect
19cfg.InferenceModel not prefixed with workers-ai/prefix added — every Cloudflare-routed call is normalised to workers-ai/{model}
20Hard call ceiling5 minute WithTimeout(ctx, 5*time.Minute). Parent-context deadlines (180 s for ghsapoc) still bind, since WithTimeout takes the min
21Non-2xx HTTP responseerror returned with first 1024 bytes of body
22Zero choices[] in responseerror returned
23choices[0].message.content is empty/null AND choices[0].message.reasoning_content is non-emptyreasoning-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)

#ConditionEffect
24responseText, after fence + whitespace stripping, exactly equals INSUFFICIENT_CODE_FOR_POC (isSentinel)PixLog row written; no Exploit row, no S3 upload. Activity returns nil.
25Trimmed responseText is emptyPixLog row written with rejected=true; activity returns an error
26Otherwise — model returned Markdownproceed to S3 + Exploit persistence

2.7 Persistence (ghsapoc.go::persistGHSAPoCExploit)

#ConditionEffect
27e.s3 == nil (S3 not configured)upload skipped; r2Bucket / r2Key written as NULL on the Exploit row; bodyContentHash and fileSize still recorded
28Uploader.Archive returns false (transient S3 failure after 15 min retry budget)upload abandoned with a warn log; same NULL r2 fallback
29Exploit UPSERT on (exploitId, source) conflictrow 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"
  • pixUuid from 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, totalTokens from usage{}
  • rejected = true for parse failures, empty responses, gateway errors, and skips; false for 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

graph TD subgraph "cmd/ghsa-git-processor/" MAIN[main.go] end subgraph "internal/processor/" PIPE[pipeline.go
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

sequenceDiagram autonumber participant Proc as ghsa-git-processor participant Pipe as pipeline.Run participant Enr as enricher.RunBatch participant Poc as RunGHSAPoC participant DB as Postgres (RDS) participant GH as GitHub API participant AIG as Cloudflare AI Gateway participant S3 as S3 Proc->>Pipe: Run(cfg{Enricher}) Pipe->>Pipe: walk + filter changed files loop per batch (200 files) Pipe->>DB: BEGIN; UPSERT CVEMetadata + children; COMMIT Pipe->>Enr: RunBatch(targets[]) loop per target Enr->>Poc: RunGHSAPoC(ctx, ghsaID, "github") Poc->>Poc: gate: source/prefix/exists alt gated Poc-->>Enr: nil (silent) else proceed Poc->>DB: SELECT Pix row Poc->>DB: SELECT title/desc/CWEs/affected/refs Poc->>DB: SELECT type='patch' refs alt no patch refs Poc->>DB: INSERT PixLog SKIPPED Poc-->>Enr: nil else fetch diffs loop per patch URL (≤3) Poc->>GH: GET /repos/.../commits/{sha} (diff) GH-->>Poc: unified diff end alt all too large / unfetchable Poc->>DB: INSERT PixLog SKIPPED Poc-->>Enr: nil else assemble context Poc->>AIG: chat/completions{system+user} AIG-->>Poc: markdown OR sentinel alt sentinel Poc->>DB: INSERT PixLog Poc-->>Enr: nil else markdown Poc->>S3: PUT vulnetix/files/{sha256}/poc-{ghsaID}.md Poc->>DB: INSERT PixLog Poc->>DB: UPSERT Exploit (source=vulnetix) Poc-->>Enr: nil end end end end end end

5. Decision Tree (per record)

flowchart TD A[RunGHSAPoC ghsaID, source] --> B{enricher enabled?} B -->|no| Z1[return nil silent] B -->|yes| C{source == github?} C -->|no| Z1 C -->|yes| D{ghsaID prefix GHSA-?} D -->|no| Z1 D -->|yes| E{Exploit vulnetix-poc-* exists?} E -->|yes| Z1 E -->|no| F[load Pix row] F --> G[gather context + patch URLs] G --> H{patch refs > 0?} H -->|no| Z2[PixLog SKIPPED return nil] H -->|yes| I[fetch diffs up to 3 commits] I --> J{any commit has diff body?} J -->|no| Z2 J -->|yes| K[callGateway system+user] K --> L{HTTP error?} L -->|yes| Z3[PixLog ERROR return err] L -->|no| M{response empty?} M -->|yes| Z4[PixLog rejected return err] M -->|no| N{== INSUFFICIENT_CODE_FOR_POC?} N -->|yes| Z5[PixLog return nil no Exploit] N -->|no| O[sha256 markdown] O --> P{S3 configured?} P -->|yes| Q[Archive vulnetix/files/sha256/poc.md] P -->|no| R[skip upload] Q --> S[UPSERT Exploit] R --> S S --> T[PixLog success] T --> Z6[return nil]

6. Data Mapping

6.1 Inputs assembled into the user-prompt JSON (ghsaPoCContext)

JSON pathSource table / columnNotes
ghsaIdinput parameteralways GHSA-*
summaryCVEMetadata.title WHERE source='github'leading CVE-YYYY-NNNN[: ] stripped
descriptionCVEDescription.value (first by lang='en' DESC, createdAt DESC)truncated to 1500 chars
cwes[]CVEProblemType.cweId distinctempty values dropped
affected[].ecosystemCVEAffected.collectionURLup to 10 rows
affected[].packageCVEAffected.packageName
references[].urlCVEMetadataReferences.url WHERE type != 'patch'canonicalised (lowercased scheme+host, tracking params dropped, trailing / trimmed)
references[].types[]CVEMetadataReferences.type collapsed across sourcesdeduped, empty dropped
references[].titleCVEMetadataReferences.titleomitted when equal to the URL
patchCommits[].urlCVEMetadataReferences.url WHERE type='patch'ordered by createdAt ASC
patchCommits[].org/repo/shaparsed from URL via commitURLRE
patchCommits[].diffGitHub API GET /repos/{org}/{repo}/commits/{sha} w/ Accept: application/vnd.github.diffpopulated only when ≤ 12 KiB and total < 32 KiB
patchCommits[].tooLargederivedtrue when oversized or cumulative cap reached

6.2 Outputs persisted

Exploit row (UPSERT, source=vulnetix)

ColumnValue
exploitIdvulnetix-poc-{ghsaID}
sourcevulnetix
titlePoC for {ghsaID}
originalUrlhttps://github.com/advisories/{ghsaID}
r2Bucket$S3_BUCKET_NAME (NULL when S3 disabled)
r2Keyvulnetix/files/{sha256}/poc-{ghsaID}.md (NULL when S3 disabled)
bodyContentHashsha256(markdown bytes), lowercase hex
fileSizelen(markdown bytes)
cveIds[ghsaID] as JSON array
createdAt / updatedAttime.Now().UnixMilli() (BigInt ms)

PixLog row (always written, with #4-#27 exceptions)

ColumnValue
vulnIdghsaID
vulnSource"github"
pixUuidPix.uuid for vulnetix.ghsapoc
providerPix.provider (cloudflare-ai-gateway)
inferenceModelPix.inferenceModel (@cf/zai-org/glm-5.2)
systemPromptTextfull system prompt (DB-sourced, with embedded fallback)
userPromptcompact JSON of ghsaPoCContext
responseTextmodel markdown, sentinel literal, "ERROR: …", or "SKIPPED: …"
originalResponsefull upstream JSON (when gateway succeeded)
promptTokens/completionTokens/totalTokensfrom gateway usage{}
rejectedtrue for skip/error/parse-fail; false for success and sentinel
orgUuidNULL (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 Exploit existence 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, the reasoning_content fallback in client.go is harmless (only fires when content is empty).

  • Prompt edit: change prompts/ghsapoc.md, then just pix-prompts-sync prod. The runtime reads Pix.systemPromptText at 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 target
    

    Prints the resulting PixLog row + Exploit row + 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)