agentmask 0.1.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 +165 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-F7TMT2OH.js +96 -0
- package/dist/chunk-F7TMT2OH.js.map +1 -0
- package/dist/chunk-P7BRPZBB.js +211 -0
- package/dist/chunk-P7BRPZBB.js.map +1 -0
- package/dist/chunk-Q7ZBIDBL.js +58 -0
- package/dist/chunk-Q7ZBIDBL.js.map +1 -0
- package/dist/chunk-YASOHGJL.js +44 -0
- package/dist/chunk-YASOHGJL.js.map +1 -0
- package/dist/cli.js +433 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/dist/post-scan-PZBRRZS6.js +50 -0
- package/dist/post-scan-PZBRRZS6.js.map +1 -0
- package/dist/pre-bash-CQ6UYBND.js +75 -0
- package/dist/pre-bash-CQ6UYBND.js.map +1 -0
- package/dist/pre-read-4YE6QMWV.js +49 -0
- package/dist/pre-read-4YE6QMWV.js.map +1 -0
- package/dist/pre-write-EBMADS22.js +53 -0
- package/dist/pre-write-EBMADS22.js.map +1 -0
- package/dist/server-3SUDWIDY.js +13944 -0
- package/dist/server-3SUDWIDY.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agentmask contributors
|
|
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,165 @@
|
|
|
1
|
+
# agentmask
|
|
2
|
+
|
|
3
|
+
Mask your secrets from AI coding agents. One command. Zero friction.
|
|
4
|
+
|
|
5
|
+
agentmask prevents Claude Code (and other AI coding assistants) from reading, leaking, or committing your secrets. It works through three reinforcing layers:
|
|
6
|
+
|
|
7
|
+
1. **Block** — Hooks that prevent secret files from being read and secret values from being written
|
|
8
|
+
2. **Redirect** — An MCP server that provides redacted file access so the agent can still work
|
|
9
|
+
3. **Instruct** — Behavioral rules that teach the agent to prefer safe alternatives
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g agentmask
|
|
15
|
+
cd your-project
|
|
16
|
+
agentmask init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`init` scans your entire repository for secrets, builds a blocklist of every file containing them, and installs hooks + MCP server + behavioral rules. Secrets are blocked before they ever enter the AI's context.
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
| Scenario | What Happens |
|
|
24
|
+
|----------|-------------|
|
|
25
|
+
| Claude tries to `Read .env` | **Blocked.** Static pattern match. Redirected to `safe_read`. |
|
|
26
|
+
| Claude tries to read `src/config.ts` (has hardcoded AWS key) | **Blocked.** Found by init scan, in blocklist. Redirected to `safe_read`. |
|
|
27
|
+
| Claude writes `sk_live_...` into source code | **Blocked.** Content scan catches it. Told to use env var instead. |
|
|
28
|
+
| Claude runs `cat .env` via Bash | **Blocked.** Command pattern match. |
|
|
29
|
+
| Claude runs `git commit` with secrets in staged files | **Blocked.** Pre-commit scan. Shown file:line of each secret. |
|
|
30
|
+
| Claude reads a new file with a secret (not yet in blocklist) | **Warned + auto-blocklisted.** First read leaks, every subsequent read is blocked. |
|
|
31
|
+
| Claude does normal coding (90%+ of operations) | **No effect.** Sub-50ms hook, completely invisible. |
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
agentmask init
|
|
37
|
+
│
|
|
38
|
+
├── Scans entire repo for secrets (Tier 1 rules — zero false positives)
|
|
39
|
+
├── .claude/agentmask-blocklist.json ← files containing detected secrets
|
|
40
|
+
├── .claude/settings.local.json ← PreToolUse + PostToolUse hooks
|
|
41
|
+
├── .claude/rules/agentmask.md ← behavioral rules for Claude
|
|
42
|
+
└── .mcp.json ← MCP server registration
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Layer 1 — Block (Hooks):**
|
|
46
|
+
PreToolUse hooks intercept every Read, Bash, Write, and Edit call. Pre-read checks two things: static file patterns (`.env`, `*.pem`, etc.) and the dynamic blocklist (files where secrets were found by the init scan or by post-scan at runtime). If matched, the operation is blocked and Claude receives guidance to use `safe_read`.
|
|
47
|
+
|
|
48
|
+
**Layer 2 — Redirect (MCP Server):**
|
|
49
|
+
When a read is blocked, Claude is directed to the `safe_read` MCP tool, which returns the file content with secrets replaced:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
DATABASE_URL=postgresql://****:****@db.example.com:5432/myapp
|
|
53
|
+
API_KEY=[REDACTED:28_chars]
|
|
54
|
+
DEBUG=true ← non-secret values kept as-is
|
|
55
|
+
PORT=3000 ← non-secret values kept as-is
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Layer 3 — Instruct (Rules):**
|
|
59
|
+
A `.claude/rules/agentmask.md` file teaches Claude to prefer safe tools and never output raw secret values.
|
|
60
|
+
|
|
61
|
+
## The Blocklist
|
|
62
|
+
|
|
63
|
+
The blocklist (`.claude/agentmask-blocklist.json`) is the key to blocking secrets before they enter context:
|
|
64
|
+
|
|
65
|
+
- **Built at init time** — `agentmask init` scans every file in the repo using Tier 1 rules (provider-specific patterns with zero false positives)
|
|
66
|
+
- **Updated at runtime** — if post-scan detects a secret in a file that wasn't in the blocklist, it's added automatically
|
|
67
|
+
- **Checked on every read** — pre-read hook looks up the file in the blocklist before allowing the read
|
|
68
|
+
- **Re-run `agentmask init`** anytime to rescan (e.g., after pulling new code)
|
|
69
|
+
- **`agentmask allow-path`** removes a file from the blocklist (after you've fixed the secret)
|
|
70
|
+
|
|
71
|
+
## Detection
|
|
72
|
+
|
|
73
|
+
Detection is powered by [gitleaks](https://github.com/gitleaks/gitleaks) — **150+ battle-tested rules** covering AWS, GitHub, Stripe, Google, GCP, Slack, SendGrid, Shopify, OpenAI, Anthropic, GitLab, Twilio, PEM keys, JWTs, database connection strings, and many more providers.
|
|
74
|
+
|
|
75
|
+
agentmask auto-downloads gitleaks if it's not already installed. No detection rules to maintain — when gitleaks updates, agentmask benefits automatically.
|
|
76
|
+
|
|
77
|
+
**Requires:** `gitleaks` (auto-installed) or `brew install gitleaks`
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
agentmask init # Scan repo, build blocklist, install hooks + MCP + rules
|
|
83
|
+
agentmask init --team # Write to shared .claude/settings.json (committed to git)
|
|
84
|
+
agentmask remove # Remove everything cleanly (hooks, rules, MCP, blocklist)
|
|
85
|
+
agentmask scan [path] # Scan files for secrets (report only)
|
|
86
|
+
agentmask scan --staged # Scan git staged files
|
|
87
|
+
agentmask scan --json # JSON output for CI/CD
|
|
88
|
+
agentmask allow-path "p" # Allowlist a path + remove from blocklist
|
|
89
|
+
agentmask allow-value "v" # Allowlist a specific value
|
|
90
|
+
agentmask serve # Start the MCP server (called automatically)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## MCP Tools
|
|
94
|
+
|
|
95
|
+
When the MCP server is running, Claude Code has access to:
|
|
96
|
+
|
|
97
|
+
| Tool | Description |
|
|
98
|
+
|------|-------------|
|
|
99
|
+
| `safe_read` | Read a file with secrets redacted |
|
|
100
|
+
| `env_names` | List .env variable names and types without values |
|
|
101
|
+
| `scan_file` | Scan a file for secrets (explicit security review) |
|
|
102
|
+
| `scan_staged` | Scan git staging area before committing |
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
Create `.agentmask.toml` in your project root:
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
# Add custom blocked file patterns
|
|
110
|
+
[scan]
|
|
111
|
+
blocked_paths = [".env.custom", "**/my-secrets.yml"]
|
|
112
|
+
|
|
113
|
+
# Allowlist paths (e.g., test fixtures with dummy secrets)
|
|
114
|
+
[[allowlists]]
|
|
115
|
+
paths = ["tests/**", "**/*.test.ts", "fixtures/**"]
|
|
116
|
+
description = "Test files may contain dummy secrets"
|
|
117
|
+
|
|
118
|
+
# Allowlist specific values
|
|
119
|
+
[[allowlists]]
|
|
120
|
+
stopwords = ["EXAMPLE_KEY_12345"]
|
|
121
|
+
description = "Known test values"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## False Positives
|
|
125
|
+
|
|
126
|
+
If agentmask blocks something it shouldn't:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Allowlist a path pattern (also removes from blocklist)
|
|
130
|
+
agentmask allow-path "tests/**"
|
|
131
|
+
|
|
132
|
+
# Allowlist a specific value
|
|
133
|
+
agentmask allow-value "EXAMPLE_KEY_12345"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
These write to `.agentmask.toml` in your project root.
|
|
137
|
+
|
|
138
|
+
## What It Cannot Do
|
|
139
|
+
|
|
140
|
+
- **First read of a brand-new secret file** still enters context — post-scan catches it and blocklists it, so subsequent reads are blocked. This is unavoidable without reading every file before Claude does.
|
|
141
|
+
- **Cannot catch every bash command** that accesses secrets (covers `cat .env`, `printenv`, etc. but not `node -e "console.log(process.env.X)"`)
|
|
142
|
+
- **Cannot prevent Claude from saying a secret** in chat output (hooks only cover tool calls)
|
|
143
|
+
- **Cannot validate** whether a detected secret is real or dummy without network calls
|
|
144
|
+
- If agentmask crashes, it **degrades gracefully** — the operation proceeds (never blocks your work due to a bug)
|
|
145
|
+
|
|
146
|
+
## Graceful Degradation
|
|
147
|
+
|
|
148
|
+
agentmask is designed to never block your work due to its own failure:
|
|
149
|
+
|
|
150
|
+
- Hook crashes → exit code 1 → Claude Code treats as non-blocking warning
|
|
151
|
+
- MCP server crashes → Claude falls back to built-in Read/Write
|
|
152
|
+
- Malformed input → allow, don't block
|
|
153
|
+
- 4-second safety timeout on every hook (Claude Code's limit is 5s)
|
|
154
|
+
|
|
155
|
+
## Protected File Patterns
|
|
156
|
+
|
|
157
|
+
By default, agentmask blocks direct reading of these patterns (static, always active):
|
|
158
|
+
|
|
159
|
+
`.env`, `.env.*`, `credentials.json`, `serviceAccountKey.json`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa`, `id_ed25519`, `.netrc`, `.npmrc`, `.pypirc`, `.docker/config.json`, `.kube/config`, `.aws/credentials`, `.azure/credentials`, `.gcloud/*.json`, `secrets.yml`, `secrets.yaml`, `secrets.json`, `.htpasswd`
|
|
160
|
+
|
|
161
|
+
In addition, `agentmask init` scans all other files and blocklists any that contain secrets.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__export
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=chunk-2H7UOFLK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/blocklist.ts
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
5
|
+
import { join, dirname, resolve } from "path";
|
|
6
|
+
var BLOCKLIST_FILENAME = "agentmask-blocklist.json";
|
|
7
|
+
function getBlocklistPath(cwd) {
|
|
8
|
+
return join(cwd, ".claude", BLOCKLIST_FILENAME);
|
|
9
|
+
}
|
|
10
|
+
function loadBlocklist(cwd) {
|
|
11
|
+
const filePath = getBlocklistPath(cwd);
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(filePath)) {
|
|
14
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
return { files: {} };
|
|
19
|
+
}
|
|
20
|
+
function saveBlocklist(cwd, data) {
|
|
21
|
+
const filePath = getBlocklistPath(cwd);
|
|
22
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
23
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
24
|
+
}
|
|
25
|
+
function isInBlocklist(filePath, cwd) {
|
|
26
|
+
const data = loadBlocklist(cwd);
|
|
27
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
28
|
+
if (data.files[normalized]) return data.files[normalized];
|
|
29
|
+
const resolved = resolve(cwd, filePath).replace(/\\/g, "/");
|
|
30
|
+
const cwdNorm = cwd.replace(/\\/g, "/");
|
|
31
|
+
const relative = resolved.startsWith(cwdNorm + "/") ? resolved.slice(cwdNorm.length + 1) : null;
|
|
32
|
+
if (relative && data.files[relative]) return data.files[relative];
|
|
33
|
+
for (const [blockedPath, entry] of Object.entries(data.files)) {
|
|
34
|
+
if (normalized.endsWith("/" + blockedPath) || normalized === blockedPath) {
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
if (resolved.endsWith("/" + blockedPath) || resolved === blockedPath) {
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
function addToBlocklist(filePath, secretDescriptions, cwd) {
|
|
44
|
+
const data = loadBlocklist(cwd);
|
|
45
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
46
|
+
const resolved = resolve(cwd, normalized).replace(/\\/g, "/");
|
|
47
|
+
const cwdNorm = cwd.replace(/\\/g, "/");
|
|
48
|
+
const key = resolved.startsWith(cwdNorm + "/") ? resolved.slice(cwdNorm.length + 1) : normalized;
|
|
49
|
+
const existing = data.files[key];
|
|
50
|
+
if (existing) {
|
|
51
|
+
const newSecrets = secretDescriptions.filter(
|
|
52
|
+
(s) => !existing.secrets.includes(s)
|
|
53
|
+
);
|
|
54
|
+
existing.secrets.push(...newSecrets);
|
|
55
|
+
existing.addedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
56
|
+
} else {
|
|
57
|
+
data.files[key] = {
|
|
58
|
+
secrets: secretDescriptions,
|
|
59
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
saveBlocklist(cwd, data);
|
|
63
|
+
}
|
|
64
|
+
function removeFromBlocklist(filePath, cwd) {
|
|
65
|
+
const data = loadBlocklist(cwd);
|
|
66
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
67
|
+
if (data.files[normalized]) {
|
|
68
|
+
delete data.files[normalized];
|
|
69
|
+
saveBlocklist(cwd, data);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const resolved = resolve(cwd, filePath).replace(/\\/g, "/");
|
|
73
|
+
const cwdNorm = cwd.replace(/\\/g, "/");
|
|
74
|
+
const relative = resolved.startsWith(cwdNorm + "/") ? resolved.slice(cwdNorm.length + 1) : null;
|
|
75
|
+
if (relative && data.files[relative]) {
|
|
76
|
+
delete data.files[relative];
|
|
77
|
+
saveBlocklist(cwd, data);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
for (const key of Object.keys(data.files)) {
|
|
81
|
+
if (normalized.endsWith("/" + key) || normalized === key) {
|
|
82
|
+
delete data.files[key];
|
|
83
|
+
saveBlocklist(cwd, data);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
saveBlocklist,
|
|
92
|
+
isInBlocklist,
|
|
93
|
+
addToBlocklist,
|
|
94
|
+
removeFromBlocklist
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=chunk-F7TMT2OH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/blocklist.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from \"node:fs\";\nimport { join, dirname, resolve } from \"node:path\";\n\n/**\n * Dynamic blocklist — files where secrets have been detected.\n *\n * Built at init time by scanning the entire repo (Tier 1 rules only).\n * Updated at runtime by post-scan when secrets are found in new files.\n * Checked by pre-read to block files before they enter context.\n */\n\nexport interface BlocklistEntry {\n secrets: string[];\n addedAt: string;\n}\n\nexport interface BlocklistData {\n /** Map of relative file path → entry */\n files: Record<string, BlocklistEntry>;\n}\n\nconst BLOCKLIST_FILENAME = \"agentmask-blocklist.json\";\n\nexport function getBlocklistPath(cwd: string): string {\n return join(cwd, \".claude\", BLOCKLIST_FILENAME);\n}\n\nexport function loadBlocklist(cwd: string): BlocklistData {\n const filePath = getBlocklistPath(cwd);\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, \"utf-8\"));\n }\n } catch {\n // Corrupted — start fresh\n }\n return { files: {} };\n}\n\nexport function saveBlocklist(cwd: string, data: BlocklistData): void {\n const filePath = getBlocklistPath(cwd);\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n}\n\n/**\n * Check if a file path is in the dynamic blocklist.\n * Matches by exact relative path or by filename suffix.\n */\nexport function isInBlocklist(\n filePath: string,\n cwd: string,\n): BlocklistEntry | undefined {\n const data = loadBlocklist(cwd);\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Try exact match\n if (data.files[normalized]) return data.files[normalized];\n\n // Try relative to cwd\n const resolved = resolve(cwd, filePath).replace(/\\\\/g, \"/\");\n const cwdNorm = cwd.replace(/\\\\/g, \"/\");\n const relative = resolved.startsWith(cwdNorm + \"/\")\n ? resolved.slice(cwdNorm.length + 1)\n : null;\n if (relative && data.files[relative]) return data.files[relative];\n\n // Try matching by suffix (handles absolute vs relative path differences)\n for (const [blockedPath, entry] of Object.entries(data.files)) {\n if (normalized.endsWith(\"/\" + blockedPath) || normalized === blockedPath) {\n return entry;\n }\n if (resolved.endsWith(\"/\" + blockedPath) || resolved === blockedPath) {\n return entry;\n }\n }\n\n return undefined;\n}\n\n/**\n * Add a file to the blocklist.\n */\nexport function addToBlocklist(\n filePath: string,\n secretDescriptions: string[],\n cwd: string,\n): void {\n const data = loadBlocklist(cwd);\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Make path relative to cwd if possible\n const resolved = resolve(cwd, normalized).replace(/\\\\/g, \"/\");\n const cwdNorm = cwd.replace(/\\\\/g, \"/\");\n const key = resolved.startsWith(cwdNorm + \"/\")\n ? resolved.slice(cwdNorm.length + 1)\n : normalized;\n\n const existing = data.files[key];\n if (existing) {\n const newSecrets = secretDescriptions.filter(\n (s) => !existing.secrets.includes(s),\n );\n existing.secrets.push(...newSecrets);\n existing.addedAt = new Date().toISOString();\n } else {\n data.files[key] = {\n secrets: secretDescriptions,\n addedAt: new Date().toISOString(),\n };\n }\n\n saveBlocklist(cwd, data);\n}\n\n/**\n * Remove a file from the blocklist.\n */\nexport function removeFromBlocklist(filePath: string, cwd: string): boolean {\n const data = loadBlocklist(cwd);\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Try exact key\n if (data.files[normalized]) {\n delete data.files[normalized];\n saveBlocklist(cwd, data);\n return true;\n }\n\n // Try relative resolution\n const resolved = resolve(cwd, filePath).replace(/\\\\/g, \"/\");\n const cwdNorm = cwd.replace(/\\\\/g, \"/\");\n const relative = resolved.startsWith(cwdNorm + \"/\")\n ? resolved.slice(cwdNorm.length + 1)\n : null;\n if (relative && data.files[relative]) {\n delete data.files[relative];\n saveBlocklist(cwd, data);\n return true;\n }\n\n // Try suffix match\n for (const key of Object.keys(data.files)) {\n if (normalized.endsWith(\"/\" + key) || normalized === key) {\n delete data.files[key];\n saveBlocklist(cwd, data);\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Delete the blocklist file entirely.\n */\nexport function deleteBlocklist(cwd: string): boolean {\n const filePath = getBlocklistPath(cwd);\n if (existsSync(filePath)) {\n unlinkSync(filePath);\n return true;\n }\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/gitleaks/runner.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, rmSync } from "fs";
|
|
6
|
+
import { join as join2 } from "path";
|
|
7
|
+
import { tmpdir } from "os";
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/gitleaks/binary.ts
|
|
11
|
+
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { pipeline } from "stream/promises";
|
|
15
|
+
import { extract } from "tar";
|
|
16
|
+
var GITLEAKS_VERSION = "8.22.1";
|
|
17
|
+
var CACHE_DIR = join(
|
|
18
|
+
process.env.HOME ?? process.env.USERPROFILE ?? "/tmp",
|
|
19
|
+
".agentmask",
|
|
20
|
+
"bin"
|
|
21
|
+
);
|
|
22
|
+
async function getGitleaksBinary() {
|
|
23
|
+
const systemBin = findInPath();
|
|
24
|
+
if (systemBin) return systemBin;
|
|
25
|
+
const cachedBin = join(CACHE_DIR, "gitleaks");
|
|
26
|
+
if (existsSync(cachedBin)) return cachedBin;
|
|
27
|
+
console.log(`gitleaks not found. Downloading v${GITLEAKS_VERSION}...`);
|
|
28
|
+
await downloadGitleaks(cachedBin);
|
|
29
|
+
return cachedBin;
|
|
30
|
+
}
|
|
31
|
+
function findInPath() {
|
|
32
|
+
try {
|
|
33
|
+
const path = execSync("which gitleaks", {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
36
|
+
}).trim();
|
|
37
|
+
if (path && existsSync(path)) return path;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function downloadGitleaks(destPath) {
|
|
43
|
+
const { platform, arch } = getPlatformArch();
|
|
44
|
+
const filename = `gitleaks_${GITLEAKS_VERSION}_${platform}_${arch}.tar.gz`;
|
|
45
|
+
const url = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${filename}`;
|
|
46
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
47
|
+
const tmpTarball = join(CACHE_DIR, `gitleaks-download.tar.gz`);
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
50
|
+
if (!response.ok || !response.body) {
|
|
51
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
52
|
+
}
|
|
53
|
+
const fileStream = createWriteStream(tmpTarball);
|
|
54
|
+
await pipeline(response.body, fileStream);
|
|
55
|
+
await extract({
|
|
56
|
+
file: tmpTarball,
|
|
57
|
+
cwd: CACHE_DIR,
|
|
58
|
+
filter: (path) => path === "gitleaks"
|
|
59
|
+
});
|
|
60
|
+
chmodSync(destPath, 493);
|
|
61
|
+
console.log(` Downloaded gitleaks v${GITLEAKS_VERSION} \u2192 ${destPath}`);
|
|
62
|
+
} finally {
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(tmpTarball);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function getPlatformArch() {
|
|
70
|
+
const platform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "windows" : process.platform;
|
|
71
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : process.arch;
|
|
72
|
+
return { platform, arch };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/gitleaks/runner.ts
|
|
76
|
+
async function scanDir(dirPath, options) {
|
|
77
|
+
const bin = await getGitleaksBinary();
|
|
78
|
+
const reportPath = tempPath("agentmask-report", ".json");
|
|
79
|
+
try {
|
|
80
|
+
const args = [
|
|
81
|
+
"dir",
|
|
82
|
+
dirPath,
|
|
83
|
+
"--report-format",
|
|
84
|
+
"json",
|
|
85
|
+
"--report-path",
|
|
86
|
+
reportPath,
|
|
87
|
+
"--no-banner",
|
|
88
|
+
"--exit-code",
|
|
89
|
+
"0"
|
|
90
|
+
];
|
|
91
|
+
if (options?.configPath) {
|
|
92
|
+
args.push("--config", options.configPath);
|
|
93
|
+
}
|
|
94
|
+
execFileSync(bin, args, {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
97
|
+
timeout: 6e4
|
|
98
|
+
});
|
|
99
|
+
return readReport(reportPath);
|
|
100
|
+
} finally {
|
|
101
|
+
tryUnlink(reportPath);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function scanFile(filePath) {
|
|
105
|
+
const bin = await getGitleaksBinary();
|
|
106
|
+
const reportPath = tempPath("agentmask-report", ".json");
|
|
107
|
+
try {
|
|
108
|
+
execFileSync(bin, [
|
|
109
|
+
"dir",
|
|
110
|
+
filePath,
|
|
111
|
+
"--report-format",
|
|
112
|
+
"json",
|
|
113
|
+
"--report-path",
|
|
114
|
+
reportPath,
|
|
115
|
+
"--no-banner",
|
|
116
|
+
"--exit-code",
|
|
117
|
+
"0"
|
|
118
|
+
], {
|
|
119
|
+
encoding: "utf-8",
|
|
120
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
121
|
+
timeout: 1e4
|
|
122
|
+
});
|
|
123
|
+
return readReport(reportPath);
|
|
124
|
+
} finally {
|
|
125
|
+
tryUnlink(reportPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function scanContent(content, filename) {
|
|
129
|
+
const bin = await getGitleaksBinary();
|
|
130
|
+
const scanDir2 = join2(tmpdir(), `agentmask-scan-${randomBytes(4).toString("hex")}`);
|
|
131
|
+
const scanFile2 = join2(scanDir2, filename ?? "content.txt");
|
|
132
|
+
const reportPath = tempPath("agentmask-report", ".json");
|
|
133
|
+
try {
|
|
134
|
+
mkdirSync2(scanDir2, { recursive: true });
|
|
135
|
+
writeFileSync(scanFile2, content);
|
|
136
|
+
execFileSync(bin, [
|
|
137
|
+
"dir",
|
|
138
|
+
scanDir2,
|
|
139
|
+
"--report-format",
|
|
140
|
+
"json",
|
|
141
|
+
"--report-path",
|
|
142
|
+
reportPath,
|
|
143
|
+
"--no-banner",
|
|
144
|
+
"--exit-code",
|
|
145
|
+
"0"
|
|
146
|
+
], {
|
|
147
|
+
encoding: "utf-8",
|
|
148
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
149
|
+
timeout: 1e4
|
|
150
|
+
});
|
|
151
|
+
return readReport(reportPath);
|
|
152
|
+
} finally {
|
|
153
|
+
tryUnlink(reportPath);
|
|
154
|
+
try {
|
|
155
|
+
rmSync(scanDir2, { recursive: true, force: true });
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function scanStaged(cwd) {
|
|
161
|
+
const bin = await getGitleaksBinary();
|
|
162
|
+
const reportPath = tempPath("agentmask-report", ".json");
|
|
163
|
+
try {
|
|
164
|
+
execFileSync(bin, [
|
|
165
|
+
"git",
|
|
166
|
+
"--staged",
|
|
167
|
+
"--report-format",
|
|
168
|
+
"json",
|
|
169
|
+
"--report-path",
|
|
170
|
+
reportPath,
|
|
171
|
+
"--no-banner",
|
|
172
|
+
"--exit-code",
|
|
173
|
+
"0"
|
|
174
|
+
], {
|
|
175
|
+
cwd,
|
|
176
|
+
encoding: "utf-8",
|
|
177
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
178
|
+
timeout: 3e4
|
|
179
|
+
});
|
|
180
|
+
return readReport(reportPath);
|
|
181
|
+
} finally {
|
|
182
|
+
tryUnlink(reportPath);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function readReport(reportPath) {
|
|
186
|
+
try {
|
|
187
|
+
const raw = readFileSync(reportPath, "utf-8");
|
|
188
|
+
const findings = JSON.parse(raw);
|
|
189
|
+
return Array.isArray(findings) ? findings : [];
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function tempPath(prefix, ext) {
|
|
195
|
+
return join2(tmpdir(), `${prefix}-${randomBytes(4).toString("hex")}${ext}`);
|
|
196
|
+
}
|
|
197
|
+
function tryUnlink(path) {
|
|
198
|
+
try {
|
|
199
|
+
unlinkSync2(path);
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
getGitleaksBinary,
|
|
206
|
+
scanDir,
|
|
207
|
+
scanFile,
|
|
208
|
+
scanContent,
|
|
209
|
+
scanStaged
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=chunk-P7BRPZBB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/gitleaks/runner.ts","../src/gitleaks/binary.ts"],"sourcesContent":["import { execSync, execFileSync } from \"node:child_process\";\nimport { readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { getGitleaksBinary } from \"./binary.js\";\n\n/**\n * A single finding from gitleaks JSON output.\n */\nexport interface GitleaksFinding {\n RuleID: string;\n Description: string;\n StartLine: number;\n EndLine: number;\n StartColumn: number;\n EndColumn: number;\n Match: string;\n Secret: string;\n File: string;\n Entropy: number;\n Fingerprint: string;\n}\n\n/**\n * Scan a directory for secrets.\n */\nexport async function scanDir(\n dirPath: string,\n options?: { configPath?: string },\n): Promise<GitleaksFinding[]> {\n const bin = await getGitleaksBinary();\n const reportPath = tempPath(\"agentmask-report\", \".json\");\n\n try {\n const args = [\n \"dir\",\n dirPath,\n \"--report-format\", \"json\",\n \"--report-path\", reportPath,\n \"--no-banner\",\n \"--exit-code\", \"0\",\n ];\n if (options?.configPath) {\n args.push(\"--config\", options.configPath);\n }\n\n execFileSync(bin, args, {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: 60_000,\n });\n\n return readReport(reportPath);\n } finally {\n tryUnlink(reportPath);\n }\n}\n\n/**\n * Scan a single file for secrets.\n */\nexport async function scanFile(filePath: string): Promise<GitleaksFinding[]> {\n const bin = await getGitleaksBinary();\n const reportPath = tempPath(\"agentmask-report\", \".json\");\n\n try {\n execFileSync(bin, [\n \"dir\",\n filePath,\n \"--report-format\", \"json\",\n \"--report-path\", reportPath,\n \"--no-banner\",\n \"--exit-code\", \"0\",\n ], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: 10_000,\n });\n\n return readReport(reportPath);\n } finally {\n tryUnlink(reportPath);\n }\n}\n\n/**\n * Scan arbitrary content by writing to a temp file.\n * Used by pre-write hook (scan content before it's written)\n * and post-scan hook (scan tool output).\n */\nexport async function scanContent(\n content: string,\n filename?: string,\n): Promise<GitleaksFinding[]> {\n const bin = await getGitleaksBinary();\n const scanDir = join(tmpdir(), `agentmask-scan-${randomBytes(4).toString(\"hex\")}`);\n const scanFile = join(scanDir, filename ?? \"content.txt\");\n const reportPath = tempPath(\"agentmask-report\", \".json\");\n\n try {\n mkdirSync(scanDir, { recursive: true });\n writeFileSync(scanFile, content);\n\n execFileSync(bin, [\n \"dir\",\n scanDir,\n \"--report-format\", \"json\",\n \"--report-path\", reportPath,\n \"--no-banner\",\n \"--exit-code\", \"0\",\n ], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: 10_000,\n });\n\n return readReport(reportPath);\n } finally {\n tryUnlink(reportPath);\n try { rmSync(scanDir, { recursive: true, force: true }); } catch {}\n }\n}\n\n/**\n * Scan git staged files for secrets.\n */\nexport async function scanStaged(cwd: string): Promise<GitleaksFinding[]> {\n const bin = await getGitleaksBinary();\n const reportPath = tempPath(\"agentmask-report\", \".json\");\n\n try {\n execFileSync(bin, [\n \"git\",\n \"--staged\",\n \"--report-format\", \"json\",\n \"--report-path\", reportPath,\n \"--no-banner\",\n \"--exit-code\", \"0\",\n ], {\n cwd,\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: 30_000,\n });\n\n return readReport(reportPath);\n } finally {\n tryUnlink(reportPath);\n }\n}\n\nfunction readReport(reportPath: string): GitleaksFinding[] {\n try {\n const raw = readFileSync(reportPath, \"utf-8\");\n const findings = JSON.parse(raw);\n return Array.isArray(findings) ? findings : [];\n } catch {\n return [];\n }\n}\n\nfunction tempPath(prefix: string, ext: string): string {\n return join(tmpdir(), `${prefix}-${randomBytes(4).toString(\"hex\")}${ext}`);\n}\n\nfunction tryUnlink(path: string): void {\n try { unlinkSync(path); } catch {}\n}\n","import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { execSync } from \"node:child_process\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createGunzip } from \"node:zlib\";\nimport { extract } from \"tar\";\n\n/**\n * Pinned gitleaks version. Update this when testing against a new release.\n */\nconst GITLEAKS_VERSION = \"8.22.1\";\n\n/**\n * Where we cache the downloaded binary.\n */\nconst CACHE_DIR = join(\n process.env.HOME ?? process.env.USERPROFILE ?? \"/tmp\",\n \".agentmask\",\n \"bin\",\n);\n\n/**\n * Find the gitleaks binary. Checks in order:\n * 1. System PATH (user already has it installed)\n * 2. Our cache directory (previously downloaded)\n * 3. Downloads it automatically\n */\nexport async function getGitleaksBinary(): Promise<string> {\n // 1. Check system PATH\n const systemBin = findInPath();\n if (systemBin) return systemBin;\n\n // 2. Check cache\n const cachedBin = join(CACHE_DIR, \"gitleaks\");\n if (existsSync(cachedBin)) return cachedBin;\n\n // 3. Download\n console.log(`gitleaks not found. Downloading v${GITLEAKS_VERSION}...`);\n await downloadGitleaks(cachedBin);\n return cachedBin;\n}\n\n/**\n * Check if gitleaks is available without downloading.\n */\nexport function isGitleaksAvailable(): boolean {\n if (findInPath()) return true;\n const cachedBin = join(CACHE_DIR, \"gitleaks\");\n return existsSync(cachedBin);\n}\n\nfunction findInPath(): string | null {\n try {\n const path = execSync(\"which gitleaks\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n if (path && existsSync(path)) return path;\n } catch {\n // not in PATH\n }\n return null;\n}\n\nasync function downloadGitleaks(destPath: string): Promise<void> {\n const { platform, arch } = getPlatformArch();\n const filename = `gitleaks_${GITLEAKS_VERSION}_${platform}_${arch}.tar.gz`;\n const url = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${filename}`;\n\n mkdirSync(CACHE_DIR, { recursive: true });\n\n // Download to temp file\n const tmpTarball = join(CACHE_DIR, `gitleaks-download.tar.gz`);\n\n try {\n const response = await fetch(url, { redirect: \"follow\" });\n if (!response.ok || !response.body) {\n throw new Error(`Download failed: ${response.status} ${response.statusText}`);\n }\n\n // Write tarball to disk\n const fileStream = createWriteStream(tmpTarball);\n // @ts-ignore - Node.js ReadableStream from fetch is compatible\n await pipeline(response.body, fileStream);\n\n // Extract the gitleaks binary from the tarball\n await extract({\n file: tmpTarball,\n cwd: CACHE_DIR,\n filter: (path) => path === \"gitleaks\",\n });\n\n // Make executable\n chmodSync(destPath, 0o755);\n\n console.log(` Downloaded gitleaks v${GITLEAKS_VERSION} → ${destPath}`);\n } finally {\n // Clean up tarball\n try {\n unlinkSync(tmpTarball);\n } catch {}\n }\n}\n\nfunction getPlatformArch(): { platform: string; arch: string } {\n const platform =\n process.platform === \"darwin\"\n ? \"darwin\"\n : process.platform === \"linux\"\n ? \"linux\"\n : process.platform === \"win32\"\n ? \"windows\"\n : process.platform;\n\n const arch =\n process.arch === \"arm64\"\n ? \"arm64\"\n : process.arch === \"x64\"\n ? \"x64\"\n : process.arch;\n\n return { platform, arch };\n}\n"],"mappings":"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","names":["mkdirSync","unlinkSync","join","scanDir","join","scanFile","mkdirSync","unlinkSync"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/scanner/file-patterns.ts
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
5
|
+
import { basename } from "path";
|
|
6
|
+
var DEFAULT_BLOCKED_PATTERNS = [
|
|
7
|
+
// Environment files
|
|
8
|
+
".env",
|
|
9
|
+
".env.*",
|
|
10
|
+
"**/.env",
|
|
11
|
+
"**/.env.*",
|
|
12
|
+
// Credential files
|
|
13
|
+
"**/credentials.json",
|
|
14
|
+
"**/serviceAccountKey.json",
|
|
15
|
+
"**/service-account*.json",
|
|
16
|
+
// Key files
|
|
17
|
+
"**/*.pem",
|
|
18
|
+
"**/*.key",
|
|
19
|
+
"**/*.p12",
|
|
20
|
+
"**/*.pfx",
|
|
21
|
+
// SSH keys
|
|
22
|
+
"**/id_rsa",
|
|
23
|
+
"**/id_ed25519",
|
|
24
|
+
"**/id_ecdsa",
|
|
25
|
+
"**/id_dsa",
|
|
26
|
+
// Auth config files
|
|
27
|
+
"**/.netrc",
|
|
28
|
+
"**/.npmrc",
|
|
29
|
+
"**/.pypirc",
|
|
30
|
+
"**/.docker/config.json",
|
|
31
|
+
"**/.kube/config",
|
|
32
|
+
"**/kubeconfig",
|
|
33
|
+
// Cloud provider credentials
|
|
34
|
+
"**/.aws/credentials",
|
|
35
|
+
"**/.aws/config",
|
|
36
|
+
"**/.azure/credentials",
|
|
37
|
+
"**/.gcloud/*.json",
|
|
38
|
+
// Other
|
|
39
|
+
"**/.htpasswd",
|
|
40
|
+
"**/secrets.yml",
|
|
41
|
+
"**/secrets.yaml",
|
|
42
|
+
"**/secrets.json"
|
|
43
|
+
];
|
|
44
|
+
function isBlockedPath(filePath, blockedPatterns = DEFAULT_BLOCKED_PATTERNS) {
|
|
45
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
46
|
+
const base = basename(normalized);
|
|
47
|
+
for (const pattern of blockedPatterns) {
|
|
48
|
+
if (minimatch(normalized, pattern, { dot: true })) return true;
|
|
49
|
+
if (minimatch(base, pattern, { dot: true })) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
DEFAULT_BLOCKED_PATTERNS,
|
|
56
|
+
isBlockedPath
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=chunk-Q7ZBIDBL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scanner/file-patterns.ts"],"sourcesContent":["import { minimatch } from \"minimatch\";\nimport { basename, resolve } from \"node:path\";\n\n/**\n * Default file patterns that should always be blocked from direct reading.\n * These files are known to contain secrets.\n */\nexport const DEFAULT_BLOCKED_PATTERNS: string[] = [\n // Environment files\n \".env\",\n \".env.*\",\n \"**/.env\",\n \"**/.env.*\",\n\n // Credential files\n \"**/credentials.json\",\n \"**/serviceAccountKey.json\",\n \"**/service-account*.json\",\n\n // Key files\n \"**/*.pem\",\n \"**/*.key\",\n \"**/*.p12\",\n \"**/*.pfx\",\n\n // SSH keys\n \"**/id_rsa\",\n \"**/id_ed25519\",\n \"**/id_ecdsa\",\n \"**/id_dsa\",\n\n // Auth config files\n \"**/.netrc\",\n \"**/.npmrc\",\n \"**/.pypirc\",\n \"**/.docker/config.json\",\n \"**/.kube/config\",\n \"**/kubeconfig\",\n\n // Cloud provider credentials\n \"**/.aws/credentials\",\n \"**/.aws/config\",\n \"**/.azure/credentials\",\n \"**/.gcloud/*.json\",\n\n // Other\n \"**/.htpasswd\",\n \"**/secrets.yml\",\n \"**/secrets.yaml\",\n \"**/secrets.json\",\n];\n\n/**\n * Check if a file path matches any of the blocked patterns.\n */\nexport function isBlockedPath(\n filePath: string,\n blockedPatterns: string[] = DEFAULT_BLOCKED_PATTERNS,\n): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const base = basename(normalized);\n\n for (const pattern of blockedPatterns) {\n // Check against full path\n if (minimatch(normalized, pattern, { dot: true })) return true;\n // Check against just the filename (for patterns like \".env\")\n if (minimatch(base, pattern, { dot: true })) return true;\n }\n\n return false;\n}\n\n/**\n * Check if a file path is in an allowlisted path.\n */\nexport function isAllowlistedPath(\n filePath: string,\n allowlistPatterns: string[],\n): boolean {\n if (allowlistPatterns.length === 0) return false;\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n for (const pattern of allowlistPatterns) {\n if (minimatch(normalized, pattern, { dot: true })) return true;\n }\n\n return false;\n}\n\n/**\n * Check if a file is likely binary (should be skipped during scanning).\n */\nexport function isBinaryFile(filePath: string): boolean {\n const ext = filePath.split(\".\").pop()?.toLowerCase();\n return BINARY_EXTENSIONS.has(ext ?? \"\");\n}\n\nconst BINARY_EXTENSIONS = new Set([\n \"png\", \"jpg\", \"jpeg\", \"gif\", \"bmp\", \"ico\", \"webp\", \"svg\",\n \"mp3\", \"mp4\", \"avi\", \"mov\", \"mkv\", \"wav\", \"flac\",\n \"zip\", \"tar\", \"gz\", \"bz2\", \"xz\", \"7z\", \"rar\",\n \"exe\", \"dll\", \"so\", \"dylib\", \"bin\",\n \"pdf\", \"doc\", \"docx\", \"xls\", \"xlsx\",\n \"woff\", \"woff2\", \"ttf\", \"eot\", \"otf\",\n \"pyc\", \"pyo\", \"class\", \"o\", \"obj\",\n \"sqlite\", \"db\", \"sqlite3\",\n]);\n"],"mappings":";;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,gBAAyB;AAM3B,IAAM,2BAAqC;AAAA;AAAA,EAEhD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,SAAS,cACd,UACA,kBAA4B,0BACnB;AACT,QAAM,aAAa,SAAS,QAAQ,OAAO,GAAG;AAC9C,QAAM,OAAO,SAAS,UAAU;AAEhC,aAAW,WAAW,iBAAiB;AAErC,QAAI,UAAU,YAAY,SAAS,EAAE,KAAK,KAAK,CAAC,EAAG,QAAO;AAE1D,QAAI,UAAU,MAAM,SAAS,EAAE,KAAK,KAAK,CAAC,EAAG,QAAO;AAAA,EACtD;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/common.ts
|
|
4
|
+
async function readStdin() {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let data = "";
|
|
7
|
+
process.stdin.setEncoding("utf-8");
|
|
8
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
9
|
+
process.stdin.on("end", () => {
|
|
10
|
+
try {
|
|
11
|
+
resolve(JSON.parse(data));
|
|
12
|
+
} catch {
|
|
13
|
+
resolve({});
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
process.stdin.on("error", reject);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function block(message) {
|
|
20
|
+
process.stderr.write(message);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
function allow(additionalContext) {
|
|
24
|
+
if (additionalContext) {
|
|
25
|
+
const output = {
|
|
26
|
+
hookSpecificOutput: { additionalContext }
|
|
27
|
+
};
|
|
28
|
+
process.stdout.write(JSON.stringify(output));
|
|
29
|
+
}
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
function startSafetyTimer(ms = 4e3) {
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}, ms).unref();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
readStdin,
|
|
40
|
+
block,
|
|
41
|
+
allow,
|
|
42
|
+
startSafetyTimer
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=chunk-YASOHGJL.js.map
|