-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathskillgate.js
More file actions
143 lines (134 loc) · 5.77 KB
/
Copy pathskillgate.js
File metadata and controls
143 lines (134 loc) · 5.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// forge skill-gate — vet a skill / MCP config BEFORE it's installed. The external
// ToxicSkills study reported ~36% of skills flawed / 13.4% critical, with ~60% of the
// risk in the instruction (markdown) layer — so string prose matters as much as code.
// Prefer a real scanner (Snyk agent-scan) when present; otherwise a built-in heuristic
// catches the loud, KNOWN-shape cases. A clean pass is NOT a safety certification.
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { shannonEntropy } from "./math.js";
// Instruction-layer + tool-layer red flags. `critical` blocks install.
const RULES = [
{
re: /\bcurl\b[^\n|]*\|\s*(sh|bash|zsh)\b/i,
sev: "critical",
msg: "pipes curl to a shell (remote code execution)",
},
{
re: /\bwget\b[^\n|]*\|\s*(sh|bash|zsh)\b/i,
sev: "critical",
msg: "pipes wget to a shell (remote code execution)",
},
{
re: /base64\s+(--decode|-d|-D)\b[^\n]*\|\s*(sh|bash)/i,
sev: "critical",
msg: "decodes and executes an obfuscated payload",
},
{
re: /ignore\s+(all\s+)?(your\s+)?previous\s+instructions|disregard\s+(your|the|all)\s+(system|previous|prior)/i,
sev: "critical",
msg: "prompt-injection phrasing in the instruction layer",
},
{
re: /(id_rsa|id_ed25519|\.aws\/credentials|\.claude\.json|\.env)\b[^\n]{0,80}\b(curl|wget|nc|https?:\/\/|base64|>\s*\/dev\/tcp)/i,
sev: "critical",
msg: "reads a credential file near a network/exfil call",
},
{
re: /\brm\s+-rf\s+[~/]/i,
sev: "high",
msg: "destructive recursive delete of an absolute/home path",
},
{
re: /\beval\s*\(\s*(await\s+)?fetch|child_process|\bexec(Sync)?\s*\(/i,
sev: "high",
msg: "dynamic code execution / process spawn",
},
];
// Obfuscation detector: a long base64-class blob whose Shannon entropy sits in
// encoded-payload territory. The RULES above are signatures for KNOWN attack shapes;
// entropy catches the unknown one hiding as an opaque blob in the instruction layer
// (nothing in a legitimate SKILL.md needs a 200-char undecodable string).
const BLOB_RE = /[A-Za-z0-9+/=]{200,}/g;
const BLOB_MIN_BITS = 4.5;
/** Pure: scan raw text for red flags. Returns findings [{sev, msg}]. */
export function heuristicScan(text) {
const s = String(text);
const findings = RULES.filter((r) => r.re.test(s)).map(({ sev, msg }) => ({
sev,
msg,
}));
const blobs = (s.match(BLOB_RE) || []).filter((b) => shannonEntropy(b) >= BLOB_MIN_BITS);
if (blobs.length) {
findings.push({
sev: "high",
msg: `${blobs.length} high-entropy encoded blob(s) — possible obfuscated payload`,
});
}
return findings;
}
/**
* Human-readable verdict. A scan with no critical/high signal is NOT a clearance to
* install — the heuristic only catches KNOWN attack shapes, so the honest verdict is the
* ABSENCE of a signature, never a certification of safety.
* @param {{critical?: boolean, high?: boolean}} sev
*/
export function verdict({ critical, high }) {
if (critical) return "BLOCKED — critical signature detected, do not install";
if (high)
return "High-severity signal(s) present — treat as unsafe; do not install without review";
return "No critical signature detected — this is NOT a safety certification. Review the source, permissions, package provenance, and network behaviour before installing.";
}
/** Run the pinned external scanner over `target`, returning its output (or null when it is
* not installed / failed). Injectable so the union logic below is testable without uvx. */
function runExternalScanner(target) {
// Pinned (verified 2026-07-05) — never @latest for code we execute; re-verify via dev-radar.
return execFileSync("uvx", ["snyk-agent-scan==0.5.12", target], {
encoding: "utf8",
stdio: "pipe",
timeout: 90000,
});
}
/**
* Scan a path (SKILL.md/.mcp.json) or raw text. The heuristic ALWAYS runs; a real scanner
* adds to it.
* @param {string} target
* @param {{runScanner?: (target: string) => string}} [opts]
*/
export function scan(target, { runScanner = runExternalScanner } = {}) {
const isPath = typeof target === "string" && existsSync(target);
const content = isPath ? readFileSync(target, "utf8") : String(target);
// The external scanner ADDS to the heuristic; it never replaces it (review B7). A clean
// exit from snyk-agent-scan used to return early, so the built-in signatures — the ones
// that catch `curl … | sh`, prompt injection and credential exfil — never ran, and a skill
// the scanner did not know made `ok: true`. Both run; the verdict is the union.
let raw = null;
let scannerCritical = false;
if (isPath && process.env.FORGE_SKILLGATE_NOEXTERNAL !== "1") {
try {
raw = runScanner(target);
scannerCritical = /\bcritical\b|tool poisoning|prompt injection|malicious/i.test(raw);
} catch (err) {
if (process.env.FORGE_DEBUG === "1")
process.stderr.write(
`forge skillgate: scanner failed, using heuristic only: ${err?.message ?? err}\n`,
);
}
}
const findings = heuristicScan(content);
if (scannerCritical) findings.push({ sev: "critical", msg: "snyk-agent-scan: critical finding" });
const critical = findings.some((f) => f.sev === "critical");
const high = findings.some((f) => f.sev === "high");
// `ok` gates install/exit-code on the blocking (critical) tier only, unchanged. `safe`
// is the stricter, honest reading: no critical AND no high — but even `safe` is only the
// absence of a known signature, never a certification (see `verdict`).
return {
ok: !critical,
critical,
high,
safe: !critical && !high,
scanner: raw == null ? "heuristic" : "snyk-agent-scan + heuristic",
findings,
...(raw == null ? {} : { raw }),
verdict: verdict({ critical, high }),
};
}