π‘οΈ A meta-harness for HackerOne workflows. Defender-side triage (dedupe, CVSS scoring, scope-check, classification) AND researcher-side recon (scope analysis, OWASP/CWE mapping, report formatting) β all read-only, safety-contained, with a
$0mock-mode for CI.
@metaharness/hackerone is a deterministic, offline-first harness for the two sides of a HackerOne engagement:
| Side | Verb | What it does |
|---|---|---|
| Defender (program owner) | triage, triage-batch |
Score incoming reports against scope, recompute CVSS, dedupe vs history, recommend a new state |
| Researcher (bounty hunter) | scope, classify, format |
Build a recon plan from a program's scope, classify free-text into CWE/OWASP, format a finding as a submission-ready report |
No ML, no embeddings, no external calls beyond the HackerOne API itself. Every classification is a transparent rule the SOC/researcher can audit and reproduce.
The harness is read-only and capability-contained in v0.1. The defaults below are hard-frozen in src/safety.ts; nothing in this codebase relaxes them:
| Boundary | Enforcement |
|---|---|
| No write API calls | HARD_SAFE_DEFAULTS.allowWrite = false (frozen); write commands not implemented in v0.1 |
| Mock-by-default | When no HACKERONE_API_KEY is found, the in-memory MockHackerOneClient is used |
| Forced mock | --mock-api flag overrides any configured key |
| Rate limit | Token bucket caps API calls at 30/min per process (tunable) |
| No credential logging | The API key is fetched transiently; only length + 4-char prefix is ever echoed |
| PII redaction | Emails, IPs, phone numbers, CC-shaped sequences stripped before logging |
| Scope validation | Program handles are regex-validated before they hit any URL builder |
| Credential-shape guard | Reports/fixtures scanned for gho_, AKIA, Bearer β¦, PEM headers before logging |
Anyone reviewing src/safety.ts should be able to answer in one sentence: "What's the worst this harness can do?" β "Issue read-only HackerOne API calls scoped to one program, with a hard rate limit, with the key fetched transiently and never logged."
npm install @metaharness/hackerone
# or
npx @metaharness/hackerone --help# 1. Generate a sample config
npx hackerone init
# 2. Inspect a program's scope and get a test plan (mock-mode β no key needed)
npx hackerone scope example-program --mock-api
# 3. Classify a finding description
npx hackerone classify "Reflected XSS via search query parameter"
# β CWE: CWE-79 β Cross-site Scripting
# β OWASP: A03:2021
# β confidence: 0.91
# 4. Format a fixture as a HackerOne-style submission report
npx hackerone format fixtures/example-finding.jsonThe assets subcommand emits newline-delimited in-scope identifiers (with
a #-prefixed banner that recon tools ignore), so the harness drops
straight into Subfinder, Amass, Nuclei, httpx, etc.:
# Subdomain enumeration on in-scope wildcards
npx hackerone assets security --expand-wildcards | subfinder -dL -
# Active enum
npx hackerone assets <handle> --type URL,DOMAIN --expand-wildcards | amass enum -df -
# Nuclei against the live targets (after subdomain enum + httpx probe)
npx hackerone assets <handle> --expand-wildcards \
| subfinder -dL - \
| httpx -silent \
| nuclei -t cves/ -severity high,critical
# Mobile-only β no URL noise
npx hackerone assets <handle> --type IOS_APP_STORE,ANDROID_PLAY_STOREBy default the asset list is in-scope only β pass
--include-out-of-scope to add a flagged # OUT-OF-SCOPE block (still
inert for recon tools, but informative for humans). The --type filter
accepts a comma-separated list of HackerOne asset types.
@metaharness/redblue@0.1.4 owns the human-gated HackerOne submit verb
(4 gates: scope / verification / per-report-confirm / no-batch; dry-run
default; scope verified live; CI/non-interactive refused). @metaharness/ hackerone does not reimplement submit. Instead, the bridge subcommand
emits the exact HackerOneReportDraft JSON redblue's submit gate
consumes, so the recon β triage β draft β submit handoff is one pipe:
# Produce a redblue-shaped draft from a finding JSON
npx hackerone export-redblue fixtures/example-finding.json > draft.json
# Pipe straight into redblue's submit gates (dry-run by default)
npx hackerone export-redblue fixtures/example-finding.json \
| npx redblue submit --in - --dry-runDefaults are safety-first:
draft.repro.confirmed = falseβ redblue's verification gate refuses the draft unless the operator explicitly attests reproduction via--repro-confirmed --repro-method "<how>".submission.auto_submit = falseβ hard-coded sentinel; redblue's submit gate refuses any draft missing this.- No submit logic in this package β it just emits the shape.
Flags:
--repro-confirmedβ operator attests they reproduced the finding--repro-method "<text>"β how the repro was confirmed (free text)--asset <id>β override the asset (must match a live in-scope entry)--family <enum>β override the family heuristic (prompt_injection/tool_overreach/data_exfiltration/role_confusion/cost_amplification); CWEβfamily heuristic applies otherwise--recommended-fix "<text>"β surfaces in the draft
When the defender side wants to file an engineering ticket for a
triaged report, export-issue emits the POST body in the right shape;
the operator pipes to curl to actually create the ticket. No
automation of the POST itself, no token handling here, no network
calls β the harness only produces the payload.
# GitHub Issues β POST body to /repos/<o>/<n>/issues
npx hackerone export-issue finding.json \
--kind github \
--repo myorg/myrepo \
--report-id 4242 \
--labels frontend,p1 \
| curl -X POST https://api.github.com/repos/myorg/myrepo/issues \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
-d @-
# Jira β POST body to /rest/api/3/issue
npx hackerone export-issue finding.json \
--kind jira \
--project SEC \
--report-id 4242 \
| curl -X POST https://myorg.atlassian.net/rest/api/3/issue \
-H "Authorization: Basic $JIRA_TOKEN" \
-H "Content-Type: application/json" \
-d @-Defaults:
- Title is auto-prefixed
[H1#<report-id>]for searchability + dedup-by-operator. - Labels for GitHub:
security,severity/<band>,cwe/<id>. - Priority for Jira: severity β
Highest|High|Medium|Low|Lowest. - Description is markdown for GitHub, ADF (Atlassian Document Format) for Jira.
- PII redaction is on by default β emails, IPs, phone numbers,
CC-shaped digit sequences are stripped before emit. Override with
--skip-pii-redaction.
Flags:
--kind jira|github(default: github)--project KEY(required for Jira)--repo owner/name(GitHub informational)--report-id <id>(prepended to title)--issue-type Bug|Task|β¦(Jira)--labels a,b,c(additive)--assignees a,b(GitHub)--skip-pii-redaction(off by default)
classify --stdin reads one finding description per line, emits one
JSON object per line ({input, classification}). Useful for piping
scanner alerts into the CWE/OWASP classifier:
# From a security log file
cat alerts.txt | npx hackerone classify --stdin > classified.jsonl
# From an inline list
printf "SQL injection on /api\nReflected XSS\n" | npx hackerone classify --stdinBlank lines and #-prefixed comments are skipped.
# 1. Configure the API key (see "Configuring the API key" below)
export HACKERONE_API_KEY=username:token
# 2. Verify it resolves and connects
npx hackerone ping
# 3. Triage every open report in a program
npx hackerone triage-batch example-program --state new --json
# 4. Triage a single report
npx hackerone triage 1001 --program example-programThe harness resolves HACKERONE_API_KEY in priority order:
process.env.HACKERONE_API_KEYβ preferred for CI/CD.envfile (cwd or any parent up to 6 levels) β local dev- GCP Secret Manager β
gcloud secrets versions access latest --secret=HACKERONE_API_KEY --project=$GCP_PROJECT
HACKERONE_API_KEY accepts either:
username:tokenβ encoded as Basic<base64(username:token)>automatically- A pre-encoded base64 token β passed through
Get yours at https://hackerone.com/users//api_tokens.
The HackerOne GraphQL endpoint authenticates public queries with just
X-Auth-Token: <token> (handled automatically when HACKERONE_API_KEY
is set). But user-bound queries (me, private program detail, your
submitted reports) need the session cookie that the web UI sends.
To use those queries from the harness:
-
Log in to https://hackerone.com in a browser
-
Open DevTools β Application/Storage β Cookies β
hackerone.com -
Copy the value of
_hackerone_session(or the wholeCookie:header) -
Export it transiently, never commit:
export HACKERONE_SESSION_COOKIE="_hackerone_session=<value>" npx hackerone ping --no-mock --json # should now report ok=true for `me` npx hackerone scope <your-private-program>
The cookie is never logged or stored on disk by the harness β same treatment as the API key. Sessions expire; refresh by re-extracting.
# One-time setup
echo -n 'username:token' | gcloud secrets create HACKERONE_API_KEY --data-file=- --project=ruv-dev
# Rotate
echo -n 'new-username:new-token' | gcloud secrets versions add HACKERONE_API_KEY --data-file=- --project=ruv-dev
# Grant CI access (one-time)
gcloud secrets add-iam-policy-binding HACKERONE_API_KEY \
--member=serviceAccount:ci@ruv-dev.iam.gserviceaccount.com \
--role=roles/secretmanager.secretAccessor \
--project=ruv-devThe harness fetches the secret transiently via gcloud at startup. It never writes the value to disk.
| Subcommand | Side | Description |
|---|---|---|
init |
both | Write .env.example and hackerone.yaml to cwd |
ping |
both | Verify the API key resolves + connects (or report mock-mode) |
programs |
both | List programs accessible to the configured key |
scope <handle> |
researcher | Recon plan: top assets ranked by bounty potential, suggested checks per asset, forbidden assets |
classify "<description>" |
researcher | Map free-text into CWE + OWASP Top-10 + confidence |
format <path> |
researcher | Render a finding JSON as a HackerOne-style submission report |
triage <id> --program <handle> |
defender | Triage one report: scope β CVSS β dedupe β recommended state + reasoning |
triage-batch <handle> |
defender | Triage every open report on a program (use --state new to filter) |
All commands accept:
--mock-apiβ force the in-memory mock client--no-mockβ fail if no live API key is configured--jsonβ emit JSON instead of human text
import {
HackerOneClient,
MockHackerOneClient,
triageReport,
buildReconPlan,
classifyDescription,
formatFinding,
resolveApiKey,
} from '@metaharness/hackerone';
const { key } = resolveApiKey();
const client = key ? new HackerOneClient({ apiKey: key }) : new MockHackerOneClient();
const program = await client.getProgram('example-program');
const plan = buildReconPlan(program);
console.log(plan.topAssets);Subpath imports for narrower bundles:
import { triageReport } from '@metaharness/hackerone/triage';
import { classifyDescription } from '@metaharness/hackerone/research';
import { HARD_SAFE_DEFAULTS } from '@metaharness/hackerone/safety';npm install
npm run build # tsc β dist/
npm test # vitest run
npm run smoke # structural smoke (no npm deps required)