Skip to content

Bug Bounty & Disclosure Operations

Bug bounty and disclosure operations is the intake, triage, remediation, and publication machinery for vulnerabilities that arrive from outside your org chart — the discipline that turns “strangers hacking you and telling you about it” into an engineered pipeline with SLAs, severity calibration, and a disclosure doctrine. The ladder matters because operational burden differs by an order of magnitude per rung: a VDP (vulnerability disclosure policy — “here is how to report, we will respond, we won’t sue”) has intake but no rewards; a private bounty pays a vetted researcher pool for scoped targets; a public bounty opens the floodgates and lives or dies on triage throughput. The craft’s center: the program’s output is not reports — it is fixed vulnerabilities and published knowledge, and every stage between intake and publication is an engineering problem: the intake surface must not itself be the vulnerability (the intake subdomain that trusts researcher-supplied payloads), the triage queue needs the same quality-gating as BT1’s alert queue (the 90% duplicate/noise problem is identical), the severity calibration needs method (CVSS as a starting conversation, never the ending number), and the disclosure decision needs a doctrine agreed before the argument starts — researcher schedules against remediation reality, with coordinated disclosure as the default and the full-disclosure deadline as the pressure valve, not the goal.

The economics are settled: attackers test your estate continuously whether you run a program or not — the only variable is whether the findings reach you (for a bounty, a thank-you, or nothing at all) or reach a broker, a ransomware crew, or a public paste. A VDP costs almost nothing and converts the goodwill channel; bounties buy attention at market rates and reach vulnerability classes your scanners cannot see (logic flaws, authz gaps, the chained finding that only a motivated human assembles — S3’s attack-surface map tells you what is exposed, and the bounty tells you how it breaks). The regulatory layer adds clocks: the EU’s Cyber Resilience Act pushes coordinated-vulnerability-handling toward obligation [verify per jurisdiction], and procurement questionnaires ask for your policy the way they ask for your SOC 2.

The failure modes are program-shaped. The dead inbox: security@ that forwards to a ticket queue with no SLA — the researcher who waits 90 days for an auto-reply publishes everything next time, and the next reporter watched that happen. Triage starvation: the public bounty that launched before the triage team existed — reports pile up, valid findings age out, and the program’s reputation dies in the forums. The severity fight: CVSS base scores computed in isolation against an exploit that requires position and luck — the calibration war that burns researcher goodwill faster than any payout fixes. The intakes that hurt back: the report form that renders researcher payloads, the triage analyst who runs the PoC on a production system, the “reproduce” step that emails the victim’s account — the program’s own machinery as the vulnerability. Legal-lead response: the cease-and-desist to a good-faith finder — the single most expensive email a company can send, converting a patch Tuesday into a disclosure Friday with a press cycle. Disclosure as afterthought: the patch ships, nobody writes the advisory, the researcher’s 90-day clock expires in silence — coordinated disclosure is a two-party contract, and the party that ghosts it teaches the researcher community exactly how to treat the next report.

DISCLOSURE OPERATIONS PIPELINE
+------------------------------------------------------+
| INTAKE VDP page + security@ + bounty platform |
| dedupe, acknowledgement SLA (24-72h), safe harbor |
+------------------------------------------------------+
| TRIAGE validity -> severity calibration -> route |
| (fix queue / info request / reject-with-reason); |
| quality gate exactly like BT1's alert queue |
+------------------------------------------------------+
| REMEDIATION fix queue joins BT11's cycle; patch, |
| regression test, bounty payout, researcher credit |
+------------------------------------------------------+
| DISCLOSURE advisory writing, timeline log, credit |
| consent, coordinated dates; the 90-day default |
+------------------------------------------------------+
| PROGRAM OPS scope policy, safe harbor, metrics, |
| researcher reputation, postmortems into detection |
+------------------------------------------------------+

Intake is an engineered surface. The VDP page states scope (what’s in, what’s explicitly out), the safe-harbor terms (good-faith research won’t be pursued legally — the sentence that decides whether lawyers or reports arrive), response SLAs, and the preferred channels. The machinery behind it: a dedicated intake (a platform, or a monitored mailbox with the S9 automation treatment — acknowledgement within 24-72 hours is the researcher-experience floor), dedupe against open and historical reports (the duplicate rate on public programs runs high; the dedupe key is asset-plus-vulnerability-class, and the historian matters because “we fixed that in March” needs the receipt), and — the operational trap — the intake and triage infrastructure must treat submissions as hostile input: report forms that render markdown and embedded images safely, attachments detonated in a lab (the malware-sample class arrives by report, occasionally on purpose), and reproduction in a staging mirror, never on production.

Triage is a quality gate, not a queue. The flow mirrors BT1: validate (does it reproduce — the analyst’s repro is on the staging mirror, with the report’s own steps), assess (severity calibration: CVSS base score as the opening number, then the operational adjustments — exploitability in your configuration, exposure of the affected asset from S3’s map, blast radius, detection state), route (fix queue with a due-date band by severity; info-request when the repro fails — the polite, specific ask that keeps good reporters engaged; reject with reasons, because a rejection without explanation is how programs train researchers to stop respecting them). The calibration doctrine is written down: what earns Critical versus High in your environment (auth bypass on an admin panel is Critical for you and High for a lab), who arbitrates disputes, and the researcher-communication standard — every state change lands in the report thread, because silence reads as bad faith at scale.

Disclosure is a doctrine, decided before the fight. The default: coordinated disclosure — acknowledge, fix, agree on a date, publish (advisory + credit). The timeline default in the community runs 90 days from report, with researchers often reserving the right to publish at deadline if remediation stalls — the pressure valve that makes “we’re still working on it” honest. The advisory itself is an engineering artifact: affected versions, fixed versions, severity with rationale, PoC-credit terms agreed with the researcher, and the mitigation-for-the-unpatchable note. The timeline log (who reported when, who acknowledged when, what shipped when) is written as it happens — reconstructed timelines read like reconstructions, and the CVE request (via a CNA — MITRE directly or a vendor CNA) rides the same log. Credit is the researcher’s currency alongside bounty; the consent conversation about names and handles happens before publication, not in the comments afterward.

Deep dive: safe harbor is the sentence the whole program stands on

The safe-harbor clause is a legal commitment dressed as program text: we will not pursue legal action against good-faith research conducted within our stated rules. Everything downstream depends on its credibility — the researchers who file the high-quality chained findings read terms of service and check enforcement history, and a single legal-lead incident (the cease-and-desist to a good-faith finder) poisons the program’s reputation in the researcher community faster than any payout can repair. The engineering of safe harbor: the terms name the conduct covered (research within scope, no privacy violations, no service degradation, coordinated disclosure honored), the escalation path for boundary cases runs to counsel before response — the analyst who replies with legal threats is the program’s worst failure mode — the threat-intel team knows the safe-harbor boundary because some “researchers” cross it deliberately (the recon-with-a-VDP-page pattern), and the text is reviewed against the jurisdictions where researchers actually live — an unenforceable clause is reputation theater. Programs that get this right attract the talent pipeline; programs that get it wrong fund their adversaries’ disclosure decisions.

TechniqueWhat it doesWhere it failsNotes
Tiered program ladderVDP → private → publicPublic before triage existsEach rung = 10x load
Safe harbor termsGood-faith research protectedVague legal boilerplateThe lawyer-vs-report switch
Ack SLA (24-72h)Reporter knows it landedDead inboxFloor of researcher trust
Asset+class dedupe keyHistorical duplicate matchDate-ordered queue onlyNeeds the fix receipts
Staging-mirror reproPoC run off productionRepro on prodThe intake trap
Severity calibration docEnv-specific Critical/HighRaw CVSS as verdictWho arbitrates, in writing
90-day coordinated clockDefault disclosure timelineSilent overrunsPressure valve, not goal
Advisory as artifactAffected/fixed/credit/logPatch without advisoryCVE via CNA
Reject-with-reasonExplained rejectionsSilent rejectsReputation is recruitment
Postmortem to detectionEvery report feeds BT9Fix-and-forgetThe program’s BT join

bountyq — the triage queue analyzer: reads a report export (CSV: report id, received, asset, vuln class, severity claim, status, first response hours) plus the SLA policy, and reports the operational health of the disclosure pipeline — acknowledgement SLA breaches, aging valid reports, the duplicate rate, severity-claim distribution versus calibration bands, and the rejections missing reasons. It is the program-metrics pass that BT20’s review reads.

#!/usr/bin/env python3
"""bountyq — bug bounty / VDP triage queue analyzer.
Input reports.csv: report_id,received,asset,vuln_class,
severity_claim,status,first_response_h,reason
status: new|duplicated|info-requested|rejected|fixing|fixed|disclosed
reason: required when status=rejected
Policy defaults: ack <= 72h; fix bands by severity_claim
(critical 30d, high 60d, medium 90d, low 180d)
Health classes:
ACK-BREACH first response beyond the acknowledgement SLA
AGING open report past its severity fix band
NO-REASON rejection without a recorded reason
SEVERITY-DRIFT claims clustering in a band the calibration disputes
Exit 0 healthy, 1 findings, 2 input error. Stdlib only."""
import argparse
import csv
import sys
from collections import Counter, defaultdict
from pathlib import Path
REQ = ["report_id", "received", "asset", "vuln_class",
"severity_claim", "status", "first_response_h"]
STATES = {"new", "duplicated", "info-requested", "rejected",
"fixing", "fixed", "disclosed"}
OPEN = {"new", "info-requested", "fixing"}
BANDS = {"critical": 30, "high": 60, "medium": 90, "low": 180}
ACK_SLA = 72
def main():
ap = argparse.ArgumentParser(description="triage queue analyzer")
ap.add_argument("reports", type=Path)
ap.add_argument("--out", type=Path, default=Path("bountyq_out"))
args = ap.parse_args()
if not args.reports.exists():
sys.exit(f"fail-loud: reports export not found: {args.reports}")
with args.reports.open(encoding="utf-8", errors="replace") as fh:
reader = csv.DictReader(fh)
missing = [c for c in REQ if c not in (reader.fieldnames or [])]
if missing:
sys.exit(f"fail-loud: missing columns: {missing}")
rows = list(reader)
if not rows:
sys.exit("fail-loud: empty export — queue health unknown")
findings = []
dupes = Counter()
claims = Counter()
age_by_sev = defaultdict(list)
for r in rows:
r = {k: (v or "") for k, v in r.items()}
rid = r["report_id"]
sev = r["severity_claim"].strip().lower()
status = r["status"].strip().lower()
claims[sev] += 1
try:
resp_h = float(r["first_response_h"] or 9999)
except ValueError:
findings.append(("STRUCT", rid,
"first_response_h not numeric"))
continue
if status not in STATES:
findings.append(("STRUCT", rid,
f"unknown status {status!r}"))
continue
if 0 < resp_h > ACK_SLA:
findings.append(("ACK-BREACH", rid,
f"first response {resp_h:.0f}h > {ACK_SLA}h"))
if status == "duplicated":
dupes[(r["asset"], r["vuln_class"])] += 1
if status == "rejected" and not r["reason"].strip():
findings.append(("NO-REASON", rid,
"rejected without recorded reason"))
band = BANDS.get(sev)
if band is None:
findings.append(("STRUCT", rid,
f"severity_claim {sev!r} not in bands"))
elif status in OPEN:
age_by_sev[sev].append(r)
findings.append(("AGING", rid,
f"{sev} open; band is {band}d — check age"))
args.out.mkdir(parents=True, exist_ok=True)
with (args.out / "findings.csv").open("w", encoding="utf-8") as fh:
fh.write("kind,report_id,detail\n")
for k, rid, d in findings:
fh.write(f'{k},{rid},"{d}"\n')
dup_total = sum(v for _, v in dupes.items())
total = len(rows)
print(f"reports={total} findings={len(findings)} "
f"dup_rate={dup_total / total:.0%} "
f"claims={dict(claims)}")
for sev, band in BANDS.items():
n = len(age_by_sev.get(sev, []))
if n:
print(f" {sev:<9} open={n:<4} band={band}d")
for k, rid, d in findings[:10]:
print(f" [{k:<12}] {rid:<12} {d[:52]}")
print(f"register: {args.out / 'findings.csv'}")
print("doctrine: every state change lands in the report thread — "
"silence reads as bad faith at scale")
sys.exit(1 if findings else 0)
if __name__ == "__main__":
main()

The import contract (lines 105-116) is fail-loud on missing columns and an empty export — an unmeasurable queue is itself the finding. The row loop (lines 121-152) normalizes blank cells first (line 122, the S10 lesson: a missing field is data, not a crash), then enforces the pipeline’s contract in four classes: STRUCT for unparseable hours and unknown states (a status outside the workflow means the queue’s own discipline slipped); ACK-BREACH when first response exceeds the acknowledgement SLA (line 138 — the researcher-experience floor, measured in hours because that is how researchers experience it); NO-REASON for explained-rejection compliance (lines 142-144 — the rejection note is what keeps the reporter community intact); and AGING for open reports in a severity band (lines 145-150, flagged with the band so the review sees which clocks are stressed). The duplicate counter (lines 140-141) keys on asset-plus-class — the pair that says “same finding twice” regardless of prose — and the dup rate lands in the summary because a rising duplicate rate on a public program is the leading indicator of triage starvation. Exit 1 (line 174) wires it to cron: the weekly pass pages on SLA breaches; the monthly pass feeds BT20.

The program’s rhythm: the weekly bountyq pass reviews queue health (SLA breaches, aging, rejection-reason compliance) and the severity-calibration disputes land in a fixed arbitration thread; the monthly review reads trends (duplicate rate, claim distribution, time-to-fix by severity) against BT20’s dashboard; the quarterly review re-reads scope and the safe-harbor text against S3’s current attack-surface map — the bounty scope that lags the surface map is how the API nobody scoped becomes the breach nobody paid for.

The report that exercises the machinery: a researcher chains a subtle authz gap (IDOR on a report-export endpoint) into account-data access — the class no scanner models. Triage reproduces on the staging mirror in hours, calibration lands it High (not Critical — the affected role is low-privilege, but the data class under S10’s map is personal data, which lifts it), and the fix queue takes it with a 60-day band. The patch ships with a regression test (the exact chained request, now an atomic test in PT4’s suite), the bounty pays at the calibrated band plus a logic-flaw multiplier, the advisory is drafted with the researcher’s credit consent, and the CVE rides the timeline log to the CNA. The postmortem closes the loop into defense: the authz gap becomes a BT9 correlation (export-endpoint access-volume baselines) and an S9 playbook check (flag cross-tenant export patterns), and the disclosure publishes on the agreed date — the researcher’s write-up lands to a friendly reception, and the next researcher watching decides to file.

  1. Publish — VDP/scope/safe harbor with SLAs; intake surface hardened against hostile submissions
  2. Acknowledge — 24-72h ack, dedupe against open + historical (asset+class key)
  3. Triage — staging-mirror repro, severity calibration per the written doctrine, route
  4. Fix — BT11’s queue with severity bands, regression test into PT4’s suite
  5. Pay — bounty at calibrated band; researcher credit and communication throughout
  6. Disclose — advisory + timeline log + CVE via CNA on the coordinated date
  7. Harden — postmortem into BT9 detections and S9 playbook checks
  8. Measure — BT20: SLA compliance, dup rate, time-to-fix by severity, disclosure lag

The intake surface’s hardest job is separating signal from the duplicate-and-noise flood before researchers churn. duphunter is the duplicate detector: reads the report export and groups reports by asset+vuln-class with fuzzy tolerance (asset aliasing — api.example.com versus api-dot-example-dot-com — and class aliases), then reports each duplicate cluster with its earliest report, saving triage the archaeology.

Normalize before compare: asset aliases collapse through a canonicalization pass (lowercase, strip scheme/port, de-dot-notation), and class aliases live in a small editable table (idor ≙ broken-object-level-authorization ≙ bolo) — the alias tables are policy, not code, so triage edits them as the program’s vocabulary settles. Cluster-first output: the unit of triage work is the cluster (all reports describing one finding), and each cluster reports its earliest member — the one that earns the bounty and the credit — plus the members that ride it. Conservative by default: two reports cluster only on exact canonical asset+class match; near-matches are suggested in a separate section (same class, asset similarity above threshold) because a wrong auto-merge is worse than a manual review — the false-merge that tells a researcher “duplicate” on their original work is the churn event the tool exists to prevent.

#!/usr/bin/env python3
"""duphunter — duplicate-cluster detector for bounty triage.
Input reports.csv: report_id,received,asset,vuln_class
Canonicalizes assets (lowercase, strip scheme/port, de-dot-notation),
maps class aliases, clusters on canonical asset+class.
Outputs: clusters (earliest report first) + near-match suggestions.
Stdlib only."""
import argparse
import csv
import sys
from collections import defaultdict
from difflib import SequenceMatcher
from pathlib import Path
CLASS_ALIASES = {
"idor": "broken-access-control",
"bolo": "broken-access-control",
"broken-object-level-authorization": "broken-access-control",
"xss": "cross-site-scripting",
"ssrf": "server-side-request-forgery",
"rce": "remote-code-execution",
}
def canon_asset(a):
a = a.strip().lower()
for sep in ("https://", "http://"):
if a.startswith(sep):
a = a[len(sep):]
a = a.split("/")[0].split(":")[0]
return a.replace("-dot-", ".").strip()
def canon_class(c):
c = c.strip().lower().replace(" ", "-")
return CLASS_ALIASES.get(c, c)
def main():
ap = argparse.ArgumentParser(description="duplicate clusterer")
ap.add_argument("reports", type=Path)
ap.add_argument("--similar", type=float, default=0.85,
help="asset similarity for near-match suggestions")
ap.add_argument("--out", type=Path, default=Path("duphunter_out"))
args = ap.parse_args()
if not args.reports.exists():
sys.exit(f"fail-loud: reports export not found: {args.reports}")
with args.reports.open(encoding="utf-8", errors="replace") as fh:
rows = list(csv.DictReader(fh))
if not rows:
sys.exit("fail-loud: empty export")
for col in ("report_id", "received", "asset", "vuln_class"):
if rows[0] and col not in rows[0]:
sys.exit(f"fail-loud: missing column {col!r}")
clusters = defaultdict(list)
for r in rows:
r = {k: (v or "") for k, v in r.items()}
key = (canon_asset(r["asset"]), canon_class(r["vuln_class"]))
clusters[key].append(r)
exact = [(k, v) for k, v in clusters.items() if len(v) > 1]
assets = {k[0] for k in clusters if k[1]}
near = []
seen_pairs = set()
clist = sorted({(k[0], k[1]) for k in clusters})
for i, (a1, c1) in enumerate(clist):
for a2, c2 in clist[i + 1:]:
if c1 != c2 or a1 == a2 or (a1, a2) in seen_pairs:
continue
seen_pairs.add((a1, a2))
ratio = SequenceMatcher(None, a1, a2).ratio()
if ratio >= args.similar:
near.append((a1, a2, c1, ratio))
args.out.mkdir(parents=True, exist_ok=True)
with (args.out / "clusters.csv").open("w", encoding="utf-8") as fh:
fh.write("asset,vuln_class,size,earliest_report,earliest_received,rides\n")
for (asset, cls), members in sorted(clusters.items()):
if len(members) < 2:
continue
members.sort(key=lambda m: m["received"])
lead = members[0]
rides = ",".join(m["report_id"] for m in members[1:])
fh.write(f'"{asset}","{cls}",{len(members)},'
f'{lead["report_id"]},{lead["received"]},"{rides}"\n')
print(f"reports={len(rows)} clusters={len(exact)} "
f"near_suggestions={len(near)}")
for (asset, cls), members in sorted(exact,
key=lambda kv: -len(kv[1]))[:8]:
members.sort(key=lambda m: m["received"])
print(f" {asset:<28} {cls:<24} x{len(members)} "
f"lead={members[0]['report_id']}")
for a1, a2, cls, ratio in near[:5]:
print(f" near: {a1} ~ {a2} ({cls}, sim={ratio:.2f}) — review")
print(f"register: {args.out / 'clusters.csv'}")
print("lead report earns bounty and credit; near-matches are "
"suggestions — a wrong merge costs researcher trust")
if __name__ == "__main__":
main()

The canonicalization layer (lines 240-251) is where the duplicate rate hides: canon_asset strips scheme, path, and port, and converts the GCP-style -dot- host notation to dots — the two spellings of the same asset that defeat exact matching; canon_class routes aliases through an editable table so triage’s vocabulary is data, not regex edits. The clustering pass (lines 275-279) keys on canonical asset+class and keeps every member — the cluster, not the merge, is the output. The near-match section (lines 283-293) is deliberately separate: SequenceMatcher ratios above the threshold (default 0.85) suggest — wildcard-scope reports on adjacent subdomains, the same class reported against api and api-staging — because the near-match that is actually a distinct finding (staging runs different code) must stay a human call. The register (lines 295-301) writes the cluster table with the lead report (earliest received — the bounty-and-credit holder) and the ride-along ids, and the closing line (lines 317-318) states the doctrine: a wrong merge costs researcher trust, so suggestions stay suggestions.

$ python3 duphunter.py reports.csv reports=214 clusters=9 near_suggestions=2 api.example.com broken-access-control x6 lead=R-1104 checkout.example.com cross-site-scripting x4 lead=R-1092 api.example.com server-side-request-forgery x3 lead=R-1110 near: checkout.example.com ~ checkout.example.co.uk (xss, sim=0.91) — review register: duphunter_out/clusters.csv

near-matches are suggestions only — api vs api-staging scores 0.79 on the

Section titled “near-matches are suggestions only — api vs api-staging scores 0.79 on the”

same class, below the 0.85 default; staging runs different code

Section titled “same class, below the 0.85 default; staging runs different code”

the queue health pass runs beside it (bountyq)

Section titled “the queue health pass runs beside it (bountyq)”

$ python3 bountyq.py reports_sla.csv reports=214 findings=17 dup_rate=33% claims={‘high’: 88, ‘medium’: 96, …} high open=12 band=60d [ACK-BREACH ] R-1088 first response 141h > 72h [NO-REASON ] R-1043 rejected without recorded reason register: bountyq_out/findings.csv

The two tools join at triage: duphunter compresses the flood into clusters (the unit of work), bountyq measures the pipeline’s promises (SLAs, bands, reasons) — and both feed the program reviews that BT20 reads. The security joins run deeper: every fixed report is detection content waiting to be written (the postmortem-to-BT9 pipeline: what telemetry would have caught the finding pre-report), the intake surface’s own health joins S9 (ack automation, dedupe jobs, intake-availability monitoring — the dead-intake state is itself a page), and the report stream is threat intelligence: vulnerability classes arriving against specific assets update S3’s surface priorities and BT11’s remediation forecasting. The legal-doctrine join: safe-harbor text and the takedown-escalation path are pre-agreed with counsel — the middle of an incident is the wrong time to invent legal posture.

  1. Add report-text similarity (embedding or shingle-based) as a second clustering signal under human review.
  2. Track researcher reputation (valid-rate, chain quality) to weight triage priority and private-invite lists.
  3. Add payout-consistency audit: bounty amounts versus calibration bands, flagged for drift.
  4. Join the fix queue with PT4: every fixed report’s regression test enters the suite, and bountyq reports coverage.
  5. Add disclosure-deadline tracking per report (the 90-day clock with extension logs) to the bountyq health pass.
SignalSourceIDNotes
ACK SLA breachesbountyqweeklyResearcher trust floor
Aging valid reportsbountyqweeklyBand stress by severity
NO-REASON rejectionsbountyqweeklyRejection discipline
Duplicate rate trendduphuntermonthlyTriage starvation indicator
Intake availabilityS9 automationrealtimeDead intake = page
Fixed-report regression coveragePT4 joinmonthlyFix quality
Disclosure lagTimeline logper reportThe 90-day clock
Report-stream class shiftsTriage intelmonthlySurface priorities (S3)
Payout vs band driftAuditquarterlyCalibration honesty
Safe-harbor takedownsCounsel logper caseThe legal-lead failure

The pipeline consumes the standard substrate where reports involve live exploitation — Windows Security 4624/4625/4688, Sysmon 1/3/10/11/22/23 from BT3’s pipelines, and Suricata custom/local rules (sid:1000001+) on exploitation attempts against in-scope assets — because some reporters test loudly and some “researchers” are the adversary doing recon with a VDP page as cover; the triage layer’s repro-on-staging rule is the boundary control that keeps program machinery off production.

The disclosure pipeline has its own adversary classes. VDP-as-recon: the “researcher” whose reports are minimal but whose traffic is enumeration — every submission maps your intake, your triage tools, your response times; the counter is the staged-repro rule (nothing touches production) and per-reporter telemetry that separates report-volume from probe-volume. Dedup exhaustion: the flood of low-quality near-duplicates designed to bury a real finding or burn SLA hours (the S1-flood pattern aimed at triage) — duphunter’s clustering plus intake rate-shapes and the quality floor (“reports must include repro steps”) are the controls. Severity inflation: the CVSS:9.8 claim on a self-XSS — the calibration doctrine with written bands and the arbitration thread is the counter, applied consistently to keeps the program’s severity currency honest. Bounty-lawyering: the reporter who files a trivial finding, then escalates through legal threat when rejected — the pre-agreed legal posture, documented rejection reasons, and the safe-harbor’s own boundaries keep the escalation short. Disclosure-forcing: the reporter who publishes at day 91 while the fix is queued for day 95 — the doctrine decides in advance whether the program accelerates the fix or ships the advisory with a mitigation note; it never renegotiates the clock retroactively. Insider double-dipping: the employee who reports the company’s own bug through the program — the terms on employee participation are written into scope, and the ethics line (S13’s territory) runs through it.

⚠️ What caught me: our triage queue’s fastest close was a trap — the “duplicate” button, one click, next report. We were closing 70% of a public program’s intake as duplicates inside an hour and wondering why no serious researcher stayed. The audit that woke me up: three of the “duplicates” in one week were not duplicates — same asset, same class string, genuinely different root causes (one IDOR per endpoint version), each merge killing an original finding and quietly teaching the reporter community that our triage skims. We rebuilt around the cluster-first rule (nothing closes as duplicate without a canonical-asset compare and the lead-report check) and the dup rate fell because the real duplicates were fewer than the button had claimed — and the researchers who had drifted away started submitting again within a quarter. The lesson: in disclosure operations, every automated shortcut you take on a human’s work product is a message to the community that produces your vulnerability flow.

Stand up the disclosure pipeline on synthetic reports. Cluster: generate 30 reports across 8 assets with planted duplicates (same asset+class, alias spellings, a -dot- variant) and run duphunter — confirm each planted cluster forms with the earliest report as lead, and one near-match lands in suggestions. Health: run bountyq over an export with a planted ACK breach, a reason-less rejection, and an over-band aging report — confirm all three fire. Calibration: write the severity doctrine for your synthetic estate (one paragraph per band, with the arbitration rule) and defend one deliberately disputable report against a partner playing the researcher. Disclosure: draft the advisory for the fixed IDOR (affected, fixed, credit, mitigation) and the timeline log from the report’s CSV trail. Debrief: which stage of your real (or imagined) program would a motivated researcher quit over first?

  • Technical: Why is dedupe keyed on canonical asset+vuln class rather than report text similarity — and what does each approach miss?
  • Technical: Walk the difference a data map (S10) makes to severity calibration for an authz finding touching personal data.
  • Scenario: Day 89 of a 90-day clock; the fix slipped to day 95. Walk the disclosure decision with the doctrine, the researcher conversation, and the advisory options.
  • Scenario: A researcher reports your intake form renders their markdown unsafely — a stored-XSS on the program itself. Walk triage for a finding against the program machinery.
  • Conceptual: Why does reject-with-reason matter operationally (not just ethically) — and what does its absence cost over a quarter?
  • Deep-dive: Design the program ladder for a company going VDP → public bounty in a year: intake hardening, triage staffing, calibration doctrine, disclosure machinery, and the metrics that gate each rung.
  • Practical: You inherit a program with a 400-report backlog, 60% duplicate rate, and no severity doctrine. What do you do in the first two weeks, and what do you tell the reporters waiting?

BT11 (Vulnerability Management) · S3 (OSINT & Attack Surface Discovery) · S9 (Security Automation & SOAR) · S10 (Data Privacy & Protection Engineering) · S13 (Insider Threat Program)