@kryptosai/mcp-observatory 1.28.2 → 1.28.3
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/CONTRIBUTORS.md +4 -1
- package/README.md +109 -14
- package/README.zh-CN.md +654 -0
- package/dist/src/adapters/http.js +13 -4
- package/dist/src/adapters/http.js.map +1 -1
- package/dist/src/checks/runtime-profile.d.ts +5 -0
- package/dist/src/checks/runtime-profile.js +214 -0
- package/dist/src/checks/runtime-profile.js.map +1 -0
- package/dist/src/checks/security-rules.js +18 -0
- package/dist/src/checks/security-rules.js.map +1 -1
- package/dist/src/checks/security.js +33 -65
- package/dist/src/checks/security.js.map +1 -1
- package/dist/src/checks/skill-scan.d.ts +46 -0
- package/dist/src/checks/skill-scan.js +665 -0
- package/dist/src/checks/skill-scan.js.map +1 -0
- package/dist/src/cli.js +15 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/enforce.d.ts +12 -0
- package/dist/src/commands/enforce.js +187 -0
- package/dist/src/commands/enforce.js.map +1 -0
- package/dist/src/commands/helpers.d.ts +2 -1
- package/dist/src/commands/helpers.js +9 -1
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/legacy.js +3 -0
- package/dist/src/commands/legacy.js.map +1 -1
- package/dist/src/commands/risk-graph.d.ts +2 -0
- package/dist/src/commands/risk-graph.js +64 -0
- package/dist/src/commands/risk-graph.js.map +1 -0
- package/dist/src/commands/scan.js +36 -9
- package/dist/src/commands/scan.js.map +1 -1
- package/dist/src/commands/score.js.map +1 -1
- package/dist/src/commands/skill-scan.d.ts +6 -0
- package/dist/src/commands/skill-scan.js +62 -0
- package/dist/src/commands/skill-scan.js.map +1 -0
- package/dist/src/commands/test.js +13 -0
- package/dist/src/commands/test.js.map +1 -1
- package/dist/src/commands/watch.js +13 -8
- package/dist/src/commands/watch.js.map +1 -1
- package/dist/src/discovery.d.ts +15 -1
- package/dist/src/discovery.js +251 -35
- package/dist/src/discovery.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/receipt.d.ts +4 -1
- package/dist/src/receipt.js +85 -0
- package/dist/src/receipt.js.map +1 -1
- package/dist/src/reporters/markdown.js +39 -0
- package/dist/src/reporters/markdown.js.map +1 -1
- package/dist/src/reporters/terminal.d.ts +2 -0
- package/dist/src/reporters/terminal.js +55 -2
- package/dist/src/reporters/terminal.js.map +1 -1
- package/dist/src/risk-graph.d.ts +64 -0
- package/dist/src/risk-graph.js +452 -0
- package/dist/src/risk-graph.js.map +1 -0
- package/dist/src/runner.js +6 -0
- package/dist/src/runner.js.map +1 -1
- package/dist/src/server.d.ts +1 -3
- package/dist/src/server.js +29 -645
- package/dist/src/server.js.map +1 -1
- package/dist/src/storage.js +13 -9
- package/dist/src/storage.js.map +1 -1
- package/dist/src/telemetry.d.ts +7 -0
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/tools/check-server.d.ts +27 -0
- package/dist/src/tools/check-server.js +46 -0
- package/dist/src/tools/check-server.js.map +1 -0
- package/dist/src/tools/ci-report.d.ts +21 -0
- package/dist/src/tools/ci-report.js +38 -0
- package/dist/src/tools/ci-report.js.map +1 -0
- package/dist/src/tools/diff-runs.d.ts +28 -0
- package/dist/src/tools/diff-runs.js +39 -0
- package/dist/src/tools/diff-runs.js.map +1 -0
- package/dist/src/tools/get-history.d.ts +21 -0
- package/dist/src/tools/get-history.js +37 -0
- package/dist/src/tools/get-history.js.map +1 -0
- package/dist/src/tools/get-last-run.d.ts +21 -0
- package/dist/src/tools/get-last-run.js +47 -0
- package/dist/src/tools/get-last-run.js.map +1 -0
- package/dist/src/tools/helpers.d.ts +8 -0
- package/dist/src/tools/helpers.js +32 -0
- package/dist/src/tools/helpers.js.map +1 -0
- package/dist/src/tools/lock-verify.d.ts +21 -0
- package/dist/src/tools/lock-verify.js +47 -0
- package/dist/src/tools/lock-verify.js.map +1 -0
- package/dist/src/tools/record.d.ts +23 -0
- package/dist/src/tools/record.js +48 -0
- package/dist/src/tools/record.js.map +1 -0
- package/dist/src/tools/replay.d.ts +21 -0
- package/dist/src/tools/replay.js +54 -0
- package/dist/src/tools/replay.js.map +1 -0
- package/dist/src/tools/scan.d.ts +18 -0
- package/dist/src/tools/scan.js +43 -0
- package/dist/src/tools/scan.js.map +1 -0
- package/dist/src/tools/score-server.d.ts +23 -0
- package/dist/src/tools/score-server.js +53 -0
- package/dist/src/tools/score-server.js.map +1 -0
- package/dist/src/tools/suggest-servers.d.ts +21 -0
- package/dist/src/tools/suggest-servers.js +114 -0
- package/dist/src/tools/suggest-servers.js.map +1 -0
- package/dist/src/tools/verify-tool.d.ts +25 -0
- package/dist/src/tools/verify-tool.js +48 -0
- package/dist/src/tools/verify-tool.js.map +1 -0
- package/dist/src/tools/watch.d.ts +23 -0
- package/dist/src/tools/watch.js +67 -0
- package/dist/src/tools/watch.js.map +1 -0
- package/dist/src/types.d.ts +26 -1
- package/dist/src/utils/security.d.ts +3 -0
- package/dist/src/utils/security.js +37 -0
- package/dist/src/utils/security.js.map +1 -0
- package/dist/src/validate.d.ts +1 -0
- package/dist/src/validate.js +119 -35
- package/dist/src/validate.js.map +1 -1
- package/docs/agent-tasks.md +1 -1
- package/docs/commercial-boundary.md +9 -1
- package/docs/contributor-recognition.md +2 -0
- package/docs/demo.svg +1 -59
- package/docs/mcp-receipts.md +9 -0
- package/docs/mcp-server-safety-index.md +21 -16
- package/docs/metrics-dashboard.md +17 -1
- package/docs/private-mcp-fleet-risk-graph.md +69 -0
- package/docs/receipt-graph.md +118 -31
- package/docs/safety-index/artifacts/chrome-devtools-mcp-server.json +1745 -0
- package/docs/safety-index/artifacts/chrome-devtools-mcp-server.md +169 -0
- package/docs/safety-index/artifacts/filesystem-mcp-server.json +786 -0
- package/docs/safety-index/artifacts/filesystem-mcp-server.md +169 -0
- package/docs/safety-index/artifacts/github-mcp-server.json +1707 -0
- package/docs/safety-index/artifacts/github-mcp-server.md +169 -0
- package/docs/safety-index/mcp-risk-graph.html +167 -0
- package/docs/safety-index/mcp-risk-graph.json +669 -0
- package/docs/safety-index/mcp-risk-graph.md +47 -0
- package/docs/safety-index/targets.json +57 -0
- package/docs/sample-private-fleet-risk-graph.md +90 -0
- package/package.json +21 -11
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, resolve } from "node:path";
|
|
3
|
+
// ── Rule Definitions ────────────────────────────────────────────────────────
|
|
4
|
+
const CREDENTIAL_PATTERNS = [
|
|
5
|
+
/\.env\b/gi,
|
|
6
|
+
/process\.env/gi,
|
|
7
|
+
/\bcredentials\b/gi,
|
|
8
|
+
/\bsecrets?\b/gi,
|
|
9
|
+
/\bAPI_KEY\b/gi,
|
|
10
|
+
/\bapi[_-]?key\b/gi,
|
|
11
|
+
/\btoken\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
12
|
+
/\bpassword\s*[:=]\s*['"][^'"]{4,}['"]/gi,
|
|
13
|
+
/\bSECRET\b/g,
|
|
14
|
+
/\baccess[_-]?key\b/gi,
|
|
15
|
+
/\bprivate[_-]?key\b/gi,
|
|
16
|
+
/export\s+\w*SECRET\w*\s*=/g,
|
|
17
|
+
/export\s+\w*TOKEN\w*\s*=/g,
|
|
18
|
+
/export\s+\w*KEY\w*\s*=/g,
|
|
19
|
+
];
|
|
20
|
+
const EXFILTRATION_PATTERNS = [
|
|
21
|
+
/webhook\.site/gi,
|
|
22
|
+
/discord\.com\/api\/webhooks/gi,
|
|
23
|
+
/slack\.com\/api\/chat\.postMessage/gi,
|
|
24
|
+
/curl\s+.*https?:\/\//gi,
|
|
25
|
+
/fetch\s*\(\s*['"]https?:\/\/[^'"]*/gi,
|
|
26
|
+
/XMLHttpRequest/gi,
|
|
27
|
+
/https?:\/\/.*requestbin/gi,
|
|
28
|
+
/https?:\/\/.*hook\.site/gi,
|
|
29
|
+
/https?:\/\/.*webhook/gi,
|
|
30
|
+
/https?:\/\/.*beeceptor/gi,
|
|
31
|
+
/send\s*\(.*https?:\/\//gi,
|
|
32
|
+
/\.post\s*\(\s*['"]https?:\/\/[^'"]*/gi,
|
|
33
|
+
/\.put\s*\(\s*['"]https?:\/\/[^'"]*/gi,
|
|
34
|
+
];
|
|
35
|
+
const REMOTE_EXEC_PATTERNS = [
|
|
36
|
+
/curl\s+.*\|\s*(?:bash|sh)\b/gi,
|
|
37
|
+
/curl\s+.*\|\s*(?:sudo\s+)?(?:bash|sh)\b/gi,
|
|
38
|
+
/wget\s+.*\|\s*(?:bash|sh)\b/gi,
|
|
39
|
+
/eval\s*\(\s*(?:fetch|curl|wget)/gi,
|
|
40
|
+
/eval\s*\(\s*(?:atob|Buffer\.from.*base64)/gi,
|
|
41
|
+
/npx\s+.*-y\b/gi,
|
|
42
|
+
/npm\s+(?:install|i)\s+-g\b/gi,
|
|
43
|
+
/pip\s+install\s+(?!.*(?:--index-url|--extra-index-url))/gi,
|
|
44
|
+
/child_process\b/gi,
|
|
45
|
+
/exec\s*\(\s*['`].*(?:curl|wget|bash|sh|python)/gi,
|
|
46
|
+
/execSync\s*\(/gi,
|
|
47
|
+
/spawn\s*\(/gi,
|
|
48
|
+
/fork\s*\(/gi,
|
|
49
|
+
/new\s+Function\s*\(/gi,
|
|
50
|
+
/vm\.runIn/gi,
|
|
51
|
+
/deno\.run\b/gi,
|
|
52
|
+
/Deno\.run\b/gi,
|
|
53
|
+
];
|
|
54
|
+
const HIDDEN_INSTRUCTION_PATTERNS = [
|
|
55
|
+
/ignore\s+(?:previous|all|above)\s+(?:instructions?|directions?|prompts?|rules?)/gi,
|
|
56
|
+
/disregard\s+(?:previous|all|above)\s+(?:instructions?|directions?)/gi,
|
|
57
|
+
/override\s+(?:system\s+|user\s+)?(?:instructions?|prompts?|rules?)/gi,
|
|
58
|
+
/bypass\s+(?:safety|security|restrictions?|filters?|policies?)/gi,
|
|
59
|
+
/you\s+must\s+(?:always|never)\s+(?:ignore|disregard|override|bypass)/gi,
|
|
60
|
+
/your\s+(?:primary|main|only)\s+(?:purpose|goal|directive)\s+is/gi,
|
|
61
|
+
/system\s+prompt\s+(?:override|injection|manipulation)/gi,
|
|
62
|
+
/jailbreak/i,
|
|
63
|
+
/act\s+as\s+(?:DAN|developer\s+mode)/i,
|
|
64
|
+
/pretend\s+(?:you\s+are|to\s+be)/gi,
|
|
65
|
+
/do\s+not\s+(?:follow|obey)\s+(?:your\s+)?(?:rules?|instructions?|guidelines?)/gi,
|
|
66
|
+
/forget\s+(?:everything|all)\s+(?:you|we)\s+(?:know|discussed|said)/gi,
|
|
67
|
+
/start\s+your\s+response\s+with/gi,
|
|
68
|
+
/respond\s+(?:only|always)\s+with/gi,
|
|
69
|
+
/do\s+not\s+(?:mention|reveal|disclose)/gi,
|
|
70
|
+
/hidden\s+(?:instruction|prompt|directive)/gi,
|
|
71
|
+
];
|
|
72
|
+
const FILESYSTEM_PATTERNS = [
|
|
73
|
+
/~\/\./g,
|
|
74
|
+
/\/etc\//g,
|
|
75
|
+
/\/var\//g,
|
|
76
|
+
/\.ssh\//g,
|
|
77
|
+
/\/root\//g,
|
|
78
|
+
/\/tmp\//g,
|
|
79
|
+
/\/proc\//g,
|
|
80
|
+
/\/sys\//g,
|
|
81
|
+
/\/dev\//g,
|
|
82
|
+
/\/boot\//g,
|
|
83
|
+
/\/mnt\//g,
|
|
84
|
+
/\/opt\//g,
|
|
85
|
+
/\/usr\/local\//g,
|
|
86
|
+
/readFile\s*\(\s*['"`]~\//gi,
|
|
87
|
+
/writeFile\s*\(\s*['"`]~\//gi,
|
|
88
|
+
/rm\s+.*-rf\b/gi,
|
|
89
|
+
/rmdir\b/gi,
|
|
90
|
+
/unlink\s*\(/gi,
|
|
91
|
+
/chmod\s+/gi,
|
|
92
|
+
/chown\s+/gi,
|
|
93
|
+
/fs\.readFileSync/gi,
|
|
94
|
+
/fs\.writeFileSync/gi,
|
|
95
|
+
/fs\.appendFileSync/gi,
|
|
96
|
+
/require\(['"`]fs['"`]\)/gi,
|
|
97
|
+
/import\s+.*\s+from\s+['"`]fs['"`]/gi,
|
|
98
|
+
];
|
|
99
|
+
const INSTALL_HOOK_PATTERNS = [
|
|
100
|
+
/"postinstall"\s*:/gi,
|
|
101
|
+
/"preinstall"\s*:/gi,
|
|
102
|
+
];
|
|
103
|
+
const NETWORK_CALL_PATTERNS = [
|
|
104
|
+
/curl\s+/gi,
|
|
105
|
+
/wget\s+/gi,
|
|
106
|
+
/fetch\s*\(/gi,
|
|
107
|
+
];
|
|
108
|
+
const OBFUSCATION_PATTERNS = [
|
|
109
|
+
/base64/gi,
|
|
110
|
+
/atob\s*\(/gi,
|
|
111
|
+
/Buffer\.from\s*\(.*base64/gi,
|
|
112
|
+
/String\.fromCharCode/gi,
|
|
113
|
+
/0x[0-9a-fA-F]{4,}/g,
|
|
114
|
+
/gzip|gunzip|inflate|deflate/gi,
|
|
115
|
+
];
|
|
116
|
+
const SUB_EXECUTION_PATTERNS = [
|
|
117
|
+
/eval\s*\(/gi,
|
|
118
|
+
/exec\s*\(/gi,
|
|
119
|
+
/execSync\s*\(/gi,
|
|
120
|
+
/spawn\s*\(/gi,
|
|
121
|
+
/new\s+Function\s*\(/gi,
|
|
122
|
+
/child_process/gi,
|
|
123
|
+
];
|
|
124
|
+
const URGENCY_PATTERNS = [
|
|
125
|
+
/\bURGENT\b/gi,
|
|
126
|
+
/\bIMPORTANT\b/gi,
|
|
127
|
+
/\bWARNING\b/gi,
|
|
128
|
+
/\bCRITICAL\b/gi,
|
|
129
|
+
/\bmandatory\b/gi,
|
|
130
|
+
/won't work/gi,
|
|
131
|
+
/will fail/gi,
|
|
132
|
+
/must (?:set|add|enter|provide|configure|put)/gi,
|
|
133
|
+
];
|
|
134
|
+
const CREDENTIAL_REQUEST_PATTERNS = [
|
|
135
|
+
/API[_\s]?[Kk]ey/gi,
|
|
136
|
+
/(?:put|set|add|enter|paste|provide)\s+(?:your\s+)?(?:API|secret|token|password|credential)/gi,
|
|
137
|
+
/\.env\b/gi,
|
|
138
|
+
/\bcredentials?\b/gi,
|
|
139
|
+
/\bsecrets?\b/gi,
|
|
140
|
+
];
|
|
141
|
+
const ENV_ACCESS_PATTERNS = [
|
|
142
|
+
/process\.env/gi,
|
|
143
|
+
/dotenv/gi,
|
|
144
|
+
/\.env\b/gi,
|
|
145
|
+
];
|
|
146
|
+
export const SKILL_SCAN_RULES = [
|
|
147
|
+
{
|
|
148
|
+
id: "credential-access",
|
|
149
|
+
severity: "high",
|
|
150
|
+
name: "Credential access patterns",
|
|
151
|
+
patterns: CREDENTIAL_PATTERNS,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "exfiltration-vector",
|
|
155
|
+
severity: "high",
|
|
156
|
+
name: "Data exfiltration vectors",
|
|
157
|
+
patterns: EXFILTRATION_PATTERNS,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "remote-execution",
|
|
161
|
+
severity: "high",
|
|
162
|
+
name: "Remote execution patterns",
|
|
163
|
+
patterns: REMOTE_EXEC_PATTERNS,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "hidden-instruction",
|
|
167
|
+
severity: "medium",
|
|
168
|
+
name: "Hidden instruction / prompt injection",
|
|
169
|
+
patterns: HIDDEN_INSTRUCTION_PATTERNS,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "filesystem-manipulation",
|
|
173
|
+
severity: "medium",
|
|
174
|
+
name: "Filesystem manipulation",
|
|
175
|
+
patterns: FILESYSTEM_PATTERNS,
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
const COMPOUND_RULES = [
|
|
179
|
+
{
|
|
180
|
+
id: "credential-exfiltration",
|
|
181
|
+
severity: "high",
|
|
182
|
+
name: "Credential access combined with exfiltration",
|
|
183
|
+
patternsA: CREDENTIAL_PATTERNS,
|
|
184
|
+
patternsB: EXFILTRATION_PATTERNS,
|
|
185
|
+
maxLineDistance: 50,
|
|
186
|
+
message: "Credential access patterns found near exfiltration vectors — possible data theft.",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "supply-chain-hijack",
|
|
190
|
+
severity: "high",
|
|
191
|
+
name: "Install hooks combined with network calls",
|
|
192
|
+
patternsA: INSTALL_HOOK_PATTERNS,
|
|
193
|
+
patternsB: NETWORK_CALL_PATTERNS,
|
|
194
|
+
maxLineDistance: 50,
|
|
195
|
+
message: "Package install hooks (postinstall/preinstall) found near network calls — possible supply chain attack.",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "remote-execute-with-env",
|
|
199
|
+
severity: "high",
|
|
200
|
+
name: "Remote execution combined with environment access",
|
|
201
|
+
patternsA: REMOTE_EXEC_PATTERNS,
|
|
202
|
+
patternsB: ENV_ACCESS_PATTERNS,
|
|
203
|
+
maxLineDistance: 50,
|
|
204
|
+
message: "Remote execution patterns found near environment variable access — possible credential theft for payload delivery.",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "hidden-execution",
|
|
208
|
+
severity: "medium",
|
|
209
|
+
name: "Obfuscation combined with code execution",
|
|
210
|
+
patternsA: OBFUSCATION_PATTERNS,
|
|
211
|
+
patternsB: SUB_EXECUTION_PATTERNS,
|
|
212
|
+
maxLineDistance: 50,
|
|
213
|
+
message: "Obfuscation patterns (base64, hex, compression) found near code execution — possible hidden malicious code.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "social-engineering",
|
|
217
|
+
severity: "medium",
|
|
218
|
+
name: "Urgency language combined with credential requests",
|
|
219
|
+
patternsA: URGENCY_PATTERNS,
|
|
220
|
+
patternsB: CREDENTIAL_REQUEST_PATTERNS,
|
|
221
|
+
maxLineDistance: 50,
|
|
222
|
+
message: "Urgency/fear language found near credential/secret patterns — possible social engineering attack.",
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
function findCompoundPatternMatches(content, lines, patterns) {
|
|
226
|
+
const matches = [];
|
|
227
|
+
for (const pattern of patterns) {
|
|
228
|
+
pattern.lastIndex = 0;
|
|
229
|
+
let m;
|
|
230
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
231
|
+
const { line, column } = findLineColumn(content, m.index);
|
|
232
|
+
matches.push({
|
|
233
|
+
line,
|
|
234
|
+
column,
|
|
235
|
+
pattern: pattern.source,
|
|
236
|
+
matchText: lines[line - 1]?.trim().slice(0, 120) ?? m[0].slice(0, 120),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return matches;
|
|
241
|
+
}
|
|
242
|
+
function applyCompoundRules(filePath, content, lines) {
|
|
243
|
+
const findings = [];
|
|
244
|
+
for (const rule of COMPOUND_RULES) {
|
|
245
|
+
const matchesA = findCompoundPatternMatches(content, lines, rule.patternsA);
|
|
246
|
+
const matchesB = findCompoundPatternMatches(content, lines, rule.patternsB);
|
|
247
|
+
if (matchesA.length === 0 || matchesB.length === 0)
|
|
248
|
+
continue;
|
|
249
|
+
const hasProximity = matchesA.some((a) => matchesB.some((b) => Math.abs(a.line - b.line) <= rule.maxLineDistance));
|
|
250
|
+
if (!hasProximity)
|
|
251
|
+
continue;
|
|
252
|
+
findings.push({
|
|
253
|
+
ruleId: rule.id,
|
|
254
|
+
severity: rule.severity,
|
|
255
|
+
filePath,
|
|
256
|
+
message: rule.message,
|
|
257
|
+
matches: [...matchesA, ...matchesB],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return findings;
|
|
261
|
+
}
|
|
262
|
+
// ── Unicode Obfuscation Detection ────────────────────────────────────────────
|
|
263
|
+
const UNICODE_OBFUSCATION_REGEX = /[\u200B-\u200D\uFEFF\u2060\u00AD\u202A-\u202E\u2066-\u2069\u{E0001}\u{E0020}-\u{E007F}]/gu;
|
|
264
|
+
function getUnicodeCharInfo(char) {
|
|
265
|
+
const cp = char.codePointAt(0);
|
|
266
|
+
const hex = cp.toString(16).toUpperCase().padStart(cp > 0xFFFF ? 5 : 4, "0");
|
|
267
|
+
if (cp === 0x200B)
|
|
268
|
+
return { name: `Zero Width Space (U+${hex})`, danger: "Can hide malicious code or alter how text is interpreted" };
|
|
269
|
+
if (cp === 0x200C)
|
|
270
|
+
return { name: `Zero Width Non-Joiner (U+${hex})`, danger: "Can hide malicious code or alter how text is interpreted" };
|
|
271
|
+
if (cp === 0x200D)
|
|
272
|
+
return { name: `Zero Width Joiner (U+${hex})`, danger: "Can hide malicious code or alter how text is interpreted" };
|
|
273
|
+
if (cp === 0xFEFF)
|
|
274
|
+
return { name: `Zero Width No-Break Space / BOM (U+${hex})`, danger: "Can hide malicious code or alter how text is interpreted" };
|
|
275
|
+
if (cp === 0x2060)
|
|
276
|
+
return { name: `Word Joiner (U+${hex})`, danger: "Can hide malicious code or alter how text is interpreted" };
|
|
277
|
+
if (cp === 0x00AD)
|
|
278
|
+
return { name: `Soft Hyphen (U+${hex})`, danger: "Can hide malicious code or alter how text is interpreted" };
|
|
279
|
+
if (cp === 0x202A)
|
|
280
|
+
return { name: `Left-to-Right Embedding (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
281
|
+
if (cp === 0x202B)
|
|
282
|
+
return { name: `Right-to-Left Embedding (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
283
|
+
if (cp === 0x202C)
|
|
284
|
+
return { name: `Pop Directional Formatting (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
285
|
+
if (cp === 0x202D)
|
|
286
|
+
return { name: `Left-to-Right Override (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
287
|
+
if (cp === 0x202E)
|
|
288
|
+
return { name: `Right-to-Left Override (U+${hex})`, danger: "Bidi override can reverse text to obscure true intent" };
|
|
289
|
+
if (cp === 0x2066)
|
|
290
|
+
return { name: `Left-to-Right Isolate (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
291
|
+
if (cp === 0x2067)
|
|
292
|
+
return { name: `Right-to-Left Isolate (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
293
|
+
if (cp === 0x2068)
|
|
294
|
+
return { name: `First Strong Isolate (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
295
|
+
if (cp === 0x2069)
|
|
296
|
+
return { name: `Pop Directional Isolate (U+${hex})`, danger: "Bidi override can reverse or reorder text to hide true intent" };
|
|
297
|
+
if (cp === 0xE0001)
|
|
298
|
+
return { name: `Language Tag (U+${hex})`, danger: "Unicode tag sequence can embed hidden metadata or tracking information" };
|
|
299
|
+
if (cp >= 0xE0020 && cp <= 0xE007E)
|
|
300
|
+
return { name: `Tag Character (U+${hex})`, danger: "Unicode tag sequence can embed hidden metadata or tracking information" };
|
|
301
|
+
if (cp === 0xE007F)
|
|
302
|
+
return { name: `Cancel Tag (U+${hex})`, danger: "Unicode tag sequence can embed hidden metadata or tracking information" };
|
|
303
|
+
return { name: `Unknown Hidden Character (U+${hex})`, danger: "Hidden Unicode character with unknown intent" };
|
|
304
|
+
}
|
|
305
|
+
function checkUnicodeObfuscation(filePath, content) {
|
|
306
|
+
UNICODE_OBFUSCATION_REGEX.lastIndex = 0;
|
|
307
|
+
const matches = [];
|
|
308
|
+
let m;
|
|
309
|
+
while ((m = UNICODE_OBFUSCATION_REGEX.exec(content)) !== null) {
|
|
310
|
+
const { line, column } = findLineColumn(content, m.index);
|
|
311
|
+
const info = getUnicodeCharInfo(m[0]);
|
|
312
|
+
matches.push({
|
|
313
|
+
line,
|
|
314
|
+
column,
|
|
315
|
+
pattern: UNICODE_OBFUSCATION_REGEX.source,
|
|
316
|
+
matchText: `${info.name}: ${info.danger}`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (matches.length > 0) {
|
|
320
|
+
return {
|
|
321
|
+
ruleId: "unicode-obfuscation",
|
|
322
|
+
severity: "high",
|
|
323
|
+
filePath,
|
|
324
|
+
message: `Found ${matches.length} hidden Unicode character(s) in ${basename(filePath)}. These can hide malicious code, obscure true intent, or embed tracking information.`,
|
|
325
|
+
matches,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
const ATTESTATION_INDICATORS = [
|
|
331
|
+
/^signed\s*(?:by|with)?/im,
|
|
332
|
+
/^\w+:sig:/m,
|
|
333
|
+
/^-----BEGIN\s+PGP\s+SIGNED\s+MESSAGE-----/m,
|
|
334
|
+
/^-----BEGIN\s+PGP\s+SIGNATURE-----/m,
|
|
335
|
+
/attestation\s*(?:hash|digest|checksum|fingerprint)\s*[:=]?\s*[a-f0-9]{32,}/im,
|
|
336
|
+
/^version:\s*\d+\s*$/im,
|
|
337
|
+
/^hash:\s*[a-f0-9]{32,}\s*$/im,
|
|
338
|
+
/^digest:\s*[a-f0-9]{32,}\s*$/im,
|
|
339
|
+
/^sha256:\s*[a-f0-9]{64}\s*$/im,
|
|
340
|
+
/^sha512:\s*[a-f0-9]{128}\s*$/im,
|
|
341
|
+
/-----BEGIN\s+CERTIFICATE-----/m,
|
|
342
|
+
/-----BEGIN\s+SIGNATURE-----/m,
|
|
343
|
+
];
|
|
344
|
+
function checkUnsigned(filePath, content) {
|
|
345
|
+
const hasAttestation = ATTESTATION_INDICATORS.some((r) => r.test(content));
|
|
346
|
+
const isPackageJson = basename(filePath) === "package.json";
|
|
347
|
+
const name = basename(filePath);
|
|
348
|
+
const isSkillFile = /^SKILL\.md$/i.test(name) || /^skill\.md$/i.test(name);
|
|
349
|
+
// Only check skill-specific files and package.json for missing attestation
|
|
350
|
+
if (!isSkillFile && !isPackageJson)
|
|
351
|
+
return undefined;
|
|
352
|
+
if (hasAttestation)
|
|
353
|
+
return undefined;
|
|
354
|
+
if (isPackageJson) {
|
|
355
|
+
try {
|
|
356
|
+
const pkg = JSON.parse(content);
|
|
357
|
+
if (pkg._integrity || pkg.dist?.shasum || pkg.dist?.integrity || pkg.signatures?.[0]) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// Invalid JSON — will be caught by other checks
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
ruleId: "unsigned-skill",
|
|
366
|
+
severity: "medium",
|
|
367
|
+
filePath,
|
|
368
|
+
message: `Skill package.json has no integrity hash, signatures, or attestation.`,
|
|
369
|
+
matches: [],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (isSkillFile || extname(filePath) === ".md") {
|
|
373
|
+
return {
|
|
374
|
+
ruleId: "unsigned-skill",
|
|
375
|
+
severity: "medium",
|
|
376
|
+
filePath,
|
|
377
|
+
message: `Skill file has no attestation, verification hash, or signature.`,
|
|
378
|
+
matches: [],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
// ── Scanner ──────────────────────────────────────────────────────────────────
|
|
384
|
+
const SKILL_FILE_NAMES = new Set([
|
|
385
|
+
"skill.md",
|
|
386
|
+
"SKILL.md",
|
|
387
|
+
"skill.mdx",
|
|
388
|
+
"SKILL.mdx",
|
|
389
|
+
]);
|
|
390
|
+
const SKILL_RELATED_FILES = new Set([
|
|
391
|
+
"package.json",
|
|
392
|
+
]);
|
|
393
|
+
function isSkillFile(filePath) {
|
|
394
|
+
const name = basename(filePath);
|
|
395
|
+
if (SKILL_FILE_NAMES.has(name))
|
|
396
|
+
return true;
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
function isSkillRelatedFile(filePath) {
|
|
400
|
+
const name = basename(filePath);
|
|
401
|
+
return SKILL_RELATED_FILES.has(name);
|
|
402
|
+
}
|
|
403
|
+
export function findLineColumn(content, matchIndex) {
|
|
404
|
+
const before = content.slice(0, matchIndex);
|
|
405
|
+
const line = (before.match(/\n/g) ?? []).length + 1;
|
|
406
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
407
|
+
const column = matchIndex - (lastNewline >= 0 ? lastNewline : 0);
|
|
408
|
+
return { line, column };
|
|
409
|
+
}
|
|
410
|
+
export function scanContent(filePath, content, rules) {
|
|
411
|
+
const findings = [];
|
|
412
|
+
const lines = content.split("\n");
|
|
413
|
+
for (const rule of rules) {
|
|
414
|
+
const matches = [];
|
|
415
|
+
for (const pattern of rule.patterns) {
|
|
416
|
+
pattern.lastIndex = 0;
|
|
417
|
+
let m;
|
|
418
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
419
|
+
const { line, column } = findLineColumn(content, m.index);
|
|
420
|
+
matches.push({
|
|
421
|
+
line,
|
|
422
|
+
column,
|
|
423
|
+
pattern: pattern.source,
|
|
424
|
+
matchText: lines[line - 1]?.trim().slice(0, 120) ?? m[0].slice(0, 120),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (matches.length > 0) {
|
|
429
|
+
findings.push({
|
|
430
|
+
ruleId: rule.id,
|
|
431
|
+
severity: rule.severity,
|
|
432
|
+
filePath,
|
|
433
|
+
message: `Found ${matches.length} match(es) for "${rule.name}" in ${basename(filePath)}.`,
|
|
434
|
+
matches,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const unsigned = checkUnsigned(filePath, content);
|
|
439
|
+
if (unsigned)
|
|
440
|
+
findings.push(unsigned);
|
|
441
|
+
const unicodeObfuscation = checkUnicodeObfuscation(filePath, content);
|
|
442
|
+
if (unicodeObfuscation)
|
|
443
|
+
findings.push(unicodeObfuscation);
|
|
444
|
+
const compoundFindings = applyCompoundRules(filePath, content, lines);
|
|
445
|
+
findings.push(...compoundFindings);
|
|
446
|
+
return findings;
|
|
447
|
+
}
|
|
448
|
+
export async function scanFile(filePath) {
|
|
449
|
+
const content = await readFile(filePath, "utf8");
|
|
450
|
+
const findings = scanContent(filePath, content, SKILL_SCAN_RULES);
|
|
451
|
+
return { filePath, findings };
|
|
452
|
+
}
|
|
453
|
+
export async function scanDirectory(dirPath) {
|
|
454
|
+
const { readdir } = await import("node:fs/promises");
|
|
455
|
+
const results = [];
|
|
456
|
+
async function walk(currentPath) {
|
|
457
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
458
|
+
for (const entry of entries) {
|
|
459
|
+
const full = resolve(currentPath, entry.name);
|
|
460
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
461
|
+
await walk(full);
|
|
462
|
+
}
|
|
463
|
+
else if (entry.isFile()) {
|
|
464
|
+
if (isSkillFile(full) || isSkillRelatedFile(full) || (entry.name.endsWith(".md") && /skill/i.test(entry.name))) {
|
|
465
|
+
results.push(await scanFile(full));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
await walk(dirPath);
|
|
471
|
+
return results;
|
|
472
|
+
}
|
|
473
|
+
export async function scanPath(inputPath) {
|
|
474
|
+
const { stat } = await import("node:fs/promises");
|
|
475
|
+
const s = await stat(inputPath);
|
|
476
|
+
if (s.isDirectory()) {
|
|
477
|
+
return scanDirectory(inputPath);
|
|
478
|
+
}
|
|
479
|
+
if (s.isFile()) {
|
|
480
|
+
return [await scanFile(inputPath)];
|
|
481
|
+
}
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
// ── Health Score ─────────────────────────────────────────────────────────────
|
|
485
|
+
export function computeSkillHealthScore(results) {
|
|
486
|
+
const allFindings = results.flatMap((r) => r.findings);
|
|
487
|
+
if (allFindings.length === 0)
|
|
488
|
+
return 100;
|
|
489
|
+
const deductions = {
|
|
490
|
+
high: 25,
|
|
491
|
+
medium: 15,
|
|
492
|
+
low: 5,
|
|
493
|
+
info: 0,
|
|
494
|
+
};
|
|
495
|
+
let score = 100;
|
|
496
|
+
for (const f of allFindings) {
|
|
497
|
+
score -= deductions[f.severity] ?? 0;
|
|
498
|
+
}
|
|
499
|
+
return Math.max(0, score);
|
|
500
|
+
}
|
|
501
|
+
export function summarizeScan(results) {
|
|
502
|
+
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
503
|
+
return {
|
|
504
|
+
totalFiles: results.length,
|
|
505
|
+
totalFindings,
|
|
506
|
+
healthScore: computeSkillHealthScore(results),
|
|
507
|
+
results,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// ── Renderers ────────────────────────────────────────────────────────────────
|
|
511
|
+
export function renderSkillScanTerminal(results, healthScore) {
|
|
512
|
+
const lines = [];
|
|
513
|
+
const allFindings = results.flatMap((r) => r.findings);
|
|
514
|
+
const highCount = allFindings.filter((f) => f.severity === "high").length;
|
|
515
|
+
const mediumCount = allFindings.filter((f) => f.severity === "medium").length;
|
|
516
|
+
const lowCount = allFindings.filter((f) => f.severity === "low").length;
|
|
517
|
+
const scoreColor = healthScore >= 80 ? "\x1b[32m" : healthScore >= 50 ? "\x1b[33m" : "\x1b[31m";
|
|
518
|
+
const reset = "\x1b[0m";
|
|
519
|
+
lines.push(`${"\x1b[1m"}Skill Scan Report${reset}`);
|
|
520
|
+
lines.push(`Health Score: ${scoreColor}${healthScore}/100${reset}`);
|
|
521
|
+
lines.push(`Files: ${results.length}, Findings: ${allFindings.length} (${highCount} high, ${mediumCount} medium, ${lowCount} low)`);
|
|
522
|
+
lines.push("");
|
|
523
|
+
for (const result of results) {
|
|
524
|
+
lines.push(`${"\x1b[1m"}${result.filePath}${reset} (${result.findings.length} finding${result.findings.length === 1 ? "" : "s"})`);
|
|
525
|
+
if (result.findings.length === 0) {
|
|
526
|
+
lines.push(` ${"\x1b[32m"}✓ No issues${reset}`);
|
|
527
|
+
lines.push("");
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
for (const finding of result.findings) {
|
|
531
|
+
const sevColor = finding.severity === "high" ? "\x1b[31m"
|
|
532
|
+
: finding.severity === "medium" ? "\x1b[33m"
|
|
533
|
+
: "\x1b[2m";
|
|
534
|
+
lines.push(` ${sevColor}[${finding.severity}]${reset} ${finding.ruleId}: ${finding.message}`);
|
|
535
|
+
for (const match of finding.matches.slice(0, 5)) {
|
|
536
|
+
lines.push(` ${"\x1b[2m"}L${match.line}:${match.column}${reset} ${match.matchText.slice(0, 80)}`);
|
|
537
|
+
}
|
|
538
|
+
if (finding.matches.length > 5) {
|
|
539
|
+
lines.push(` ${"\x1b[2m"}...and ${finding.matches.length - 5} more matches${reset}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
lines.push("");
|
|
543
|
+
}
|
|
544
|
+
return lines.join("\n");
|
|
545
|
+
}
|
|
546
|
+
export function renderSkillScanMarkdown(results, healthScore) {
|
|
547
|
+
const allFindings = results.flatMap((r) => r.findings);
|
|
548
|
+
const highCount = allFindings.filter((f) => f.severity === "high").length;
|
|
549
|
+
const mediumCount = allFindings.filter((f) => f.severity === "medium").length;
|
|
550
|
+
const lowCount = allFindings.filter((f) => f.severity === "low").length;
|
|
551
|
+
const grade = healthScore >= 80 ? "A" : healthScore >= 60 ? "B" : healthScore >= 40 ? "C" : healthScore >= 20 ? "D" : "F";
|
|
552
|
+
const lines = [
|
|
553
|
+
`# Skill Scan Report`,
|
|
554
|
+
``,
|
|
555
|
+
`**Health Score: ${healthScore}/100 (${grade})**`,
|
|
556
|
+
``,
|
|
557
|
+
`| Metric | Value |`,
|
|
558
|
+
`| --- | --- |`,
|
|
559
|
+
`| Files scanned | ${results.length} |`,
|
|
560
|
+
`| Total findings | ${allFindings.length} |`,
|
|
561
|
+
`| High | ${highCount} |`,
|
|
562
|
+
`| Medium | ${mediumCount} |`,
|
|
563
|
+
`| Low | ${lowCount} |`,
|
|
564
|
+
``,
|
|
565
|
+
];
|
|
566
|
+
for (const result of results) {
|
|
567
|
+
lines.push(`## ${result.filePath}`, ``);
|
|
568
|
+
if (result.findings.length === 0) {
|
|
569
|
+
lines.push(`✓ No issues found.`, ``);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
lines.push(`${result.findings.length} finding${result.findings.length === 1 ? "" : "s"}:`, ``);
|
|
573
|
+
lines.push(`| Rule | Severity | Line | Match |`);
|
|
574
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
575
|
+
for (const finding of result.findings) {
|
|
576
|
+
for (const match of finding.matches.slice(0, 3)) {
|
|
577
|
+
lines.push(`| ${finding.ruleId} | ${finding.severity} | ${match.line}:${match.column} | \`${match.matchText.slice(0, 60).replace(/\|/g, "\\|")}\` |`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
lines.push(``);
|
|
581
|
+
}
|
|
582
|
+
return lines.join("\n");
|
|
583
|
+
}
|
|
584
|
+
export function renderSkillScanJson(summary) {
|
|
585
|
+
return JSON.stringify(summary, null, 2);
|
|
586
|
+
}
|
|
587
|
+
export function renderSkillScanSarif(results, healthScore) {
|
|
588
|
+
const sarifResults = [];
|
|
589
|
+
const rules = [];
|
|
590
|
+
const seenRules = new Set();
|
|
591
|
+
for (const result of results) {
|
|
592
|
+
for (const finding of result.findings) {
|
|
593
|
+
if (!seenRules.has(finding.ruleId)) {
|
|
594
|
+
seenRules.add(finding.ruleId);
|
|
595
|
+
rules.push({
|
|
596
|
+
id: finding.ruleId,
|
|
597
|
+
name: finding.ruleId,
|
|
598
|
+
shortDescription: { text: finding.message },
|
|
599
|
+
defaultConfiguration: {
|
|
600
|
+
level: finding.severity === "high" ? "error" : finding.severity === "medium" ? "warning" : "note",
|
|
601
|
+
},
|
|
602
|
+
properties: {
|
|
603
|
+
tags: ["skill-scan", "mcp-observatory", finding.ruleId],
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
for (const match of finding.matches) {
|
|
608
|
+
sarifResults.push({
|
|
609
|
+
ruleId: finding.ruleId,
|
|
610
|
+
level: finding.severity === "high" ? "error" : finding.severity === "medium" ? "warning" : "note",
|
|
611
|
+
message: { text: `${finding.ruleId}: ${match.matchText}` },
|
|
612
|
+
locations: [{
|
|
613
|
+
physicalLocation: {
|
|
614
|
+
artifactLocation: { uri: result.filePath },
|
|
615
|
+
region: { startLine: match.line, startColumn: match.column },
|
|
616
|
+
},
|
|
617
|
+
}],
|
|
618
|
+
properties: {
|
|
619
|
+
ruleId: finding.ruleId,
|
|
620
|
+
severity: finding.severity,
|
|
621
|
+
filePath: result.filePath,
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (finding.matches.length === 0) {
|
|
626
|
+
sarifResults.push({
|
|
627
|
+
ruleId: finding.ruleId,
|
|
628
|
+
level: finding.severity === "high" ? "error" : finding.severity === "medium" ? "warning" : "note",
|
|
629
|
+
message: { text: finding.message },
|
|
630
|
+
locations: [{
|
|
631
|
+
physicalLocation: {
|
|
632
|
+
artifactLocation: { uri: result.filePath },
|
|
633
|
+
region: { startLine: 1 },
|
|
634
|
+
},
|
|
635
|
+
}],
|
|
636
|
+
properties: {
|
|
637
|
+
ruleId: finding.ruleId,
|
|
638
|
+
severity: finding.severity,
|
|
639
|
+
filePath: result.filePath,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const sarif = {
|
|
646
|
+
$schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json",
|
|
647
|
+
version: "2.1.0",
|
|
648
|
+
runs: [{
|
|
649
|
+
tool: {
|
|
650
|
+
driver: {
|
|
651
|
+
name: "mcp-observatory-skill-scan",
|
|
652
|
+
version: "1.0.0",
|
|
653
|
+
informationUri: "https://github.com/KryptosAI/mcp-observatory",
|
|
654
|
+
rules,
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
results: sarifResults,
|
|
658
|
+
properties: {
|
|
659
|
+
healthScore,
|
|
660
|
+
},
|
|
661
|
+
}],
|
|
662
|
+
};
|
|
663
|
+
return JSON.stringify(sarif, null, 2);
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=skill-scan.js.map
|