skillsio 1.1.1 → 1.1.2
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/README.md +28 -2
- package/dist/cli.mjs +142 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,23 @@ domain patterns that regex rules can't — letting you eyeball where a skill wan
|
|
|
50
50
|
With `--yes`, URL-only prompts are auto-continued. Skills with high/critical findings always show URLs alongside the
|
|
51
51
|
findings summary.
|
|
52
52
|
|
|
53
|
+
### Third-Party Audits via skills.sh
|
|
54
|
+
|
|
55
|
+
For GitHub-sourced skills, the CLI automatically checks [skills.sh](https://skills.sh) — Vercel's official skill
|
|
56
|
+
directory — which runs independent third-party security audits from three auditors: **Snyk**, **Socket**, and
|
|
57
|
+
**Gen Agent Trust Hub**. Results appear alongside local scan output:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
◆ skills.sh: 3 audits [Snyk ✗] [Socket ✓] [Trust Hub ✗]
|
|
61
|
+
https://skills.sh/inference-sh-3/skills/agent-tools
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Green ✓ = auditor passed, Red ✗ = auditor failed, Dim ~ = no result yet
|
|
65
|
+
- If any auditor returns a **Fail** verdict, severity is escalated to at least **High**, triggering a confirmation
|
|
66
|
+
prompt
|
|
67
|
+
- skills.sh lookup runs in parallel with VT and never blocks installation on error (graceful fallback)
|
|
68
|
+
- Only fires for GitHub-sourced skills that are listed on skills.sh — silent for everything else
|
|
69
|
+
|
|
53
70
|
### Optional: VirusTotal Integration
|
|
54
71
|
|
|
55
72
|
When a [VirusTotal](https://www.virustotal.com/) API key is provided, the CLI also hashes each skill's content
|
|
@@ -306,10 +323,12 @@ pnpm format # Format code with Prettier
|
|
|
306
323
|
- `src/scanner.ts` — Rules engine. Defines ~81 regex rules across 8 threat categories, a correlation engine for
|
|
307
324
|
multi-signal detection, and optional deep taint analysis integration. Supports loading external rules from JSON
|
|
308
325
|
files via `--rules`.
|
|
309
|
-
- `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs
|
|
310
|
-
escalation logic and user confirmation prompts.
|
|
326
|
+
- `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs VT and skills.sh lookups in parallel,
|
|
327
|
+
handles escalation logic and user confirmation prompts.
|
|
311
328
|
- `src/vt.ts` — VirusTotal API client. SHA-256 hashing, `GET /api/v3/files/{hash}` lookup, verdict mapping, graceful
|
|
312
329
|
error handling.
|
|
330
|
+
- `src/skills-sh.ts` — skills.sh audit client. Fetches and HTML-parses third-party audit results (Snyk, Socket, Gen
|
|
331
|
+
Agent Trust Hub) for GitHub-sourced skills with a 5-second timeout; always resolves gracefully.
|
|
313
332
|
- `src/deep-scan/` — Deep taint analysis engine (enabled via `--deep-scan`). Regex-based tokenizers extract sources,
|
|
314
333
|
sinks, and assignments from Python/JS/TS files; a forward taint tracker propagates data flow; a cross-file analyzer
|
|
315
334
|
detects multi-file attack patterns via import graph analysis. See [docs/deep-scan.md](docs/deep-scan.md).
|
|
@@ -318,6 +337,13 @@ pnpm format # Format code with Prettier
|
|
|
318
337
|
|
|
319
338
|
## Changelog
|
|
320
339
|
|
|
340
|
+
### 1.1.2
|
|
341
|
+
|
|
342
|
+
- **skills.sh audit integration**: for GitHub-sourced skills, the CLI now fetches third-party audit results from
|
|
343
|
+
[skills.sh](https://skills.sh) (Snyk, Socket, Gen Agent Trust Hub) and displays them alongside local scan output
|
|
344
|
+
- A skills.sh Fail verdict from any auditor escalates severity to at least High, triggering a confirmation prompt
|
|
345
|
+
- Lookups run in parallel with VirusTotal and fail silently on any network or parse error
|
|
346
|
+
|
|
321
347
|
### 1.1.1
|
|
322
348
|
|
|
323
349
|
- Removed anonymous usage telemetry inherited from the original Vercel `skills` CLI
|
package/dist/cli.mjs
CHANGED
|
@@ -29,6 +29,14 @@ function getOwnerRepo(parsed) {
|
|
|
29
29
|
} catch {}
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
+
function parseOwnerRepo(ownerRepo) {
|
|
33
|
+
const match = ownerRepo.match(/^([^/]+)\/([^/]+)$/);
|
|
34
|
+
if (match) return {
|
|
35
|
+
owner: match[1],
|
|
36
|
+
repo: match[2]
|
|
37
|
+
};
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
32
40
|
async function isRepoPrivate(owner, repo) {
|
|
33
41
|
try {
|
|
34
42
|
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
|
@@ -3247,7 +3255,7 @@ function extractRemoteSkillFiles(remoteSkill) {
|
|
|
3247
3255
|
function extractWellKnownSkillFiles(skill) {
|
|
3248
3256
|
return skill.files;
|
|
3249
3257
|
}
|
|
3250
|
-
const NOT_FOUND = {
|
|
3258
|
+
const NOT_FOUND$1 = {
|
|
3251
3259
|
found: false,
|
|
3252
3260
|
verdict: "unknown",
|
|
3253
3261
|
maliciousCount: 0,
|
|
@@ -3258,19 +3266,19 @@ async function lookupFileHash(sha256, apiKey) {
|
|
|
3258
3266
|
try {
|
|
3259
3267
|
response = await fetch(`https://www.virustotal.com/api/v3/files/${sha256}`, { headers: { "x-apikey": apiKey } });
|
|
3260
3268
|
} catch {
|
|
3261
|
-
return NOT_FOUND;
|
|
3269
|
+
return NOT_FOUND$1;
|
|
3262
3270
|
}
|
|
3263
|
-
if (response.status === 404) return NOT_FOUND;
|
|
3264
|
-
if (response.status === 429) return NOT_FOUND;
|
|
3265
|
-
if (!response.ok) return NOT_FOUND;
|
|
3271
|
+
if (response.status === 404) return NOT_FOUND$1;
|
|
3272
|
+
if (response.status === 429) return NOT_FOUND$1;
|
|
3273
|
+
if (!response.ok) return NOT_FOUND$1;
|
|
3266
3274
|
let body;
|
|
3267
3275
|
try {
|
|
3268
3276
|
body = await response.json();
|
|
3269
3277
|
} catch {
|
|
3270
|
-
return NOT_FOUND;
|
|
3278
|
+
return NOT_FOUND$1;
|
|
3271
3279
|
}
|
|
3272
3280
|
const attrs = body.data?.attributes;
|
|
3273
|
-
if (!attrs) return NOT_FOUND;
|
|
3281
|
+
if (!attrs) return NOT_FOUND$1;
|
|
3274
3282
|
const stats = attrs.last_analysis_stats;
|
|
3275
3283
|
const maliciousCount = stats?.malicious ?? 0;
|
|
3276
3284
|
const totalEngines = stats ? Object.values(stats).reduce((sum, n) => sum + n, 0) : 0;
|
|
@@ -3296,6 +3304,64 @@ async function lookupFileHash(sha256, apiKey) {
|
|
|
3296
3304
|
async function checkSkillOnVT(skillContent, apiKey) {
|
|
3297
3305
|
return lookupFileHash(createHash("sha256").update(skillContent).digest("hex"), apiKey);
|
|
3298
3306
|
}
|
|
3307
|
+
const AUDITORS = [
|
|
3308
|
+
{
|
|
3309
|
+
id: "snyk",
|
|
3310
|
+
displayName: "Snyk"
|
|
3311
|
+
},
|
|
3312
|
+
{
|
|
3313
|
+
id: "socket",
|
|
3314
|
+
displayName: "Socket"
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
id: "agent-trust-hub",
|
|
3318
|
+
displayName: "Gen Agent Trust Hub"
|
|
3319
|
+
}
|
|
3320
|
+
];
|
|
3321
|
+
const NOT_FOUND = {
|
|
3322
|
+
found: false,
|
|
3323
|
+
permalink: "",
|
|
3324
|
+
audits: [],
|
|
3325
|
+
anyFail: false
|
|
3326
|
+
};
|
|
3327
|
+
function parseAudits(html, baseUrl) {
|
|
3328
|
+
return AUDITORS.map(({ id, displayName }) => {
|
|
3329
|
+
const pattern = new RegExp(`\\/security\\/${id}[^]{0,400}?\\b(Pass|Fail)\\b`, "is");
|
|
3330
|
+
const match = html.match(pattern);
|
|
3331
|
+
let status = "unknown";
|
|
3332
|
+
if (match) status = match[1].toLowerCase() === "pass" ? "pass" : "fail";
|
|
3333
|
+
return {
|
|
3334
|
+
auditor: id,
|
|
3335
|
+
displayName,
|
|
3336
|
+
status,
|
|
3337
|
+
permalink: `${baseUrl}/security/${id}`
|
|
3338
|
+
};
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
async function checkSkillOnSkillsSh(source) {
|
|
3342
|
+
const { owner, repo, skillFolder } = source;
|
|
3343
|
+
const permalink = `https://skills.sh/${owner}/${repo}/${skillFolder}`;
|
|
3344
|
+
try {
|
|
3345
|
+
const controller = new AbortController();
|
|
3346
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
3347
|
+
let response;
|
|
3348
|
+
try {
|
|
3349
|
+
response = await fetch(permalink, { signal: controller.signal });
|
|
3350
|
+
} finally {
|
|
3351
|
+
clearTimeout(timeout);
|
|
3352
|
+
}
|
|
3353
|
+
if (!response.ok) return NOT_FOUND;
|
|
3354
|
+
const audits = parseAudits(await response.text(), permalink);
|
|
3355
|
+
return {
|
|
3356
|
+
found: true,
|
|
3357
|
+
permalink,
|
|
3358
|
+
audits,
|
|
3359
|
+
anyFail: audits.some((a) => a.status === "fail")
|
|
3360
|
+
};
|
|
3361
|
+
} catch {
|
|
3362
|
+
return NOT_FOUND;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3299
3365
|
const SEVERITY_LABELS = {
|
|
3300
3366
|
critical: import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" CRITICAL "))),
|
|
3301
3367
|
high: import_picocolors.default.red(import_picocolors.default.bold("HIGH")),
|
|
@@ -3324,6 +3390,17 @@ function displayVTVerdict(verdict) {
|
|
|
3324
3390
|
}
|
|
3325
3391
|
if (verdict.permalink) M.message(import_picocolors.default.dim(` ${verdict.permalink}`));
|
|
3326
3392
|
}
|
|
3393
|
+
function displaySkillsShResult(result) {
|
|
3394
|
+
if (!result.found || result.audits.length === 0) return;
|
|
3395
|
+
const badges = result.audits.map((a) => {
|
|
3396
|
+
const name = a.auditor === "agent-trust-hub" ? "Trust Hub" : a.displayName;
|
|
3397
|
+
if (a.status === "pass") return `[${import_picocolors.default.green(`${name} ✓`)}]`;
|
|
3398
|
+
if (a.status === "fail") return `[${import_picocolors.default.red(`${name} ✗`)}]`;
|
|
3399
|
+
return `[${import_picocolors.default.dim(`${name} ~`)}]`;
|
|
3400
|
+
}).join(" ");
|
|
3401
|
+
M.message(` ${import_picocolors.default.cyan("◆")} skills.sh: ${result.audits.length} audits ${badges}`);
|
|
3402
|
+
M.message(import_picocolors.default.dim(` ${result.permalink}`));
|
|
3403
|
+
}
|
|
3327
3404
|
async function presentScanResults(results, options) {
|
|
3328
3405
|
const allFindings = results.flatMap((r) => r.findings.map((f) => ({
|
|
3329
3406
|
...f,
|
|
@@ -3331,15 +3408,26 @@ async function presentScanResults(results, options) {
|
|
|
3331
3408
|
})));
|
|
3332
3409
|
const allUrls = [...new Set(results.flatMap((r) => r.urls))];
|
|
3333
3410
|
const vtVerdicts = /* @__PURE__ */ new Map();
|
|
3411
|
+
const skillsShResults = /* @__PURE__ */ new Map();
|
|
3334
3412
|
let vtEscalate = false;
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3413
|
+
let skillsShEscalate = false;
|
|
3414
|
+
await Promise.all([(async () => {
|
|
3415
|
+
if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
|
|
3416
|
+
const verdict = await checkSkillOnVT(content, options.vtKey);
|
|
3417
|
+
vtVerdicts.set(skillName, verdict);
|
|
3418
|
+
if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
|
|
3419
|
+
} catch {}
|
|
3420
|
+
})(), (async () => {
|
|
3421
|
+
if (options.skillsShSources) await Promise.all([...options.skillsShSources.entries()].map(async ([skillName, source]) => {
|
|
3422
|
+
const result = await checkSkillOnSkillsSh(source);
|
|
3423
|
+
skillsShResults.set(skillName, result);
|
|
3424
|
+
if (result.anyFail) skillsShEscalate = true;
|
|
3425
|
+
}));
|
|
3426
|
+
})()]);
|
|
3427
|
+
if (allFindings.length === 0 && !vtEscalate && !skillsShEscalate) {
|
|
3341
3428
|
M.success(import_picocolors.default.green("Security scan passed — no issues found"));
|
|
3342
3429
|
if (vtVerdicts.size > 0) for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
|
|
3430
|
+
for (const [, result] of skillsShResults) displaySkillsShResult(result);
|
|
3343
3431
|
if (allUrls.length > 0) return displayUrlsAndPrompt(allUrls, options);
|
|
3344
3432
|
return true;
|
|
3345
3433
|
}
|
|
@@ -3363,6 +3451,10 @@ async function presentScanResults(results, options) {
|
|
|
3363
3451
|
console.log();
|
|
3364
3452
|
for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
|
|
3365
3453
|
}
|
|
3454
|
+
if (skillsShResults.size > 0) {
|
|
3455
|
+
console.log();
|
|
3456
|
+
for (const [, result] of skillsShResults) displaySkillsShResult(result);
|
|
3457
|
+
}
|
|
3366
3458
|
if (allUrls.length > 0) {
|
|
3367
3459
|
console.log();
|
|
3368
3460
|
M.info(`External URLs found in skill files (${allUrls.length}):`);
|
|
@@ -3370,6 +3462,7 @@ async function presentScanResults(results, options) {
|
|
|
3370
3462
|
}
|
|
3371
3463
|
console.log();
|
|
3372
3464
|
if (vtEscalate) overallMax = "critical";
|
|
3465
|
+
if (skillsShEscalate && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
|
|
3373
3466
|
if (SEVERITY_ORDER[overallMax] <= SEVERITY_ORDER["medium"]) {
|
|
3374
3467
|
M.info(import_picocolors.default.dim("Low/medium severity findings — proceeding with installation"));
|
|
3375
3468
|
return true;
|
|
@@ -3414,6 +3507,24 @@ function resolveExternalRules(options) {
|
|
|
3414
3507
|
_externalRulesCache.set(key, loaded);
|
|
3415
3508
|
return loaded;
|
|
3416
3509
|
}
|
|
3510
|
+
function buildSkillsShSourcesForRemote(remoteSkill, _url) {
|
|
3511
|
+
const sourceUrl = remoteSkill.sourceUrl;
|
|
3512
|
+
if (!sourceUrl || !sourceUrl.includes("github.com")) return void 0;
|
|
3513
|
+
try {
|
|
3514
|
+
const parts = new URL(sourceUrl).pathname.slice(1).replace(/\.git$/, "").split("/").filter(Boolean);
|
|
3515
|
+
const ownerRepo = parseOwnerRepo(parts.slice(0, 2).join("/"));
|
|
3516
|
+
if (!ownerRepo) return void 0;
|
|
3517
|
+
const skillFolder = parts.length > 2 ? parts.at(-1) : remoteSkill.installName;
|
|
3518
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3519
|
+
sources.set(remoteSkill.installName, {
|
|
3520
|
+
...ownerRepo,
|
|
3521
|
+
skillFolder
|
|
3522
|
+
});
|
|
3523
|
+
return sources;
|
|
3524
|
+
} catch {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3417
3528
|
function shortenPath$1(fullPath, cwd) {
|
|
3418
3529
|
const home = homedir();
|
|
3419
3530
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
@@ -3673,10 +3784,12 @@ async function handleRemoteSkill(source, url, options, spinner) {
|
|
|
3673
3784
|
});
|
|
3674
3785
|
const vtKey = options.vtKey || process.env.VT_API_KEY;
|
|
3675
3786
|
const skillContents = vtKey ? new Map([[remoteSkill.installName, remoteSkill.content]]) : void 0;
|
|
3787
|
+
const skillsShSources = buildSkillsShSourcesForRemote(remoteSkill, url);
|
|
3676
3788
|
if (!await presentScanResults([scanResult], {
|
|
3677
3789
|
yes: options.yes,
|
|
3678
3790
|
vtKey,
|
|
3679
|
-
skillContents
|
|
3791
|
+
skillContents,
|
|
3792
|
+
skillsShSources
|
|
3680
3793
|
})) {
|
|
3681
3794
|
xe("Installation cancelled due to security concerns");
|
|
3682
3795
|
process.exit(0);
|
|
@@ -4485,10 +4598,24 @@ async function runAdd(args, options = {}) {
|
|
|
4485
4598
|
}
|
|
4486
4599
|
}
|
|
4487
4600
|
spinner.stop("Security scan complete");
|
|
4601
|
+
const skillsShSources = /* @__PURE__ */ new Map();
|
|
4602
|
+
if (parsed.type === "github") {
|
|
4603
|
+
const ownerRepoStr = getOwnerRepo(parsed);
|
|
4604
|
+
const ownerRepo = ownerRepoStr ? parseOwnerRepo(ownerRepoStr) : null;
|
|
4605
|
+
if (ownerRepo) for (const skill of selectedSkills) {
|
|
4606
|
+
const displayName = getSkillDisplayName(skill);
|
|
4607
|
+
const skillFolder = skill.path.split("/").at(-1) ?? skill.name;
|
|
4608
|
+
skillsShSources.set(displayName, {
|
|
4609
|
+
...ownerRepo,
|
|
4610
|
+
skillFolder
|
|
4611
|
+
});
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4488
4614
|
if (!await presentScanResults(scanResults, {
|
|
4489
4615
|
yes: options.yes,
|
|
4490
4616
|
vtKey,
|
|
4491
|
-
skillContents
|
|
4617
|
+
skillContents,
|
|
4618
|
+
skillsShSources: skillsShSources.size > 0 ? skillsShSources : void 0
|
|
4492
4619
|
})) {
|
|
4493
4620
|
xe("Installation cancelled due to security concerns");
|
|
4494
4621
|
await cleanup(tempDir);
|