Bugcrowd Crowdstream Processor — Design Document

1. Overview

Purpose: Fetch Bugcrowd crowdstream submissions and disclosed vulnerability reports via the unauthenticated JSON API, and store them across CVEMetadata (+ relations), Exploit, and BugBountySubmission tables.

Data source: Bugcrowd Crowdstream JSON API — GET https://bugcrowd.com/crowdstream.json

API notes:

  • Unauthenticated — no API key or session required
  • Three tiers of data:
    1. Global feed (/crowdstream.json?page=N) — 7-day rolling window, ~925 entries, 20/page
    2. Per-program feed (/engagements/{code}/crowdstream.json?page=N) — configurable window (7 days to 6+ months), includes disclosed entries
    3. Disclosure reports (/disclosures/{uuid}/{slug}) — full HTML writeups with CVE IDs, CWEs, technical descriptions
  • Offset pagination (page number, not cursor)
  • No documented rate limits, but browser UA headers required to avoid bot detection

Schedule: Daily at 06:30 UTC (cron cron(30 6 * * ? *) — Terraform EventBridge)

Timeout: 60 minutes

Resources: 256 CPU units, 512 MB memory

What it reads:

  • Bugcrowd Crowdstream JSON API (public, no auth)
  • Bugcrowd disclosure report HTML pages (public, no auth)

What it writes:

  • BugBountySubmission (source=bugcrowd — every submission)
  • Exploit (source=bugcrowd — every submission, category=bug-bounty)
  • ExploitCVE (junction to the BUGCROWD-* record, plus one per real CVE found on a disclosure page)
  • ExploitAffectedProduct (vendor/product from program info)
  • CVEMetadata (source=bugcrowd) — one row per submission, keyed BUGCROWD-{YYYY}-{submission uuid}, plus a minimal placeholder row per real CVE id found on a disclosure page (db.EnsureMinimalCVEMetadata)
  • CVEDescription, CVEMetadataReferences, CVEMetric, CVEProblemType (CVE relation tables, all keyed on the BUGCROWD-* id)
  • CVEAlias (db.InsertAliases — links the BUGCROWD-* id to every CVE id parsed off the disclosure page)
  • BulkDataDumpTracker (source=bugcrowd_crowdstream — freshness)

Identifier scheme

Bugcrowd submissions have no public advisory identifier, so the processor mints one: BUGCROWD-{year}-{submission uuid}, where the year comes from created_at (main.go:702-708). This id is the CVEMetadata.cveId for every crowdstream entry — disclosed or not, CVE-bearing or not — which is why the row count tracks total submissions (~8.6k) rather than the disclosed subset. Real CVE ids from a disclosure page become CVEAlias edges on that record, never the primary id. No GcveIssuance row is minted for these ids.

Environment variables:

  • DATABASE_URL — required
  • DATABASE_URL_READ — optional

2. API Details

Global Crowdstream

Endpoint: GET https://bugcrowd.com/crowdstream.json?page={N}

Response structure:

{
  "results": [ ... ],
  "pagination_meta": {
    "total_pages": 47,
    "totalCount": 925,
    "limit": 20,
    "currentPage": 1
  },
  "cutoff_date_label": "7 days"
}

Per-Program Crowdstream

Endpoint: GET https://bugcrowd.com/engagements/{code}/crowdstream.json?page={N}

Same response structure. Programs configure their own retention window (Atlassian: 6 months, Tesla: 6 months, most others: 7 days). Returns richer data including disclosed entries.

Submission Entry (Non-Disclosed)

{
  "id": "f8a831c3-601f-45ab-a926-c3eba637784e",
  "engagement_name": "OpenAI",
  "engagement_code": "openai",
  "engagement_path": "/engagements/openai",
  "engagement_in_progress": true,
  "visibility_public": false,
  "target": "Sora",
  "priority": 3,
  "substate": "unresolved",
  "amount": "$500",
  "points": 10,
  "crowdstream_amount_visible": true,
  "disclosed": null,
  "created_at": "2026-03-16T22:24:00.200Z",
  "accepted_at": "9 Apr 2026",
  "claimed_at": null,
  "closed_at": null,
  "is_pinned": false,
  "submission_state_text": "Submission accepted on target: Sora",
  "submission_state_date_text": "Accepted on 9 Apr 2026",
  "researcher_username": null,
  "logo_url": "https://logos.bugcrowdusercontent.com/...",
  "logo_color": "fff"
}

Submission Entry (Disclosed)

Disclosed entries have additional fields:

{
  "id": "2c29d932-f428-4d5c-ac74-9d195398944e",
  "engagement_name": "Atlassian",
  "engagement_code": "atlassian",
  "target": "Bitbucket Data Center",
  "priority": 1,
  "substate": "resolved",
  "disclosed": true,
  "title": "RCE in Bitbucket DataCenter via HazelCastPort",
  "disclosure_report_url": "/disclosures/ce3d9a93-2168-4785-90b7-47f66a5b8162/rce-in-bitbucket-datacenter-via-hazelcastport",
  "disclosed_at": "12 Apr 2022",
  "researcher_username": "SnowyOwl",
  "created_at": "2022-01-26T22:12:19.045Z",
  "accepted_at": "17 Feb 2022",
  "closed_at": "2022-03-18T19:56:36.076Z"
}

Disclosure Report Pages

HTML pages at https://bugcrowd.com/disclosures/{uuid}/{slug} contain:

  • Vulnerability title and description
  • CVE IDs (e.g., CVE-2022-26133, CVE-2021-26084)
  • CWE classifications (e.g., CWE-917, CWE-116)
  • Technical details, attack vectors, root cause analysis
  • External references (vendor advisories, GitHub issues, Snyk entries)
  • Researcher info and timeline

Example: The disclosure for “RCE on Confluence Data Center via OGNL Injection” contains CVE-2021-26084, CWE-917, CWE-116, and references to the Atlassian security advisory.

Priority Mapping

Bugcrowd PrioritySeverity RatingDescription
1criticalP1 — Critical
2highP2 — High
3mediumP3 — Medium
4lowP4 — Low
5informationalP5 — Informational
null(omitted)Unrated

3. Business Logic

Two-Phase Processing Strategy

Phase 1 — Discover programs and collect submissions:

  1. Fetch global crowdstream (/crowdstream.json), paginating all pages
  2. Extract unique engagement_code values (active programs)
  3. For each program, fetch per-program crowdstream (all pages) — this captures disclosed entries not in the 7-day global window
  4. Batch upsert all submissions into BugBountySubmission + Exploit (500 per transaction with SAVEPOINTs)
  5. Collect entries where disclosed: true and disclosure_report_url is present

Phase 2 — Enrich disclosed submissions: 6. Skip disclosures already stored as Exploit (check LoadProcessedExploitIDs) 7. For each new disclosure, fetch the HTML report page 8. Parse CVE IDs, CWE IDs, description, and reference URLs via regex 9. Update BugBountySubmission with CVE/CWE data 10. For each CVE ID: EnsureMinimalCVEMetadata, UpsertCVEMetadata, InsertReferences, InsertProblemTypes, InsertDescriptions, InsertExploitCVE

Per-Submission Processing (Phase 1)

Every submission (disclosed or not) — processSubmission, main.go:373-515:

  1. UpsertBugBountySubmission — full metadata
  2. UpsertExploit — category=bug-bounty
  3. DeleteExploitAffectedProducts + InsertExploitAffectedProduct — from program info
  4. UpsertCVEMetadata for the minted BUGCROWD-{year}-{uuid} id
  5. InsertReferences — the disclosure report URL, when present
  6. InsertDescriptions — the submission title
  7. InsertMetrics — a cvssV3_1 row carrying only baseSeverity, mapped from the Bugcrowd priority (no vector, no score)
  8. InsertExploitCVE — junction between the Exploit and the BUGCROWD-* record

Per-Disclosure Processing (Phase 2)

Disclosed submissions whose report page yielded a CVE id or a descriptionenrichDisclosure, main.go:520-698. All writes still key on the BUGCROWD-* id: 9. UpsertBugBountySubmission / UpsertExploit again, now with cveIds + weaknessCweId 10. UpsertCVEMetadata again, now with the aliases JSON column populated 11. InsertReferences — report URL (advisory) + extracted external references (technical) 12. InsertProblemTypes — CWEs from the disclosure page 13. InsertDescriptions — description from the disclosure page (or title as fallback) 14. db.InsertAliasesBUGCROWD-* ⇄ every CVE id on the page 15. EnsureMinimalCVEMetadata + InsertExploitCVE per real CVE id

Bounty Amount Cleaning

Bugcrowd returns amounts as formatted strings (e.g., "$10,000"). The processor strips $ and , before storing as bountyAmount (string, preserving decimal precision).

Timestamp Handling

  • created_at, closed_at — RFC3339 format, parsed to Unix ms
  • disclosed_at — human-readable format ("19 Jul 2023"), parsed via "2 Jan 2006" layout
  • accepted_at — display-only format ("9 Apr 2026"), not stored

Rate Limiting

  • 500ms between per-program crowdstream JSON fetches
  • 1s + 0-200ms random jitter between disclosure HTML page fetches
  • Soft deadline: EXPECTED_DURATION_MINUTES - 10 minutes — but the fallback is a hardcoded 50 minutes, applied even when the variable is unset (main.go:61-65), so a local backfill is also truncated at 50 minutes

Resume / Freshness

  • BulkDataDumpTracker (source=bugcrowd_crowdstream) — skip if last run was less than frequency seconds ago
  • Disclosed reports: loadEnrichedExploitIDs skips a disclosure only when its Exploit.cveIds column is already populated (main.go:760-762). A disclosure page that yielded no CVE id is therefore re-fetched on every run.
  • On incremental runs: only programs appearing in the global 7-day feed are re-fetched

Idempotency

BugBountySubmission and Exploit are keyed (reportId, source) and (exploitId, source) and use ON CONFLICT DO UPDATE, so those are idempotent. CVEMetadata is keyed (cveId, source) and likewise upserts. CVEDescription and CVEMetric replace their rows.

CVEMetadataReferences and CVEProblemType are not idempotent: their shared helpers issue a bare ON CONFLICT DO NOTHING while the tables carry no unique index beyond the uuid primary key, so every re-run appends a fresh duplicate row. Production currently holds ~42k reference rows for ~830 distinct (cveId, url) pairs under this source.


4. Architecture Diagram

graph TD subgraph "cmd/bugcrowd-json-processor/" MAIN[main.go] DISCOVER[Phase 1: Discover programs
from global crowdstream] PERPROG[Fetch per-program
crowdstream pages] BATCH[processSubmissionBatch
500 per tx with SAVEPOINTs] ENRICH[Phase 2: Enrich disclosed
fetch HTML, parse CVEs] end subgraph "internal/bugcrowd/" TYPES[types.go — JSON response structs] CLIENT[client.go — FetchCrowdstream,
FetchProgramCrowdstream,
FetchDisclosurePageHTML] PARSER[parser.go — ParseDisclosurePage,
PriorityToSeverity, CleanBountyAmount] end subgraph "internal/db/ (reused)" BBS[bugbounty.go — UpsertBugBountySubmission] EXPLOIT[exploit.go — UpsertExploit, InsertExploitCVE] CVE[cvemetadata.go — UpsertCVEMetadata, EnsureMinimal] REFS[cvereference.go — InsertReferences] PROBLEMS[cveproblemtype.go — InsertProblemTypes] DESCS[cvedescription.go — InsertDescriptions] TRACKER[tracker.go — GetTracker, UpsertTracker] end subgraph "internal/ (reused)" HTTP[httpclient — browser UA headers] NOTIFY[notify — SNS → Slack] end MAIN --> DISCOVER DISCOVER --> CLIENT DISCOVER --> PERPROG PERPROG --> CLIENT CLIENT --> HTTP PERPROG --> BATCH BATCH --> BBS BATCH --> EXPLOIT MAIN --> ENRICH ENRICH --> CLIENT ENRICH --> PARSER ENRICH --> CVE ENRICH --> REFS ENRICH --> PROBLEMS ENRICH --> DESCS MAIN --> TRACKER MAIN --> NOTIFY

5. Deployment Diagram

flowchart TD HOOK[post-push-ecr.sh] -->|push ARM64 image| ECR[ECR: go-processors] ECR --> TASKDEF[ECS Task Definition
go-bugcrowd-json-processor] TASKDEF --> EB[EventBridge Schedule
cron 30 6 * * ? *] EB -->|trigger daily| FARGATE[ECS Fargate
vdb-scheduler cluster] FARGATE -->|GET JSON| BC_API[bugcrowd.com/crowdstream.json] FARGATE -->|GET JSON| BC_PROG[bugcrowd.com/engagements/*/crowdstream.json] FARGATE -->|GET HTML| BC_DISC[bugcrowd.com/disclosures/*] FARGATE --> WRITE[RDS Write Proxy]

6. Processing Flow

flowchart TD START([Start]) --> ENV{DATABASE_URL set?} ENV -->|no| FAIL([Exit 1]) ENV -->|yes| CONNECT[Connect DB pool] CONNECT --> TRACKER{Check BulkDataDumpTracker
bugcrowd_crowdstream} TRACKER -->|fresh| NOWORK([NoWork — exit 0]) TRACKER -->|stale or missing| P1[Phase 1: Fetch global crowdstream
all pages — discover programs] P1 --> PERPROG[For each program:
fetch per-program crowdstream
all pages] PERPROG --> UPSERT[Batch upsert submissions
BugBountySubmission + Exploit
500 per tx] UPSERT --> P2{Disclosed entries
with report URLs?} P2 -->|none| UPDATE[UpsertTracker] P2 -->|yes| SKIP{Already in Exploit
table?} SKIP -->|yes| NEXT[Next disclosure] SKIP -->|no| FETCH_HTML[Fetch disclosure HTML
1s + jitter delay] FETCH_HTML --> PARSE[Parse CVE IDs, CWEs,
description, references] PARSE --> CVE_LOOP{Has CVE IDs?} CVE_LOOP -->|yes| CVE_WRITE[UpsertCVEMetadata
InsertReferences
InsertProblemTypes
InsertDescriptions
InsertExploitCVE] CVE_LOOP -->|no| NEXT CVE_WRITE --> NEXT NEXT --> MORE{More disclosures?} MORE -->|yes| SKIP MORE -->|no| UPDATE UPDATE --> DONE{Errors?} DONE -->|yes| ERRORED([Errored — exit 1]) DONE -->|no| COMPLETED([Completed — exit 0])

7. Data Mapping

erDiagram BugBountySubmission { string uuid PK string reportId UK "f8a831c3-..." string source UK "bugcrowd" string title "RCE in Bitbucket..." string substate "resolved" string teamHandle "atlassian" string teamName "Atlassian" string reporterUsername "SnowyOwl" string bountyAmount "10000" string severityRating "critical" string weaknessCweId "CWE-917" string cveIds "JSON array" bigint disclosedAt "Unix ms" } Exploit { string uuid PK string exploitId UK "2c29d932-..." string source UK "bugcrowd" string title "RCE in Bitbucket..." string author "SnowyOwl" string category "bug-bounty" string originalUrl "disclosure URL" string cveIds "JSON array" } CVEMetadata { string cveId PK "BUGCROWD-2022-2c29d932-..." string source PK "bugcrowd" string state "PUBLISHED" string title "RCE in Bitbucket..." string affectedVendor "atlassian" string affectedProduct "Atlassian" string aliases "JSON array of real CVE ids" } CVEMetadataReferences { string uuid PK string url "disclosure URL" string type "advisory" string referenceSource "bugcrowd" } CVEProblemType { string uuid PK string cweId "CWE-917" string description "CWE-917" } CVEDescription { string uuid PK string lang "en" string value "Report description" } ExploitCVE { string exploitUuid FK string cveId "CVE-2022-26133" string source "bugcrowd" } ExploitAffectedProduct { string exploitUuid FK string vendor "atlassian" string product "Atlassian" string homepage "Bitbucket Data Center" } BugBountySubmission ||--o| Exploit : "same submission" Exploit ||--o{ ExploitCVE : "links to" Exploit ||--o{ ExploitAffectedProduct : "affects" CVEMetadata ||--o{ ExploitCVE : "referenced by" CVEMetadata ||--o{ CVEMetadataReferences : "has" CVEMetadata ||--o{ CVEProblemType : "has" CVEMetadata ||--o{ CVEDescription : "has"

8. Field Mapping Reference

Bugcrowd FieldTarget Table.Column
id (UUID)BugBountySubmission.reportId, Exploit.exploitId
"bugcrowd" (constant)BugBountySubmission.source, Exploit.source
title (disclosed) / submission_state_textBugBountySubmission.title, Exploit.title
disclosure_report_url (prefixed)BugBountySubmission.reportUrl, Exploit.originalUrl
substateBugBountySubmission.substate
engagement_codeBugBountySubmission.teamHandle, CVEMetadata.affectedVendor, ExploitAffectedProduct.vendor
engagement_nameBugBountySubmission.teamName, CVEMetadata.affectedProduct, ExploitAffectedProduct.product
researcher_usernameBugBountySubmission.reporterUsername, Exploit.author
amount (cleaned)BugBountySubmission.bountyAmount
targetBugBountySubmission.assetIdentifier, ExploitAffectedProduct.homepage
priority (mapped)BugBountySubmission.severityRating
CVE IDs (from disclosure HTML)BugBountySubmission.cveIds, Exploit.cveIds, CVEMetadata.aliases + CVEAlias, ExploitCVE.cveId
id + created_at year (minted)CVEMetadata.cveId = BUGCROWD-{year}-{id}
CWE IDs (from disclosure HTML)BugBountySubmission.weaknessCweId, CVEProblemType.cweId
Description (from disclosure HTML)CVEDescription.value
References (from disclosure HTML)CVEMetadataReferences.url (type=technical)
Disclosure report URLCVEMetadataReferences.url (type=advisory)
created_atBugBountySubmission.createdAtSource
disclosed_atBugBountySubmission.disclosedAt, Exploit.datePublished, CVEMetadata.datePublished
closed_atBugBountySubmission.closedAt

9. Infrastructure

SystemNameValue
cmd/ directorybugcrowd-json-processorcmd/bugcrowd-json-processor/main.go
Containerfile targetbugcrowd-json-processorFROM scratch AS bugcrowd-json-processor
ECR image taggo-bugcrowd-json-processor-latest
ECS task familygo-bugcrowd-json-processor
EventBridge schedulego-bugcrowd-json-processorcron(30 6 * * ? *)
CloudWatch log group/ecs/vdb-scheduler/go-bugcrowd-json-processor
task-manager.toml[tasks.bugcrowd-json-processor]
Terraform modulebugcrowd_json_processorterraform/go-schedules.tf
justfile recipego-bugcrowd-json-backfill
ecr-build targetssimpleTargetstargets.go
ECR hookpost-push-ecr.sh TARGETSauto-discovered

10. Verification

# Local test
just go-bugcrowd-json-backfill

# Check BugBountySubmission
psql "$DATABASE_URL" -c 'SELECT COUNT(*) FROM "BugBountySubmission" WHERE source = '\''bugcrowd'\'''

# Check CVE population (disclosed reports only)
psql "$DATABASE_URL" -c 'SELECT COUNT(*) FROM "CVEMetadata" WHERE source = '\''bugcrowd'\'''

# Check Exploit population
psql "$DATABASE_URL" -c 'SELECT COUNT(*) FROM "Exploit" WHERE source = '\''bugcrowd'\'''

# Check junctions
psql "$DATABASE_URL" -c 'SELECT COUNT(*) FROM "ExploitCVE" WHERE source = '\''bugcrowd'\'''

# Check disclosed with CVEs
psql "$DATABASE_URL" -c 'SELECT "reportId", "title", "cveIds", "weaknessCweId" FROM "BugBountySubmission" WHERE source = '\''bugcrowd'\'' AND "cveIds" IS NOT NULL LIMIT 10'

# Idempotency — run again, counts should not increase
just go-bugcrowd-json-backfill

# Docker build
podman build --target bugcrowd-json-processor -f Containerfile.go-processors scripts/go-processors/

S3 filenames

The unit of work is a crowdstream submission, not a file, so the payload is the submission re-serialised to canonical JSON and {filename} is {submission uuid}.json (main.go:313-323). Archive fires only for submissions whose savepoint committed; quarantine fires for per-submission failures and for every submission in a batch whose transaction rolled back (main.go:358-367). The scraped disclosure-page HTML is not archived.

S3 Persistence

  • Archive path: bugcrowd/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/bugcrowd-json-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Failure reasons emitted: store-error

Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).

flowchart LR SRC[Source feed] --> PROC[bugcrowd-json-processor] PROC -->|success| ARCHIVE[("S3: bugcrowd/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/bugcrowd-json-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.