/** * Bounty Platform Connectors — t3mp3st integrations for popular * bug-bounty and vulnerability-disclosure platforms. * * DESIGN INVARIANT: same as disclosure-gen — DRAFTS ONLY. * These connectors prepare API-ready payloads and, when configured, * can submit via the platform's API — but only after the human * reviews and confirms. No auto-submit, no silent sends. * * Supported platforms: * - HackerOne (API v1, most programs) * - Bugcrowd (REST API v4) * - Intigriti (REST API) * - Immunefi (web3/DeFi bounties) * - Huntr (open-source/dev tool bounties) * - Code4rena (competitive smart-contract audits) * * Each connector implements BountyConnector: * - formatReport(finding) → platform-specific payload * - validateScope(finding) → check if the finding matches a program's scope * - submit(payload, opts) → POST to platform API (requires explicit confirm) * - listPrograms(query) → search available programs */ import { readFileSync, existsSync, writeFileSync } from 'fs'; import { join } from 'path'; // ============================================================================= // TYPES // ============================================================================= export type BountyPlatform = 'hackerone' | 'bugcrowd' | 'intigriti' | 'immunefi' | 'huntr' | 'code4rena'; export interface BountyCredentials { platform: BountyPlatform; apiKey?: string; username?: string; /** HackerOne uses API key + identifier pair */ apiIdentifier?: string; /** Immunefi uses wallet-based auth for web3 */ walletAddress?: string; } export interface BountyProgram { platform: BountyPlatform; handle: string; name: string; scope: BountyScopeItem[]; minSeverity?: string; maxBounty?: { low?: number; medium?: number; high?: number; critical?: number }; responseTime?: string; managed?: boolean; } export interface BountyScopeItem { type: 'web' | 'api' | 'mobile' | 'hardware' | 'iot' | 'smart_contract' | 'source_code' | 'other'; target: string; eligible: boolean; bountyRange?: string; } export interface BountyFinding { slug: string; title: string; severity: 'none' | 'low' | 'medium' | 'high' | 'critical'; cvssVector?: string; cvssScore?: number; cwe?: string; summary: string; rootCause: string; poc: string; impact: string; remediation: string; component?: string; versionsAffected?: string; /** Structured evidence from t3mp3st's verify pipeline */ evidence?: Array<{ type: string; content: string }>; } export interface BountyReport { platform: BountyPlatform; programHandle: string; title: string; severity: string; body: string; structuredData?: Record; /** The raw API payload, ready to POST */ apiPayload?: Record; /** Human-review checklist items */ checklist: string[]; } export interface BountySubmitResult { success: boolean; reportId?: string; reportUrl?: string; error?: string; /** Did the human confirm before we sent? */ confirmed: boolean; } export interface BountyConnector { platform: BountyPlatform; formatReport(finding: BountyFinding, programHandle: string): BountyReport; validateScope(finding: BountyFinding, program: BountyProgram): { inScope: boolean; reason: string }; submit(report: BountyReport, credentials: BountyCredentials, opts: { dryRun?: boolean }): Promise; listPrograms(query: string, credentials: BountyCredentials): Promise; } // ============================================================================= // SHARED HELPERS // ============================================================================= function severityLabel(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); } function buildMarkdownReport(f: BountyFinding): string { let md = `## Summary\n\n${f.summary}\n\n`; md += `## Severity\n\n**${severityLabel(f.severity)}**`; if (f.cvssVector) md += ` — CVSS ${f.cvssScore ?? '?'} (${f.cvssVector})`; if (f.cwe) md += `\nCWE: ${f.cwe}`; md += '\n\n'; md += `## Root Cause\n\n${f.rootCause}\n\n`; md += `## Steps to Reproduce\n\n${f.poc}\n\n`; md += `## Impact\n\n${f.impact}\n\n`; md += `## Suggested Remediation\n\n${f.remediation}\n\n`; if (f.component) md += `## Affected Component\n\n${f.component}\n\n`; if (f.versionsAffected) md += `## Versions Affected\n\n${f.versionsAffected}\n\n`; return md; } const BASE_CHECKLIST = [ 'Verify the finding is real and reproducible before submitting', 'Confirm the target is IN SCOPE for this program', 'Check if this is a duplicate — search the program\'s disclosed reports', 'Ensure no sensitive data (API keys, credentials, PII) is in the report', 'Review severity rating — does it match CVSS and actual impact?', 'Attach all necessary evidence (screenshots, logs, PoC scripts)', ]; // ============================================================================= // HACKERONE CONNECTOR // ============================================================================= export const hackeroneConnector: BountyConnector = { platform: 'hackerone', formatReport(finding: BountyFinding, programHandle: string): BountyReport { const body = buildMarkdownReport(finding); const severityRating = ({ none: 'none', low: 'low', medium: 'medium', high: 'high', critical: 'critical' } as const)[finding.severity] || 'medium'; return { platform: 'hackerone', programHandle, title: finding.title, severity: finding.severity, body, apiPayload: { data: { type: 'report', attributes: { team_handle: programHandle, title: finding.title, vulnerability_information: body, severity_rating: severityRating, weakness_id: finding.cwe ? parseInt(finding.cwe.replace(/\D/g, ''), 10) : undefined, }, }, }, checklist: [ ...BASE_CHECKLIST, `Program handle: ${programHandle}`, 'HackerOne: check program policy for safe harbor / legal provisions', 'HackerOne: attach PoC as a separate file if > 2000 chars', ], }; }, validateScope(finding: BountyFinding, program: BountyProgram) { const component = (finding.component || '').toLowerCase(); const match = program.scope.find(s => s.eligible && component.includes(s.target.toLowerCase())); if (match) return { inScope: true, reason: `Matches scope target: ${match.target}` }; return { inScope: false, reason: `No scope target matched component "${finding.component}"` }; }, async submit(report: BountyReport, credentials: BountyCredentials, opts = {}): Promise { if (opts.dryRun) { return { success: true, confirmed: false, reportId: 'DRY-RUN', reportUrl: `https://hackerone.com/${report.programHandle}/reports/DRY-RUN` }; } if (!credentials.apiKey || !credentials.apiIdentifier) { return { success: false, confirmed: false, error: 'HackerOne requires both apiKey and apiIdentifier' }; } const auth = Buffer.from(`${credentials.apiIdentifier}:${credentials.apiKey}`).toString('base64'); const res = await fetch('https://api.hackerone.com/v1/hackers/reports', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Basic ${auth}` }, body: JSON.stringify(report.apiPayload), }); if (!res.ok) { const text = await res.text(); return { success: false, confirmed: true, error: `HackerOne API ${res.status}: ${text.slice(0, 500)}` }; } const data = await res.json() as any; return { success: true, confirmed: true, reportId: data.data?.id, reportUrl: `https://hackerone.com/reports/${data.data?.id}`, }; }, async listPrograms(query: string, credentials: BountyCredentials): Promise { if (!credentials.apiKey || !credentials.apiIdentifier) return []; const auth = Buffer.from(`${credentials.apiIdentifier}:${credentials.apiKey}`).toString('base64'); const res = await fetch(`https://api.hackerone.com/v1/hackers/programs?filter[name]=${encodeURIComponent(query)}`, { headers: { Authorization: `Basic ${auth}` }, }); if (!res.ok) return []; const data = await res.json() as any; return (data.data || []).map((p: any) => ({ platform: 'hackerone' as const, handle: p.attributes?.handle || p.id, name: p.attributes?.name || p.id, scope: [], managed: p.attributes?.state === 'public_mode', })); }, }; // ============================================================================= // BUGCROWD CONNECTOR // ============================================================================= export const bugcrowdConnector: BountyConnector = { platform: 'bugcrowd', formatReport(finding: BountyFinding, programHandle: string): BountyReport { const body = buildMarkdownReport(finding); const bugcrowdSeverity = ({ none: 1, low: 2, medium: 3, high: 4, critical: 5 } as const)[finding.severity] || 3; return { platform: 'bugcrowd', programHandle, title: finding.title, severity: finding.severity, body, apiPayload: { submission: { title: finding.title, description: body, severity: bugcrowdSeverity, vrt_id: finding.cwe || undefined, extra_info: finding.poc, }, }, checklist: [ ...BASE_CHECKLIST, `Bugcrowd program: ${programHandle}`, 'Bugcrowd: use their VRT (Vulnerability Rating Taxonomy) for classification', 'Bugcrowd: check if the program accepts PoC scripts as attachments', ], }; }, validateScope(finding: BountyFinding, program: BountyProgram) { const component = (finding.component || '').toLowerCase(); const match = program.scope.find(s => s.eligible && component.includes(s.target.toLowerCase())); if (match) return { inScope: true, reason: `Matches scope target: ${match.target}` }; return { inScope: false, reason: `No scope target matched component "${finding.component}"` }; }, async submit(report: BountyReport, credentials: BountyCredentials, opts = {}): Promise { if (opts.dryRun) { return { success: true, confirmed: false, reportId: 'DRY-RUN' }; } if (!credentials.apiKey) { return { success: false, confirmed: false, error: 'Bugcrowd requires an API token' }; } const res = await fetch(`https://api.bugcrowd.com/programs/${report.programHandle}/submissions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Token ${credentials.apiKey}` }, body: JSON.stringify(report.apiPayload), }); if (!res.ok) { const text = await res.text(); return { success: false, confirmed: true, error: `Bugcrowd API ${res.status}: ${text.slice(0, 500)}` }; } const data = await res.json() as any; return { success: true, confirmed: true, reportId: data.id || data.uuid }; }, async listPrograms(query: string, credentials: BountyCredentials): Promise { if (!credentials.apiKey) return []; const res = await fetch(`https://api.bugcrowd.com/programs?search=${encodeURIComponent(query)}`, { headers: { Authorization: `Token ${credentials.apiKey}` }, }); if (!res.ok) return []; const data = await res.json() as any; return (data.programs || data.data || []).map((p: any) => ({ platform: 'bugcrowd' as const, handle: p.code || p.id, name: p.name || p.code || p.id, scope: [], })); }, }; // ============================================================================= // INTIGRITI CONNECTOR // ============================================================================= export const intigritiConnector: BountyConnector = { platform: 'intigriti', formatReport(finding: BountyFinding, programHandle: string): BountyReport { const body = buildMarkdownReport(finding); return { platform: 'intigriti', programHandle, title: finding.title, severity: finding.severity, body, apiPayload: { title: finding.title, description: body, severity: finding.severity.toUpperCase(), domain_or_target: finding.component || '', proof_of_concept: finding.poc, }, checklist: [ ...BASE_CHECKLIST, `Intigriti program: ${programHandle}`, 'Intigriti: check program\'s confidentiality requirements', ], }; }, validateScope(finding: BountyFinding, program: BountyProgram) { const component = (finding.component || '').toLowerCase(); const match = program.scope.find(s => s.eligible && component.includes(s.target.toLowerCase())); if (match) return { inScope: true, reason: `Matches scope target: ${match.target}` }; return { inScope: false, reason: `No scope target matched component "${finding.component}"` }; }, async submit(_report: BountyReport, _credentials: BountyCredentials, opts = {}): Promise { if (opts.dryRun) return { success: true, confirmed: false, reportId: 'DRY-RUN' }; return { success: false, confirmed: false, error: 'Intigriti submission requires their web portal — use formatReport to prepare, then submit manually' }; }, async listPrograms(_query: string, _credentials: BountyCredentials): Promise { return []; }, }; // ============================================================================= // IMMUNEFI CONNECTOR (Web3 / DeFi) // ============================================================================= export const immunefiConnector: BountyConnector = { platform: 'immunefi', formatReport(finding: BountyFinding, programHandle: string): BountyReport { const body = buildMarkdownReport(finding); return { platform: 'immunefi', programHandle, title: finding.title, severity: finding.severity, body, apiPayload: { project: programHandle, title: finding.title, severity: finding.severity, description: body, proof_of_concept: finding.poc, impact: finding.impact, }, checklist: [ ...BASE_CHECKLIST, `Immunefi program: ${programHandle}`, 'Immunefi: reports go through their Bugfix Review process', 'Immunefi: smart contract bugs require on-chain PoC (testnet fork preferred)', 'Immunefi: check if the program uses Immunefi\'s severity scale vs CVSS', ], }; }, validateScope(finding: BountyFinding, program: BountyProgram) { const component = (finding.component || '').toLowerCase(); const isSmartContract = component.includes('contract') || component.includes('solidity') || component.includes('.sol'); const match = program.scope.find(s => s.eligible && ( component.includes(s.target.toLowerCase()) || (isSmartContract && s.type === 'smart_contract') )); if (match) return { inScope: true, reason: `Matches scope: ${match.target} (${match.type})` }; return { inScope: false, reason: `No scope match for "${finding.component}"` }; }, async submit(_report: BountyReport, _credentials: BountyCredentials, opts = {}): Promise { if (opts.dryRun) return { success: true, confirmed: false, reportId: 'DRY-RUN' }; return { success: false, confirmed: false, error: 'Immunefi submissions go through bugs.immunefi.com — use formatReport to prepare, then submit via their portal' }; }, async listPrograms(_query: string, _credentials: BountyCredentials): Promise { return []; }, }; // ============================================================================= // HUNTR CONNECTOR (open-source / dev tools) // ============================================================================= export const huntrConnector: BountyConnector = { platform: 'huntr', formatReport(finding: BountyFinding, programHandle: string): BountyReport { const body = buildMarkdownReport(finding); return { platform: 'huntr', programHandle, title: finding.title, severity: finding.severity, body, apiPayload: { repository: programHandle, title: finding.title, description: body, severity: finding.severity, cwe: finding.cwe, steps_to_reproduce: finding.poc, }, checklist: [ ...BASE_CHECKLIST, `Huntr repo: ${programHandle}`, 'Huntr: targets open-source repos — confirm the repo is in their directory', 'Huntr: automated triage, usually < 48h response', ], }; }, validateScope(_finding: BountyFinding, program: BountyProgram) { if (program.scope.length === 0 && program.handle) { return { inScope: true, reason: 'Huntr programs scope by repository — assume in scope if repo matches' }; } return { inScope: false, reason: 'Unable to verify scope without program details' }; }, async submit(_report: BountyReport, _credentials: BountyCredentials, opts = {}): Promise { if (opts.dryRun) return { success: true, confirmed: false, reportId: 'DRY-RUN' }; return { success: false, confirmed: false, error: 'Huntr submission requires their web portal at huntr.com — use formatReport to prepare' }; }, async listPrograms(_query: string, _credentials: BountyCredentials): Promise { return []; }, }; // ============================================================================= // CODE4RENA CONNECTOR (competitive smart-contract audits) // ============================================================================= export const code4renaConnector: BountyConnector = { platform: 'code4rena', formatReport(finding: BountyFinding, programHandle: string): BountyReport { const riskLabel = ({ critical: 'H', high: 'H', medium: 'M', low: 'QA' } as Record)[finding.severity] || 'M'; let body = `# [${riskLabel}] ${finding.title}\n\n`; body += `## Summary\n${finding.summary}\n\n`; body += `## Vulnerability Detail\n${finding.rootCause}\n\n`; body += `## Impact\n${finding.impact}\n\n`; body += `## Proof of Concept\n${finding.poc}\n\n`; body += `## Recommended Mitigation\n${finding.remediation}\n`; return { platform: 'code4rena', programHandle, title: `[${riskLabel}] ${finding.title}`, severity: finding.severity, body, checklist: [ ...BASE_CHECKLIST, `Code4rena contest: ${programHandle}`, 'Code4rena: format as [H/M/QA] — High, Medium, or QA-grade', 'Code4rena: submit via their contest portal during the active window', 'Code4rena: include Foundry/Hardhat test as PoC when possible', ], }; }, validateScope(finding: BountyFinding, program: BountyProgram) { const component = (finding.component || '').toLowerCase(); const match = program.scope.find(s => s.eligible && component.includes(s.target.toLowerCase())); if (match) return { inScope: true, reason: `In contest scope: ${match.target}` }; return { inScope: false, reason: `"${finding.component}" not found in contest scope` }; }, async submit(_report: BountyReport, _credentials: BountyCredentials, opts = {}): Promise { if (opts.dryRun) return { success: true, confirmed: false, reportId: 'DRY-RUN' }; return { success: false, confirmed: false, error: 'Code4rena submissions go through their contest portal — use formatReport to prepare your finding in C4 format' }; }, async listPrograms(_query: string, _credentials: BountyCredentials): Promise { return []; }, }; // ============================================================================= // REGISTRY — resolve platform name → connector // ============================================================================= const CONNECTORS: Record = { hackerone: hackeroneConnector, bugcrowd: bugcrowdConnector, intigriti: intigritiConnector, immunefi: immunefiConnector, huntr: huntrConnector, code4rena: code4renaConnector, }; export function getConnector(platform: BountyPlatform): BountyConnector { const c = CONNECTORS[platform]; if (!c) throw new Error(`Unknown bounty platform: ${platform}`); return c; } export function listConnectors(): BountyPlatform[] { return Object.keys(CONNECTORS) as BountyPlatform[]; } /** * Convert a t3mp3st finding JSON (as used in bench/wild-hunt/findings/) * into the BountyFinding format expected by connectors. */ export function findingToBountyFinding(raw: Record): BountyFinding { return { slug: raw.slug || raw.id || 'unknown', title: raw.summary || raw.title || raw.slug || 'Untitled', severity: (raw.severity_computed || raw.severity_self || 'medium').toLowerCase(), cvssVector: raw.cvss_vector, cvssScore: raw.cvss_score, cwe: raw.cwe, summary: raw.summary || '', rootCause: raw.root_cause || '', poc: raw.poc || '', impact: raw.impact || '', remediation: raw.remediation || '', component: raw.component, versionsAffected: raw.versions_affected, }; } /** * Load credentials from the bounty credentials file. * Stored in .keys.bounty.json (gitignored alongside .keys.local). */ export function loadBountyCredentials(repoRoot: string): Record { const credPath = join(repoRoot, '.keys.bounty.json'); if (!existsSync(credPath)) return {} as any; return JSON.parse(readFileSync(credPath, 'utf8')); } /** * Save credentials to the bounty credentials file. */ export function saveBountyCredentials(repoRoot: string, creds: Record): void { writeFileSync(join(repoRoot, '.keys.bounty.json'), JSON.stringify(creds, null, 2) + '\n'); }