@vtx-labs/recon
v0.2.0
Published
Secret intelligence for authorized engagements — find a key, then prove what it can actually do. Read-only capability ladders, gated by construction, with a redacted evidence bundle.
Maintainers
Readme
██████╗ ███████╗ ██████╗ ██████╗ ███╗ ██╗
██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ██║
██████╔╝█████╗ ██║ ██║ ██║██╔██╗ ██║
██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╗██║
██║ ██║███████╗╚██████╗╚██████╔╝██║ ╚████║
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝@vtx-labs/recon
Secret intelligence for authorized engagements — find a key, then prove what it can actually do. The native TypeScript/Node implementation of vtx-recon.
⚠️ Authorized use only
vtx-recon is for security testing of systems you are explicitly authorized to test — an in-scope bug-bounty program or a signed engagement. Validating a credential beyond authentication may constitute unauthorized access under the US CFAA, the UK Computer Misuse Act, and equivalent laws. On HackerOne and similar programs: report leaked credentials first; do not exercise their functionality beyond what the program permits. Provided "as is", no warranty, no liability. By using it you accept the TERMS.
A found secret that merely exists is not a finding. A found secret you can prove does something the program cares about is. TruffleHog finds secrets and tells you if they're live; vtx-recon picks up from there — it runs ordered, read-only capability ladders to prove depth of access, tiers each key as PROVEN / VALID / DENIED, and emits a redacted, timestamped evidence bundle you can drop into a report.
trufflehog ──▶ findings ──▶ vtx-recon ladder ──▶ PROVEN / VALID / DENIED ──▶ evidence bundle
(find + (live/ (safe, ordered (impact tier) (JSON,
verify) dead) read-only probes) secrets redacted)This package is a faithful, native port of the Python implementation: the
same pipeline, the same safety guarantees, the same providers, the same CLI
surface — idiomatic ESM TypeScript with zero runtime dependencies (built-in
fetch, node:crypto, node:child_process).
Requirements
vtx-recon orchestrates the TruffleHog binary for the find/verify stage — install it first:
# macOS / Linux
brew install trufflehog
# or: curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/binThe binary is located on PATH on demand — it is never spawned at import time.
Install
npx @vtx-labs/recon find . --i-am-authorized "bugbounty:acme"
# or install it:
pnpm add -g @vtx-labs/recon
npm install -g @vtx-labs/reconQuick start
# Find + verify secrets in a repo, then ladder the live ones (read-only):
vtx-recon find . --i-am-authorized "bugbounty:acme"
# Validate a single key you already have:
echo "AIza..." | vtx-recon ladder --i-am-authorized "bugbounty:acme"
# Machine-readable evidence bundle:
vtx-recon ladder --key ghp_xxx --i-am-authorized "h1:acme" --jsonBy default only safe, read-only probes run. Probes that cost money, read PII, or change state are gated (see below) and never run unless you explicitly arm them.
Programmatic API
import { Consent, getLadder, Finding } from "@vtx-labs/recon";
const consent = new Consent({ authorizedScope: "bugbounty:acme" });
const finding = new Finding({ detectorName: "GoogleAI", verified: true, raw: "AIza..." });
const ladder = getLadder(finding.detectorName);
if (ladder) {
const result = await ladder(finding, consent);
console.log(result.verdict, JSON.stringify(result.toPublic(), null, 2));
}The safety model — read-only by default, gated by construction
Every probe is one of two tiers:
- SAFE — read-only, non-billable, idempotent (list models,
GetCallerIdentity,auth.test, read token scopes). Runs by default. - GATED — billable, PII-reading, state-changing, or resource-creating
(Gemini
generateContent/ file upload, a billable Maps call, Firebase anonymous signup, a Stripe account read).
A gated probe is structurally unreachable unless you pass both:
--prove # arm gated probes
--i-am-authorized "<scope>" # name the engagement you're authorized to testThis is enforced in code (src/safety.ts), not just docs:
guard(consent, tier, probeName)is a no-op forSAFEand throwsGatedProbeBlockedbefore any I/O forGATEDunlessconsent.proveand a non-emptyauthorizedScopeare both present.gated(probeName, fn)wraps a probe so the guard runs before the body, and fails closed: if noConsentis reachable in the arguments it is treated as denied (blocked), never allowed.Consentis frozen at construction — a probe cannot mutate its own consent to escalate its tier mid-run.Consent.requireLadderScope()makes the whole ladder refuse to run (throwsScopeRequired) without a named scope, which is recorded verbatim in the evidence bundle so every action is attributable.
There is no flag, env var, or config that runs a gated probe with only one of the two conditions met. Both are required, every time.
Capability ladders
51 providers ship with a dedicated ladder, auto-detected from the key's
shape / TruffleHog detector name. Each climbs from cheapest/safest to deepest:
identity/whoami first, then reach/scope, then the impactful rung. Gated rungs
are marked 🔒. Where a rung needs an out-of-band value the engine cannot derive
from the secret alone (a second credential half, an account/host/project id),
it never fires a live call — it prints the exact copy-pasteable safe curl with
the secret kept as $KEY (marked manual) for an authorized operator to run by
hand. getLadder(detectorName) returns the registered ladder so you can
introspect or invoke any of them programmatically.
Cloud & infra
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| Google / Gemini (AIza…) | list models → files → cachedContents → corpora; read-only Referer-bypass on a referer-restricted key | generateContent, file upload, billable Maps probe, Firebase anon-signup |
| GCP (service-account JSON) | mint OAuth2 token → tokeninfo → list projects (manual: engine can't sign the JWT) | list storage buckets 🔒 (manual) |
| AWS (AKIA/ASIA) | STS GetCallerIdentity (node:crypto SigV4, needs paired secret) | iam:GetAccountAuthorizationDetails 🔒 (bulk org/PII read) |
| Azure (Storage SAS) | SAS resource probe → service-principal token (manual: needs account/container) | list blobs 🔒 (manual) |
| Cloudflare (API token) | verify token → permission groups → list zones | edit DNS record 🔒 (manual) |
| DigitalOcean (dop_v1_…) | /account → list droplets | create droplet 🔒 (manual) |
| Heroku (Platform key) | /account → list apps | read app config vars 🔒 (manual) |
| Render (rnd_) | list owners → list services | read service env vars 🔒 (manual) |
| Vercel (access token) | /user → list projects | read decrypted project env 🔒 (manual) |
| Netlify (PAT) | /user → list sites | read site/account env 🔒 (manual) |
| Fastly (Fastly-Key) | token self → list services | purge-all cache 🔒 (manual) |
Data stores
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| Supabase (service_role JWT) | REST root / OpenAPI schema (manual: needs project ref) | list table rows, list auth users 🔒 (manual) |
| PlanetScale (pscale_tkn_) | list orgs → list databases (manual: needs token id + org) | create branch 🔒 (manual) |
| Snowflake (account+user+pass) | CURRENT_USER() → list databases (manual: needs KEYPAIR_JWT) | exfil table data 🔒 (manual) |
| Airtable (pat…) | whoami+scopes → list bases | list base records 🔒 (manual) |
| Algolia (admin key) | own-key ACL → list all keys → list indices (manual: needs App ID) | clear index 🔒 (manual) |
CI/CD & packages
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| GitHub (ghp_/github_pat_) | /user identity → scopes → dangerous-scope flag → reachable private repos → org walk | demonstration state-changing PUT 🔒 (proves the boundary is wired) |
| GitLab (glpat-) | /user identity → token scopes | — |
| Bitbucket (ATBB… app pw) | /user whoami → repo permissions (manual: needs paired username) | create repository 🔒 (manual) |
| CircleCI (CCIPAT_) | /me whoami → collaborations | trigger pipeline 🔒 (manual) |
| Travis CI (token) | /user whoami → list repos | trigger build 🔒 (manual) |
| Terraform Cloud (.atlasv1.) | account details → list orgs | create run 🔒 (manual) |
| Docker Hub (dckr_pat_) | auth-token exchange → list namespace repos (manual: needs username/JWT) | delete repository 🔒 (manual) |
| npm (NpmToken) | /-/whoami → token type | publish package 🔒 (manual) |
| PyPI (pypi- macaroon) | — (upload-only token, no read surface) | publish package 🔒 (manual) |
Comms & email
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| Slack (xox…) | auth.test → conversations → users → files | read channel history, post message 🔒 (manual) |
| Discord (bot token) | /users/@me → guilds | read channel history, send message 🔒 (manual) |
| Twilio (AC… SID) | account fetch → phone numbers (manual: needs AuthToken) | read balance 🔒 (manual) |
| SendGrid (SG.…) | /v3/scopes (validity + scopes) | send mail 🔒 |
| Mailgun (key-…) | list domains → list DKIM keys | send message 🔒 (manual) |
| Mailchimp (…-us21) | API root whoami → list audiences | add list member 🔒 (manual) |
| Postmark (server token) | /server → delivery stats | send email 🔒 (manual) |
| Pusher (channel key) | list channels → channel info (manual: needs secret+app_id HMAC) | trigger event 🔒 (manual) |
Support & productivity
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| Asana (PAT/OAuth) | /users/me → list workspaces | list workspace users (PII) 🔒 (manual) |
| Linear (lin_api_) | viewer identity → organization | list org users (PII) 🔒 |
| Notion (secret_/ntn_) | /users/me bot user | list users, search shared content 🔒 |
| Intercom (access token) | /me → list admins | list contacts (PII) 🔒 |
| HubSpot (pat-…/OAuth) | token-info / account-info (whoami + scopes) | list CRM contacts (PII) 🔒 |
| Zendesk (ZendeskApi) | current user → list users (manual: needs subdomain+email) | list tickets (PII) 🔒 (manual) |
| Figma (figd_) | /v1/me whoami → list team projects (manual) | — |
Observability & ops
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| Datadog (DD-API-KEY) | /validate (live check) → current user → list monitors (manual: needs app key) | — |
| Grafana (glsa_…) | current user → user permissions → list datasources (manual: needs host) | — |
| New Relic (NRAK-) | viewer identity → list accounts | — |
| Sentry (sntryu_/sntrys_) | list organizations → list org projects (manual) | read project issues (PII) 🔒 (manual) |
| PagerDuty (API key) | /abilities → list users | create incident 🔒 (manual) |
Payments & SaaS
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| Stripe (sk_/rk_) | /v1/balance auth → products → balance transactions | account read, charges list (PII) 🔒 |
| PayPal (PaypalOauth) | OAuth2 token → userinfo (manual: needs client id+secret) | create payout 🔒 (manual) |
| Square (EAAA…) | list locations → merchant /me → team members | create payment 🔒 (manual) |
| Shopify (shpat_) | access scopes → shop info (manual: needs shop domain) | list customers (PII) 🔒 (manual) |
AI
| Provider | Safe rungs (read-only) | Gated rungs 🔒 |
| :------- | :--------------------- | :------------- |
| OpenAI (sk-/sk-proj-) | /v1/models (validity) | chat completion (billable) 🔒 |
| Anthropic (sk-ant-) | /v1/models (validity) | create message (billable) 🔒 |
Beyond these 51, the generic declarative layer is a runtime extensibility
hook: loadSpecs([...]) (or registerSpec) plugs a new provider in as data —
header + read-only endpoint per rung — with no core changes. A billable rung
declared SAFE is rejected; when a spec has no automated rung it prints the
exact safe curl for the operator. It is no longer a catch-all for the named
providers above (every one now has its own ladder); BUILTIN_SPECS ships empty.
Impact tiers
| Verdict | Meaning | | :------ | :------ | | PROVEN | A gated probe ran (with consent) and demonstrated real impact. Report this. | | VALID | Authenticates and safe probes proved access depth, but nothing impactful was exercised. Usually informative. | | DENIED | Live but every probed capability was refused. | | N/A | Could not verify, or no ladder for this provider. |
Exit codes
| Code | Meaning |
| :--- | :------ |
| 0 | Success |
| 1 | Runtime error |
| 2 | Usage error |
| 3 | TruffleHog binary not found |
| 4 | Authorized scope required (laddering without --i-am-authorized) |
| 5 | A gated probe was blocked (missing --prove / scope) |
Development
pnpm install
pnpm typecheck # tsc --noEmit (strict)
pnpm test # vitest run (all HTTP / spawn mocked — never hits real APIs)
pnpm build # tsup -> dist/ (ESM + .d.ts + sourcemaps)
pnpm dev -- --help