vskill 0.1.2 → 0.1.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/dist/audit/__fixtures__/clean-project/app.d.ts +1 -0
- package/dist/audit/__fixtures__/clean-project/app.js +8 -0
- package/dist/audit/__fixtures__/clean-project/app.js.map +1 -0
- package/dist/audit/__fixtures__/clean-project/utils.d.ts +2 -0
- package/dist/audit/__fixtures__/clean-project/utils.js +8 -0
- package/dist/audit/__fixtures__/clean-project/utils.js.map +1 -0
- package/dist/audit/__fixtures__/mixed-project/risky.d.ts +1 -0
- package/dist/audit/__fixtures__/mixed-project/risky.js +6 -0
- package/dist/audit/__fixtures__/mixed-project/risky.js.map +1 -0
- package/dist/audit/__fixtures__/mixed-project/safe.d.ts +1 -0
- package/dist/audit/__fixtures__/mixed-project/safe.js +5 -0
- package/dist/audit/__fixtures__/mixed-project/safe.js.map +1 -0
- package/dist/audit/__fixtures__/vulnerable-project/handler.d.ts +4 -0
- package/dist/audit/__fixtures__/vulnerable-project/handler.js +21 -0
- package/dist/audit/__fixtures__/vulnerable-project/handler.js.map +1 -0
- package/dist/audit/audit-integration.test.d.ts +1 -0
- package/dist/audit/audit-integration.test.js +92 -0
- package/dist/audit/audit-integration.test.js.map +1 -0
- package/dist/audit/audit-llm.d.ts +25 -0
- package/dist/audit/audit-llm.js +139 -0
- package/dist/audit/audit-llm.js.map +1 -0
- package/dist/audit/audit-llm.test.d.ts +1 -0
- package/dist/audit/audit-llm.test.js +110 -0
- package/dist/audit/audit-llm.test.js.map +1 -0
- package/dist/audit/audit-patterns.d.ts +31 -0
- package/dist/audit/audit-patterns.js +239 -0
- package/dist/audit/audit-patterns.js.map +1 -0
- package/dist/audit/audit-patterns.test.d.ts +1 -0
- package/dist/audit/audit-patterns.test.js +91 -0
- package/dist/audit/audit-patterns.test.js.map +1 -0
- package/dist/audit/audit-scanner.d.ts +16 -0
- package/dist/audit/audit-scanner.js +151 -0
- package/dist/audit/audit-scanner.js.map +1 -0
- package/dist/audit/audit-scanner.test.d.ts +1 -0
- package/dist/audit/audit-scanner.test.js +112 -0
- package/dist/audit/audit-scanner.test.js.map +1 -0
- package/dist/audit/audit-types.d.ts +98 -0
- package/dist/audit/audit-types.js +19 -0
- package/dist/audit/audit-types.js.map +1 -0
- package/dist/audit/audit-types.test.d.ts +1 -0
- package/dist/audit/audit-types.test.js +140 -0
- package/dist/audit/audit-types.test.js.map +1 -0
- package/dist/audit/config.d.ts +13 -0
- package/dist/audit/config.js +90 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/config.test.d.ts +1 -0
- package/dist/audit/config.test.js +44 -0
- package/dist/audit/config.test.js.map +1 -0
- package/dist/audit/file-discovery.d.ts +15 -0
- package/dist/audit/file-discovery.js +153 -0
- package/dist/audit/file-discovery.js.map +1 -0
- package/dist/audit/file-discovery.test.d.ts +1 -0
- package/dist/audit/file-discovery.test.js +120 -0
- package/dist/audit/file-discovery.test.js.map +1 -0
- package/dist/audit/fix-suggestions.d.ts +13 -0
- package/dist/audit/fix-suggestions.js +84 -0
- package/dist/audit/fix-suggestions.js.map +1 -0
- package/dist/audit/fix-suggestions.test.d.ts +1 -0
- package/dist/audit/fix-suggestions.test.js +35 -0
- package/dist/audit/fix-suggestions.test.js.map +1 -0
- package/dist/audit/formatters/json-formatter.d.ts +8 -0
- package/dist/audit/formatters/json-formatter.js +10 -0
- package/dist/audit/formatters/json-formatter.js.map +1 -0
- package/dist/audit/formatters/json-formatter.test.d.ts +1 -0
- package/dist/audit/formatters/json-formatter.test.js +49 -0
- package/dist/audit/formatters/json-formatter.test.js.map +1 -0
- package/dist/audit/formatters/report-formatter.d.ts +8 -0
- package/dist/audit/formatters/report-formatter.js +97 -0
- package/dist/audit/formatters/report-formatter.js.map +1 -0
- package/dist/audit/formatters/report-formatter.test.d.ts +1 -0
- package/dist/audit/formatters/report-formatter.test.js +51 -0
- package/dist/audit/formatters/report-formatter.test.js.map +1 -0
- package/dist/audit/formatters/sarif-formatter.d.ts +11 -0
- package/dist/audit/formatters/sarif-formatter.js +87 -0
- package/dist/audit/formatters/sarif-formatter.js.map +1 -0
- package/dist/audit/formatters/sarif-formatter.test.d.ts +1 -0
- package/dist/audit/formatters/sarif-formatter.test.js +71 -0
- package/dist/audit/formatters/sarif-formatter.test.js.map +1 -0
- package/dist/audit/formatters/terminal-formatter.d.ts +9 -0
- package/dist/audit/formatters/terminal-formatter.js +61 -0
- package/dist/audit/formatters/terminal-formatter.js.map +1 -0
- package/dist/audit/formatters/terminal-formatter.test.d.ts +1 -0
- package/dist/audit/formatters/terminal-formatter.test.js +51 -0
- package/dist/audit/formatters/terminal-formatter.test.js.map +1 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.js +2 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/blocklist/blocklist-e2e.test.d.ts +1 -0
- package/dist/blocklist/blocklist-e2e.test.js +346 -0
- package/dist/blocklist/blocklist-e2e.test.js.map +1 -0
- package/dist/commands/add-blocklist-e2e.test.d.ts +1 -0
- package/dist/commands/add-blocklist-e2e.test.js +390 -0
- package/dist/commands/add-blocklist-e2e.test.js.map +1 -0
- package/dist/commands/add.js +184 -7
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +159 -19
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/audit.d.ts +23 -0
- package/dist/commands/audit.js +100 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/audit.test.d.ts +1 -0
- package/dist/commands/audit.test.js +79 -0
- package/dist/commands/audit.test.js.map +1 -0
- package/dist/discovery/github-tree.d.ts +15 -0
- package/dist/discovery/github-tree.js +56 -0
- package/dist/discovery/github-tree.js.map +1 -0
- package/dist/discovery/github-tree.test.d.ts +1 -0
- package/dist/discovery/github-tree.test.js +143 -0
- package/dist/discovery/github-tree.test.js.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/lockfile/index.d.ts +1 -0
- package/dist/lockfile/index.js +1 -0
- package/dist/lockfile/index.js.map +1 -1
- package/dist/lockfile/lockfile.js +2 -1
- package/dist/lockfile/lockfile.js.map +1 -1
- package/dist/lockfile/project-root.d.ts +11 -0
- package/dist/lockfile/project-root.js +29 -0
- package/dist/lockfile/project-root.js.map +1 -0
- package/dist/lockfile/project-root.test.d.ts +1 -0
- package/dist/lockfile/project-root.test.js +49 -0
- package/dist/lockfile/project-root.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// E2E integration tests: vskill add with blocklist enforcement
|
|
3
|
+
//
|
|
4
|
+
// Tests the FULL error output chain when a user tries to install
|
|
5
|
+
// known-malicious skills from the ClawHub research. Verifies:
|
|
6
|
+
// 1. Installation is REFUSED with exit code 1
|
|
7
|
+
// 2. Error message shows exact threat type, severity, and reason
|
|
8
|
+
// 3. --force override shows prominent WARNING box with full details
|
|
9
|
+
// 4. Tier 1 scan is NEVER reached for blocked skills (without --force)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mocks
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const mockMkdirSync = vi.fn();
|
|
16
|
+
const mockWriteFileSync = vi.fn();
|
|
17
|
+
const mockReadFileSync = vi.fn();
|
|
18
|
+
const mockExistsSync = vi.fn();
|
|
19
|
+
const mockCpSync = vi.fn();
|
|
20
|
+
const mockChmodSync = vi.fn();
|
|
21
|
+
const mockReaddirSync = vi.fn();
|
|
22
|
+
const mockStatSync = vi.fn();
|
|
23
|
+
vi.mock("node:fs", () => ({
|
|
24
|
+
mkdirSync: (...args) => mockMkdirSync(...args),
|
|
25
|
+
writeFileSync: (...args) => mockWriteFileSync(...args),
|
|
26
|
+
readFileSync: (...args) => mockReadFileSync(...args),
|
|
27
|
+
existsSync: (...args) => mockExistsSync(...args),
|
|
28
|
+
cpSync: (...args) => mockCpSync(...args),
|
|
29
|
+
chmodSync: (...args) => mockChmodSync(...args),
|
|
30
|
+
readdirSync: (...args) => mockReaddirSync(...args),
|
|
31
|
+
statSync: (...args) => mockStatSync(...args),
|
|
32
|
+
copyFileSync: vi.fn(),
|
|
33
|
+
}));
|
|
34
|
+
vi.mock("node:path", async () => {
|
|
35
|
+
const actual = await vi.importActual("node:path");
|
|
36
|
+
return { ...actual, join: (...args) => actual.join(...args) };
|
|
37
|
+
});
|
|
38
|
+
const mockDigest = vi.fn().mockReturnValue("abcdef123456xxxx");
|
|
39
|
+
const mockUpdate = vi.fn().mockReturnValue({ digest: mockDigest });
|
|
40
|
+
vi.mock("node:crypto", () => ({
|
|
41
|
+
createHash: () => ({ update: mockUpdate }),
|
|
42
|
+
}));
|
|
43
|
+
const mockDetectInstalledAgents = vi.fn();
|
|
44
|
+
vi.mock("../agents/agents-registry.js", () => ({
|
|
45
|
+
detectInstalledAgents: (...args) => mockDetectInstalledAgents(...args),
|
|
46
|
+
}));
|
|
47
|
+
const mockEnsureLockfile = vi.fn();
|
|
48
|
+
const mockWriteLockfile = vi.fn();
|
|
49
|
+
vi.mock("../lockfile/index.js", () => ({
|
|
50
|
+
ensureLockfile: (...args) => mockEnsureLockfile(...args),
|
|
51
|
+
writeLockfile: (...args) => mockWriteLockfile(...args),
|
|
52
|
+
}));
|
|
53
|
+
const mockRunTier1Scan = vi.fn();
|
|
54
|
+
vi.mock("../scanner/index.js", () => ({
|
|
55
|
+
runTier1Scan: (...args) => mockRunTier1Scan(...args),
|
|
56
|
+
}));
|
|
57
|
+
const mockCheckPlatformSecurity = vi.fn();
|
|
58
|
+
vi.mock("../security/index.js", () => ({
|
|
59
|
+
checkPlatformSecurity: (...args) => mockCheckPlatformSecurity(...args),
|
|
60
|
+
}));
|
|
61
|
+
// ---- REAL blocklist module (not mocked!) ----
|
|
62
|
+
// We mock only the filesystem layer so checkBlocklist reads from our test cache.
|
|
63
|
+
// The blocklist module itself runs its real logic.
|
|
64
|
+
// Mock output utilities to capture colored output as plain text
|
|
65
|
+
vi.mock("../utils/output.js", () => ({
|
|
66
|
+
bold: (s) => s,
|
|
67
|
+
green: (s) => s,
|
|
68
|
+
red: (s) => s,
|
|
69
|
+
yellow: (s) => s,
|
|
70
|
+
dim: (s) => s,
|
|
71
|
+
cyan: (s) => s,
|
|
72
|
+
spinner: () => ({ stop: vi.fn() }),
|
|
73
|
+
}));
|
|
74
|
+
const { addCommand } = await import("./add.js");
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Seed data cache — same data that the API would return
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
const SEED_CACHE = {
|
|
79
|
+
entries: [
|
|
80
|
+
{
|
|
81
|
+
skillName: "Clawhub",
|
|
82
|
+
sourceUrl: "https://github.com/hightower6eu/Clawhub",
|
|
83
|
+
sourceRegistry: "clawhub",
|
|
84
|
+
threatType: "platform-impersonation",
|
|
85
|
+
severity: "critical",
|
|
86
|
+
reason: "Impersonates the ClawHub/GitHub platform to trick users into running malicious code",
|
|
87
|
+
evidenceUrls: ["https://snyk.io/blog/toxicskills-mcp-exploit"],
|
|
88
|
+
discoveredAt: "2025-12-01T00:00:00.000Z",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
skillName: "polymarket-traiding-bot",
|
|
92
|
+
sourceUrl: "https://github.com/Aslaep123/polymarket-traiding-bot",
|
|
93
|
+
sourceRegistry: "clawhub",
|
|
94
|
+
threatType: "credential-theft",
|
|
95
|
+
severity: "critical",
|
|
96
|
+
reason: "Base64-encoded credential exfiltration via hidden HTTP requests",
|
|
97
|
+
evidenceUrls: ["https://snyk.io/blog/toxicskills-mcp-exploit"],
|
|
98
|
+
discoveredAt: "2025-12-01T00:00:00.000Z",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
skillName: "Skills Auto-Updater",
|
|
102
|
+
sourceUrl: "https://github.com/hightower6eu/skills-auto-updater",
|
|
103
|
+
sourceRegistry: "clawhub",
|
|
104
|
+
threatType: "auto-updater-trojan",
|
|
105
|
+
severity: "high",
|
|
106
|
+
reason: "Auto-updater trojan that downloads and executes malicious payloads",
|
|
107
|
+
evidenceUrls: ["https://snyk.io/blog/toxicskills-mcp-exploit"],
|
|
108
|
+
discoveredAt: "2025-12-01T00:00:00.000Z",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
skillName: "google-qx4",
|
|
112
|
+
sourceUrl: "https://github.com/aztr0nutzs/google-qx4",
|
|
113
|
+
sourceRegistry: "clawhub",
|
|
114
|
+
threatType: "prompt-injection",
|
|
115
|
+
severity: "critical",
|
|
116
|
+
reason: "Prompt injection via fake Google integration skill",
|
|
117
|
+
evidenceUrls: ["https://www.aikido.dev/blog/malicious-mcp-servers"],
|
|
118
|
+
discoveredAt: "2025-12-01T00:00:00.000Z",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
skillName: "clawhud",
|
|
122
|
+
sourceUrl: "https://github.com/zaycv/clawhud",
|
|
123
|
+
sourceRegistry: "clawhub",
|
|
124
|
+
threatType: "typosquatting",
|
|
125
|
+
severity: "high",
|
|
126
|
+
reason: "Typosquatting ClawHub (clawhud vs clawhub) to mislead users",
|
|
127
|
+
evidenceUrls: ["https://www.aikido.dev/blog/malicious-mcp-servers"],
|
|
128
|
+
discoveredAt: "2025-12-01T00:00:00.000Z",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
count: 5,
|
|
132
|
+
lastUpdated: "2026-02-19T00:00:00Z",
|
|
133
|
+
fetchedAt: new Date().toISOString(),
|
|
134
|
+
etag: '"seed"',
|
|
135
|
+
};
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Helpers
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
function setupBlocklistCache() {
|
|
140
|
+
// The blocklist module reads from ~/.vskill/blocklist.json
|
|
141
|
+
// We intercept existsSync and readFileSync to serve our seed cache
|
|
142
|
+
const originalExistsSync = mockExistsSync.getMockImplementation();
|
|
143
|
+
mockExistsSync.mockImplementation((p) => {
|
|
144
|
+
if (p.includes("blocklist.json"))
|
|
145
|
+
return true;
|
|
146
|
+
if (originalExistsSync)
|
|
147
|
+
return originalExistsSync(p);
|
|
148
|
+
return false;
|
|
149
|
+
});
|
|
150
|
+
const originalReadFileSync = mockReadFileSync.getMockImplementation();
|
|
151
|
+
mockReadFileSync.mockImplementation((p, ...args) => {
|
|
152
|
+
if (p.includes("blocklist.json"))
|
|
153
|
+
return JSON.stringify(SEED_CACHE);
|
|
154
|
+
if (originalReadFileSync)
|
|
155
|
+
return originalReadFileSync(p, ...args);
|
|
156
|
+
return "";
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function collectOutput(spy) {
|
|
160
|
+
return spy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Tests
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
const originalFetch = globalThis.fetch;
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
vi.clearAllMocks();
|
|
168
|
+
vi.spyOn(console, "log").mockImplementation(() => { });
|
|
169
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
170
|
+
mockCheckPlatformSecurity.mockResolvedValue(null);
|
|
171
|
+
// Mock fetch for GitHub path (fetching SKILL.md)
|
|
172
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
173
|
+
ok: true,
|
|
174
|
+
text: async () => "# Safe Skill\nNormal content",
|
|
175
|
+
json: async () => ({ entries: [], count: 0, lastUpdated: null }),
|
|
176
|
+
headers: { get: () => null },
|
|
177
|
+
});
|
|
178
|
+
setupBlocklistCache();
|
|
179
|
+
});
|
|
180
|
+
afterEach(() => {
|
|
181
|
+
globalThis.fetch = originalFetch;
|
|
182
|
+
});
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// 1. GitHub path — blocking with exact error details
|
|
185
|
+
// ============================================================================
|
|
186
|
+
describe("E2E: vskill add blocks ClawHub malicious skills (GitHub path)", () => {
|
|
187
|
+
it("refuses to install hightower6eu's Clawhub with full threat details", async () => {
|
|
188
|
+
const mockExit = vi
|
|
189
|
+
.spyOn(process, "exit")
|
|
190
|
+
.mockImplementation(() => {
|
|
191
|
+
throw new Error("process.exit");
|
|
192
|
+
});
|
|
193
|
+
await expect(addCommand("hightower6eu/Clawhub", {})).rejects.toThrow("process.exit");
|
|
194
|
+
const errorOutput = collectOutput(console.error);
|
|
195
|
+
// Must show BLOCKED status
|
|
196
|
+
expect(errorOutput).toContain("BLOCKED");
|
|
197
|
+
// Must show the exact skill name
|
|
198
|
+
expect(errorOutput).toContain("Clawhub");
|
|
199
|
+
// Must show the threat type
|
|
200
|
+
expect(errorOutput).toContain("platform-impersonation");
|
|
201
|
+
// Must show the severity
|
|
202
|
+
expect(errorOutput).toContain("critical");
|
|
203
|
+
// Must show the reason WHY
|
|
204
|
+
expect(errorOutput).toContain("Impersonates the ClawHub");
|
|
205
|
+
// Must show how to override
|
|
206
|
+
expect(errorOutput).toContain("--force");
|
|
207
|
+
// Exit code must be 1
|
|
208
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
209
|
+
// Tier 1 scan must NEVER be called (blocked before scan)
|
|
210
|
+
expect(mockRunTier1Scan).not.toHaveBeenCalled();
|
|
211
|
+
mockExit.mockRestore();
|
|
212
|
+
});
|
|
213
|
+
it("refuses to install Aslaep123's credential-stealing bot with Base64 exfil details", async () => {
|
|
214
|
+
const mockExit = vi
|
|
215
|
+
.spyOn(process, "exit")
|
|
216
|
+
.mockImplementation(() => {
|
|
217
|
+
throw new Error("process.exit");
|
|
218
|
+
});
|
|
219
|
+
await expect(addCommand("Aslaep123/polymarket-traiding-bot", {})).rejects.toThrow("process.exit");
|
|
220
|
+
const errorOutput = collectOutput(console.error);
|
|
221
|
+
expect(errorOutput).toContain("BLOCKED");
|
|
222
|
+
expect(errorOutput).toContain("polymarket-traiding-bot");
|
|
223
|
+
expect(errorOutput).toContain("credential-theft");
|
|
224
|
+
expect(errorOutput).toContain("critical");
|
|
225
|
+
expect(errorOutput).toContain("Base64-encoded credential exfiltration");
|
|
226
|
+
expect(mockRunTier1Scan).not.toHaveBeenCalled();
|
|
227
|
+
mockExit.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
it("refuses to install aztr0nutzs's prompt injection skill", async () => {
|
|
230
|
+
const mockExit = vi
|
|
231
|
+
.spyOn(process, "exit")
|
|
232
|
+
.mockImplementation(() => {
|
|
233
|
+
throw new Error("process.exit");
|
|
234
|
+
});
|
|
235
|
+
await expect(addCommand("aztr0nutzs/google-qx4", {})).rejects.toThrow("process.exit");
|
|
236
|
+
const errorOutput = collectOutput(console.error);
|
|
237
|
+
expect(errorOutput).toContain("BLOCKED");
|
|
238
|
+
expect(errorOutput).toContain("google-qx4");
|
|
239
|
+
expect(errorOutput).toContain("prompt-injection");
|
|
240
|
+
expect(errorOutput).toContain("critical");
|
|
241
|
+
expect(errorOutput).toContain("Prompt injection");
|
|
242
|
+
expect(mockRunTier1Scan).not.toHaveBeenCalled();
|
|
243
|
+
mockExit.mockRestore();
|
|
244
|
+
});
|
|
245
|
+
it("refuses to install zaycv's typosquatting skill", async () => {
|
|
246
|
+
const mockExit = vi
|
|
247
|
+
.spyOn(process, "exit")
|
|
248
|
+
.mockImplementation(() => {
|
|
249
|
+
throw new Error("process.exit");
|
|
250
|
+
});
|
|
251
|
+
await expect(addCommand("zaycv/clawhud", {})).rejects.toThrow("process.exit");
|
|
252
|
+
const errorOutput = collectOutput(console.error);
|
|
253
|
+
expect(errorOutput).toContain("BLOCKED");
|
|
254
|
+
expect(errorOutput).toContain("clawhud");
|
|
255
|
+
expect(errorOutput).toContain("typosquatting");
|
|
256
|
+
expect(errorOutput).toContain("high");
|
|
257
|
+
expect(errorOutput).toContain("clawhud vs clawhub");
|
|
258
|
+
mockExit.mockRestore();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// 2. --force override shows WARNING with full details
|
|
263
|
+
// ============================================================================
|
|
264
|
+
describe("E2E: --force override shows warning box with threat details", () => {
|
|
265
|
+
it("shows credential-theft warning box but continues for polymarket-traiding-bot", async () => {
|
|
266
|
+
mockRunTier1Scan.mockReturnValue({
|
|
267
|
+
verdict: "PASS",
|
|
268
|
+
findings: [],
|
|
269
|
+
score: 100,
|
|
270
|
+
patternsChecked: 37,
|
|
271
|
+
criticalCount: 0,
|
|
272
|
+
highCount: 0,
|
|
273
|
+
mediumCount: 0,
|
|
274
|
+
lowCount: 0,
|
|
275
|
+
infoCount: 0,
|
|
276
|
+
durationMs: 1,
|
|
277
|
+
});
|
|
278
|
+
mockDetectInstalledAgents.mockResolvedValue([
|
|
279
|
+
{
|
|
280
|
+
id: "claude-code",
|
|
281
|
+
displayName: "Claude Code",
|
|
282
|
+
localSkillsDir: ".claude/commands",
|
|
283
|
+
globalSkillsDir: "~/.claude/commands",
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
mockEnsureLockfile.mockReturnValue({
|
|
287
|
+
version: 1,
|
|
288
|
+
agents: [],
|
|
289
|
+
skills: {},
|
|
290
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
291
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
292
|
+
});
|
|
293
|
+
await addCommand("Aslaep123/polymarket-traiding-bot", { force: true });
|
|
294
|
+
const errorOutput = collectOutput(console.error);
|
|
295
|
+
// WARNING box must be shown
|
|
296
|
+
expect(errorOutput).toContain("WARNING");
|
|
297
|
+
expect(errorOutput).toContain("known-malicious");
|
|
298
|
+
// Must show full threat details in the warning
|
|
299
|
+
expect(errorOutput).toContain("polymarket-traiding-bot");
|
|
300
|
+
expect(errorOutput).toContain("credential-theft");
|
|
301
|
+
expect(errorOutput).toContain("critical");
|
|
302
|
+
expect(errorOutput).toContain("Base64-encoded credential exfiltration");
|
|
303
|
+
// Must show --force was used
|
|
304
|
+
expect(errorOutput).toContain("--force");
|
|
305
|
+
// Despite the warning, Tier 1 scan MUST proceed
|
|
306
|
+
expect(mockRunTier1Scan).toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
it("shows platform-impersonation warning for Clawhub with --force", async () => {
|
|
309
|
+
mockRunTier1Scan.mockReturnValue({
|
|
310
|
+
verdict: "PASS",
|
|
311
|
+
findings: [],
|
|
312
|
+
score: 100,
|
|
313
|
+
patternsChecked: 37,
|
|
314
|
+
criticalCount: 0,
|
|
315
|
+
highCount: 0,
|
|
316
|
+
mediumCount: 0,
|
|
317
|
+
lowCount: 0,
|
|
318
|
+
infoCount: 0,
|
|
319
|
+
durationMs: 1,
|
|
320
|
+
});
|
|
321
|
+
mockDetectInstalledAgents.mockResolvedValue([
|
|
322
|
+
{
|
|
323
|
+
id: "claude-code",
|
|
324
|
+
displayName: "Claude Code",
|
|
325
|
+
localSkillsDir: ".claude/commands",
|
|
326
|
+
globalSkillsDir: "~/.claude/commands",
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
mockEnsureLockfile.mockReturnValue({
|
|
330
|
+
version: 1,
|
|
331
|
+
agents: [],
|
|
332
|
+
skills: {},
|
|
333
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
334
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
335
|
+
});
|
|
336
|
+
await addCommand("hightower6eu/Clawhub", { force: true });
|
|
337
|
+
const errorOutput = collectOutput(console.error);
|
|
338
|
+
expect(errorOutput).toContain("WARNING");
|
|
339
|
+
expect(errorOutput).toContain("Clawhub");
|
|
340
|
+
expect(errorOutput).toContain("platform-impersonation");
|
|
341
|
+
expect(errorOutput).toContain("critical");
|
|
342
|
+
expect(errorOutput).toContain("Impersonates the ClawHub");
|
|
343
|
+
expect(mockRunTier1Scan).toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// 3. Safe skills pass through without blocklist interference
|
|
348
|
+
// ============================================================================
|
|
349
|
+
describe("E2E: safe skills are not affected by blocklist", () => {
|
|
350
|
+
it("installs legitimate skill without any BLOCKED or WARNING output", async () => {
|
|
351
|
+
mockRunTier1Scan.mockReturnValue({
|
|
352
|
+
verdict: "PASS",
|
|
353
|
+
findings: [],
|
|
354
|
+
score: 100,
|
|
355
|
+
patternsChecked: 37,
|
|
356
|
+
criticalCount: 0,
|
|
357
|
+
highCount: 0,
|
|
358
|
+
mediumCount: 0,
|
|
359
|
+
lowCount: 0,
|
|
360
|
+
infoCount: 0,
|
|
361
|
+
durationMs: 1,
|
|
362
|
+
});
|
|
363
|
+
mockDetectInstalledAgents.mockResolvedValue([
|
|
364
|
+
{
|
|
365
|
+
id: "claude-code",
|
|
366
|
+
displayName: "Claude Code",
|
|
367
|
+
localSkillsDir: ".claude/commands",
|
|
368
|
+
globalSkillsDir: "~/.claude/commands",
|
|
369
|
+
},
|
|
370
|
+
]);
|
|
371
|
+
mockEnsureLockfile.mockReturnValue({
|
|
372
|
+
version: 1,
|
|
373
|
+
agents: [],
|
|
374
|
+
skills: {},
|
|
375
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
376
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
377
|
+
});
|
|
378
|
+
await addCommand("anthropic/code-review-assistant", {});
|
|
379
|
+
const allErrorOutput = collectOutput(console.error);
|
|
380
|
+
// No BLOCKED or WARNING messages
|
|
381
|
+
expect(allErrorOutput).not.toContain("BLOCKED");
|
|
382
|
+
expect(allErrorOutput).not.toContain("WARNING");
|
|
383
|
+
expect(allErrorOutput).not.toContain("malicious");
|
|
384
|
+
// Tier 1 scan MUST proceed
|
|
385
|
+
expect(mockRunTier1Scan).toHaveBeenCalled();
|
|
386
|
+
// Lockfile MUST be updated
|
|
387
|
+
expect(mockEnsureLockfile).toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
//# sourceMappingURL=add-blocklist-e2e.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-blocklist-e2e.test.js","sourceRoot":"","sources":["../../src/commands/add-blocklist-e2e.test.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/add.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// vskill add -- install a skill from GitHub or local plugin directory
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync,
|
|
4
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, copyFileSync, statSync, chmodSync, readdirSync, rmSync, } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
7
8
|
import { resolveTilde } from "../utils/paths.js";
|
|
8
9
|
import { detectInstalledAgents } from "../agents/agents-registry.js";
|
|
9
10
|
import { ensureLockfile, writeLockfile } from "../lockfile/index.js";
|
|
@@ -11,7 +12,74 @@ import { runTier1Scan } from "../scanner/index.js";
|
|
|
11
12
|
import { getPluginSource, getPluginVersion } from "../marketplace/index.js";
|
|
12
13
|
import { checkBlocklist } from "../blocklist/blocklist.js";
|
|
13
14
|
import { checkPlatformSecurity } from "../security/index.js";
|
|
15
|
+
import { discoverSkills } from "../discovery/github-tree.js";
|
|
14
16
|
import { bold, green, red, yellow, dim, cyan, spinner, } from "../utils/output.js";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Command file filter (prevents plugin internals leaking as slash commands)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Returns true for .md files that should NOT be installed into an agent's
|
|
22
|
+
* commands directory. Claude Code (and similar agents) register every .md
|
|
23
|
+
* file they find recursively as a slash command, so plugin-internal files
|
|
24
|
+
* must be excluded.
|
|
25
|
+
*/
|
|
26
|
+
function shouldSkipFromCommands(relPath) {
|
|
27
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
28
|
+
const parts = normalized.split("/");
|
|
29
|
+
const filename = parts[parts.length - 1];
|
|
30
|
+
if (!filename.endsWith(".md"))
|
|
31
|
+
return false;
|
|
32
|
+
if (parts.length === 1 && filename === "PLUGIN.md")
|
|
33
|
+
return true;
|
|
34
|
+
if (filename === "README.md")
|
|
35
|
+
return true;
|
|
36
|
+
if (filename === "FRESHNESS.md")
|
|
37
|
+
return true;
|
|
38
|
+
if (parts.length > 1 && parts[0].startsWith("."))
|
|
39
|
+
return true;
|
|
40
|
+
const internalRootDirs = new Set(["knowledge-base", "lib", "templates", "scripts", "hooks"]);
|
|
41
|
+
if (parts.length > 1 && internalRootDirs.has(parts[0]))
|
|
42
|
+
return true;
|
|
43
|
+
if (parts[0] === "skills" && parts.length > 2 && filename !== "SKILL.md")
|
|
44
|
+
return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
function copyPluginFiltered(sourceDir, targetDir, relBase = "") {
|
|
48
|
+
mkdirSync(targetDir, { recursive: true });
|
|
49
|
+
const entries = readdirSync(sourceDir);
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const relPath = relBase ? `${relBase}/${entry}` : entry;
|
|
52
|
+
const sourcePath = join(sourceDir, entry);
|
|
53
|
+
const stat = statSync(sourcePath);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
// Flatten: root-level commands/ and skills/ merge into the parent target dir
|
|
56
|
+
const isFlattened = !relBase && (entry === "commands" || entry === "skills");
|
|
57
|
+
const nextTargetDir = isFlattened ? targetDir : join(targetDir, entry);
|
|
58
|
+
copyPluginFiltered(sourcePath, nextTargetDir, relPath);
|
|
59
|
+
}
|
|
60
|
+
else if (stat.isFile() && !shouldSkipFromCommands(relPath)) {
|
|
61
|
+
copyFileSync(sourcePath, join(targetDir, entry));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Plugin cache cleanup
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Remove a plugin installed via Claude Code's plugin system.
|
|
70
|
+
* The cache at ~/.claude/plugins/cache/ contains ALL files without filtering,
|
|
71
|
+
* causing internal .md files to leak as ghost slash commands.
|
|
72
|
+
*
|
|
73
|
+
* Uses `claude plugin uninstall` CLI to properly remove the plugin through
|
|
74
|
+
* Claude Code's own API rather than directly manipulating internal files.
|
|
75
|
+
*/
|
|
76
|
+
function cleanPluginCache(pluginName, marketplace) {
|
|
77
|
+
const pluginKey = `${pluginName}@${marketplace}`;
|
|
78
|
+
try {
|
|
79
|
+
execSync(`claude plugin uninstall "${pluginKey}"`, { stdio: "ignore", timeout: 10_000 });
|
|
80
|
+
}
|
|
81
|
+
catch { /* ignore - plugin might not be installed via CLI */ }
|
|
82
|
+
}
|
|
15
83
|
async function fetchSkillContent(url) {
|
|
16
84
|
const spin = spinner("Fetching skill");
|
|
17
85
|
try {
|
|
@@ -175,14 +243,27 @@ async function installPluginDir(basePath, pluginName, opts) {
|
|
|
175
243
|
const marketplacePath = join(basePath, ".claude-plugin", "marketplace.json");
|
|
176
244
|
const marketplaceContent = readFileSync(marketplacePath, "utf-8");
|
|
177
245
|
const version = getPluginVersion(pluginName, marketplaceContent) || "0.0.0";
|
|
246
|
+
// Remove unfiltered plugin cache that Claude Code's plugin system creates.
|
|
247
|
+
// The cache at ~/.claude/plugins/cache/ contains ALL files without filtering,
|
|
248
|
+
// causing internal .md files to leak as ghost slash commands.
|
|
249
|
+
try {
|
|
250
|
+
const marketplaceName = JSON.parse(marketplaceContent).name;
|
|
251
|
+
if (marketplaceName) {
|
|
252
|
+
cleanPluginCache(pluginName, marketplaceName);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore parse errors */ }
|
|
178
256
|
// Install: recursively copy plugin directory to cache
|
|
179
257
|
const sha = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
180
258
|
const locations = [];
|
|
181
259
|
for (const agent of agents) {
|
|
182
260
|
const cacheDir = join(opts.global ? resolveTilde(agent.globalSkillsDir) : join(process.cwd(), agent.localSkillsDir), pluginName);
|
|
183
261
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
262
|
+
// Full clean before copy: removes stale files from older installs
|
|
263
|
+
if (existsSync(cacheDir)) {
|
|
264
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
copyPluginFiltered(pluginDir, cacheDir);
|
|
186
267
|
fixHookPermissions(cacheDir);
|
|
187
268
|
locations.push(`${agent.displayName}: ${cacheDir}`);
|
|
188
269
|
}
|
|
@@ -209,6 +290,53 @@ async function installPluginDir(basePath, pluginName, opts) {
|
|
|
209
290
|
}
|
|
210
291
|
console.log(dim(`\nSHA: ${sha} | Version: ${version}`));
|
|
211
292
|
}
|
|
293
|
+
async function installOneGitHubSkill(owner, repo, skillName, rawUrl, opts, agents) {
|
|
294
|
+
// Fetch content (non-exiting for multi-skill support)
|
|
295
|
+
let content;
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch(rawUrl);
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
return { skillName, installed: false, verdict: "FETCH_FAILED" };
|
|
300
|
+
}
|
|
301
|
+
content = await res.text();
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return { skillName, installed: false, verdict: "FETCH_FAILED" };
|
|
305
|
+
}
|
|
306
|
+
// Blocklist check
|
|
307
|
+
const blocked = await checkBlocklist(skillName, undefined);
|
|
308
|
+
if (blocked && !opts.force) {
|
|
309
|
+
printBlockedError(blocked);
|
|
310
|
+
return { skillName, installed: false, verdict: "BLOCKED" };
|
|
311
|
+
}
|
|
312
|
+
if (blocked && opts.force) {
|
|
313
|
+
printBlockedWarning(blocked);
|
|
314
|
+
}
|
|
315
|
+
// Platform security check
|
|
316
|
+
const platformSecurity = await checkPlatformSecurity(skillName);
|
|
317
|
+
if (platformSecurity && platformSecurity.hasCritical && !opts.force) {
|
|
318
|
+
return { skillName, installed: false, verdict: "SECURITY_FAIL" };
|
|
319
|
+
}
|
|
320
|
+
// Tier 1 scan
|
|
321
|
+
const scanResult = runTier1Scan(content);
|
|
322
|
+
if ((scanResult.verdict === "FAIL" || scanResult.verdict === "CONCERNS") && !opts.force) {
|
|
323
|
+
return { skillName, installed: false, verdict: scanResult.verdict };
|
|
324
|
+
}
|
|
325
|
+
// Install to each agent
|
|
326
|
+
const sha = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
327
|
+
for (const agent of agents) {
|
|
328
|
+
const baseDir = opts.global
|
|
329
|
+
? resolveTilde(agent.globalSkillsDir)
|
|
330
|
+
: join(process.cwd(), agent.localSkillsDir);
|
|
331
|
+
const skillDir = join(baseDir, skillName);
|
|
332
|
+
try {
|
|
333
|
+
mkdirSync(skillDir, { recursive: true });
|
|
334
|
+
writeFileSync(join(skillDir, "SKILL.md"), content, "utf-8");
|
|
335
|
+
}
|
|
336
|
+
catch { /* skip agent on write error */ }
|
|
337
|
+
}
|
|
338
|
+
return { skillName, installed: true, verdict: scanResult.verdict, sha };
|
|
339
|
+
}
|
|
212
340
|
// ---------------------------------------------------------------------------
|
|
213
341
|
// Main entry point
|
|
214
342
|
// ---------------------------------------------------------------------------
|
|
@@ -224,14 +352,63 @@ export async function addCommand(source, opts) {
|
|
|
224
352
|
process.exit(1);
|
|
225
353
|
}
|
|
226
354
|
const [owner, repo] = parts;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
355
|
+
// --skill flag: single-skill mode (no discovery, backward compat)
|
|
356
|
+
if (opts.skill) {
|
|
357
|
+
return installSingleSkillLegacy(owner, repo, opts.skill, opts);
|
|
358
|
+
}
|
|
359
|
+
// Discovery mode: find all skills in the repo
|
|
360
|
+
const discovered = (await discoverSkills(owner, repo)) || [];
|
|
361
|
+
if (discovered.length === 0) {
|
|
362
|
+
// Fallback: discovery returned nothing — try root SKILL.md directly
|
|
363
|
+
return installSingleSkillLegacy(owner, repo, undefined, opts);
|
|
364
|
+
}
|
|
365
|
+
// Multi-skill install
|
|
366
|
+
const agents = await detectInstalledAgents();
|
|
367
|
+
if (agents.length === 0) {
|
|
368
|
+
console.error(red("No AI agents detected. Run ") + cyan("vskill init") + red(" first."));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const results = [];
|
|
372
|
+
for (const skill of discovered) {
|
|
373
|
+
console.log(dim(`\nInstalling skill: ${bold(skill.name)}...`));
|
|
374
|
+
const result = await installOneGitHubSkill(owner, repo, skill.name, skill.rawUrl, opts, agents);
|
|
375
|
+
results.push(result);
|
|
376
|
+
}
|
|
377
|
+
// Update lockfile with all installed skills
|
|
378
|
+
const lock = ensureLockfile();
|
|
379
|
+
for (const r of results) {
|
|
380
|
+
if (r.installed && r.sha) {
|
|
381
|
+
lock.skills[r.skillName] = {
|
|
382
|
+
version: "0.0.0",
|
|
383
|
+
sha: r.sha,
|
|
384
|
+
tier: "SCANNED",
|
|
385
|
+
installedAt: new Date().toISOString(),
|
|
386
|
+
source: `github:${owner}/${repo}`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
lock.agents = agents.map((a) => a.id);
|
|
391
|
+
writeLockfile(lock);
|
|
392
|
+
// Summary
|
|
393
|
+
const installed = results.filter((r) => r.installed);
|
|
394
|
+
const skipped = results.filter((r) => !r.installed);
|
|
395
|
+
console.log(green(`\nInstalled ${bold(String(installed.length))} of ${results.length} skills:\n`));
|
|
396
|
+
for (const r of results) {
|
|
397
|
+
const icon = r.installed ? green("✓") : red("✗");
|
|
398
|
+
const detail = r.installed ? dim(`(${r.verdict})`) : red(`(${r.verdict})`);
|
|
399
|
+
console.log(` ${icon} ${r.skillName} ${detail}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Legacy single-skill install (preserves original behavior exactly)
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
async function installSingleSkillLegacy(owner, repo, skill, opts) {
|
|
406
|
+
const skillSubpath = skill ? `skills/${skill}/SKILL.md` : "SKILL.md";
|
|
230
407
|
const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/${skillSubpath}`;
|
|
231
408
|
// Fetch SKILL.md
|
|
232
409
|
const content = await fetchSkillContent(url);
|
|
233
410
|
// Blocklist check (before scanning)
|
|
234
|
-
const skillName =
|
|
411
|
+
const skillName = skill || repo;
|
|
235
412
|
const blocked = await checkBlocklist(skillName, undefined);
|
|
236
413
|
if (blocked && !opts.force) {
|
|
237
414
|
printBlockedError(blocked);
|