compromising-position 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/bin/compromising-position +29 -0
- package/dist/checks/hibp-email.d.ts +7 -0
- package/dist/checks/hibp-email.d.ts.map +1 -0
- package/dist/checks/hibp-email.js +99 -0
- package/dist/checks/hibp-email.js.map +1 -0
- package/dist/checks/hibp-password.d.ts +13 -0
- package/dist/checks/hibp-password.d.ts.map +1 -0
- package/dist/checks/hibp-password.js +119 -0
- package/dist/checks/hibp-password.js.map +1 -0
- package/dist/checks/local-check.d.ts +9 -0
- package/dist/checks/local-check.d.ts.map +1 -0
- package/dist/checks/local-check.js +36 -0
- package/dist/checks/local-check.js.map +1 -0
- package/dist/checks/plugin.d.ts +29 -0
- package/dist/checks/plugin.d.ts.map +1 -0
- package/dist/checks/plugin.js +2 -0
- package/dist/checks/plugin.js.map +1 -0
- package/dist/checks/plugins/common-secrets-plugin.d.ts +3 -0
- package/dist/checks/plugins/common-secrets-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/common-secrets-plugin.js +130 -0
- package/dist/checks/plugins/common-secrets-plugin.js.map +1 -0
- package/dist/checks/plugins/dehashed-plugin.d.ts +3 -0
- package/dist/checks/plugins/dehashed-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/dehashed-plugin.js +86 -0
- package/dist/checks/plugins/dehashed-plugin.js.map +1 -0
- package/dist/checks/plugins/emailrep-plugin.d.ts +3 -0
- package/dist/checks/plugins/emailrep-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/emailrep-plugin.js +95 -0
- package/dist/checks/plugins/emailrep-plugin.js.map +1 -0
- package/dist/checks/plugins/gitguardian-hsl-plugin.d.ts +3 -0
- package/dist/checks/plugins/gitguardian-hsl-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/gitguardian-hsl-plugin.js +75 -0
- package/dist/checks/plugins/gitguardian-hsl-plugin.js.map +1 -0
- package/dist/checks/plugins/hibp-email-plugin.d.ts +3 -0
- package/dist/checks/plugins/hibp-email-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/hibp-email-plugin.js +73 -0
- package/dist/checks/plugins/hibp-email-plugin.js.map +1 -0
- package/dist/checks/plugins/hibp-password-plugin.d.ts +3 -0
- package/dist/checks/plugins/hibp-password-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/hibp-password-plugin.js +39 -0
- package/dist/checks/plugins/hibp-password-plugin.js.map +1 -0
- package/dist/checks/plugins/intelx-plugin.d.ts +3 -0
- package/dist/checks/plugins/intelx-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/intelx-plugin.js +113 -0
- package/dist/checks/plugins/intelx-plugin.js.map +1 -0
- package/dist/checks/plugins/leakcheck-plugin.d.ts +3 -0
- package/dist/checks/plugins/leakcheck-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/leakcheck-plugin.js +82 -0
- package/dist/checks/plugins/leakcheck-plugin.js.map +1 -0
- package/dist/checks/plugins/local-analysis-plugin.d.ts +3 -0
- package/dist/checks/plugins/local-analysis-plugin.d.ts.map +1 -0
- package/dist/checks/plugins/local-analysis-plugin.js +36 -0
- package/dist/checks/plugins/local-analysis-plugin.js.map +1 -0
- package/dist/checks/registry.d.ts +24 -0
- package/dist/checks/registry.d.ts.map +1 -0
- package/dist/checks/registry.js +53 -0
- package/dist/checks/registry.js.map +1 -0
- package/dist/config/config.d.ts +10 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +56 -0
- package/dist/config/config.js.map +1 -0
- package/dist/core/entropy.d.ts +23 -0
- package/dist/core/entropy.d.ts.map +1 -0
- package/dist/core/entropy.js +180 -0
- package/dist/core/entropy.js.map +1 -0
- package/dist/core/fingerprint.d.ts +7 -0
- package/dist/core/fingerprint.d.ts.map +1 -0
- package/dist/core/fingerprint.js +10 -0
- package/dist/core/fingerprint.js.map +1 -0
- package/dist/core/key-identifier.d.ts +9 -0
- package/dist/core/key-identifier.d.ts.map +1 -0
- package/dist/core/key-identifier.js +310 -0
- package/dist/core/key-identifier.js.map +1 -0
- package/dist/core/sanitize.d.ts +7 -0
- package/dist/core/sanitize.d.ts.map +1 -0
- package/dist/core/sanitize.js +15 -0
- package/dist/core/sanitize.js.map +1 -0
- package/dist/core/secure-buffer.d.ts +61 -0
- package/dist/core/secure-buffer.d.ts.map +1 -0
- package/dist/core/secure-buffer.js +122 -0
- package/dist/core/secure-buffer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +472 -0
- package/dist/index.js.map +1 -0
- package/dist/input/batch-parser.d.ts +21 -0
- package/dist/input/batch-parser.d.ts.map +1 -0
- package/dist/input/batch-parser.js +65 -0
- package/dist/input/batch-parser.js.map +1 -0
- package/dist/input/secure-prompt.d.ts +11 -0
- package/dist/input/secure-prompt.d.ts.map +1 -0
- package/dist/input/secure-prompt.js +105 -0
- package/dist/input/secure-prompt.js.map +1 -0
- package/dist/output/audit-log.d.ts +11 -0
- package/dist/output/audit-log.d.ts.map +1 -0
- package/dist/output/audit-log.js +50 -0
- package/dist/output/audit-log.js.map +1 -0
- package/dist/output/csv.d.ts +6 -0
- package/dist/output/csv.d.ts.map +1 -0
- package/dist/output/csv.js +28 -0
- package/dist/output/csv.js.map +1 -0
- package/dist/output/formatter.d.ts +12 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +154 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/output/sarif.d.ts +6 -0
- package/dist/output/sarif.d.ts.map +1 -0
- package/dist/output/sarif.js +52 -0
- package/dist/output/sarif.js.map +1 -0
- package/dist/types/index.d.ts +141 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +45 -0
- package/dist/types/index.js.map +1 -0
- package/dist/verification/anthropic-verifier.d.ts +3 -0
- package/dist/verification/anthropic-verifier.d.ts.map +1 -0
- package/dist/verification/anthropic-verifier.js +56 -0
- package/dist/verification/anthropic-verifier.js.map +1 -0
- package/dist/verification/aws-verifier.d.ts +14 -0
- package/dist/verification/aws-verifier.d.ts.map +1 -0
- package/dist/verification/aws-verifier.js +30 -0
- package/dist/verification/aws-verifier.js.map +1 -0
- package/dist/verification/github-verifier.d.ts +4 -0
- package/dist/verification/github-verifier.d.ts.map +1 -0
- package/dist/verification/github-verifier.js +62 -0
- package/dist/verification/github-verifier.js.map +1 -0
- package/dist/verification/openai-verifier.d.ts +4 -0
- package/dist/verification/openai-verifier.d.ts.map +1 -0
- package/dist/verification/openai-verifier.js +59 -0
- package/dist/verification/openai-verifier.js.map +1 -0
- package/dist/verification/slack-verifier.d.ts +4 -0
- package/dist/verification/slack-verifier.d.ts.map +1 -0
- package/dist/verification/slack-verifier.js +67 -0
- package/dist/verification/slack-verifier.js.map +1 -0
- package/dist/verification/verifier-registry.d.ts +13 -0
- package/dist/verification/verifier-registry.d.ts.map +1 -0
- package/dist/verification/verifier-registry.js +19 -0
- package/dist/verification/verifier-registry.js.map +1 -0
- package/dist/verification/verifier.d.ts +24 -0
- package/dist/verification/verifier.d.ts.map +1 -0
- package/dist/verification/verifier.js +2 -0
- package/dist/verification/verifier.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tommy Yau
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# compromising-position
|
|
2
|
+
|
|
3
|
+
**Were your API keys compromised? Find out.**
|
|
4
|
+
|
|
5
|
+
A privacy-preserving credential exposure checker that identifies what kind of key you have, checks if it appears in breach databases, and optionally verifies if it's still active — all from a single CLI command.
|
|
6
|
+
|
|
7
|
+
Built in response to the [OpenClaw security crisis](https://www.theregister.com/2026/02/05/openclaw_skills_marketplace_leaky_security) where tens of thousands of API keys were exposed through leaky skills, malicious plugins, and publicly accessible instances. Security advisories tell you to rotate your keys — but **compromising-position** tells you what happened *before* you rotated.
|
|
8
|
+
|
|
9
|
+
> **Disclaimer:** I am not a security expert. This tool was vibe-coded in the age of personal software — built because I needed it and thought others might too. It aggregates publicly available breach-checking APIs and applies well-documented techniques (k-anonymity, SHA-256 hashing, Shannon entropy). It is not a substitute for professional security auditing, penetration testing, or incident response. Use it as one signal among many, not as your only line of defence. If you find a vulnerability, please [open an issue](https://github.com/tommyyau/compromising-position/issues).
|
|
10
|
+
|
|
11
|
+
## The Problem
|
|
12
|
+
|
|
13
|
+
You connected your API keys to a service. That service got breached, misconfigured, or had a supply chain attack. You rotated your keys. But:
|
|
14
|
+
|
|
15
|
+
- Were your old keys sold on dark web marketplaces?
|
|
16
|
+
- Did someone use your Anthropic key to run up charges?
|
|
17
|
+
- Was your Slack token used to read company messages?
|
|
18
|
+
- Is your old key *still active* because you forgot to revoke it?
|
|
19
|
+
|
|
20
|
+
**No existing open-source tool answers all of these questions.**
|
|
21
|
+
|
|
22
|
+
## What It Does
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
echo "sk-proj-abc123..." | npx compromising-position check
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
--- Credential Exposure Report ---
|
|
30
|
+
|
|
31
|
+
Risk Level: [CRITICAL]
|
|
32
|
+
Summary: Identified as OpenAI — EXPOSED in 3 breach(es) — KEY IS CURRENTLY ACTIVE
|
|
33
|
+
|
|
34
|
+
Local Analysis
|
|
35
|
+
Provider: OpenAI (high confidence)
|
|
36
|
+
Entropy: 4.82 bits/char (0.81 normalized)
|
|
37
|
+
|
|
38
|
+
HIBP Password Check (k-Anonymity)
|
|
39
|
+
FOUND in breach data — 3 occurrence(s)
|
|
40
|
+
|
|
41
|
+
Plugin Results
|
|
42
|
+
Common/Weak Secret Detection: Not found in common secrets blocklist
|
|
43
|
+
GitGuardian HasMySecretLeaked: Found in 2 public GitHub repo(s)
|
|
44
|
+
|
|
45
|
+
Active Key Verification
|
|
46
|
+
KEY IS ACTIVE — rotate immediately!
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### The Pipeline
|
|
50
|
+
|
|
51
|
+
1. **Identify** — Recognizes 39 API key formats (OpenAI, Anthropic, AWS, GitHub, Stripe, Slack, and 33 more)
|
|
52
|
+
2. **Analyze** — Shannon entropy, encoding detection, weak/common secret blocklist (NIST SP 800-63B compliant)
|
|
53
|
+
3. **Check breach databases** — HIBP (k-anonymity), GitGuardian, EmailRep.io, DeHashed, LeakCheck, Intelligence X
|
|
54
|
+
4. **Verify liveness** — Optionally calls the provider API to check if the key still works (read-only, with explicit consent)
|
|
55
|
+
5. **Risk score** — Single assessment from `info` to `critical`
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Install
|
|
61
|
+
npm install -g compromising-position
|
|
62
|
+
|
|
63
|
+
# Check a single key (interactive, hidden input)
|
|
64
|
+
compromising-position check
|
|
65
|
+
|
|
66
|
+
# Pipe a key in
|
|
67
|
+
echo "ghp_abc123..." | compromising-position check
|
|
68
|
+
|
|
69
|
+
# Check without network calls
|
|
70
|
+
echo "sk_live_abc123" | compromising-position check --offline
|
|
71
|
+
|
|
72
|
+
# Check + verify if key is still active (asks for consent)
|
|
73
|
+
echo "sk-proj-abc123" | compromising-position check --verify
|
|
74
|
+
|
|
75
|
+
# Check an email for breaches (requires HIBP API key)
|
|
76
|
+
compromising-position check-email user@example.com
|
|
77
|
+
|
|
78
|
+
# Batch check an entire .env file
|
|
79
|
+
compromising-position check-batch .env.old
|
|
80
|
+
|
|
81
|
+
# Output as SARIF for GitHub Advanced Security
|
|
82
|
+
compromising-position check-batch secrets.json --format sarif
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Privacy Model
|
|
86
|
+
|
|
87
|
+
Your secrets never leave your machine unless you explicitly opt in:
|
|
88
|
+
|
|
89
|
+
| Check | What's Sent | Where |
|
|
90
|
+
|-------|-------------|-------|
|
|
91
|
+
| Local analysis | Nothing | Local only |
|
|
92
|
+
| Common secrets blocklist | Nothing | Local only |
|
|
93
|
+
| HIBP password | 5-char SHA-1 prefix | api.pwnedpasswords.com |
|
|
94
|
+
| GitGuardian | SHA-256 hash | api.gitguardian.com |
|
|
95
|
+
| EmailRep.io | Full email | emailrep.io |
|
|
96
|
+
| Active verification | Full key | Provider API (opt-in) |
|
|
97
|
+
|
|
98
|
+
Run `compromising-position check --privacy` to see the full data flow summary.
|
|
99
|
+
|
|
100
|
+
## Supported Key Formats (39)
|
|
101
|
+
|
|
102
|
+
| Provider | Prefix | Confidence |
|
|
103
|
+
|----------|--------|------------|
|
|
104
|
+
| OpenAI | `sk-proj-`, `sk-svcacct-`, `sk-` | high |
|
|
105
|
+
| Anthropic | `sk-ant-api03-` | high |
|
|
106
|
+
| AWS | `AKIA` | high |
|
|
107
|
+
| GitHub | `ghp_`, `github_pat_` | high |
|
|
108
|
+
| Stripe | `sk_live_`, `sk_test_` | high |
|
|
109
|
+
| Google | `AIza` | high |
|
|
110
|
+
| Slack | `xoxb-`, `xoxp-` | high |
|
|
111
|
+
| GitLab | `glpat-`, `glptt-` | high |
|
|
112
|
+
| npm | `npm_` | high |
|
|
113
|
+
| PyPI | `pypi-AgEIcHlwaS5vcmc` | high |
|
|
114
|
+
| Shopify | `shppa_`, `shpat_` | high |
|
|
115
|
+
| DigitalOcean | `dop_v1_`, `doo_v1_` | high |
|
|
116
|
+
| Supabase | `sbp_` | high |
|
|
117
|
+
| HashiCorp Vault | `hvs.` | high |
|
|
118
|
+
| Terraform Cloud | `atlasv1-` | high |
|
|
119
|
+
| PlanetScale | `pscale_tkn_` | high |
|
|
120
|
+
| Postman | `PMAK-` | high |
|
|
121
|
+
| Grafana | `glsa_` | high |
|
|
122
|
+
| Linear | `lin_api_` | high |
|
|
123
|
+
| Netlify | `nfp_` | high |
|
|
124
|
+
| Doppler | `dp.st.`, `dp.sa.` | high |
|
|
125
|
+
| Buildkite | `bkua_` | high |
|
|
126
|
+
| Atlassian | `ATATT3xFfGF0` | high |
|
|
127
|
+
| Figma | `figd_` | high |
|
|
128
|
+
| SendGrid | `SG.` | high |
|
|
129
|
+
| Twilio | `SK` | high |
|
|
130
|
+
| Mailgun | `key-` | high |
|
|
131
|
+
| Discord | token format | medium |
|
|
132
|
+
| Telegram | bot token format | high |
|
|
133
|
+
| CircleCI | `CIRCLE` | medium |
|
|
134
|
+
| Notion | `secret_` | medium |
|
|
135
|
+
|
|
136
|
+
## Data Sources
|
|
137
|
+
|
|
138
|
+
### Free (no API key needed)
|
|
139
|
+
- **HIBP Passwords** — k-anonymity, checks 600M+ breached passwords
|
|
140
|
+
- **Common secrets blocklist** — Top passwords, default credentials, placeholders (fully local)
|
|
141
|
+
- **EmailRep.io** — Email reputation, dark web presence (100 lookups/day free)
|
|
142
|
+
|
|
143
|
+
### Requires API Key
|
|
144
|
+
- **HIBP Email** — Breaches, stealer logs, pastes ($3.50/mo)
|
|
145
|
+
- **GitGuardian** — Public GitHub repo secret scanning (free tier available)
|
|
146
|
+
- **DeHashed** — 15B+ records, deep/dark web
|
|
147
|
+
- **LeakCheck** — 28B+ records
|
|
148
|
+
- **Intelligence X** — Tor, I2P, paste sites
|
|
149
|
+
|
|
150
|
+
Configure keys via environment variables or `.env` file:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
export HIBP_API_KEY=your-key
|
|
154
|
+
export GITGUARDIAN_API_TOKEN=your-token
|
|
155
|
+
export EMAILREP_API_KEY=your-key # optional, higher rate limits
|
|
156
|
+
export DEHASHED_EMAIL=your@email.com
|
|
157
|
+
export DEHASHED_API_KEY=your-key
|
|
158
|
+
export LEAKCHECK_API_KEY=your-key
|
|
159
|
+
export INTELX_API_KEY=your-key
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Active Key Verification
|
|
163
|
+
|
|
164
|
+
The `--verify` flag checks if a key is still active by calling the provider's API:
|
|
165
|
+
|
|
166
|
+
| Provider | Endpoint | Method |
|
|
167
|
+
|----------|----------|--------|
|
|
168
|
+
| OpenAI | `/v1/models` | GET |
|
|
169
|
+
| Anthropic | `/v1/models` | GET |
|
|
170
|
+
| GitHub | `/user` | GET |
|
|
171
|
+
| Slack | `auth.test` | POST |
|
|
172
|
+
| AWS | Requires secret key — reports format only | — |
|
|
173
|
+
|
|
174
|
+
All verification is:
|
|
175
|
+
- **Opt-in only** — requires `--verify` flag
|
|
176
|
+
- **Consent-gated** — interactive prompt before each call
|
|
177
|
+
- **Read-only** — never makes write operations
|
|
178
|
+
|
|
179
|
+
## CI/CD Integration
|
|
180
|
+
|
|
181
|
+
### GitHub Actions
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
- name: Check rotated secrets
|
|
185
|
+
run: |
|
|
186
|
+
echo "${{ secrets.OLD_API_KEY }}" | npx compromising-position check --json
|
|
187
|
+
# Exit code 1 = exposed, 0 = clean
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Batch + SARIF
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
- name: Scan secrets file
|
|
194
|
+
run: npx compromising-position check-batch secrets.json --format sarif > results.sarif
|
|
195
|
+
|
|
196
|
+
- name: Upload SARIF
|
|
197
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
198
|
+
with:
|
|
199
|
+
sarif_file: results.sarif
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Pre-commit Hook
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# .pre-commit-config.yaml
|
|
206
|
+
- repo: local
|
|
207
|
+
hooks:
|
|
208
|
+
- id: check-secrets
|
|
209
|
+
name: Check for weak secrets
|
|
210
|
+
entry: bash -c 'echo "$1" | npx compromising-position check --offline'
|
|
211
|
+
language: system
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Output Formats
|
|
215
|
+
|
|
216
|
+
| Flag | Format | Use Case |
|
|
217
|
+
|------|--------|----------|
|
|
218
|
+
| (default) | Human-readable | Terminal |
|
|
219
|
+
| `--json` | JSON | Scripting, pipelines |
|
|
220
|
+
| `--format sarif` | SARIF 2.1.0 | GitHub Advanced Security |
|
|
221
|
+
| `--format csv` | CSV | Spreadsheets, reporting |
|
|
222
|
+
|
|
223
|
+
## Security Design
|
|
224
|
+
|
|
225
|
+
- **SecureBuffer** — All secrets held in zeroable Buffer wrappers, never plain strings
|
|
226
|
+
- **Constant-time comparison** — HIBP suffix matching uses `timingSafeEqual`
|
|
227
|
+
- **Memory zeroing** — Buffers zeroed on disposal, secure heap allocation via `--secure-heap`
|
|
228
|
+
- **No secret logging** — Audit logs contain only truncated SHA-256 fingerprints
|
|
229
|
+
- **Terminal injection protection** — All external data sanitized before terminal output
|
|
230
|
+
- **Explicit Resource Management** — Supports TC39 `using` keyword for automatic cleanup
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
git clone https://github.com/tommyyau/compromising-position.git
|
|
236
|
+
cd compromising-position
|
|
237
|
+
npm install
|
|
238
|
+
npm run build
|
|
239
|
+
npm test # 196 tests across 23 test files
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## A Note on How This Was Built
|
|
243
|
+
|
|
244
|
+
This project was vibe-coded — built with AI assistance (Claude) in a single session, from idea to published repo. The architecture, code, and tests were generated collaboratively. I'm not a security researcher; I'm a developer who connected API keys to OpenClaw, watched the breach news unfold, and wanted a tool to answer the question: *"was I actually compromised?"*
|
|
245
|
+
|
|
246
|
+
If you're a security professional and spot something wrong, PRs and issues are very welcome.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Launch with secure heap allocation to reduce risk of secrets
|
|
4
|
+
// lingering in process memory after buffer zeroing.
|
|
5
|
+
//
|
|
6
|
+
// If --secure-heap is not supported (older Node), fall back gracefully.
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const entry = resolve(__dirname, "..", "dist", "index.js");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
execFileSync(
|
|
17
|
+
process.execPath,
|
|
18
|
+
["--secure-heap=16384", "--secure-heap-min=8", entry, ...process.argv.slice(2)],
|
|
19
|
+
{ stdio: "inherit" },
|
|
20
|
+
);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
// If secure heap not available, run without it
|
|
23
|
+
if (err && typeof err === "object" && "status" in err) {
|
|
24
|
+
process.exit(err.status as number);
|
|
25
|
+
}
|
|
26
|
+
execFileSync(process.execPath, [entry, ...process.argv.slice(2)], {
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HibpEmailResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Check an email against HIBP breached accounts, stealer logs, and paste endpoints.
|
|
4
|
+
* Requires a paid HIBP API key ($3.50/mo).
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkHibpEmail(email: string, apiKey: string): Promise<HibpEmailResult>;
|
|
7
|
+
//# sourceMappingURL=hibp-email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hibp-email.d.ts","sourceRoot":"","sources":["../../src/checks/hibp-email.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,eAAe,EAGhB,MAAM,mBAAmB,CAAC;AA0G3B;;;GAGG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CA0B1B"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { sanitizeForTerminal } from "../core/sanitize.js";
|
|
2
|
+
const HIBP_BASE = "https://haveibeenpwned.com/api/v3";
|
|
3
|
+
const USER_AGENT = "compromising-position/1.0.0";
|
|
4
|
+
const MAX_RETRIES = 1;
|
|
5
|
+
/** Delay for rate limiting. */
|
|
6
|
+
function delay(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fetch wrapper that:
|
|
11
|
+
* - Never exposes the API key in error messages
|
|
12
|
+
* - Handles 429 rate limiting with Retry-After header
|
|
13
|
+
*/
|
|
14
|
+
async function hibpFetch(url, apiKey, retries = 0) {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetch(url, {
|
|
18
|
+
headers: {
|
|
19
|
+
"hibp-api-key": apiKey,
|
|
20
|
+
"User-Agent": USER_AGENT,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
// Wrap network errors to prevent any header/key leakage
|
|
26
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
27
|
+
throw new Error(`HIBP request failed: ${sanitizeForTerminal(msg)}`);
|
|
28
|
+
}
|
|
29
|
+
// Handle rate limiting with exponential backoff
|
|
30
|
+
if (response.status === 429 && retries < MAX_RETRIES) {
|
|
31
|
+
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "2", 10);
|
|
32
|
+
const waitMs = (Number.isNaN(retryAfter) ? 2 : retryAfter) * 1000;
|
|
33
|
+
await delay(waitMs);
|
|
34
|
+
return hibpFetch(url, apiKey, retries + 1);
|
|
35
|
+
}
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
async function fetchBreaches(email, apiKey) {
|
|
39
|
+
const encoded = encodeURIComponent(email);
|
|
40
|
+
const response = await hibpFetch(`${HIBP_BASE}/breachedaccount/${encoded}?truncateResponse=false`, apiKey);
|
|
41
|
+
if (response.status === 404)
|
|
42
|
+
return [];
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Breaches API returned ${response.status}: ${sanitizeForTerminal(response.statusText)}`);
|
|
45
|
+
}
|
|
46
|
+
return (await response.json());
|
|
47
|
+
}
|
|
48
|
+
async function fetchStealerLogs(email, apiKey) {
|
|
49
|
+
const encoded = encodeURIComponent(email);
|
|
50
|
+
const response = await hibpFetch(`${HIBP_BASE}/stealerlogsbyemail/${encoded}`, apiKey);
|
|
51
|
+
if (response.status === 404)
|
|
52
|
+
return [];
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`Stealer logs API returned ${response.status}: ${sanitizeForTerminal(response.statusText)}`);
|
|
55
|
+
}
|
|
56
|
+
return (await response.json());
|
|
57
|
+
}
|
|
58
|
+
async function fetchPastes(email, apiKey) {
|
|
59
|
+
const encoded = encodeURIComponent(email);
|
|
60
|
+
const response = await hibpFetch(`${HIBP_BASE}/pasteaccount/${encoded}`, apiKey);
|
|
61
|
+
if (response.status === 404)
|
|
62
|
+
return [];
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`Pastes API returned ${response.status}: ${sanitizeForTerminal(response.statusText)}`);
|
|
65
|
+
}
|
|
66
|
+
return (await response.json());
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check an email against HIBP breached accounts, stealer logs, and paste endpoints.
|
|
70
|
+
* Requires a paid HIBP API key ($3.50/mo).
|
|
71
|
+
*/
|
|
72
|
+
export async function checkHibpEmail(email, apiKey) {
|
|
73
|
+
try {
|
|
74
|
+
// Fetch sequentially to respect rate limits
|
|
75
|
+
const breaches = await fetchBreaches(email, apiKey);
|
|
76
|
+
await delay(1600);
|
|
77
|
+
const stealerLogs = await fetchStealerLogs(email, apiKey);
|
|
78
|
+
await delay(1600);
|
|
79
|
+
const pastes = await fetchPastes(email, apiKey);
|
|
80
|
+
return {
|
|
81
|
+
checked: true,
|
|
82
|
+
breaches,
|
|
83
|
+
stealerLogs,
|
|
84
|
+
pastes,
|
|
85
|
+
error: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
return {
|
|
91
|
+
checked: true,
|
|
92
|
+
breaches: [],
|
|
93
|
+
stealerLogs: [],
|
|
94
|
+
pastes: [],
|
|
95
|
+
error: sanitizeForTerminal(message),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=hibp-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hibp-email.js","sourceRoot":"","sources":["../../src/checks/hibp-email.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SecureBuffer } from "../core/secure-buffer.js";
|
|
2
|
+
import type { HibpPasswordResult } from "../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check a secret against the HIBP Pwned Passwords API using k-anonymity.
|
|
5
|
+
*
|
|
6
|
+
* Only the first 5 characters of the SHA-1 hash are sent to the server.
|
|
7
|
+
* The remaining 35 characters are compared locally using constant-time comparison.
|
|
8
|
+
*
|
|
9
|
+
* Uses Buffer-based SHA-1 computation to avoid leaving the full hash
|
|
10
|
+
* as an immutable string on the V8 heap.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkHibpPassword(secret: SecureBuffer): Promise<HibpPasswordResult>;
|
|
13
|
+
//# sourceMappingURL=hibp-password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hibp-password.d.ts","sourceRoot":"","sources":["../../src/checks/hibp-password.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAc5D;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,kBAAkB,CAAC,CA0D7B"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { sanitizeForTerminal } from "../core/sanitize.js";
|
|
3
|
+
const HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";
|
|
4
|
+
const USER_AGENT = "compromising-position/1.0.0";
|
|
5
|
+
/**
|
|
6
|
+
* Number of hex chars for the prefix (5 = 2.5 bytes of SHA-1).
|
|
7
|
+
* This is the only portion transmitted to the HIBP server.
|
|
8
|
+
*/
|
|
9
|
+
const PREFIX_HEX_LENGTH = 5;
|
|
10
|
+
/** Number of hex chars for the suffix (35 = remaining SHA-1). */
|
|
11
|
+
const SUFFIX_HEX_LENGTH = 35;
|
|
12
|
+
/**
|
|
13
|
+
* Check a secret against the HIBP Pwned Passwords API using k-anonymity.
|
|
14
|
+
*
|
|
15
|
+
* Only the first 5 characters of the SHA-1 hash are sent to the server.
|
|
16
|
+
* The remaining 35 characters are compared locally using constant-time comparison.
|
|
17
|
+
*
|
|
18
|
+
* Uses Buffer-based SHA-1 computation to avoid leaving the full hash
|
|
19
|
+
* as an immutable string on the V8 heap.
|
|
20
|
+
*/
|
|
21
|
+
export async function checkHibpPassword(secret) {
|
|
22
|
+
// Compute SHA-1 as raw bytes (20 bytes), not a hex string.
|
|
23
|
+
// This avoids leaving the full 40-char hex hash as an un-zeroable string.
|
|
24
|
+
const sha1Buf = secret.sha1Buffer();
|
|
25
|
+
// Extract prefix as hex for the API call (first 2.5 bytes = 5 hex chars).
|
|
26
|
+
// We need to read 3 bytes and take the first 5 hex chars.
|
|
27
|
+
const prefixBytes = sha1Buf.subarray(0, 3);
|
|
28
|
+
const prefix = prefixBytes.toString("hex").toUpperCase().slice(0, PREFIX_HEX_LENGTH);
|
|
29
|
+
// Build the suffix as a fixed-size uppercase hex Buffer for constant-time comparison.
|
|
30
|
+
const suffixHex = sha1Buf.subarray(2).toString("hex").toUpperCase().slice(1); // skip first nibble (already in prefix)
|
|
31
|
+
const suffixBuffer = Buffer.from(suffixHex, "utf-8");
|
|
32
|
+
// Zero the raw SHA-1 buffer now that we've extracted what we need
|
|
33
|
+
sha1Buf.fill(0);
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${HIBP_RANGE_URL}${prefix}`, {
|
|
36
|
+
headers: {
|
|
37
|
+
"User-Agent": USER_AGENT,
|
|
38
|
+
"Add-Padding": "true",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
return {
|
|
43
|
+
checked: true,
|
|
44
|
+
found: false,
|
|
45
|
+
occurrences: 0,
|
|
46
|
+
hashPrefix: prefix,
|
|
47
|
+
error: `HIBP API returned ${response.status}: ${sanitizeForTerminal(response.statusText)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const body = await response.text();
|
|
51
|
+
const match = findMatchConstantTime(suffixBuffer, body);
|
|
52
|
+
return {
|
|
53
|
+
checked: true,
|
|
54
|
+
found: match.found,
|
|
55
|
+
occurrences: match.occurrences,
|
|
56
|
+
hashPrefix: prefix,
|
|
57
|
+
error: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return {
|
|
63
|
+
checked: true,
|
|
64
|
+
found: false,
|
|
65
|
+
occurrences: 0,
|
|
66
|
+
hashPrefix: prefix,
|
|
67
|
+
error: `Network error: ${sanitizeForTerminal(message)}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
// Zero the suffix buffer
|
|
72
|
+
suffixBuffer.fill(0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Compare our suffix against all returned suffixes using constant-time comparison.
|
|
77
|
+
*
|
|
78
|
+
* Every entry is compared with timingSafeEqual regardless of length match.
|
|
79
|
+
* Entries that don't match the expected length are padded to avoid skipping
|
|
80
|
+
* the comparison (which would create a timing side-channel).
|
|
81
|
+
*
|
|
82
|
+
* The loop never breaks early. Result variables are updated using the same
|
|
83
|
+
* code path on every iteration to minimize data-dependent timing variance.
|
|
84
|
+
*/
|
|
85
|
+
function findMatchConstantTime(targetSuffix, responseBody) {
|
|
86
|
+
const targetLen = targetSuffix.length;
|
|
87
|
+
let found = false;
|
|
88
|
+
let occurrences = 0;
|
|
89
|
+
const lines = responseBody.split("\n");
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (trimmed.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
const colonIndex = trimmed.indexOf(":");
|
|
95
|
+
if (colonIndex === -1)
|
|
96
|
+
continue;
|
|
97
|
+
const entrySuffix = trimmed.slice(0, colonIndex);
|
|
98
|
+
const countStr = trimmed.slice(colonIndex + 1);
|
|
99
|
+
const count = parseInt(countStr, 10);
|
|
100
|
+
// Skip entries with invalid count (NaN, negative)
|
|
101
|
+
if (Number.isNaN(count) || count < 0)
|
|
102
|
+
continue;
|
|
103
|
+
// Pad or truncate entry to target length so we always call timingSafeEqual.
|
|
104
|
+
// This avoids a length-dependent branch that skips the comparison.
|
|
105
|
+
const entryBuf = Buffer.alloc(targetLen);
|
|
106
|
+
const entryRaw = Buffer.from(entrySuffix.toUpperCase(), "utf-8");
|
|
107
|
+
entryRaw.copy(entryBuf, 0, 0, Math.min(entryRaw.length, targetLen));
|
|
108
|
+
const lengthMatch = entryRaw.length === targetLen;
|
|
109
|
+
const bytesMatch = timingSafeEqual(entryBuf, targetSuffix);
|
|
110
|
+
// Both length and content must match. Use branchless-style update:
|
|
111
|
+
// always evaluate both conditions, never break early.
|
|
112
|
+
if (lengthMatch && bytesMatch) {
|
|
113
|
+
found = true;
|
|
114
|
+
occurrences = count;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { found, occurrences };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=hibp-password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hibp-password.js","sourceRoot":"","sources":["../../src/checks/hibp-password.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SecureBuffer } from "../core/secure-buffer.js";
|
|
2
|
+
import { type LocalCheckResult } from "../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Perform local-only analysis: provider identification, entropy, format checks.
|
|
5
|
+
* Uses Buffer-based operations where possible to minimize the lifetime
|
|
6
|
+
* of secret data as immutable JS strings (which cannot be zeroed).
|
|
7
|
+
*/
|
|
8
|
+
export declare function performLocalCheck(secret: SecureBuffer): LocalCheckResult;
|
|
9
|
+
//# sourceMappingURL=local-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-check.d.ts","sourceRoot":"","sources":["../../src/checks/local-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,gBAAgB,CAqCxE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { identifyKey } from "../core/key-identifier.js";
|
|
2
|
+
import { analyzeEntropyFromBuffer } from "../core/entropy.js";
|
|
3
|
+
import { KeyProvider } from "../types/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Perform local-only analysis: provider identification, entropy, format checks.
|
|
6
|
+
* Uses Buffer-based operations where possible to minimize the lifetime
|
|
7
|
+
* of secret data as immutable JS strings (which cannot be zeroed).
|
|
8
|
+
*/
|
|
9
|
+
export function performLocalCheck(secret) {
|
|
10
|
+
const identification = identifyKey(secret);
|
|
11
|
+
const entropy = analyzeEntropyFromBuffer(secret);
|
|
12
|
+
const warnings = [];
|
|
13
|
+
// Collect warnings
|
|
14
|
+
if (entropy.warning) {
|
|
15
|
+
warnings.push(entropy.warning);
|
|
16
|
+
}
|
|
17
|
+
if (entropy.length < 8) {
|
|
18
|
+
warnings.push("Input is very short — unlikely to be a real API key");
|
|
19
|
+
}
|
|
20
|
+
if (identification.provider === KeyProvider.StripeTest) {
|
|
21
|
+
warnings.push("This is a Stripe TEST key — not a production secret, but still should not be shared");
|
|
22
|
+
}
|
|
23
|
+
if (identification.provider === KeyProvider.Unknown && entropy.shannonEntropy < 3.0) {
|
|
24
|
+
warnings.push("Unrecognized format with low entropy — may be a password or placeholder");
|
|
25
|
+
}
|
|
26
|
+
// Heuristic: does this look like a real secret?
|
|
27
|
+
const looksLikeSecret = identification.provider !== KeyProvider.Unknown ||
|
|
28
|
+
(entropy.shannonEntropy >= 3.5 && entropy.length >= 16);
|
|
29
|
+
return {
|
|
30
|
+
identification,
|
|
31
|
+
entropy,
|
|
32
|
+
warnings,
|
|
33
|
+
looksLikeSecret,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=local-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-check.js","sourceRoot":"","sources":["../../src/checks/local-check.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,mBAAmB,CAAC;AAEvE;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAoB;IACpD,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,mBAAmB;IACnB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,cAAc,CAAC,QAAQ,KAAK,WAAW,CAAC,UAAU,EAAE,CAAC;QACvD,QAAQ,CAAC,IAAI,CACX,qFAAqF,CACtF,CAAC;IACJ,CAAC;IAED,IAAI,cAAc,CAAC,QAAQ,KAAK,WAAW,CAAC,OAAO,IAAI,OAAO,CAAC,cAAc,GAAG,GAAG,EAAE,CAAC;QACpF,QAAQ,CAAC,IAAI,CACX,yEAAyE,CAC1E,CAAC;IACJ,CAAC;IAED,gDAAgD;IAChD,MAAM,eAAe,GACnB,cAAc,CAAC,QAAQ,KAAK,WAAW,CAAC,OAAO;QAC/C,CAAC,OAAO,CAAC,cAAc,IAAI,GAAG,IAAI,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAE1D,OAAO;QACL,cAAc;QACd,OAAO;QACP,QAAQ;QACR,eAAe;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SecureBuffer } from "../core/secure-buffer.js";
|
|
2
|
+
import type { AppConfig, PluginCheckResult, PluginInputKind } from "../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interface for check plugins. Each plugin checks a secret or email
|
|
5
|
+
* against a specific data source and returns a standardized result.
|
|
6
|
+
*/
|
|
7
|
+
export interface CheckPlugin {
|
|
8
|
+
/** Unique identifier for this plugin. */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** Human-readable name for display. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** What kind of input this plugin operates on. */
|
|
13
|
+
readonly inputKind: PluginInputKind;
|
|
14
|
+
/** Whether this plugin makes network requests. */
|
|
15
|
+
readonly requiresNetwork: boolean;
|
|
16
|
+
/** Config keys (env var names) that must be set for this plugin to run. */
|
|
17
|
+
readonly requiredConfigKeys: string[];
|
|
18
|
+
/** Whether this plugin is free (no API key purchase needed). */
|
|
19
|
+
readonly isFree: boolean;
|
|
20
|
+
/** Short description of what data is sent and where, for --privacy output. */
|
|
21
|
+
readonly privacySummary: string;
|
|
22
|
+
/**
|
|
23
|
+
* Run the check.
|
|
24
|
+
* @param input - The secret (SecureBuffer) or email (string), depending on inputKind.
|
|
25
|
+
* @param config - App configuration including API keys.
|
|
26
|
+
*/
|
|
27
|
+
check(input: SecureBuffer | string, config: AppConfig): Promise<PluginCheckResult>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/checks/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EACjB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IAEpC,kDAAkD;IAClD,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAElC,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAEtC,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEzB,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAEhC;;;;OAIG;IACH,KAAK,CACH,KAAK,EAAE,YAAY,GAAG,MAAM,EAC5B,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/checks/plugin.ts"],"names":[],"mappings":""}
|