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.
Files changed (3) hide show
  1. package/README.md +28 -2
  2. package/dist/cli.mjs +142 -15
  3. 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 optional VT lookups, handles
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
- if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
3336
- const verdict = await checkSkillOnVT(content, options.vtKey);
3337
- vtVerdicts.set(skillName, verdict);
3338
- if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
3339
- } catch {}
3340
- if (allFindings.length === 0 && !vtEscalate) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsio",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "The SECURE open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {