@reneturcios/hookcheck 0.0.0-stage → 0.1.1
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 +120 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +92 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +30 -0
- package/dist/core.js +597 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +26 -0
- package/dist/mcp.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -6
- package/server.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rene Turcios
|
|
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
CHANGED
|
@@ -1,3 +1,121 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hookcheck
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Find the hooks that run before you do. One scan, every auto-exec surface, one verdict.**
|
|
4
|
+
|
|
5
|
+
Modern supply chain attacks do not need you to run anything. They persist through hooks that run automatically: an npm `postinstall` script, a Claude Code `SessionStart` hook in `.claude/settings.json`, a VS Code task set to run on folder open. The Shai-Hulud npm worm used exactly this pattern, planting a preinstall dropper and then persisting through agent and editor hooks so it survived cleanup and stole credentials on the next open.
|
|
6
|
+
|
|
7
|
+
hookcheck scans a local repo for every auto-exec hook, classifies each one, and prints a single verdict: CLEAN, REVIEW, or COMPROMISED-PATTERN, with the file, command, rules, and evidence for every finding. Deterministic only. No LLM, no network in the core scan.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
Reports in October 2026 described AI agents orchestrating attacks and a self-propagating npm worm that backdoored hundreds of packages. Its persistence layer was not exotic malware. It was ordinary config files that every developer and every coding agent already trusts: package.json install scripts, `.claude/settings.json` hooks, and `.vscode/tasks.json` folderOpen tasks. Defenders needed a fast local check that an agent itself can call before opening, installing, or trusting a repo. That is hookcheck.
|
|
12
|
+
|
|
13
|
+
## What it checks
|
|
14
|
+
|
|
15
|
+
| surface | what it looks for |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `package.json` | preinstall, install, postinstall, prepare scripts |
|
|
18
|
+
| `.claude/settings.json` and `settings.local.json` | SessionStart, PreToolUse, and other hook commands |
|
|
19
|
+
| `.vscode/tasks.json` | tasks with runOn folderOpen, plus task inventory |
|
|
20
|
+
| `.husky/` | every hook file and its full command content |
|
|
21
|
+
| `lefthook.yml` | run commands |
|
|
22
|
+
| `.pre-commit-config.yaml` | entry commands and remote repo URLs |
|
|
23
|
+
| `.cursor/` and `.codex/` | config commands and files |
|
|
24
|
+
| MCP configs | `.mcp.json`, `mcp.json`, `.vscode/mcp.json`, `.cursor/mcp.json` servers that autostart |
|
|
25
|
+
| `.github/workflows/` | run steps, as inventory |
|
|
26
|
+
| `.githooks/` | hook files |
|
|
27
|
+
|
|
28
|
+
Classification per hook:
|
|
29
|
+
|
|
30
|
+
- **SAFE**: matches known safe dev patterns such as test, lint, build, typecheck, husky, or lint-staged, and matches no risky rule.
|
|
31
|
+
- **RISKY**: matches at least one risky rule: network fetch, curl piped to a shell, base64 decode piped to a shell, credential store paths such as ~/.ssh or ~/.aws, environment secret access, base64 decoding or eval style obfuscation, shell spawning, inline code execution, or a remote URL.
|
|
32
|
+
- **UNKNOWN**: matches neither safe nor risky patterns. A local script name alone is UNKNOWN until a human reads it. When a hook runs a local file such as `node setup.mjs`, hookcheck reads that file and escalates the finding if the file itself matches risky rules. The same one hop following applies to direct script execution (`./setup.sh`), `node --require ./setup.mjs`, and package.json script chains (`postinstall: npm run other-script`).
|
|
33
|
+
|
|
34
|
+
Verdict rules:
|
|
35
|
+
|
|
36
|
+
- **CLEAN**: every finding is SAFE, or no hooks were found. Exit code 0.
|
|
37
|
+
- **REVIEW**: at least one UNKNOWN finding, or a RISKY finding without a remote fetch, credential, or decode-and-execute rule. Exit code 1.
|
|
38
|
+
- **COMPROMISED-PATTERN**: at least one RISKY finding that fetches remote code, touches credential stores, or decodes a payload into a shell. This is the Shai-Hulud persistence shape. Treat the repo as untrusted until proven otherwise. Exit code 1.
|
|
39
|
+
|
|
40
|
+
Every finding reports file, hook id, command text, classification, rule ids, and evidence. Malformed JSON is reported as a warning, never silently ignored. Scanning a path that does not exist, or that is a file instead of a directory, is a clean error with exit code 2.
|
|
41
|
+
|
|
42
|
+
## Limits
|
|
43
|
+
|
|
44
|
+
Honest boundaries, pinned by tests in test/hardening.test.ts:
|
|
45
|
+
|
|
46
|
+
- One hop only. hookcheck follows a hook into the local file or package.json script it directly invokes, and classifies that target. It does not follow a second hop, and it does not trace data flow inside a script. A payload assembled at runtime from pieces that never appear as a recognizable pattern in any single file will not be named; the hook that runs it will usually still land in UNKNOWN or REVIEW, which is the signal to read it by hand.
|
|
47
|
+
- Pattern based, not execution based. hookcheck never runs the hooks it finds. A RISKY verdict means the command text matches a known bad shape, not that the payload was observed doing harm. A CLEAN verdict means no known shape matched, not that the repo is proven safe.
|
|
48
|
+
- Remote pre-commit repos and remote MCP servers are inventoried, not verified. hookcheck cannot see inside a remote pre-commit repo, so those entries are UNKNOWN with a note to review the pinned rev. A remote MCP server URL is RISKY by construction, because the server autostarts and is remote.
|
|
49
|
+
- Duplicate JSON keys resolve last-wins, exactly as JSON.parse, Node, and npm resolve them, so the scanner sees the same value that would actually run.
|
|
50
|
+
- Non-string package.json script values are skipped: npm only executes string script values, so an array or number in scripts cannot auto-run.
|
|
51
|
+
- Classification reads up to 200,000 characters of a hook file or a referenced local file. Beyond that depth a payload can hide; the cap exists so a hostile file cannot make the scan unbounded.
|
|
52
|
+
- GitHub workflow run steps in block scalars (`run: |`) are inventoried only when they appear as single line run values; multi-line block scalar bodies are not parsed yet.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @reneturcios/hookcheck scan
|
|
60
|
+
npx @reneturcios/hookcheck scan ./some-repo
|
|
61
|
+
npx @reneturcios/hookcheck scan ./some-repo --json
|
|
62
|
+
npx @reneturcios/hookcheck explain ./some-repo package.json:postinstall
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Example output:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
hookcheck ./some-repo: COMPROMISED-PATTERN
|
|
69
|
+
2 hook(s): 0 SAFE, 2 RISKY, 0 UNKNOWN
|
|
70
|
+
[RISKY] package.json:postinstall (package.json)
|
|
71
|
+
command: node setup.mjs
|
|
72
|
+
rules: NET-FETCH, ENV-ACCESS
|
|
73
|
+
evidence: referenced local file setup.mjs is RISKY: NET-FETCH: performs a network fetch
|
|
74
|
+
COMPROMISED-PATTERN: at least one risky hook fetches remote code or touches credential stores.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### MCP server
|
|
78
|
+
|
|
79
|
+
Add to your MCP client config:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"hookcheck": {
|
|
85
|
+
"command": "npx",
|
|
86
|
+
"args": ["-y", "@reneturcios/hookcheck", "mcp"]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Tools:
|
|
93
|
+
|
|
94
|
+
- `scan_hooks(path)`: full scan with verdict, findings, counts, and warnings.
|
|
95
|
+
- `explain_hook(path, hookId or file)`: one hook explained in plain language with its evidence.
|
|
96
|
+
|
|
97
|
+
### Docker
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker run -i ghcr.io/cloudcorprecords/hookcheck mcp
|
|
101
|
+
docker run --rm -v "$PWD:/repo" ghcr.io/cloudcorprecords/hookcheck scan /repo
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## The toolkit
|
|
105
|
+
|
|
106
|
+
hookcheck is part of a family of small, single-purpose developer tools by the same author, all built the same way: one bin that serves a CLI and an MCP server, deterministic checks with evidence, no invented answers.
|
|
107
|
+
|
|
108
|
+
- [greenlight](https://github.com/CloudCorpRecords/greenlight): pre-flight checks for open-source contributions: is this issue actually landable?
|
|
109
|
+
- [shipcheck](https://github.com/CloudCorpRecords/shipcheck): verify a release actually shipped on every surface, or NO-GO.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm install
|
|
115
|
+
npm run build
|
|
116
|
+
npm test
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hookcheck CLI.
|
|
4
|
+
* hookcheck scan [path] [--json]
|
|
5
|
+
* hookcheck mcp
|
|
6
|
+
* hookcheck --version
|
|
7
|
+
* Exit codes: 0 CLEAN, 1 REVIEW or COMPROMISED-PATTERN, 2 usage error.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, statSync } from "node:fs";
|
|
10
|
+
import { explainHook, renderReport, scanHooks, VERSION } from "./core.js";
|
|
11
|
+
/** A scan target must be an existing directory; anything else is a usage error (exit 2). */
|
|
12
|
+
function requireDirectory(target) {
|
|
13
|
+
if (!existsSync(target)) {
|
|
14
|
+
console.error(`error: path does not exist: ${target}`);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
let isDir = false;
|
|
18
|
+
try {
|
|
19
|
+
isDir = statSync(target).isDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
isDir = false;
|
|
23
|
+
}
|
|
24
|
+
if (!isDir) {
|
|
25
|
+
console.error(`error: path is not a directory: ${target}`);
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function usage() {
|
|
30
|
+
console.error(`Usage:
|
|
31
|
+
hookcheck scan [path] [--json]
|
|
32
|
+
hookcheck explain [path] <hookId-or-file>
|
|
33
|
+
hookcheck mcp
|
|
34
|
+
hookcheck --version`);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
async function main() {
|
|
38
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
39
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
40
|
+
console.log(VERSION);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (cmd === "mcp") {
|
|
44
|
+
await import("./mcp.js");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!cmd)
|
|
48
|
+
usage();
|
|
49
|
+
try {
|
|
50
|
+
if (cmd === "scan") {
|
|
51
|
+
const asJson = rest.includes("--json");
|
|
52
|
+
const target = rest.find((a) => !a.startsWith("--")) ?? ".";
|
|
53
|
+
requireDirectory(target);
|
|
54
|
+
const report = scanHooks(target);
|
|
55
|
+
if (asJson)
|
|
56
|
+
console.log(JSON.stringify(report, null, 2));
|
|
57
|
+
else
|
|
58
|
+
console.log(renderReport(report));
|
|
59
|
+
process.exit(report.verdict === "CLEAN" ? 0 : 1);
|
|
60
|
+
}
|
|
61
|
+
else if (cmd === "explain") {
|
|
62
|
+
const args = rest.filter((a) => !a.startsWith("--"));
|
|
63
|
+
const asJson = rest.includes("--json");
|
|
64
|
+
let target = ".";
|
|
65
|
+
let id = "";
|
|
66
|
+
if (args.length === 1)
|
|
67
|
+
id = args[0];
|
|
68
|
+
else if (args.length >= 2) {
|
|
69
|
+
target = args[0];
|
|
70
|
+
id = args[1];
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
usage();
|
|
74
|
+
requireDirectory(target);
|
|
75
|
+
const result = explainHook(target, id);
|
|
76
|
+
if (asJson)
|
|
77
|
+
console.log(JSON.stringify(result, null, 2));
|
|
78
|
+
else
|
|
79
|
+
console.log(result.explanation);
|
|
80
|
+
process.exit(result.finding ? 0 : 1);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
usage();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
void main();
|
|
92
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import type { Classification, HookFinding, ScanReport, Verdict } from "./types.js";
|
|
3
|
+
export declare const VERSION = "0.1.1";
|
|
4
|
+
interface RuleDef {
|
|
5
|
+
id: string;
|
|
6
|
+
pattern: RegExp;
|
|
7
|
+
evidence: string;
|
|
8
|
+
/** True when this rule alone indicates remote code fetch. */
|
|
9
|
+
remote?: boolean;
|
|
10
|
+
/** True when this rule indicates credential store access. */
|
|
11
|
+
credential?: boolean;
|
|
12
|
+
/** True when this pattern is severe enough on its own (decode and execute). */
|
|
13
|
+
severe?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const RISKY_RULES: RuleDef[];
|
|
16
|
+
export declare function classifyCommand(command: string): {
|
|
17
|
+
classification: Classification;
|
|
18
|
+
rules: string[];
|
|
19
|
+
evidence: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function computeVerdict(findings: HookFinding[]): Verdict;
|
|
22
|
+
export declare function scanHooks(root: string): ScanReport;
|
|
23
|
+
export declare function explainHook(root: string, idOrFile: string): {
|
|
24
|
+
finding: HookFinding | null;
|
|
25
|
+
explanation: string;
|
|
26
|
+
report: ScanReport;
|
|
27
|
+
};
|
|
28
|
+
export declare function renderReport(report: ScanReport): string;
|
|
29
|
+
export { relative };
|
|
30
|
+
export type { Classification, HookFinding, ScanReport, Verdict };
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hookcheck core: deterministic local scan for auto-exec hooks.
|
|
3
|
+
*
|
|
4
|
+
* No network, no LLM. Every classification comes from explicit pattern
|
|
5
|
+
* rules so the same repo always produces the same report.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
9
|
+
export const VERSION = "0.1.1";
|
|
10
|
+
export const RISKY_RULES = [
|
|
11
|
+
{ id: "DECODE-PIPE-SH", pattern: /\bbase64\s+(-d|--decode|-D)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/, evidence: "decodes a base64 payload and pipes it straight into a shell", severe: true },
|
|
12
|
+
{ id: "CURL-PIPE-SH", pattern: /\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/, evidence: "pipes a network download straight into a shell", remote: true },
|
|
13
|
+
{ id: "NET-FETCH", pattern: /\b(curl|wget)\b|\bfetch\s*\(|https?\s*\.get\s*\(|requests\.(get|post)\b|Invoke-WebRequest\b|Invoke-RestMethod\b/, evidence: "performs a network fetch", remote: true },
|
|
14
|
+
{ id: "REMOTE-URL", pattern: /https?:\/\/[^\s"'`]+/, evidence: "references a remote URL", remote: true },
|
|
15
|
+
{ id: "CRED-ACCESS", pattern: /~\/\.ssh|\$HOME\/\.ssh|\.aws\/|~\/\.aws|\.gnupg|id_rsa|id_ed25519|\.npmrc\b|\.netrc\b|credentials(\.json)?\b|\.config\/gh|keychain/i, evidence: "touches a credential store path", credential: true },
|
|
16
|
+
{ id: "ENV-ACCESS", pattern: /process\.env|os\.environ|getenv\s*\(|\benv\b.*\b(print|export)|\$\{?\s*(AWS_SECRET|AWS_ACCESS|GITHUB_TOKEN|GH_TOKEN|NPM_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY)/, evidence: "reads environment variables that commonly hold secrets" },
|
|
17
|
+
{ id: "ENV-EXFIL", pattern: /(process\.env|os\.environ)[\s\S]{0,80}(fetch|https?|request|post|send|upload)/i, evidence: "combines environment access with an outbound send pattern", credential: true, remote: true },
|
|
18
|
+
{ id: "OBFUSCATION", pattern: /base64\s+(-d|--decode)|atob\s*\(|Buffer\.from\s*\([^)]*base64|\\x[0-9a-fA-F]{2}|\beval\b\s*\(|\beval\b\s+["'$(]|Function\s*\(/, evidence: "uses decoding or dynamic evaluation often used to hide payloads" },
|
|
19
|
+
{ id: "SHELL-EXEC", pattern: /\b(child_process|execSync|spawnSync|exec\s*\()\b|\/bin\/(sh|bash)\b|powershell(\.exe)?\b|cmd\.exe\b/, evidence: "spawns a shell or child process" },
|
|
20
|
+
{ id: "WRITE-OUTSIDE", pattern: /(>>?|tee)\s+~\/|(>>?|tee)\s+\$HOME\/|chmod\s+\+x/, evidence: "writes outside the repo or changes executable bits" },
|
|
21
|
+
{ id: "DOWNLOAD-EXEC", pattern: /\bnode\s+-e\b|\bpython3?\s+-c\b|\bdeno\s+eval\b|\bbun\s+-e\b/, evidence: "executes inline code supplied on the command line" },
|
|
22
|
+
];
|
|
23
|
+
const SAFE_PATTERNS = [
|
|
24
|
+
/^(npm|npx|yarn|pnpm|bun)\s+(run\s+)?(test|lint|build|dev|start|typecheck|check|format|prepare:.*|prepublishOnly.*)$/i,
|
|
25
|
+
/^(npx\s+)?(tsc|eslint|prettier|vitest|jest|mocha|husky|lint-staged|lefthook|pre-commit)(\s|$)/i,
|
|
26
|
+
/^node\s+(--test|scripts\/|tools\/|build)/i,
|
|
27
|
+
/^(echo|true|exit 0)\b/i,
|
|
28
|
+
/^git\s+(config|rev-parse|status)/i,
|
|
29
|
+
/^patch-package$/i,
|
|
30
|
+
/^simple-git-hooks$/i,
|
|
31
|
+
];
|
|
32
|
+
export function classifyCommand(command) {
|
|
33
|
+
// Normalize shell line continuations and stray carriage returns first, so a
|
|
34
|
+
// payload split as `curl ... \` + newline + `| sh` classifies the same as
|
|
35
|
+
// the one line form.
|
|
36
|
+
const cmd = command.replace(/\\\r?\n/g, " ").replace(/\r/g, " ").trim();
|
|
37
|
+
const fired = [];
|
|
38
|
+
for (const rule of RISKY_RULES) {
|
|
39
|
+
if (rule.pattern.test(cmd))
|
|
40
|
+
fired.push(rule);
|
|
41
|
+
}
|
|
42
|
+
if (fired.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
classification: "RISKY",
|
|
45
|
+
rules: fired.map((r) => r.id),
|
|
46
|
+
evidence: fired.map((r) => `${r.id}: ${r.evidence}`).join("; "),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
for (const safe of SAFE_PATTERNS) {
|
|
50
|
+
if (safe.test(cmd)) {
|
|
51
|
+
return { classification: "SAFE", rules: [], evidence: "matches a known safe dev pattern (test, lint, build, dev tooling)" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// A bare local script or binary with no risky indicators is UNKNOWN, not SAFE:
|
|
55
|
+
// we cannot prove what a local file does without reading it, and when the
|
|
56
|
+
// referenced local file itself contains risky patterns we escalate below.
|
|
57
|
+
return { classification: "UNKNOWN", rules: [], evidence: "no known safe pattern and no risky pattern matched; manual review needed" };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* If a command references local script files, inspect those files for risky
|
|
61
|
+
* patterns too (one hop). References are found by token, not only after an
|
|
62
|
+
* interpreter name, so `node setup.mjs`, `./setup.sh`, `node --require
|
|
63
|
+
* ./setup.mjs`, and `bash setup.sh` all resolve. References that escape the
|
|
64
|
+
* scanned root are never followed.
|
|
65
|
+
*/
|
|
66
|
+
function escalateWithLocalFile(root, finding) {
|
|
67
|
+
const tokenRe = /[A-Za-z0-9_.\/-]+\.(?:mjs|cjs|js|ts|sh|py)\b/g;
|
|
68
|
+
const tokens = Array.from(new Set(finding.command.match(tokenRe) ?? [])).slice(0, 5);
|
|
69
|
+
if (tokens.length === 0)
|
|
70
|
+
return finding;
|
|
71
|
+
const rootAbs = resolve(root);
|
|
72
|
+
let out = finding;
|
|
73
|
+
for (const token of tokens) {
|
|
74
|
+
const rel = token.replace(/^\.\//, "");
|
|
75
|
+
if (rel.includes("://"))
|
|
76
|
+
continue;
|
|
77
|
+
const abs = resolve(rootAbs, rel);
|
|
78
|
+
if (abs !== rootAbs && !abs.startsWith(rootAbs + sep))
|
|
79
|
+
continue;
|
|
80
|
+
if (!existsSync(abs))
|
|
81
|
+
continue;
|
|
82
|
+
let st;
|
|
83
|
+
try {
|
|
84
|
+
st = statSync(abs);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!st.isFile())
|
|
90
|
+
continue;
|
|
91
|
+
let content = "";
|
|
92
|
+
try {
|
|
93
|
+
content = readFileSync(abs, "utf8").slice(0, 200000);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const inner = classifyCommand(content.replace(/\n/g, " "));
|
|
99
|
+
if (inner.classification === "RISKY") {
|
|
100
|
+
const rules = Array.from(new Set([...out.rules, ...inner.rules]));
|
|
101
|
+
out = {
|
|
102
|
+
...out,
|
|
103
|
+
classification: "RISKY",
|
|
104
|
+
rules,
|
|
105
|
+
evidence: `${out.evidence === "" ? "" : out.evidence + "; "}referenced local file ${rel} is RISKY: ${inner.evidence}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** Classify a command string, including one hop into local files it runs. */
|
|
112
|
+
function classifyWithLocalFile(root, source, command) {
|
|
113
|
+
const c = classifyCommand(command);
|
|
114
|
+
const temp = { id: "", file: "", source, hook: "", command, classification: c.classification, rules: c.rules, evidence: c.evidence };
|
|
115
|
+
const escalated = escalateWithLocalFile(root, temp);
|
|
116
|
+
return { classification: escalated.classification, rules: escalated.rules, evidence: escalated.evidence };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A watched install script may chain to another package.json script
|
|
120
|
+
* (`postinstall: npm run evil`). Follow that chain one hop: if the target
|
|
121
|
+
* script is RISKY (directly or through a local file it runs), the install
|
|
122
|
+
* hook inherits the classification.
|
|
123
|
+
*/
|
|
124
|
+
function escalateWithScriptChain(root, scripts, currentName, finding) {
|
|
125
|
+
const chainRe = /\b(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?([A-Za-z0-9:_-]+)/g;
|
|
126
|
+
let out = finding;
|
|
127
|
+
let m;
|
|
128
|
+
while ((m = chainRe.exec(finding.command)) !== null) {
|
|
129
|
+
const name = m[1];
|
|
130
|
+
if (name === currentName)
|
|
131
|
+
continue;
|
|
132
|
+
const chained = scripts[name];
|
|
133
|
+
if (typeof chained !== "string" || !chained.trim())
|
|
134
|
+
continue;
|
|
135
|
+
const inner = classifyWithLocalFile(root, "package.json", chained);
|
|
136
|
+
if (inner.classification === "RISKY") {
|
|
137
|
+
const rules = Array.from(new Set([...out.rules, ...inner.rules]));
|
|
138
|
+
out = {
|
|
139
|
+
...out,
|
|
140
|
+
classification: "RISKY",
|
|
141
|
+
rules,
|
|
142
|
+
evidence: `${out.evidence === "" ? "" : out.evidence + "; "}via script chain to "${name}": ${inner.evidence}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
export function computeVerdict(findings) {
|
|
149
|
+
let review = false;
|
|
150
|
+
for (const f of findings) {
|
|
151
|
+
if (f.classification === "RISKY") {
|
|
152
|
+
const hasRemote = f.rules.some((id) => RISKY_RULES.find((r) => r.id === id)?.remote);
|
|
153
|
+
const hasCred = f.rules.some((id) => RISKY_RULES.find((r) => r.id === id)?.credential);
|
|
154
|
+
const hasSevere = f.rules.some((id) => RISKY_RULES.find((r) => r.id === id)?.severe);
|
|
155
|
+
if (hasRemote || hasCred || hasSevere)
|
|
156
|
+
return "COMPROMISED-PATTERN";
|
|
157
|
+
review = true;
|
|
158
|
+
}
|
|
159
|
+
else if (f.classification === "UNKNOWN") {
|
|
160
|
+
review = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return review ? "REVIEW" : "CLEAN";
|
|
164
|
+
}
|
|
165
|
+
function makeFinding(root, file, source, hook, command, idSuffix) {
|
|
166
|
+
const c = classifyCommand(command);
|
|
167
|
+
const id = `${file}:${hook}${idSuffix !== undefined ? `:${idSuffix}` : ""}`;
|
|
168
|
+
const base = {
|
|
169
|
+
id, file, source, hook, command,
|
|
170
|
+
classification: c.classification, rules: c.rules,
|
|
171
|
+
evidence: c.evidence,
|
|
172
|
+
};
|
|
173
|
+
return escalateWithLocalFile(root, base);
|
|
174
|
+
}
|
|
175
|
+
function readText(abs) {
|
|
176
|
+
try {
|
|
177
|
+
return readFileSync(abs, "utf8");
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function readJson(root, rel, warnings) {
|
|
184
|
+
const raw = readText(join(root, rel));
|
|
185
|
+
if (raw === null)
|
|
186
|
+
return null;
|
|
187
|
+
// Strip a UTF-8 BOM first: Windows editors add one, JSON.parse rejects it,
|
|
188
|
+
// and skipping the file would hide every hook inside it.
|
|
189
|
+
// Strip simple JSONC comments and trailing commas for VS Code style files.
|
|
190
|
+
const cleaned = raw
|
|
191
|
+
.replace(/^\uFEFF/, "")
|
|
192
|
+
.replace(/^\s*\/\/.*$/gm, "")
|
|
193
|
+
.replace(/,\s*([}\]])/g, "$1");
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(cleaned);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
warnings.push(`Malformed JSON in ${rel}: file skipped for structured parsing`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function scanPackageJson(root, findings, warnings) {
|
|
203
|
+
const rel = "package.json";
|
|
204
|
+
if (!existsSync(join(root, rel)))
|
|
205
|
+
return;
|
|
206
|
+
const data = readJson(root, rel, warnings);
|
|
207
|
+
if (!data) {
|
|
208
|
+
// readJson already warned when the file exists but is malformed. If it
|
|
209
|
+
// parsed to a non object, note it as well.
|
|
210
|
+
if (readText(join(root, rel)) !== null && !warnings.some((w) => w.includes(rel))) {
|
|
211
|
+
warnings.push(`Could not parse ${rel} as an object`);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (typeof data !== "object" || Array.isArray(data)) {
|
|
216
|
+
warnings.push(`${rel} root is not a JSON object: file skipped for structured parsing`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (data.scripts !== undefined && (typeof data.scripts !== "object" || data.scripts === null || Array.isArray(data.scripts))) {
|
|
220
|
+
warnings.push(`scripts key in ${rel} is not an object: install hooks skipped`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const scripts = (data.scripts ?? {});
|
|
224
|
+
const watched = ["preinstall", "install", "postinstall", "prepare", "prepublish", "prepublishOnly"];
|
|
225
|
+
for (const name of watched) {
|
|
226
|
+
const cmd = scripts[name];
|
|
227
|
+
if (typeof cmd === "string" && cmd.trim()) {
|
|
228
|
+
const finding = makeFinding(root, rel, "package.json", name, cmd);
|
|
229
|
+
findings.push(escalateWithScriptChain(root, scripts, name, finding));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function collectClaudeHooks(node, file, root, findings, prefix) {
|
|
234
|
+
if (!node || typeof node !== "object")
|
|
235
|
+
return;
|
|
236
|
+
const obj = node;
|
|
237
|
+
if (typeof obj.command === "string" && obj.command.trim()) {
|
|
238
|
+
findings.push(makeFinding(root, file, "claude-settings", prefix || "hook", obj.command, String(findings.length)));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
value.forEach((item, i) => collectClaudeHooks(item, file, root, findings, prefix ? `${prefix}:${key}` : key));
|
|
244
|
+
}
|
|
245
|
+
else if (value && typeof value === "object") {
|
|
246
|
+
collectClaudeHooks(value, file, root, findings, prefix ? `${prefix}:${key}` : key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function scanClaude(root, findings, warnings) {
|
|
251
|
+
for (const rel of [".claude/settings.json", ".claude/settings.local.json"]) {
|
|
252
|
+
if (!existsSync(join(root, rel)))
|
|
253
|
+
continue;
|
|
254
|
+
const data = readJson(root, rel, warnings);
|
|
255
|
+
if (!data || typeof data !== "object")
|
|
256
|
+
continue;
|
|
257
|
+
if (!("hooks" in data)) {
|
|
258
|
+
warnings.push(`No hooks key in ${rel}`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const hooks = data.hooks;
|
|
262
|
+
if (!hooks || typeof hooks !== "object") {
|
|
263
|
+
warnings.push(`hooks key in ${rel} is not an object: hooks skipped`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
collectClaudeHooks(hooks, rel, root, findings, "");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function scanVscode(root, findings, warnings) {
|
|
270
|
+
const rel = ".vscode/tasks.json";
|
|
271
|
+
if (existsSync(join(root, rel))) {
|
|
272
|
+
const data = readJson(root, rel, warnings);
|
|
273
|
+
if (data && data.tasks !== undefined && !Array.isArray(data.tasks)) {
|
|
274
|
+
warnings.push(`tasks key in ${rel} is not an array: tasks skipped`);
|
|
275
|
+
}
|
|
276
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
277
|
+
tasks.forEach((rawTask, idx) => {
|
|
278
|
+
// Malformed entries (null, strings, numbers) are skipped, never fatal:
|
|
279
|
+
// one bad task must not hide every other surface in the repo.
|
|
280
|
+
if (!rawTask || typeof rawTask !== "object")
|
|
281
|
+
return;
|
|
282
|
+
const task = rawTask;
|
|
283
|
+
const runOptions = (task.runOptions ?? {});
|
|
284
|
+
const options = (task.options ?? {});
|
|
285
|
+
const optionsRunOptions = (options.runOptions ?? {});
|
|
286
|
+
// folderOpen lives in runOptions in the standard schema, but real
|
|
287
|
+
// world files also nest it under options or put runOn on the task.
|
|
288
|
+
const runOn = runOptions.runOn ?? optionsRunOptions.runOn ?? task.runOn;
|
|
289
|
+
const isFolderOpen = runOn === "folderOpen" || (Array.isArray(runOn) && runOn.includes("folderOpen"));
|
|
290
|
+
const command = typeof task.command === "string" ? task.command : "";
|
|
291
|
+
const args = Array.isArray(task.args) ? task.args.filter((a) => typeof a === "string").join(" ") : "";
|
|
292
|
+
const full = [command, args].filter(Boolean).join(" ").trim();
|
|
293
|
+
if (!full)
|
|
294
|
+
return;
|
|
295
|
+
const label = typeof task.label === "string" ? task.label : `task-${idx}`;
|
|
296
|
+
if (isFolderOpen) {
|
|
297
|
+
findings.push(makeFinding(root, rel, "vscode-tasks", `folderOpen:${label}`, full, String(idx)));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// Inventory non auto-run tasks at lower weight: still record them so
|
|
301
|
+
// explain can find them, classified by their command.
|
|
302
|
+
const f = makeFinding(root, rel, "vscode-tasks", label, full, String(idx));
|
|
303
|
+
findings.push(f);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// MCP config inside .vscode
|
|
308
|
+
scanMcpFile(root, ".vscode/mcp.json", findings, warnings);
|
|
309
|
+
}
|
|
310
|
+
function scanDirFiles(root, dirRel, source, findings) {
|
|
311
|
+
const abs = join(root, dirRel);
|
|
312
|
+
if (!existsSync(abs))
|
|
313
|
+
return;
|
|
314
|
+
let entries = [];
|
|
315
|
+
try {
|
|
316
|
+
entries = readdirSync(abs);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
for (const name of entries.sort()) {
|
|
322
|
+
const fileAbs = join(abs, name);
|
|
323
|
+
let st;
|
|
324
|
+
try {
|
|
325
|
+
st = statSync(fileAbs);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!st.isFile())
|
|
331
|
+
continue;
|
|
332
|
+
if (name === "_" || name.startsWith("."))
|
|
333
|
+
continue;
|
|
334
|
+
const content = readText(fileAbs);
|
|
335
|
+
if (content === null)
|
|
336
|
+
continue;
|
|
337
|
+
const firstMeaningful = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("set -")).join(" ");
|
|
338
|
+
const fileRel = `${dirRel}/${name}`;
|
|
339
|
+
// Entire file content is the command surface for husky style hooks.
|
|
340
|
+
// Classify generously deep into the file: a payload hidden behind
|
|
341
|
+
// padding must still be seen. Display code truncates separately.
|
|
342
|
+
findings.push(makeFinding(root, fileRel, source, name, content.trim().slice(0, 200000)));
|
|
343
|
+
void firstMeaningful;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function scanLefthook(root, findings) {
|
|
347
|
+
for (const rel of ["lefthook.yml", "lefthook.yaml", ".lefthook.yml"]) {
|
|
348
|
+
if (!existsSync(join(root, rel)))
|
|
349
|
+
continue;
|
|
350
|
+
const raw = readText(join(root, rel)) ?? "";
|
|
351
|
+
const runRe = /^\s*run:\s*(.+)$/gm;
|
|
352
|
+
let m;
|
|
353
|
+
let i = 0;
|
|
354
|
+
while ((m = runRe.exec(raw)) !== null) {
|
|
355
|
+
findings.push(makeFinding(root, rel, "lefthook", "run", m[1].trim().replace(/^["']|["']$/g, ""), String(i++)));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function scanPreCommit(root, findings) {
|
|
360
|
+
const rel = ".pre-commit-config.yaml";
|
|
361
|
+
if (!existsSync(join(root, rel)))
|
|
362
|
+
return;
|
|
363
|
+
const raw = readText(join(root, rel)) ?? "";
|
|
364
|
+
const entryRe = /^\s*entry:\s*(.+)$/gm;
|
|
365
|
+
let m;
|
|
366
|
+
let i = 0;
|
|
367
|
+
while ((m = entryRe.exec(raw)) !== null) {
|
|
368
|
+
findings.push(makeFinding(root, rel, "pre-commit", "entry", m[1].trim().replace(/^["']|["']$/g, ""), String(i++)));
|
|
369
|
+
}
|
|
370
|
+
const repoRe = /^\s*-\s*repo:\s*(https?:\/\/\S+)$/gm;
|
|
371
|
+
while ((m = repoRe.exec(raw)) !== null) {
|
|
372
|
+
// A remote hook repo is the standard pre-commit pattern, not a finding
|
|
373
|
+
// of compromise: the URL alone is inventoried as UNKNOWN so a human can
|
|
374
|
+
// review the pinned rev. Classifying the bare URL with the command
|
|
375
|
+
// rules would verdict every normal pre-commit config COMPROMISED.
|
|
376
|
+
findings.push({
|
|
377
|
+
id: `${rel}:remote-repo:${i++}`,
|
|
378
|
+
file: rel,
|
|
379
|
+
source: "pre-commit",
|
|
380
|
+
hook: "remote-repo",
|
|
381
|
+
command: m[1].trim(),
|
|
382
|
+
classification: "UNKNOWN",
|
|
383
|
+
rules: [],
|
|
384
|
+
evidence: "remote pre-commit hook repo (standard pattern): its hooks run locally at commit time; hookcheck cannot verify remote contents, review the pinned rev",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function scanAgentDirs(root, findings, warnings) {
|
|
389
|
+
for (const [dir, source] of [[".cursor", "cursor"], [".codex", "codex"]]) {
|
|
390
|
+
const abs = join(root, dir);
|
|
391
|
+
if (!existsSync(abs))
|
|
392
|
+
continue;
|
|
393
|
+
let entries = [];
|
|
394
|
+
try {
|
|
395
|
+
entries = readdirSync(abs);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
for (const name of entries.sort()) {
|
|
401
|
+
const fileAbs = join(abs, name);
|
|
402
|
+
let st;
|
|
403
|
+
try {
|
|
404
|
+
st = statSync(fileAbs);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!st.isFile())
|
|
410
|
+
continue;
|
|
411
|
+
const rel = `${dir}/${name}`;
|
|
412
|
+
if (name.endsWith(".json")) {
|
|
413
|
+
const data = readJson(root, rel, warnings);
|
|
414
|
+
if (data)
|
|
415
|
+
collectGenericCommands(data, rel, root, findings, source);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const content = readText(fileAbs);
|
|
419
|
+
if (content && content.trim()) {
|
|
420
|
+
findings.push(makeFinding(root, rel, source, name, content.trim().slice(0, 200000)));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (name === "mcp.json")
|
|
424
|
+
scanMcpFile(root, rel, findings, warnings);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function collectGenericCommands(node, file, root, findings, source, prefix = "") {
|
|
429
|
+
if (!node || typeof node !== "object")
|
|
430
|
+
return;
|
|
431
|
+
const obj = node;
|
|
432
|
+
if (typeof obj.command === "string" && obj.command.trim()) {
|
|
433
|
+
const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string").join(" ") : "";
|
|
434
|
+
findings.push(makeFinding(root, file, source, prefix || "command", [obj.command, args].filter(Boolean).join(" "), String(findings.length)));
|
|
435
|
+
}
|
|
436
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
437
|
+
if (value && typeof value === "object")
|
|
438
|
+
collectGenericCommands(value, file, root, findings, source, prefix ? `${prefix}:${key}` : key);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function scanMcpFile(root, rel, findings, warnings) {
|
|
442
|
+
if (!existsSync(join(root, rel)))
|
|
443
|
+
return;
|
|
444
|
+
const data = readJson(root, rel, warnings);
|
|
445
|
+
if (!data)
|
|
446
|
+
return;
|
|
447
|
+
const servers = (data.mcpServers ?? data.servers ?? {});
|
|
448
|
+
if (!servers || typeof servers !== "object")
|
|
449
|
+
return;
|
|
450
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
451
|
+
if (!cfg || typeof cfg !== "object")
|
|
452
|
+
continue;
|
|
453
|
+
const c = cfg;
|
|
454
|
+
const command = typeof c.command === "string" ? c.command : "";
|
|
455
|
+
const args = Array.isArray(c.args) ? c.args.filter((a) => typeof a === "string").join(" ") : "";
|
|
456
|
+
const full = [command, args].filter(Boolean).join(" ").trim();
|
|
457
|
+
if (full)
|
|
458
|
+
findings.push(makeFinding(root, rel, "mcp-config", `mcp:${name}`, full));
|
|
459
|
+
if (typeof c.url === "string" && c.url) {
|
|
460
|
+
findings.push(makeFinding(root, rel, "mcp-config", `mcp:${name}:url`, c.url));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function scanWorkflows(root, findings) {
|
|
465
|
+
const dir = ".github/workflows";
|
|
466
|
+
const abs = join(root, dir);
|
|
467
|
+
if (!existsSync(abs))
|
|
468
|
+
return;
|
|
469
|
+
let entries = [];
|
|
470
|
+
try {
|
|
471
|
+
entries = readdirSync(abs);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
for (const name of entries.sort()) {
|
|
477
|
+
if (!/\.(yml|yaml)$/.test(name))
|
|
478
|
+
continue;
|
|
479
|
+
const rel = `${dir}/${name}`;
|
|
480
|
+
const raw = readText(join(root, rel)) ?? "";
|
|
481
|
+
const runRe = /^\s*-?\s*run:\s*(.+)$/gm;
|
|
482
|
+
let m;
|
|
483
|
+
let i = 0;
|
|
484
|
+
while ((m = runRe.exec(raw)) !== null) {
|
|
485
|
+
const cmd = m[1].trim();
|
|
486
|
+
if (cmd === "|" || cmd === ">" || cmd === "")
|
|
487
|
+
continue;
|
|
488
|
+
findings.push(makeFinding(root, rel, "github-workflow", "run", cmd, String(i++)));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function scanGitHooks(root, findings) {
|
|
493
|
+
scanDirFiles(root, ".githooks", "githooks", findings);
|
|
494
|
+
}
|
|
495
|
+
export function scanHooks(root) {
|
|
496
|
+
const findings = [];
|
|
497
|
+
const warnings = [];
|
|
498
|
+
let rootStat = null;
|
|
499
|
+
try {
|
|
500
|
+
rootStat = statSync(root);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
rootStat = null;
|
|
504
|
+
}
|
|
505
|
+
if (!rootStat) {
|
|
506
|
+
warnings.push(`Path does not exist: ${root}`);
|
|
507
|
+
return {
|
|
508
|
+
path: root, verdict: "REVIEW", findings, warnings,
|
|
509
|
+
counts: { total: 0, safe: 0, risky: 0, unknown: 0 },
|
|
510
|
+
scannedAt: new Date().toISOString(),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (!rootStat.isDirectory()) {
|
|
514
|
+
warnings.push(`Path is not a directory: ${root}`);
|
|
515
|
+
return {
|
|
516
|
+
path: root, verdict: "REVIEW", findings, warnings,
|
|
517
|
+
counts: { total: 0, safe: 0, risky: 0, unknown: 0 },
|
|
518
|
+
scannedAt: new Date().toISOString(),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
scanPackageJson(root, findings, warnings);
|
|
522
|
+
scanClaude(root, findings, warnings);
|
|
523
|
+
scanVscode(root, findings, warnings);
|
|
524
|
+
scanDirFiles(root, ".husky", "husky", findings);
|
|
525
|
+
scanLefthook(root, findings);
|
|
526
|
+
scanPreCommit(root, findings);
|
|
527
|
+
scanAgentDirs(root, findings, warnings);
|
|
528
|
+
for (const rel of [".mcp.json", "mcp.json", ".cursor/mcp.json"])
|
|
529
|
+
scanMcpFile(root, rel, findings, warnings);
|
|
530
|
+
scanWorkflows(root, findings);
|
|
531
|
+
scanGitHooks(root, findings);
|
|
532
|
+
// Dedupe identical ids (agent dir scan may double count mcp.json).
|
|
533
|
+
const seen = new Set();
|
|
534
|
+
const deduped = findings.filter((f) => {
|
|
535
|
+
const key = `${f.id}|${f.command}`;
|
|
536
|
+
if (seen.has(key))
|
|
537
|
+
return false;
|
|
538
|
+
seen.add(key);
|
|
539
|
+
return true;
|
|
540
|
+
});
|
|
541
|
+
const counts = {
|
|
542
|
+
total: deduped.length,
|
|
543
|
+
safe: deduped.filter((f) => f.classification === "SAFE").length,
|
|
544
|
+
risky: deduped.filter((f) => f.classification === "RISKY").length,
|
|
545
|
+
unknown: deduped.filter((f) => f.classification === "UNKNOWN").length,
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
path: root,
|
|
549
|
+
verdict: computeVerdict(deduped),
|
|
550
|
+
findings: deduped,
|
|
551
|
+
counts,
|
|
552
|
+
warnings,
|
|
553
|
+
scannedAt: new Date().toISOString(),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
export function explainHook(root, idOrFile) {
|
|
557
|
+
const report = scanHooks(root);
|
|
558
|
+
const finding = report.findings.find((f) => f.id === idOrFile || f.file === idOrFile || f.id.startsWith(idOrFile)) ?? null;
|
|
559
|
+
if (!finding) {
|
|
560
|
+
return { finding: null, explanation: `No hook matched "${idOrFile}". ${report.findings.length} hook(s) found in this repo.`, report };
|
|
561
|
+
}
|
|
562
|
+
const parts = [
|
|
563
|
+
`Hook ${finding.id} in ${finding.file} (${finding.source}, event ${finding.hook}).`,
|
|
564
|
+
`Command: ${finding.command}`,
|
|
565
|
+
`Classification: ${finding.classification}.`,
|
|
566
|
+
`Evidence: ${finding.evidence || "none"}.`,
|
|
567
|
+
finding.classification === "RISKY"
|
|
568
|
+
? "This hook runs automatically and matched risky patterns. Treat the repo as untrusted until this hook is removed or proven benign."
|
|
569
|
+
: finding.classification === "SAFE"
|
|
570
|
+
? "This hook matches known safe dev tooling patterns and matched no risky rules."
|
|
571
|
+
: "This hook matched neither safe nor risky patterns. Read the command and any file it invokes before trusting this repo.",
|
|
572
|
+
];
|
|
573
|
+
return { finding, explanation: parts.join(" "), report };
|
|
574
|
+
}
|
|
575
|
+
export function renderReport(report) {
|
|
576
|
+
const lines = [];
|
|
577
|
+
lines.push(`hookcheck ${report.path}: ${report.verdict}`);
|
|
578
|
+
lines.push(` ${report.counts.total} hook(s): ${report.counts.safe} SAFE, ${report.counts.risky} RISKY, ${report.counts.unknown} UNKNOWN`);
|
|
579
|
+
for (const f of report.findings) {
|
|
580
|
+
lines.push(` [${f.classification}] ${f.id} (${f.source})`);
|
|
581
|
+
lines.push(` command: ${f.command.slice(0, 200)}`);
|
|
582
|
+
if (f.rules.length)
|
|
583
|
+
lines.push(` rules: ${f.rules.join(", ")}`);
|
|
584
|
+
lines.push(` evidence: ${f.evidence}`);
|
|
585
|
+
}
|
|
586
|
+
for (const w of report.warnings)
|
|
587
|
+
lines.push(` [warn] ${w}`);
|
|
588
|
+
if (report.verdict === "CLEAN")
|
|
589
|
+
lines.push("CLEAN: no risky or unknown auto-exec hooks found.");
|
|
590
|
+
else if (report.verdict === "REVIEW")
|
|
591
|
+
lines.push("REVIEW: at least one hook needs manual review.");
|
|
592
|
+
else
|
|
593
|
+
lines.push("COMPROMISED-PATTERN: at least one risky hook fetches remote code or touches credential stores. Treat as a Shai-Hulud style persistence pattern until proven otherwise.");
|
|
594
|
+
return lines.join("\n");
|
|
595
|
+
}
|
|
596
|
+
export { relative };
|
|
597
|
+
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC"}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hookcheck MCP server. Tools: scan_hooks, explain_hook.
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { explainHook, scanHooks, VERSION } from "./core.js";
|
|
8
|
+
const server = new McpServer({ name: "hookcheck", version: VERSION });
|
|
9
|
+
server.tool("scan_hooks", "Scan a local repo directory for auto-exec hooks: npm install scripts, Claude Code settings hooks, VS Code folderOpen tasks, husky, lefthook, pre-commit, Cursor and Codex configs, MCP autostart configs, and GitHub workflow run steps. Classifies each hook SAFE, RISKY, or UNKNOWN and returns a CLEAN, REVIEW, or COMPROMISED-PATTERN verdict with evidence per finding. Deterministic, local only, no network.", { path: z.string().describe("Local directory to scan, e.g. /home/user/project") }, async ({ path }) => {
|
|
10
|
+
const report = scanHooks(path);
|
|
11
|
+
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
12
|
+
});
|
|
13
|
+
server.tool("explain_hook", "Explain one hook found by scan_hooks: its command, classification, rules that fired, and what the classification means. Pass the hook id (e.g. package.json:postinstall) or its file path.", {
|
|
14
|
+
path: z.string().describe("Local directory to scan"),
|
|
15
|
+
hookId: z.string().optional().describe("Hook id from a scan report, e.g. package.json:postinstall"),
|
|
16
|
+
file: z.string().optional().describe("File path of the hook as an alternative selector"),
|
|
17
|
+
}, async ({ path, hookId, file }) => {
|
|
18
|
+
const selector = hookId ?? file ?? "";
|
|
19
|
+
const result = explainHook(path, selector);
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21
|
+
});
|
|
22
|
+
async function main() {
|
|
23
|
+
await server.connect(new StdioServerTransport());
|
|
24
|
+
}
|
|
25
|
+
void main();
|
|
26
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE5D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAEtE,MAAM,CAAC,IAAI,CACT,YAAY,EACZ,qZAAqZ,EACrZ,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,kDAAkD,CAAC,EAAE,EACjF,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;IACjB,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACzF,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,IAAI,CACT,cAAc,EACd,4LAA4L,EAC5L;IACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IACpD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2DAA2D,CAAC;IACnG,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kDAAkD,CAAC;CACzF,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;IAC/B,MAAM,QAAQ,GAAG,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3C,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACzF,CAAC,CACF,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,IAAI,EAAE,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hookcheck types.
|
|
3
|
+
*
|
|
4
|
+
* A HookFinding is one auto-exec hook discovered in a repo: where it lives,
|
|
5
|
+
* the exact command text, how it was classified, and the rule ids plus
|
|
6
|
+
* evidence that produced that classification. Nothing is inferred silently.
|
|
7
|
+
*/
|
|
8
|
+
export type Classification = "SAFE" | "RISKY" | "UNKNOWN";
|
|
9
|
+
export type Verdict = "CLEAN" | "REVIEW" | "COMPROMISED-PATTERN";
|
|
10
|
+
export type HookSource = "package.json" | "claude-settings" | "vscode-tasks" | "husky" | "lefthook" | "pre-commit" | "cursor" | "codex" | "mcp-config" | "github-workflow" | "githooks";
|
|
11
|
+
export interface HookFinding {
|
|
12
|
+
/** Stable id, e.g. "package.json:postinstall" or ".claude/settings.json:SessionStart:0". */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Repo relative file path the hook was found in. */
|
|
15
|
+
file: string;
|
|
16
|
+
source: HookSource;
|
|
17
|
+
/** Hook name or event, e.g. postinstall, SessionStart, folderOpen. */
|
|
18
|
+
hook: string;
|
|
19
|
+
/** Exact command text that would auto-execute. */
|
|
20
|
+
command: string;
|
|
21
|
+
classification: Classification;
|
|
22
|
+
/** Rule ids that fired, e.g. ["NET-FETCH", "CRED-ACCESS"]. Empty for SAFE. */
|
|
23
|
+
rules: string[];
|
|
24
|
+
/** Human readable evidence for the classification. */
|
|
25
|
+
evidence: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ScanReport {
|
|
28
|
+
path: string;
|
|
29
|
+
verdict: Verdict;
|
|
30
|
+
findings: HookFinding[];
|
|
31
|
+
counts: {
|
|
32
|
+
total: number;
|
|
33
|
+
safe: number;
|
|
34
|
+
risky: number;
|
|
35
|
+
unknown: number;
|
|
36
|
+
};
|
|
37
|
+
warnings: string[];
|
|
38
|
+
scannedAt: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ExplainResult {
|
|
41
|
+
finding: HookFinding | null;
|
|
42
|
+
explanation: string;
|
|
43
|
+
report: ScanReport;
|
|
44
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hookcheck types.
|
|
3
|
+
*
|
|
4
|
+
* A HookFinding is one auto-exec hook discovered in a repo: where it lives,
|
|
5
|
+
* the exact command text, how it was classified, and the rule ids plus
|
|
6
|
+
* evidence that produced that classification. Nothing is inferred silently.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@reneturcios/hookcheck",
|
|
3
|
-
"version": "0.0.0-stage",
|
|
4
|
-
"stub": true,
|
|
5
|
-
"description": "Temporary package placeholder for staged publishing"
|
|
6
|
-
}
|
|
1
|
+
{"bin":{"hookcheck":"dist/cli.js"},"dependencies":{"@modelcontextprotocol/sdk":"^1","zod":"^3"},"description":"AI persistence hook auditor: scan a repo for auto-exec hooks (npm install scripts, Claude Code hooks, VS Code folderOpen tasks, husky, MCP autostart) and flag Shai-Hulud style persistence patterns.","devDependencies":{"@types/node":"^20","tsx":"^4","typescript":"^5"},"engines":{"node":">=20"},"files":["dist","server.json"],"homepage":"https://github.com/CloudCorpRecords/hookcheck#readme","license":"MIT","mcpName":"io.github.CloudCorpRecords/hookcheck","name":"@reneturcios/hookcheck","private":false,"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/CloudCorpRecords/hookcheck.git"},"scripts":{"build":"tsc","dev":"tsx src/cli.ts","test":"tsx --test test/*.test.ts"},"type":"module","version":"0.1.1"}
|
package/server.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"description": "Persistence hook auditor: scan repos for auto-exec hooks, flag Shai-Hulud patterns.",
|
|
4
|
+
"name": "io.github.CloudCorpRecords/hookcheck",
|
|
5
|
+
"packages": [
|
|
6
|
+
{
|
|
7
|
+
"identifier": "@reneturcios/hookcheck",
|
|
8
|
+
"packageArguments": [
|
|
9
|
+
{
|
|
10
|
+
"type": "positional",
|
|
11
|
+
"value": "mcp"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
|
+
"registryType": "npm",
|
|
16
|
+
"runtimeHint": "npx",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
},
|
|
20
|
+
"version": "0.1.1"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"identifier": "ghcr.io/cloudcorprecords/hookcheck:0.1.1",
|
|
24
|
+
"packageArguments": [
|
|
25
|
+
{
|
|
26
|
+
"type": "positional",
|
|
27
|
+
"value": "mcp"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"registryType": "oci",
|
|
31
|
+
"runtimeArguments": [
|
|
32
|
+
{
|
|
33
|
+
"name": "--rm",
|
|
34
|
+
"type": "named"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "-i",
|
|
38
|
+
"type": "named"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"runtimeHint": "docker",
|
|
42
|
+
"transport": {
|
|
43
|
+
"type": "stdio"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"source": "github",
|
|
49
|
+
"url": "https://github.com/CloudCorpRecords/hookcheck"
|
|
50
|
+
},
|
|
51
|
+
"version": "0.1.1"
|
|
52
|
+
}
|