Skip to content

feat: collector that builds, signs and verifies public program bundles - #1

Merged
0xmanhnv merged 1 commit into
mainfrom
feat/initial-collector
Oct 10, 2026
Merged

0xmanhnv merged 1 commit into
mainfrom
feat/initial-collector

Conversation

@0xmanhnv

Copy link
Copy Markdown
Member

What

The first version of the public program collector: it reads open, machine-readable sources of public bug-bounty and vulnerability-disclosure programs, normalises them (typed scope, rules, terms, contact, provenance), validates every record and scope entry, and publishes a signed snapshot plus delta per run as a GitHub Release. The OpenCTEM platform imports the bundle into its public program monitor.

The repository layout, signing (DSSE, offline root, versioned key set, monotonic sequence, 7-day expiry), caps and verification checks follow the OpenCTEM vulnerability feed, so the platform can share one verifier; only the payload types (application/vnd.openctem.programfeed.*) and record files (programs, changes) differ.

Sources

Source Status
disclose.io diodb (CC0-1.0) supported: 2,424 programs, directory data, no structured scope
security.txt (RFC 9116) over CISA dotgov-data federal domains (CC0-1.0) + seeds/securitytxt.txt supported: 11 federal programs with a Policy field (5 open, 6 paused because the file expired)
HackerOne, Intigriti not collected: APIs need an account token
Bugcrowd, YesWeHack not collected: terms forbid redistribution
Immunefi not collected: terms forbid automated access without written consent
Open Bug Bounty not collected: bot protection
HackenProof, WhiteHub, BugBase, other regional platforms not collected: no official machine-readable listing

Live dry run (2026-10-10, not published): 2,435 programs (2,408 open, 27 paused, 440 bounty), 67 inferred targets, 0 rejected. A second run with --prev gave 0 changes (stable ids and ordering), and replaying the same sequence was refused.

Threat model

  • Forged, stale or rolled-back bundle: same trust chain as the vulnerability feed (pinned offline root, key set whose version only goes up, online key only in the publish environment; sequence and expiry checks). A key set or manifest of another feed is refused (feed-specific payload types, test included).
  • Poisoned scope: every target is normalised; public suffixes (*.com, *.co.uk, *.github.io), reserved names, private/reserved/documentation/metadata addresses and ranges, CIDRs wider than /16 or /48, credentialed or non-http(s) URLs and control/bidi characters are moved to rejected with the reason. The verifier refuses a whole bundle carrying one such target even under a valid signature (tests).
  • Guesses presented as scope: targets the collector derives are confidence: inferred, and scope_published says whether the source has structured scope at all. Nothing in this feed authorises testing.
  • Source outage: a source that loses more than half its programs is treated as broken for the run (override flag in the workflow); a host that does not answer keeps its previous record; closed programs stay 180 days before they are dropped.
  • Cross-adapter writes: an adapter can only write ids under its own prefix (test).
  • Collector as an SSRF pivot: connections only to public unicast addresses, checked at connect time after DNS; https only, at most 3 redirects, each re-checked against robots.txt; size caps and timeouts (tests). The live run exposed a robots.txt check that recursed on sites redirecting every path; fixed, with a test.
  • Politeness: named User-Agent and From: security@openctem.io, robots.txt honoured (an unreachable robots.txt means do not fetch), 1 request per second per host, conditional requests from a cache kept between runs. No login, cookies or bot-protection bypass.

Owner actions

  1. Key ceremony (keys/README.md), with a root distinct from the vulnerability feed root: commit keys/root-keyid.txt and keys/keyset.dsse.json.
  2. Create the GitHub environment publish (deployments from main only) with the secret PROGRAMFEED_SIGNING_KEY. No other secret is needed.
  3. Give platforms the root key id (PROGRAMFEED_ROOT_KEY_ID).

Until step 1 is done the scheduled workflow logs a warning and publishes nothing.

Verification

gofmt, go vet, go test -race ./... (recorded fixtures, no network), 30 s fuzz of the scope normaliser, govulncheck clean. End to end on live data: keys, build, sign, verify, build with --prev, sign, verify; replay refused.

Collects public bug-bounty and vulnerability-disclosure programs from open,
machine-readable sources, normalises them into one record format with typed
scope, validates every record and scope entry, and publishes a signed
snapshot and delta per run.

- sources: disclose.io diodb (CC0-1.0) and RFC 9116 security.txt over the
  CISA dotgov-data federal domain list (CC0-1.0) plus a repository seed file
- polite fetch client: named User-Agent and contact, robots.txt, one request
  per second per host, conditional requests, size caps, public addresses only
- scope rules: public suffixes, reserved names, private/reserved ranges and
  CIDRs wider than /16 or /48 are rejected with the reason; out of scope wins
- change log per run: programs added/closed/reopened/dropped, targets
  added/removed/modified, rules, terms and status changes
- bundle and trust model shared with the vulnerability feed: DSSE envelopes,
  offline root and versioned key set, monotonic sequence, 7-day expiry
- daily publish workflow (build job without the key, publish job in the
  publish environment); JSON Schemas; recorded fixtures, no network in tests
@0xmanhnv
0xmanhnv merged commit 11fa2e1 into main Oct 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant