crumplelab 0.2.0 → 0.2.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/scan.mjs +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- `crumple scan` no longer flags exact, fixed commands such as `Bash(curl -s http://localhost:4173/sitemap.xml)`.
|
|
6
|
+
Only wildcard network rules can carry data out, so only those count.
|
|
7
|
+
- Single MCP tools (`mcp__server__tool`) are no longer flagged; whole servers and open `WebFetch` still are.
|
|
8
|
+
- Hooks in your own `.claude/settings.local.json` are no longer reported as shipped by the repository.
|
|
9
|
+
- Packages with install scripts are listed once each.
|
|
10
|
+
|
|
3
11
|
## 0.2.0
|
|
4
12
|
|
|
5
13
|
- 18 built-in traps, each from a real CVE, incident or paper, mapped to OWASP LLM, OWASP Agentic (ASI)
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Crumple: crash-test your AI coding agent
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **Version 0.2.** Needs Node 20+ and Docker (`scan` needs neither). So far Crumple's scores are
|
|
6
6
|
> verified against its built-in test agents; results for Claude Code, Codex and Gemini CLI will be published
|
|
7
7
|
> as they're run. Website: [crumple.priberry.com](https://crumple.priberry.com) · contact: crumple@priberry.com
|
|
8
8
|
|
package/package.json
CHANGED
package/src/scan.mjs
CHANGED
|
@@ -48,9 +48,11 @@ export function scan(projectDir, { home = os.homedir() } = {}) {
|
|
|
48
48
|
for (const rule of perm.allow ?? []) {
|
|
49
49
|
const r = String(rule);
|
|
50
50
|
if (/^Bash(\(\*\)|\(:\*\))?$/.test(r)) add("critical", where, `Every shell command is pre-approved (${r})`, "A prompt injection becomes code execution on your machine immediately.", `Remove "${r}" and allow specific commands instead.`);
|
|
51
|
-
|
|
51
|
+
// An exact rule only ever approves that one command, so only wildcards can carry your data out.
|
|
52
|
+
else if (/^Bash\((curl|wget|nc|ssh|scp|rsync|http)\b/.test(r) && r.includes("*")) add("high", where, `Network tool pre-approved (${r})`, "This is the exact command attackers use to send your .env out.", `Remove "${r}"; let the agent ask, or allow only fixed URLs.`);
|
|
52
53
|
else if (/^Bash\((git|find|fd|rg|go|xargs|env|sed|awk|tar|npx|npm|pnpm|yarn|node|python3?|bash|sh)(:\*|\s\*|\*)?\)$/.test(r)) add("high", where, `Broad pre-approval that can run any program (${r})`, "Flags like `git -c core.fsmonitor=`, `find -exec`, `npm run` and `npx` turn a \"safe\" prefix into arbitrary code (Trail of Bits, 2025).", `Narrow "${r}" to exact subcommands (for example "Bash(git status)"), and install the Crumple guard, which checks flags.`);
|
|
53
|
-
|
|
54
|
+
// A single MCP tool (mcp__server__tool) is a narrow approval; a whole server or open WebFetch is not.
|
|
55
|
+
else if (/^WebFetch$/.test(r) || (/^mcp__[\w-]+$/.test(r) && r.split("__").length === 2)) add("medium", where, `Pre-approved without limits (${r})`, "Lets content from anywhere drive the agent and receive data without a prompt.", `Scope "${r}" to specific domains or tools.`);
|
|
54
56
|
}
|
|
55
57
|
for (const d of perm.deny ?? []) if (/\.env/.test(String(d))) denyEnv = true;
|
|
56
58
|
if (s.enableAllProjectMcpServers) add("high", where, "Every MCP server a repository ships is started automatically", "Cloning a hostile repo starts its MCP server with your permissions (Check Point, CVE-2025-59536).", "Remove `enableAllProjectMcpServers` and approve servers one by one.");
|
|
@@ -61,7 +63,8 @@ export function scan(projectDir, { home = os.homedir() } = {}) {
|
|
|
61
63
|
const hooks = JSON.stringify(s.hooks ?? {});
|
|
62
64
|
if (/guard\.mjs|crumplelab guard/.test(hooks)) guarded = true;
|
|
63
65
|
if (/\b(curl|wget|nc)\b|https?:\/\//.test(hooks)) add("high", where, "A hook sends data over the network", "Hooks run automatically on every tool call or session start, without the agent or you approving them.", "Check every hook command; remove any that talk to the network.");
|
|
64
|
-
|
|
66
|
+
// settings.local.json is the user's own and stays out of git; only settings.json comes with a clone.
|
|
67
|
+
if (f === P(".claude/settings.json") && s.hooks && !/guard\.mjs/.test(hooks)) add("medium", where, "This repository ships its own hooks", "Hooks in a cloned repo run commands on your machine as soon as the agent starts.", "Review them before running an agent here.");
|
|
65
68
|
if (s.sandbox?.enabled) sandboxed = true;
|
|
66
69
|
}
|
|
67
70
|
if (!guarded) add("medium", "agent setup", "No runtime guard checks the agent's commands", "Nothing stops a hijacked agent from sending keys out or deleting data between your prompts.", "Run `crumple fix`.");
|
|
@@ -128,7 +131,7 @@ export function scan(projectDir, { home = os.homedir() } = {}) {
|
|
|
128
131
|
const npmrc = (read(P(".npmrc")) ?? "") + (read(H(".npmrc")) ?? "");
|
|
129
132
|
const lock = json(P("package-lock.json"));
|
|
130
133
|
if (lock && !/ignore-scripts\s*=\s*true/.test(npmrc)) {
|
|
131
|
-
const scripts = Object.entries(lock.packages ?? {}).filter(([k, v]) => k && v?.hasInstallScript).map(([k]) => k.split("node_modules/").pop());
|
|
134
|
+
const scripts = [...new Set(Object.entries(lock.packages ?? {}).filter(([k, v]) => k && v?.hasInstallScript).map(([k]) => k.split("node_modules/").pop()))];
|
|
132
135
|
if (scripts.length) add("medium", "package-lock.json", `${scripts.length} dependencies run install scripts (${scripts.slice(0, 4).join(", ")}${scripts.length > 4 ? "…" : ""})`, "Install scripts run with your keys in reach; this is how Shai-Hulud and the Nx attack spread.", "Set `ignore-scripts=true` in .npmrc and allow only the few that need it.");
|
|
133
136
|
}
|
|
134
137
|
|