@h0tp/shucky 0.1.0 → 0.4.4
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 +131 -29
- package/LICENSE +21 -21
- package/NOTICE +24 -0
- package/README.md +214 -119
- package/SKILL.md +168 -124
- package/bin/shucky.js +13 -13
- package/config.json +28 -28
- package/lib/agents.js +163 -0
- package/lib/approvals.js +50 -50
- package/lib/archive.js +173 -0
- package/lib/cli.js +782 -118
- package/lib/config.js +52 -52
- package/lib/discover.js +143 -0
- package/lib/fetch.js +303 -0
- package/lib/find.js +162 -0
- package/lib/lock.js +119 -0
- package/lib/place.js +247 -0
- package/lib/registry.js +141 -0
- package/lib/report.js +53 -53
- package/lib/rules.js +162 -162
- package/lib/safeurl.js +139 -0
- package/lib/scan.js +148 -148
- package/lib/sources.js +311 -0
- package/package.json +43 -41
package/lib/report.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function human(result) {
|
|
4
|
-
const out = [];
|
|
5
|
-
out.push('shucky verdict: ' + result.verdict.toUpperCase() + ' (policy: ' + result.policy + ')');
|
|
6
|
-
out.push('target: ' + result.target +
|
|
7
|
-
(result.source ? ' source: ' + result.source : '') +
|
|
8
|
-
(result.version ? '@' + result.version : '') +
|
|
9
|
-
(result.relaxed ? ' [trusted: relaxed]' : ''));
|
|
10
|
-
const c = result.counts;
|
|
11
|
-
out.push('files scanned: ' + result.files.length +
|
|
12
|
-
' findings: ' + result.findings.length +
|
|
13
|
-
' (critical ' + (c.critical || 0) + ', high ' + (c.high || 0) +
|
|
14
|
-
', medium ' + (c.medium || 0) + ', low ' + (c.low || 0) + ')');
|
|
15
|
-
out.push('');
|
|
16
|
-
|
|
17
|
-
if (result.findings.length === 0) {
|
|
18
|
-
out.push(' no deterministic red flags found.');
|
|
19
|
-
} else {
|
|
20
|
-
for (const f of result.findings) {
|
|
21
|
-
out.push(' [' + f.severity.toUpperCase() + '] ' + f.ruleId + ' ' + f.file + ':' + f.line);
|
|
22
|
-
if (f.snippet) out.push(' ' + f.snippet);
|
|
23
|
-
out.push(' → ' + f.why);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
out.push('');
|
|
28
|
-
if (result.overriddenByApproval) {
|
|
29
|
-
const a = result.overriddenByApproval;
|
|
30
|
-
out.push('APPROVED OVERRIDE on file: "' + (a.reason || '(no reason)') + '"' +
|
|
31
|
-
' — by ' + (a.approvedBy || '?') + ' on ' + (a.date || '?'));
|
|
32
|
-
out.push('(deterministic verdict before override was: ' + result.rawVerdict.toUpperCase() + ')');
|
|
33
|
-
}
|
|
34
|
-
if (result.requireAgentReview) {
|
|
35
|
-
out.push('NOTE: this is the deterministic floor only. A human/agent semantic review is');
|
|
36
|
-
out.push('still required (intent vs. description, novel obfuscation, social engineering).');
|
|
37
|
-
}
|
|
38
|
-
if (result.verdict === 'block') {
|
|
39
|
-
out.push('DECISION: BLOCKED — do not install without an explicit, logged override.');
|
|
40
|
-
} else if (result.verdict === 'warn') {
|
|
41
|
-
out.push('DECISION: WARN — review the findings above before trusting this skill.');
|
|
42
|
-
} else {
|
|
43
|
-
out.push('DECISION: PASS' + (result.overriddenByApproval ? ' (by override)' : ' (deterministic)') +
|
|
44
|
-
' — still do the semantic review before trusting.');
|
|
45
|
-
}
|
|
46
|
-
return out.join('\n');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function json(result) {
|
|
50
|
-
return JSON.stringify(result, null, 2);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
module.exports = { human, json };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function human(result) {
|
|
4
|
+
const out = [];
|
|
5
|
+
out.push('shucky verdict: ' + result.verdict.toUpperCase() + ' (policy: ' + result.policy + ')');
|
|
6
|
+
out.push('target: ' + result.target +
|
|
7
|
+
(result.source ? ' source: ' + result.source : '') +
|
|
8
|
+
(result.version ? '@' + result.version : '') +
|
|
9
|
+
(result.relaxed ? ' [trusted: relaxed]' : ''));
|
|
10
|
+
const c = result.counts;
|
|
11
|
+
out.push('files scanned: ' + result.files.length +
|
|
12
|
+
' findings: ' + result.findings.length +
|
|
13
|
+
' (critical ' + (c.critical || 0) + ', high ' + (c.high || 0) +
|
|
14
|
+
', medium ' + (c.medium || 0) + ', low ' + (c.low || 0) + ')');
|
|
15
|
+
out.push('');
|
|
16
|
+
|
|
17
|
+
if (result.findings.length === 0) {
|
|
18
|
+
out.push(' no deterministic red flags found.');
|
|
19
|
+
} else {
|
|
20
|
+
for (const f of result.findings) {
|
|
21
|
+
out.push(' [' + f.severity.toUpperCase() + '] ' + f.ruleId + ' ' + f.file + ':' + f.line);
|
|
22
|
+
if (f.snippet) out.push(' ' + f.snippet);
|
|
23
|
+
out.push(' → ' + f.why);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
out.push('');
|
|
28
|
+
if (result.overriddenByApproval) {
|
|
29
|
+
const a = result.overriddenByApproval;
|
|
30
|
+
out.push('APPROVED OVERRIDE on file: "' + (a.reason || '(no reason)') + '"' +
|
|
31
|
+
' — by ' + (a.approvedBy || '?') + ' on ' + (a.date || '?'));
|
|
32
|
+
out.push('(deterministic verdict before override was: ' + result.rawVerdict.toUpperCase() + ')');
|
|
33
|
+
}
|
|
34
|
+
if (result.requireAgentReview) {
|
|
35
|
+
out.push('NOTE: this is the deterministic floor only. A human/agent semantic review is');
|
|
36
|
+
out.push('still required (intent vs. description, novel obfuscation, social engineering).');
|
|
37
|
+
}
|
|
38
|
+
if (result.verdict === 'block') {
|
|
39
|
+
out.push('DECISION: BLOCKED — do not install without an explicit, logged override.');
|
|
40
|
+
} else if (result.verdict === 'warn') {
|
|
41
|
+
out.push('DECISION: WARN — review the findings above before trusting this skill.');
|
|
42
|
+
} else {
|
|
43
|
+
out.push('DECISION: PASS' + (result.overriddenByApproval ? ' (by override)' : ' (deterministic)') +
|
|
44
|
+
' — still do the semantic review before trusting.');
|
|
45
|
+
}
|
|
46
|
+
return out.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function json(result) {
|
|
50
|
+
return JSON.stringify(result, null, 2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { human, json };
|
package/lib/rules.js
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Deterministic red-flag rules — the "floor" a malicious skill cannot talk the
|
|
4
|
-
// reviewing agent out of. Patterns are intentionally conservative; the agent's
|
|
5
|
-
// semantic review (see SKILL.md) covers intent and novel obfuscation on top.
|
|
6
|
-
//
|
|
7
|
-
// Some checks (browser_session, agent_state_access, IP-literal URLs) are adapted from
|
|
8
|
-
// the community skill-vetter skill (spclaudehome, MIT-0).
|
|
9
|
-
//
|
|
10
|
-
// Each rule: { id, severity, patterns: [RegExp], why }
|
|
11
|
-
|
|
12
|
-
const RULES = [
|
|
13
|
-
{
|
|
14
|
-
id: 'secret_access',
|
|
15
|
-
severity: 'critical',
|
|
16
|
-
patterns: [
|
|
17
|
-
/\.ssh\/(id_[a-z0-9]+|authorized_keys|config)/i,
|
|
18
|
-
/\bid_(rsa|ed25519|ecdsa|dsa)\b/i,
|
|
19
|
-
/\.aws\/credentials/i,
|
|
20
|
-
/\.config\/gcloud/i,
|
|
21
|
-
/\.git-credentials\b/i,
|
|
22
|
-
/(^|\s)\.netrc\b/i,
|
|
23
|
-
/(^|[^a-z0-9_.])\.npmrc\b/i,
|
|
24
|
-
/(^|\s)env\s*\|/, // `env | ...` (dumping environment)
|
|
25
|
-
/\bprintenv\b/,
|
|
26
|
-
/169\.254\.169\.254/, // cloud instance metadata
|
|
27
|
-
/metadata\.google\.internal/i,
|
|
28
|
-
/\/\.env(['"\s)]|$)/ // reading a .env file
|
|
29
|
-
],
|
|
30
|
-
why: 'Accesses credentials/secrets (keys, env dump, cloud metadata, .env, .netrc).'
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'agent_state_access',
|
|
34
|
-
severity: 'medium',
|
|
35
|
-
patterns: [
|
|
36
|
-
/\b(SOUL|IDENTITY|MEMORY|USER)\.md\b/,
|
|
37
|
-
/\.config\/openclaw/i,
|
|
38
|
-
/\.claude\/(memory|projects)/i
|
|
39
|
-
],
|
|
40
|
-
why: "Reads the agent's own memory/identity/state files (exfil or tampering risk)."
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: 'browser_session',
|
|
44
|
-
severity: 'high',
|
|
45
|
-
patterns: [
|
|
46
|
-
/cookies\.sqlite/i,
|
|
47
|
-
/(key4\.db|logins\.json|signons\.sqlite)/i,
|
|
48
|
-
/Login Data\b/,
|
|
49
|
-
/(Chrome|Chromium|Firefox|Edge|Safari|Brave)[^\n]*(Cookies|Profile|User Data)/i
|
|
50
|
-
],
|
|
51
|
-
why: 'Accesses browser cookies / saved sessions / stored credentials.'
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: 'obfuscation',
|
|
55
|
-
severity: 'high',
|
|
56
|
-
patterns: [
|
|
57
|
-
/base64\s+(-d|--decode)[^\n]*\|\s*(sh|bash|zsh)/i,
|
|
58
|
-
/\|\s*base64\s+(-d|--decode)/i,
|
|
59
|
-
/\beval\s*[("`$]/,
|
|
60
|
-
/(curl|wget)\b[^\n|]*\|\s*(sh|bash|zsh)/i, // curl ... | sh
|
|
61
|
-
/\b(gzip|gunzip|xxd|openssl)\b[^\n]*\|\s*(sh|bash)/i,
|
|
62
|
-
/\b(iex|invoke-expression)\b/i, // PowerShell exec
|
|
63
|
-
/\b(python[0-9.]*|perl|ruby|node)\s+-(e|c)\b[^\n]*(base64|eval|exec\(|atob|fromCharCode|http)/i
|
|
64
|
-
],
|
|
65
|
-
why: 'Decodes/obfuscates then executes code (classic dropper pattern).'
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: 'network_exfil',
|
|
69
|
-
severity: 'high',
|
|
70
|
-
patterns: [
|
|
71
|
-
/(curl|wget|nc|ncat)\b[^\n]*(--data|--data-binary|-d\s|@\$|@\/|@~|@-)/i,
|
|
72
|
-
/(curl|wget)\b[^\n]*\$\(/, // url/args built from command substitution
|
|
73
|
-
/\|\s*(curl|wget|nc|ncat)\b/i, // piping data out
|
|
74
|
-
/(curl|wget)\b[^\n]*(webhook|requestbin|interactsh|burpcollab|pipedream|\.ngrok\.)/i,
|
|
75
|
-
/(Invoke-WebRequest|Invoke-RestMethod|iwr|Net\.WebClient|DownloadString|DownloadFile)/i,
|
|
76
|
-
/\b(scp|rsync|sftp)\b[^\n]*@[^\n]*:/i, // copying data to a remote host
|
|
77
|
-
/https?:\/\/(?!127\.0\.0\.1|0\.0\.0\.0|localhost)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i // raw-IP URL
|
|
78
|
-
],
|
|
79
|
-
why: 'Sends data to a remote host (possible exfiltration), incl. raw-IP endpoints.'
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
id: 'destructive',
|
|
83
|
-
severity: 'high',
|
|
84
|
-
patterns: [
|
|
85
|
-
/\brm\s+-[rf]{1,2}\b/i,
|
|
86
|
-
/\bmkfs\b/i,
|
|
87
|
-
/\bdd\b[^\n]*\bof=/i,
|
|
88
|
-
/\bchmod\s+-?R?\s*777\b/,
|
|
89
|
-
/:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, // fork bomb
|
|
90
|
-
/git\s+push\b[^\n]*--force/i,
|
|
91
|
-
/(^|\s)sudo\s/
|
|
92
|
-
],
|
|
93
|
-
why: 'Destructive or privilege-escalating command.'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: 'persistence',
|
|
97
|
-
severity: 'high',
|
|
98
|
-
patterns: [
|
|
99
|
-
/\bcrontab\b/i,
|
|
100
|
-
/\/etc\/(cron|init\.d)|\/Library\/Launch(Agents|Daemons)/i,
|
|
101
|
-
/\blaunchctl\s+(load|bootstrap|enable)/i,
|
|
102
|
-
/\bsystemctl\s+(--user\s+)?enable/i,
|
|
103
|
-
/HK(CU|LM)\\[^\n]*\\Run/i,
|
|
104
|
-
/>>\s*~?\/?(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)\b/i,
|
|
105
|
-
/\bschtasks\b[^\n]*\/create/i
|
|
106
|
-
],
|
|
107
|
-
why: 'Establishes persistence (autostart, cron, service, shell-rc, registry Run key).'
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: 'prompt_injection',
|
|
111
|
-
severity: 'high',
|
|
112
|
-
patterns: [
|
|
113
|
-
/ignore\b[^.\n]{0,40}(prior|previous|earlier|above)\b[^.\n]{0,30}(instruction|rule|prompt)/i,
|
|
114
|
-
/disregard\b[^.\n]{0,40}(prior|previous|earlier|above|instruction|rule)/i,
|
|
115
|
-
/do\s+not\s+(tell|inform|mention|report|alert|warn|reveal|disclose)\b/i,
|
|
116
|
-
/this\s+(skill|file|tool|package)\s+(is|has been|was)\s+[^.\n]{0,20}(safe|approved|trusted|pre-?approved|verified|vetted|legit)/i,
|
|
117
|
-
/\byou\s+are\s+now\b/i,
|
|
118
|
-
/\balways\s+run\b/i,
|
|
119
|
-
/do\s+not\s+(run|use|invoke)\s+(any\s+)?(scanner|security|review|shucky|check)/i
|
|
120
|
-
],
|
|
121
|
-
why: 'Text aimed at the reviewing agent (instruction override / hiding actions).'
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: 'supply_chain',
|
|
125
|
-
severity: 'medium',
|
|
126
|
-
patterns: [
|
|
127
|
-
/(curl|wget)\b[^\n|]*\|\s*(sh|bash)/i, // installer one-liner (also flagged as obfuscation)
|
|
128
|
-
/\bnpm\s+(i|install)\b[^\n]*(http|git\+|github:)/i,
|
|
129
|
-
/\bpip\s+install\b[^\n]*(http|git\+)/i,
|
|
130
|
-
/\bnpx\s+(--yes\s+|-y\s+)?[@a-z][^\n]*@latest/i
|
|
131
|
-
],
|
|
132
|
-
why: 'Fetches/installs remote code at run time (unpinned supply chain).'
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
id: 'excessive_scope',
|
|
136
|
-
severity: 'low',
|
|
137
|
-
patterns: [
|
|
138
|
-
/\bnc\s+-l/i, // listener
|
|
139
|
-
/(^|\s)0\.0\.0\.0/,
|
|
140
|
-
/\bfind\s+\/\s/, // find / ...
|
|
141
|
-
/\bchmod\s+-R\b/i
|
|
142
|
-
],
|
|
143
|
-
why: 'Broad/unscoped access beyond a typical skill task.'
|
|
144
|
-
}
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
// `undeclared_capability` is intentionally NOT a deterministic rule — it requires
|
|
148
|
-
// comparing behavior against the SKILL.md description, which is the agent's job.
|
|
149
|
-
|
|
150
|
-
const SUSPICIOUS_BINARY_EXT = new Set([
|
|
151
|
-
'.pyc', '.wasm', '.so', '.dylib', '.exe', '.dll', '.node', '.class', '.o', '.a', '.bin'
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
function isProbablyBinary(buf) {
|
|
155
|
-
const n = Math.min(buf.length, 8000);
|
|
156
|
-
for (let i = 0; i < n; i++) {
|
|
157
|
-
if (buf[i] === 0) return true;
|
|
158
|
-
}
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = { RULES, SUSPICIOUS_BINARY_EXT, isProbablyBinary };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Deterministic red-flag rules — the "floor" a malicious skill cannot talk the
|
|
4
|
+
// reviewing agent out of. Patterns are intentionally conservative; the agent's
|
|
5
|
+
// semantic review (see SKILL.md) covers intent and novel obfuscation on top.
|
|
6
|
+
//
|
|
7
|
+
// Some checks (browser_session, agent_state_access, IP-literal URLs) are adapted from
|
|
8
|
+
// the community skill-vetter skill (spclaudehome, MIT-0).
|
|
9
|
+
//
|
|
10
|
+
// Each rule: { id, severity, patterns: [RegExp], why }
|
|
11
|
+
|
|
12
|
+
const RULES = [
|
|
13
|
+
{
|
|
14
|
+
id: 'secret_access',
|
|
15
|
+
severity: 'critical',
|
|
16
|
+
patterns: [
|
|
17
|
+
/\.ssh\/(id_[a-z0-9]+|authorized_keys|config)/i,
|
|
18
|
+
/\bid_(rsa|ed25519|ecdsa|dsa)\b/i,
|
|
19
|
+
/\.aws\/credentials/i,
|
|
20
|
+
/\.config\/gcloud/i,
|
|
21
|
+
/\.git-credentials\b/i,
|
|
22
|
+
/(^|\s)\.netrc\b/i,
|
|
23
|
+
/(^|[^a-z0-9_.])\.npmrc\b/i,
|
|
24
|
+
/(^|\s)env\s*\|/, // `env | ...` (dumping environment)
|
|
25
|
+
/\bprintenv\b/,
|
|
26
|
+
/169\.254\.169\.254/, // cloud instance metadata
|
|
27
|
+
/metadata\.google\.internal/i,
|
|
28
|
+
/\/\.env(['"\s)]|$)/ // reading a .env file
|
|
29
|
+
],
|
|
30
|
+
why: 'Accesses credentials/secrets (keys, env dump, cloud metadata, .env, .netrc).'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'agent_state_access',
|
|
34
|
+
severity: 'medium',
|
|
35
|
+
patterns: [
|
|
36
|
+
/\b(SOUL|IDENTITY|MEMORY|USER)\.md\b/,
|
|
37
|
+
/\.config\/openclaw/i,
|
|
38
|
+
/\.claude\/(memory|projects)/i
|
|
39
|
+
],
|
|
40
|
+
why: "Reads the agent's own memory/identity/state files (exfil or tampering risk)."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'browser_session',
|
|
44
|
+
severity: 'high',
|
|
45
|
+
patterns: [
|
|
46
|
+
/cookies\.sqlite/i,
|
|
47
|
+
/(key4\.db|logins\.json|signons\.sqlite)/i,
|
|
48
|
+
/Login Data\b/,
|
|
49
|
+
/(Chrome|Chromium|Firefox|Edge|Safari|Brave)[^\n]*(Cookies|Profile|User Data)/i
|
|
50
|
+
],
|
|
51
|
+
why: 'Accesses browser cookies / saved sessions / stored credentials.'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'obfuscation',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
patterns: [
|
|
57
|
+
/base64\s+(-d|--decode)[^\n]*\|\s*(sh|bash|zsh)/i,
|
|
58
|
+
/\|\s*base64\s+(-d|--decode)/i,
|
|
59
|
+
/\beval\s*[("`$]/,
|
|
60
|
+
/(curl|wget)\b[^\n|]*\|\s*(sh|bash|zsh)/i, // curl ... | sh
|
|
61
|
+
/\b(gzip|gunzip|xxd|openssl)\b[^\n]*\|\s*(sh|bash)/i,
|
|
62
|
+
/\b(iex|invoke-expression)\b/i, // PowerShell exec
|
|
63
|
+
/\b(python[0-9.]*|perl|ruby|node)\s+-(e|c)\b[^\n]*(base64|eval|exec\(|atob|fromCharCode|http)/i
|
|
64
|
+
],
|
|
65
|
+
why: 'Decodes/obfuscates then executes code (classic dropper pattern).'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'network_exfil',
|
|
69
|
+
severity: 'high',
|
|
70
|
+
patterns: [
|
|
71
|
+
/(curl|wget|nc|ncat)\b[^\n]*(--data|--data-binary|-d\s|@\$|@\/|@~|@-)/i,
|
|
72
|
+
/(curl|wget)\b[^\n]*\$\(/, // url/args built from command substitution
|
|
73
|
+
/\|\s*(curl|wget|nc|ncat)\b/i, // piping data out
|
|
74
|
+
/(curl|wget)\b[^\n]*(webhook|requestbin|interactsh|burpcollab|pipedream|\.ngrok\.)/i,
|
|
75
|
+
/(Invoke-WebRequest|Invoke-RestMethod|iwr|Net\.WebClient|DownloadString|DownloadFile)/i,
|
|
76
|
+
/\b(scp|rsync|sftp)\b[^\n]*@[^\n]*:/i, // copying data to a remote host
|
|
77
|
+
/https?:\/\/(?!127\.0\.0\.1|0\.0\.0\.0|localhost)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i // raw-IP URL
|
|
78
|
+
],
|
|
79
|
+
why: 'Sends data to a remote host (possible exfiltration), incl. raw-IP endpoints.'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'destructive',
|
|
83
|
+
severity: 'high',
|
|
84
|
+
patterns: [
|
|
85
|
+
/\brm\s+-[rf]{1,2}\b/i,
|
|
86
|
+
/\bmkfs\b/i,
|
|
87
|
+
/\bdd\b[^\n]*\bof=/i,
|
|
88
|
+
/\bchmod\s+-?R?\s*777\b/,
|
|
89
|
+
/:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, // fork bomb
|
|
90
|
+
/git\s+push\b[^\n]*--force/i,
|
|
91
|
+
/(^|\s)sudo\s/
|
|
92
|
+
],
|
|
93
|
+
why: 'Destructive or privilege-escalating command.'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'persistence',
|
|
97
|
+
severity: 'high',
|
|
98
|
+
patterns: [
|
|
99
|
+
/\bcrontab\b/i,
|
|
100
|
+
/\/etc\/(cron|init\.d)|\/Library\/Launch(Agents|Daemons)/i,
|
|
101
|
+
/\blaunchctl\s+(load|bootstrap|enable)/i,
|
|
102
|
+
/\bsystemctl\s+(--user\s+)?enable/i,
|
|
103
|
+
/HK(CU|LM)\\[^\n]*\\Run/i,
|
|
104
|
+
/>>\s*~?\/?(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)\b/i,
|
|
105
|
+
/\bschtasks\b[^\n]*\/create/i
|
|
106
|
+
],
|
|
107
|
+
why: 'Establishes persistence (autostart, cron, service, shell-rc, registry Run key).'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'prompt_injection',
|
|
111
|
+
severity: 'high',
|
|
112
|
+
patterns: [
|
|
113
|
+
/ignore\b[^.\n]{0,40}(prior|previous|earlier|above)\b[^.\n]{0,30}(instruction|rule|prompt)/i,
|
|
114
|
+
/disregard\b[^.\n]{0,40}(prior|previous|earlier|above|instruction|rule)/i,
|
|
115
|
+
/do\s+not\s+(tell|inform|mention|report|alert|warn|reveal|disclose)\b/i,
|
|
116
|
+
/this\s+(skill|file|tool|package)\s+(is|has been|was)\s+[^.\n]{0,20}(safe|approved|trusted|pre-?approved|verified|vetted|legit)/i,
|
|
117
|
+
/\byou\s+are\s+now\b/i,
|
|
118
|
+
/\balways\s+run\b/i,
|
|
119
|
+
/do\s+not\s+(run|use|invoke)\s+(any\s+)?(scanner|security|review|shucky|check)/i
|
|
120
|
+
],
|
|
121
|
+
why: 'Text aimed at the reviewing agent (instruction override / hiding actions).'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'supply_chain',
|
|
125
|
+
severity: 'medium',
|
|
126
|
+
patterns: [
|
|
127
|
+
/(curl|wget)\b[^\n|]*\|\s*(sh|bash)/i, // installer one-liner (also flagged as obfuscation)
|
|
128
|
+
/\bnpm\s+(i|install)\b[^\n]*(http|git\+|github:)/i,
|
|
129
|
+
/\bpip\s+install\b[^\n]*(http|git\+)/i,
|
|
130
|
+
/\bnpx\s+(--yes\s+|-y\s+)?[@a-z][^\n]*@latest/i
|
|
131
|
+
],
|
|
132
|
+
why: 'Fetches/installs remote code at run time (unpinned supply chain).'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'excessive_scope',
|
|
136
|
+
severity: 'low',
|
|
137
|
+
patterns: [
|
|
138
|
+
/\bnc\s+-l/i, // listener
|
|
139
|
+
/(^|\s)0\.0\.0\.0/,
|
|
140
|
+
/\bfind\s+\/\s/, // find / ...
|
|
141
|
+
/\bchmod\s+-R\b/i
|
|
142
|
+
],
|
|
143
|
+
why: 'Broad/unscoped access beyond a typical skill task.'
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// `undeclared_capability` is intentionally NOT a deterministic rule — it requires
|
|
148
|
+
// comparing behavior against the SKILL.md description, which is the agent's job.
|
|
149
|
+
|
|
150
|
+
const SUSPICIOUS_BINARY_EXT = new Set([
|
|
151
|
+
'.pyc', '.wasm', '.so', '.dylib', '.exe', '.dll', '.node', '.class', '.o', '.a', '.bin'
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
function isProbablyBinary(buf) {
|
|
155
|
+
const n = Math.min(buf.length, 8000);
|
|
156
|
+
for (let i = 0; i < n; i++) {
|
|
157
|
+
if (buf[i] === 0) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { RULES, SUSPICIOUS_BINARY_EXT, isProbablyBinary };
|
package/lib/safeurl.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// shucky SSRF guard — validate a remote URL before any fetch.
|
|
4
|
+
// shucky is a security tool that now reaches out to the network; this is the gate that
|
|
5
|
+
// keeps an attacker-controlled "source" from pointing the fetcher at internal services
|
|
6
|
+
// or the cloud-metadata endpoint. NO sockets here — pure validation + DNS lookup, so it
|
|
7
|
+
// is unit-testable with an injected resolver. The redirect re-guard lives in fetch.js,
|
|
8
|
+
// which calls assertSafeHttpsUrl() again on every hop.
|
|
9
|
+
|
|
10
|
+
const dns = require('dns');
|
|
11
|
+
const net = require('net');
|
|
12
|
+
|
|
13
|
+
// ---- IPv4 ----------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function ipv4ToInt(ip) {
|
|
16
|
+
const parts = ip.split('.');
|
|
17
|
+
if (parts.length !== 4) return null;
|
|
18
|
+
let n = 0;
|
|
19
|
+
for (let i = 0; i < 4; i++) {
|
|
20
|
+
const p = Number(parts[i]);
|
|
21
|
+
if (!Number.isInteger(p) || p < 0 || p > 255) return null;
|
|
22
|
+
n = (n << 8) + p;
|
|
23
|
+
}
|
|
24
|
+
return n >>> 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function inCidr4(intIp, cidr) {
|
|
28
|
+
const slash = cidr.split('/');
|
|
29
|
+
const baseInt = ipv4ToInt(slash[0]);
|
|
30
|
+
const bits = Number(slash[1]);
|
|
31
|
+
if (baseInt === null) return false;
|
|
32
|
+
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
|
|
33
|
+
return (intIp & mask) === (baseInt & mask);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Private, loopback, link-local (incl. 169.254.169.254 metadata), CGNAT, reserved, multicast.
|
|
37
|
+
const BLOCKED_V4 = [
|
|
38
|
+
'0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8',
|
|
39
|
+
'169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/24', '192.168.0.0/16',
|
|
40
|
+
'198.18.0.0/15', '224.0.0.0/4', '240.0.0.0/4'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function isBlockedIPv4(ip) {
|
|
44
|
+
const n = ipv4ToInt(ip);
|
|
45
|
+
if (n === null) return true; // unparseable → block (fail closed)
|
|
46
|
+
for (const c of BLOCKED_V4) if (inCidr4(n, c)) return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- IPv6 ----------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function isBlockedIPv6(ip) {
|
|
53
|
+
let s = ip.toLowerCase().replace(/^\[/, '').replace(/\]$/, '');
|
|
54
|
+
// IPv4-mapped / -embedded (::ffff:1.2.3.4, ::1.2.3.4) → validate the v4 part.
|
|
55
|
+
const v4 = s.match(/(\d+\.\d+\.\d+\.\d+)$/);
|
|
56
|
+
if (v4 && (s.indexOf('::ffff:') === 0 || s.indexOf('::') === 0)) return isBlockedIPv4(v4[1]);
|
|
57
|
+
if (s === '::1' || s === '::') return true; // loopback / unspecified
|
|
58
|
+
if (/^fe[89ab]/.test(s)) return true; // fe80::/10 link-local
|
|
59
|
+
if (s[0] === 'f' && (s[1] === 'c' || s[1] === 'd')) return true; // fc00::/7 unique-local
|
|
60
|
+
if (s[0] === 'f' && s[1] === 'f') return true; // ff00::/8 multicast
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isBlockedIp(ip) {
|
|
65
|
+
return ip.indexOf(':') !== -1 ? isBlockedIPv6(ip) : isBlockedIPv4(ip);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- hostnames -----------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const BLOCKED_HOSTNAMES = [
|
|
71
|
+
'localhost',
|
|
72
|
+
'metadata.google.internal',
|
|
73
|
+
'metadata.goog',
|
|
74
|
+
'instance-data',
|
|
75
|
+
'metadata' // common k8s/cloud alias
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function isBlockedHostname(host) {
|
|
79
|
+
host = host.toLowerCase();
|
|
80
|
+
if (BLOCKED_HOSTNAMES.indexOf(host) !== -1) return true;
|
|
81
|
+
if (/\.(internal|local|localhost|home\.arpa)$/.test(host)) return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- public API ----------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function resolveAll(host, resolver) {
|
|
88
|
+
const lookup = resolver || dns.lookup;
|
|
89
|
+
return new Promise(function (res, rej) {
|
|
90
|
+
lookup(host, { all: true }, function (err, addrs) {
|
|
91
|
+
if (err) return rej(err);
|
|
92
|
+
if (!Array.isArray(addrs)) addrs = addrs ? [{ address: addrs }] : [];
|
|
93
|
+
res(addrs.map(function (a) { return typeof a === 'string' ? a : a.address; }));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Throws if `input` is unsafe to fetch; resolves to the parsed URL otherwise.
|
|
99
|
+
// opts: { allowHttp?:bool, resolver?:fn } — resolver is injectable for tests.
|
|
100
|
+
async function assertSafeHttpsUrl(input, opts) {
|
|
101
|
+
opts = opts || {};
|
|
102
|
+
let u;
|
|
103
|
+
try { u = new URL(input); }
|
|
104
|
+
catch (e) { throw new Error('invalid URL: ' + input); }
|
|
105
|
+
|
|
106
|
+
if (u.protocol !== 'https:' && !(opts.allowHttp && u.protocol === 'http:')) {
|
|
107
|
+
throw new Error('refusing non-https URL: ' + input);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const bareHost = u.hostname.replace(/^\[/, '').replace(/\]$/, '');
|
|
111
|
+
if (!bareHost) throw new Error('URL has no host: ' + input);
|
|
112
|
+
if (isBlockedHostname(bareHost)) throw new Error('refusing internal host: ' + u.hostname);
|
|
113
|
+
|
|
114
|
+
// Literal IP → check directly (no DNS).
|
|
115
|
+
if (net.isIP(bareHost)) {
|
|
116
|
+
if (isBlockedIp(bareHost)) throw new Error('refusing internal/reserved IP: ' + u.hostname);
|
|
117
|
+
return u;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Hostname → resolve and reject if ANY address is internal (DNS-rebind defense).
|
|
121
|
+
let addrs;
|
|
122
|
+
try { addrs = await resolveAll(bareHost, opts.resolver); }
|
|
123
|
+
catch (e) { throw new Error('DNS resolution failed for ' + u.hostname + ': ' + e.message); }
|
|
124
|
+
if (!addrs.length) throw new Error('no DNS records for ' + u.hostname);
|
|
125
|
+
for (const ip of addrs) {
|
|
126
|
+
if (isBlockedIp(ip)) {
|
|
127
|
+
throw new Error('host ' + u.hostname + ' resolves to internal IP ' + ip + ' — SSRF blocked');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return u;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
assertSafeHttpsUrl,
|
|
135
|
+
isBlockedIp,
|
|
136
|
+
isBlockedIPv4,
|
|
137
|
+
isBlockedIPv6,
|
|
138
|
+
isBlockedHostname
|
|
139
|
+
};
|