skill-checker 0.1.11 → 0.1.13

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/cli.js CHANGED
@@ -328,6 +328,70 @@ var structuralChecks = {
328
328
  }
329
329
  };
330
330
 
331
+ // src/types.ts
332
+ var SEVERITY_SCORES = {
333
+ CRITICAL: 25,
334
+ HIGH: 10,
335
+ MEDIUM: 3,
336
+ LOW: 1
337
+ };
338
+ function computeGrade(score) {
339
+ if (score >= 90) return "A";
340
+ if (score >= 75) return "B";
341
+ if (score >= 60) return "C";
342
+ if (score >= 40) return "D";
343
+ return "F";
344
+ }
345
+ var REDUCE_MAP = {
346
+ CRITICAL: "HIGH",
347
+ HIGH: "MEDIUM",
348
+ MEDIUM: "LOW",
349
+ LOW: "LOW"
350
+ };
351
+ function reduceSeverity(original, reason) {
352
+ let reduced = REDUCE_MAP[original];
353
+ if (original === "CRITICAL" && reduced === "LOW") {
354
+ reduced = "MEDIUM";
355
+ }
356
+ return {
357
+ severity: reduced,
358
+ reducedFrom: original,
359
+ annotation: `[reduced: ${reason}]`
360
+ };
361
+ }
362
+ var DEFAULT_CONFIG = {
363
+ policy: "balanced",
364
+ overrides: {},
365
+ ignore: []
366
+ };
367
+ function getHookAction(policy, severity) {
368
+ const matrix = {
369
+ strict: {
370
+ CRITICAL: "deny",
371
+ HIGH: "deny",
372
+ MEDIUM: "ask",
373
+ LOW: "report"
374
+ },
375
+ balanced: {
376
+ CRITICAL: "deny",
377
+ HIGH: "ask",
378
+ MEDIUM: "report",
379
+ LOW: "report"
380
+ },
381
+ permissive: {
382
+ CRITICAL: "ask",
383
+ HIGH: "report",
384
+ MEDIUM: "report",
385
+ LOW: "report"
386
+ }
387
+ };
388
+ const row = matrix[policy];
389
+ if (!row) {
390
+ return matrix.balanced[severity];
391
+ }
392
+ return row[severity];
393
+ }
394
+
331
395
  // src/utils/context.ts
332
396
  function isInCodeBlock(lines, lineIndex) {
333
397
  let inBlock = false;
@@ -391,6 +455,49 @@ function isLicenseFile(filePath) {
391
455
  function isLocalhostURL(url) {
392
456
  return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])/i.test(url);
393
457
  }
458
+ function isInEducationalContext(lines, lineIndex) {
459
+ const line = lines[lineIndex];
460
+ if (/^#{1,6}\s+/.test(line)) return true;
461
+ if (/^\s*[-*]?\s*(\*\*[^*]+\*\*\s*:|[A-Z][^:]{0,40}:)\s/.test(line))
462
+ return true;
463
+ for (let i = lineIndex; i >= Math.max(0, lineIndex - 15); i--) {
464
+ if (/^#{1,4}\s+.*(strateg|guide|framework|structure|model|overview|comparison|concept|principle|example|tutorial|reference|approach|method)/i.test(
465
+ lines[i]
466
+ )) {
467
+ return true;
468
+ }
469
+ }
470
+ return false;
471
+ }
472
+ var PROMOTIONAL_INTENT_PATTERNS = [
473
+ /\d+%\s*off\b/i,
474
+ /\blimited\s+time\b/i,
475
+ /\bact\s+now\b/i,
476
+ /\bhurry\b/i,
477
+ /\btoday\s+only\b/i,
478
+ /\bdon'?t\s+miss\b/i,
479
+ /\bsave\s+\d+%/i,
480
+ /\bexclusive\s+(offer|deal)\b/i,
481
+ /\bsign\s+up\s+(now|today)\b/i,
482
+ /\bget\s+started\b/i,
483
+ /\bclaim\s+(now|yours?)\b/i,
484
+ /\boffer\s+ends?\b/i,
485
+ /\blast\s+chance\b/i,
486
+ /\bonly\s+\d+\s+left\b/i,
487
+ /\bends?\s+in\s+\d+/i,
488
+ /\bstart\s+(your\s+)?free\s+trial\b/i
489
+ ];
490
+ function hasPromotionalIntent(line) {
491
+ return PROMOTIONAL_INTENT_PATTERNS.some((p) => p.test(line));
492
+ }
493
+ function hasPromotionalIntentNearby(lines, lineIndex, window = 3) {
494
+ const start = Math.max(0, lineIndex - window);
495
+ const end = Math.min(lines.length - 1, lineIndex + window);
496
+ for (let i = start; i <= end; i++) {
497
+ if (hasPromotionalIntent(lines[i])) return true;
498
+ }
499
+ return false;
500
+ }
394
501
  function parseURLPath(url) {
395
502
  try {
396
503
  const u = new URL(url);
@@ -422,16 +529,18 @@ var LOREM_PATTERNS = [
422
529
  /dolor\s+sit\s+amet/i,
423
530
  /consectetur\s+adipiscing/i
424
531
  ];
425
- var AD_PATTERNS = [
532
+ var STRONG_AD_PATTERNS = [
426
533
  /\bbuy\s+now\b/i,
427
- /\bfree\s+trial\b/i,
534
+ /\bclick\s+here\s+to\s+(buy|subscribe|download)/i,
535
+ /\buse\s+code\b.*\b\d+%?\s*off\b/i
536
+ ];
537
+ var SOFT_AD_PATTERNS = [
428
538
  /\bdiscount\b/i,
539
+ /\bfree\s+trial\b/i,
429
540
  /\bpromo\s*code\b/i,
430
541
  /\bsubscribe\s+(to|now)\b/i,
431
542
  /\bsponsored\s+by\b/i,
432
543
  /\baffiliate\s+link\b/i,
433
- /\bclick\s+here\s+to\s+(buy|subscribe|download)/i,
434
- /\buse\s+code\b.*\b\d+%?\s*off\b/i,
435
544
  /\bcheck\s+out\s+my\b/i
436
545
  ];
437
546
  var contentChecks = {
@@ -496,7 +605,8 @@ var contentChecks = {
496
605
  checkDescriptionMismatch(results, skill);
497
606
  for (let i = 0; i < skill.bodyLines.length; i++) {
498
607
  const line = skill.bodyLines[i];
499
- for (const pattern of AD_PATTERNS) {
608
+ let matched = false;
609
+ for (const pattern of STRONG_AD_PATTERNS) {
500
610
  if (pattern.test(line)) {
501
611
  results.push({
502
612
  id: "CONT-005",
@@ -505,8 +615,43 @@ var contentChecks = {
505
615
  title: "Promotional/advertising content",
506
616
  message: `Line ${skill.bodyStartLine + i}: Contains ad-like content.`,
507
617
  line: skill.bodyStartLine + i,
508
- snippet: line.trim().slice(0, 120)
618
+ snippet: line.trim().slice(0, 120),
619
+ source: "SKILL.md"
509
620
  });
621
+ matched = true;
622
+ break;
623
+ }
624
+ }
625
+ if (matched) continue;
626
+ for (const pattern of SOFT_AD_PATTERNS) {
627
+ if (pattern.test(line)) {
628
+ const inCode = isInCodeBlock(skill.bodyLines, i);
629
+ const inEducational = isInEducationalContext(skill.bodyLines, i);
630
+ if ((inCode || inEducational) && !hasPromotionalIntentNearby(skill.bodyLines, i)) {
631
+ const reduction = reduceSeverity("HIGH", "educational/descriptive context");
632
+ results.push({
633
+ id: "CONT-005",
634
+ category: "CONT",
635
+ severity: reduction.severity,
636
+ title: "Promotional/advertising content",
637
+ message: `Line ${skill.bodyStartLine + i}: Contains ad-like content. ${reduction.annotation}`,
638
+ line: skill.bodyStartLine + i,
639
+ snippet: line.trim().slice(0, 120),
640
+ reducedFrom: reduction.reducedFrom,
641
+ source: "SKILL.md"
642
+ });
643
+ } else {
644
+ results.push({
645
+ id: "CONT-005",
646
+ category: "CONT",
647
+ severity: "HIGH",
648
+ title: "Promotional/advertising content",
649
+ message: `Line ${skill.bodyStartLine + i}: Contains ad-like content.`,
650
+ line: skill.bodyStartLine + i,
651
+ snippet: line.trim().slice(0, 120),
652
+ source: "SKILL.md"
653
+ });
654
+ }
510
655
  break;
511
656
  }
512
657
  }
@@ -985,70 +1130,6 @@ function dedup(results) {
985
1130
  });
986
1131
  }
987
1132
 
988
- // src/types.ts
989
- var SEVERITY_SCORES = {
990
- CRITICAL: 25,
991
- HIGH: 10,
992
- MEDIUM: 3,
993
- LOW: 1
994
- };
995
- function computeGrade(score) {
996
- if (score >= 90) return "A";
997
- if (score >= 75) return "B";
998
- if (score >= 60) return "C";
999
- if (score >= 40) return "D";
1000
- return "F";
1001
- }
1002
- var REDUCE_MAP = {
1003
- CRITICAL: "HIGH",
1004
- HIGH: "MEDIUM",
1005
- MEDIUM: "LOW",
1006
- LOW: "LOW"
1007
- };
1008
- function reduceSeverity(original, reason) {
1009
- let reduced = REDUCE_MAP[original];
1010
- if (original === "CRITICAL" && reduced === "LOW") {
1011
- reduced = "MEDIUM";
1012
- }
1013
- return {
1014
- severity: reduced,
1015
- reducedFrom: original,
1016
- annotation: `[reduced: ${reason}]`
1017
- };
1018
- }
1019
- var DEFAULT_CONFIG = {
1020
- policy: "balanced",
1021
- overrides: {},
1022
- ignore: []
1023
- };
1024
- function getHookAction(policy, severity) {
1025
- const matrix = {
1026
- strict: {
1027
- CRITICAL: "deny",
1028
- HIGH: "deny",
1029
- MEDIUM: "ask",
1030
- LOW: "report"
1031
- },
1032
- balanced: {
1033
- CRITICAL: "deny",
1034
- HIGH: "ask",
1035
- MEDIUM: "report",
1036
- LOW: "report"
1037
- },
1038
- permissive: {
1039
- CRITICAL: "ask",
1040
- HIGH: "report",
1041
- MEDIUM: "report",
1042
- LOW: "report"
1043
- }
1044
- };
1045
- const row = matrix[policy];
1046
- if (!row) {
1047
- return matrix.balanced[severity];
1048
- }
1049
- return row[severity];
1050
- }
1051
-
1052
1133
  // src/checks/code-safety.ts
1053
1134
  var EVAL_PATTERNS = [
1054
1135
  /\beval\s*\(/,
@@ -1525,8 +1606,8 @@ import { homedir } from "os";
1525
1606
 
1526
1607
  // src/ioc/indicators.ts
1527
1608
  var DEFAULT_IOC = {
1528
- version: "2026.03.06",
1529
- updated: "2026-03-06",
1609
+ version: "2026.03.16",
1610
+ updated: "2026-03-16",
1530
1611
  c2_ips: [
1531
1612
  "91.92.242.30",
1532
1613
  "91.92.242.39",
@@ -1535,36 +1616,65 @@ var DEFAULT_IOC = {
1535
1616
  "45.155.205.233"
1536
1617
  ],
1537
1618
  malicious_hashes: {
1538
- // NOTE: Never add the SHA-256 of an empty file (e3b0c44298fc...b855)
1619
+ // NOTE: Never add the SHA256 of an empty file (e3b0c44298fc...b855)
1539
1620
  // as it causes false positives on any empty file.
1540
1621
  "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "clawhavoc-exfiltrator"
1541
1622
  },
1542
- malicious_domains: [
1543
- "webhook.site",
1544
- "requestbin.com",
1545
- "pipedream.com",
1546
- "pipedream.net",
1547
- "hookbin.com",
1548
- "beeceptor.com",
1549
- "ngrok.io",
1550
- "ngrok-free.app",
1551
- "serveo.net",
1552
- "localtunnel.me",
1553
- "bore.pub",
1554
- "interact.sh",
1555
- "oast.fun",
1556
- "oastify.com",
1557
- "dnslog.cn",
1558
- "ceye.io",
1559
- "burpcollaborator.net",
1560
- "pastebin.com",
1561
- "paste.ee",
1562
- "hastebin.com",
1563
- "ghostbin.com",
1564
- "evil.com",
1565
- "malware.com",
1566
- "exploit.in"
1567
- ],
1623
+ malicious_domains: {
1624
+ exfiltration: [
1625
+ "webhook.site",
1626
+ "requestbin.com",
1627
+ "requestcatcher.com",
1628
+ "pipedream.com",
1629
+ "pipedream.net",
1630
+ "hookbin.com",
1631
+ "beeceptor.com",
1632
+ "postb.in",
1633
+ "webhook.lol",
1634
+ "requestinspector.com",
1635
+ "mockbin.org"
1636
+ ],
1637
+ tunnel: [
1638
+ "ngrok.io",
1639
+ "ngrok-free.app",
1640
+ "serveo.net",
1641
+ "localtunnel.me",
1642
+ "bore.pub",
1643
+ "localhost.run",
1644
+ "loca.lt",
1645
+ "telebit.cloud",
1646
+ "playit.gg",
1647
+ "portmap.io",
1648
+ "pagekite.me"
1649
+ ],
1650
+ oast: [
1651
+ "interact.sh",
1652
+ "oast.fun",
1653
+ "oastify.com",
1654
+ "dnslog.cn",
1655
+ "ceye.io",
1656
+ "burpcollaborator.net",
1657
+ "canarytokens.com",
1658
+ "requestrepo.com"
1659
+ ],
1660
+ paste: [
1661
+ "pastebin.com",
1662
+ "paste.ee",
1663
+ "hastebin.com",
1664
+ "ghostbin.com",
1665
+ "dpaste.org",
1666
+ "rentry.co",
1667
+ "0bin.net",
1668
+ "privatebin.net",
1669
+ "paste.mozilla.org"
1670
+ ],
1671
+ c2: [
1672
+ "evil.com",
1673
+ "malware.com",
1674
+ "exploit.in",
1675
+ "darkweb.onion"
1676
+ ]
1677
+ },
1568
1678
  typosquat: {
1569
1679
  known_patterns: [
1570
1680
  "clawhub1",
@@ -1621,10 +1731,21 @@ function mergeIOC(base, ext) {
1621
1731
  Object.assign(base.malicious_hashes, ext.malicious_hashes);
1622
1732
  }
1623
1733
  if (ext.malicious_domains) {
1624
- base.malicious_domains = dedupe([
1625
- ...base.malicious_domains,
1626
- ...ext.malicious_domains
1627
- ]);
1734
+ const categories = [
1735
+ "exfiltration",
1736
+ "tunnel",
1737
+ "oast",
1738
+ "paste",
1739
+ "c2"
1740
+ ];
1741
+ for (const cat of categories) {
1742
+ if (ext.malicious_domains[cat]) {
1743
+ base.malicious_domains[cat] = dedupe([
1744
+ ...base.malicious_domains[cat],
1745
+ ...ext.malicious_domains[cat]
1746
+ ]);
1747
+ }
1748
+ }
1628
1749
  }
1629
1750
  if (ext.typosquat) {
1630
1751
  if (ext.typosquat.known_patterns) {
@@ -1649,6 +1770,22 @@ function mergeIOC(base, ext) {
1649
1770
  if (ext.version) base.version = ext.version;
1650
1771
  if (ext.updated) base.updated = ext.updated;
1651
1772
  }
1773
+ function getAllDomains(ioc) {
1774
+ const { exfiltration, tunnel, oast, paste, c2 } = ioc.malicious_domains;
1775
+ return [...exfiltration, ...tunnel, ...oast, ...paste, ...c2];
1776
+ }
1777
+ function getDomainCategory(ioc, domain) {
1778
+ const d = domain.toLowerCase();
1779
+ const categories = ["exfiltration", "tunnel", "oast", "paste", "c2"];
1780
+ for (const cat of categories) {
1781
+ if (ioc.malicious_domains[cat].some(
1782
+ (entry) => d === entry || d.endsWith("." + entry)
1783
+ )) {
1784
+ return cat;
1785
+ }
1786
+ }
1787
+ return void 0;
1788
+ }
1652
1789
  function dedupe(arr) {
1653
1790
  return [...new Set(arr)];
1654
1791
  }
@@ -1657,6 +1794,24 @@ function dedupe(arr) {
1657
1794
  var FALLBACK_SUSPICIOUS_DOMAINS = [
1658
1795
  "darkweb.onion"
1659
1796
  ];
1797
+ function isSensitiveDomainCombo(line) {
1798
+ if (/curl\b[^\n]*(?:-d|--data|--data-binary|--data-raw|--data-urlencode)\s+@/i.test(line)) {
1799
+ return true;
1800
+ }
1801
+ if (/curl\b[^\n]*(?:-F|--form)\s+[^\s=]+=@/i.test(line)) {
1802
+ return true;
1803
+ }
1804
+ if (/wget\b[^\n]*--post-file/i.test(line)) {
1805
+ return true;
1806
+ }
1807
+ if (/\|\s*(?:sh|bash|zsh|python|node)\b/i.test(line)) {
1808
+ return true;
1809
+ }
1810
+ if (/(?:\.env|\.ssh|id_rsa|\.aws|credentials|\.netrc|\.git-credentials)/i.test(line)) {
1811
+ return true;
1812
+ }
1813
+ return false;
1814
+ }
1660
1815
  var MCP_SERVER_PATTERN = /\bmcp[-_]?server\b/i;
1661
1816
  var NPX_Y_PATTERN = /\bnpx\s+-y\s+/;
1662
1817
  var NPM_INSTALL_PATTERN = /\bnpm\s+install\b/;
@@ -1681,7 +1836,10 @@ var supplyChainChecks = {
1681
1836
  const results = [];
1682
1837
  const allText = getAllText(skill);
1683
1838
  const ioc = loadIOC();
1684
- const suspiciousDomains = ioc.malicious_domains.length > 0 ? ioc.malicious_domains : FALLBACK_SUSPICIOUS_DOMAINS;
1839
+ const suspiciousDomains = getAllDomains(ioc);
1840
+ if (suspiciousDomains.length === 0) {
1841
+ suspiciousDomains.push(...FALLBACK_SUSPICIOUS_DOMAINS);
1842
+ }
1685
1843
  for (let i = 0; i < allText.length; i++) {
1686
1844
  const { line, lineNum, source } = allText[i];
1687
1845
  if (MCP_SERVER_PATTERN.test(line)) {
@@ -1844,15 +2002,46 @@ var supplyChainChecks = {
1844
2002
  const hostname = extractHostname(url);
1845
2003
  for (const domain of suspiciousDomains) {
1846
2004
  if (hostnameMatchesDomain(hostname, domain)) {
2005
+ const category = getDomainCategory(ioc, domain);
2006
+ const categoryLabel = category ? ` (${category})` : "";
2007
+ let severity = "HIGH";
2008
+ let reducedFrom;
2009
+ let msgSuffix = "";
2010
+ if (isSensitiveDomainCombo(line)) {
2011
+ severity = "CRITICAL";
2012
+ msgSuffix = " [escalated: combined with sensitive operation]";
2013
+ } else {
2014
+ const srcLines = getLinesForSource(skill, source);
2015
+ const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
2016
+ const inCodeBlock = localIdx >= 0 && isInCodeBlock(srcLines, localIdx);
2017
+ if (inCodeBlock) {
2018
+ severity = "MEDIUM";
2019
+ reducedFrom = "HIGH";
2020
+ msgSuffix = " [reduced: in code block]";
2021
+ } else {
2022
+ const allLines = getAllLines(skill);
2023
+ const globalIdx = findGlobalLineIndex(allLines, source, lineNum);
2024
+ const isDoc = source === "SKILL.md" && globalIdx >= 0 && isInDocumentationContext(
2025
+ allLines.map((l) => l.line),
2026
+ globalIdx
2027
+ );
2028
+ if (isDoc) {
2029
+ severity = "LOW";
2030
+ reducedFrom = "HIGH";
2031
+ msgSuffix = " [reduced: in documentation context]";
2032
+ }
2033
+ }
2034
+ }
1847
2035
  results.push({
1848
2036
  id: "SUPPLY-007",
1849
2037
  category: "SUPPLY",
1850
- severity: "CRITICAL",
1851
- title: "Suspicious domain detected",
1852
- message: `${source}:${lineNum}: References suspicious domain "${domain}".`,
2038
+ severity,
2039
+ title: `Suspicious domain${categoryLabel} detected`,
2040
+ message: `${source}:${lineNum}: References suspicious domain "${domain}".${msgSuffix}`,
1853
2041
  line: lineNum,
1854
2042
  snippet: url,
1855
- source
2043
+ source,
2044
+ reducedFrom
1856
2045
  });
1857
2046
  break;
1858
2047
  }