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/index.d.ts CHANGED
@@ -141,16 +141,19 @@ declare function generateHookResponse(report: ScanReport, config?: SkillCheckerC
141
141
  */
142
142
  declare function loadConfig(startDir?: string, configPath?: string): SkillCheckerConfig;
143
143
 
144
- /**
145
- * IOC (Indicators of Compromise) database type definition and seed data.
146
- * Seed data is compiled from publicly available threat intelligence.
147
- */
144
+ interface CategorizedDomains {
145
+ exfiltration: string[];
146
+ tunnel: string[];
147
+ oast: string[];
148
+ paste: string[];
149
+ c2: string[];
150
+ }
148
151
  interface IOCDatabase {
149
152
  version: string;
150
153
  updated: string;
151
154
  c2_ips: string[];
152
155
  malicious_hashes: Record<string, string>;
153
- malicious_domains: string[];
156
+ malicious_domains: CategorizedDomains;
154
157
  typosquat: {
155
158
  known_patterns: string[];
156
159
  protected_names: string[];
package/dist/index.js CHANGED
@@ -336,6 +336,70 @@ var structuralChecks = {
336
336
  }
337
337
  };
338
338
 
339
+ // src/types.ts
340
+ var SEVERITY_SCORES = {
341
+ CRITICAL: 25,
342
+ HIGH: 10,
343
+ MEDIUM: 3,
344
+ LOW: 1
345
+ };
346
+ function computeGrade(score) {
347
+ if (score >= 90) return "A";
348
+ if (score >= 75) return "B";
349
+ if (score >= 60) return "C";
350
+ if (score >= 40) return "D";
351
+ return "F";
352
+ }
353
+ var REDUCE_MAP = {
354
+ CRITICAL: "HIGH",
355
+ HIGH: "MEDIUM",
356
+ MEDIUM: "LOW",
357
+ LOW: "LOW"
358
+ };
359
+ function reduceSeverity(original, reason) {
360
+ let reduced = REDUCE_MAP[original];
361
+ if (original === "CRITICAL" && reduced === "LOW") {
362
+ reduced = "MEDIUM";
363
+ }
364
+ return {
365
+ severity: reduced,
366
+ reducedFrom: original,
367
+ annotation: `[reduced: ${reason}]`
368
+ };
369
+ }
370
+ var DEFAULT_CONFIG = {
371
+ policy: "balanced",
372
+ overrides: {},
373
+ ignore: []
374
+ };
375
+ function getHookAction(policy, severity) {
376
+ const matrix = {
377
+ strict: {
378
+ CRITICAL: "deny",
379
+ HIGH: "deny",
380
+ MEDIUM: "ask",
381
+ LOW: "report"
382
+ },
383
+ balanced: {
384
+ CRITICAL: "deny",
385
+ HIGH: "ask",
386
+ MEDIUM: "report",
387
+ LOW: "report"
388
+ },
389
+ permissive: {
390
+ CRITICAL: "ask",
391
+ HIGH: "report",
392
+ MEDIUM: "report",
393
+ LOW: "report"
394
+ }
395
+ };
396
+ const row = matrix[policy];
397
+ if (!row) {
398
+ return matrix.balanced[severity];
399
+ }
400
+ return row[severity];
401
+ }
402
+
339
403
  // src/utils/context.ts
340
404
  function isInCodeBlock(lines, lineIndex) {
341
405
  let inBlock = false;
@@ -399,6 +463,49 @@ function isLicenseFile(filePath) {
399
463
  function isLocalhostURL(url) {
400
464
  return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])/i.test(url);
401
465
  }
466
+ function isInEducationalContext(lines, lineIndex) {
467
+ const line = lines[lineIndex];
468
+ if (/^#{1,6}\s+/.test(line)) return true;
469
+ if (/^\s*[-*]?\s*(\*\*[^*]+\*\*\s*:|[A-Z][^:]{0,40}:)\s/.test(line))
470
+ return true;
471
+ for (let i = lineIndex; i >= Math.max(0, lineIndex - 15); i--) {
472
+ if (/^#{1,4}\s+.*(strateg|guide|framework|structure|model|overview|comparison|concept|principle|example|tutorial|reference|approach|method)/i.test(
473
+ lines[i]
474
+ )) {
475
+ return true;
476
+ }
477
+ }
478
+ return false;
479
+ }
480
+ var PROMOTIONAL_INTENT_PATTERNS = [
481
+ /\d+%\s*off\b/i,
482
+ /\blimited\s+time\b/i,
483
+ /\bact\s+now\b/i,
484
+ /\bhurry\b/i,
485
+ /\btoday\s+only\b/i,
486
+ /\bdon'?t\s+miss\b/i,
487
+ /\bsave\s+\d+%/i,
488
+ /\bexclusive\s+(offer|deal)\b/i,
489
+ /\bsign\s+up\s+(now|today)\b/i,
490
+ /\bget\s+started\b/i,
491
+ /\bclaim\s+(now|yours?)\b/i,
492
+ /\boffer\s+ends?\b/i,
493
+ /\blast\s+chance\b/i,
494
+ /\bonly\s+\d+\s+left\b/i,
495
+ /\bends?\s+in\s+\d+/i,
496
+ /\bstart\s+(your\s+)?free\s+trial\b/i
497
+ ];
498
+ function hasPromotionalIntent(line) {
499
+ return PROMOTIONAL_INTENT_PATTERNS.some((p) => p.test(line));
500
+ }
501
+ function hasPromotionalIntentNearby(lines, lineIndex, window = 3) {
502
+ const start = Math.max(0, lineIndex - window);
503
+ const end = Math.min(lines.length - 1, lineIndex + window);
504
+ for (let i = start; i <= end; i++) {
505
+ if (hasPromotionalIntent(lines[i])) return true;
506
+ }
507
+ return false;
508
+ }
402
509
  function parseURLPath(url) {
403
510
  try {
404
511
  const u = new URL(url);
@@ -430,16 +537,18 @@ var LOREM_PATTERNS = [
430
537
  /dolor\s+sit\s+amet/i,
431
538
  /consectetur\s+adipiscing/i
432
539
  ];
433
- var AD_PATTERNS = [
540
+ var STRONG_AD_PATTERNS = [
434
541
  /\bbuy\s+now\b/i,
435
- /\bfree\s+trial\b/i,
542
+ /\bclick\s+here\s+to\s+(buy|subscribe|download)/i,
543
+ /\buse\s+code\b.*\b\d+%?\s*off\b/i
544
+ ];
545
+ var SOFT_AD_PATTERNS = [
436
546
  /\bdiscount\b/i,
547
+ /\bfree\s+trial\b/i,
437
548
  /\bpromo\s*code\b/i,
438
549
  /\bsubscribe\s+(to|now)\b/i,
439
550
  /\bsponsored\s+by\b/i,
440
551
  /\baffiliate\s+link\b/i,
441
- /\bclick\s+here\s+to\s+(buy|subscribe|download)/i,
442
- /\buse\s+code\b.*\b\d+%?\s*off\b/i,
443
552
  /\bcheck\s+out\s+my\b/i
444
553
  ];
445
554
  var contentChecks = {
@@ -504,7 +613,8 @@ var contentChecks = {
504
613
  checkDescriptionMismatch(results, skill);
505
614
  for (let i = 0; i < skill.bodyLines.length; i++) {
506
615
  const line = skill.bodyLines[i];
507
- for (const pattern of AD_PATTERNS) {
616
+ let matched = false;
617
+ for (const pattern of STRONG_AD_PATTERNS) {
508
618
  if (pattern.test(line)) {
509
619
  results.push({
510
620
  id: "CONT-005",
@@ -513,8 +623,43 @@ var contentChecks = {
513
623
  title: "Promotional/advertising content",
514
624
  message: `Line ${skill.bodyStartLine + i}: Contains ad-like content.`,
515
625
  line: skill.bodyStartLine + i,
516
- snippet: line.trim().slice(0, 120)
626
+ snippet: line.trim().slice(0, 120),
627
+ source: "SKILL.md"
517
628
  });
629
+ matched = true;
630
+ break;
631
+ }
632
+ }
633
+ if (matched) continue;
634
+ for (const pattern of SOFT_AD_PATTERNS) {
635
+ if (pattern.test(line)) {
636
+ const inCode = isInCodeBlock(skill.bodyLines, i);
637
+ const inEducational = isInEducationalContext(skill.bodyLines, i);
638
+ if ((inCode || inEducational) && !hasPromotionalIntentNearby(skill.bodyLines, i)) {
639
+ const reduction = reduceSeverity("HIGH", "educational/descriptive context");
640
+ results.push({
641
+ id: "CONT-005",
642
+ category: "CONT",
643
+ severity: reduction.severity,
644
+ title: "Promotional/advertising content",
645
+ message: `Line ${skill.bodyStartLine + i}: Contains ad-like content. ${reduction.annotation}`,
646
+ line: skill.bodyStartLine + i,
647
+ snippet: line.trim().slice(0, 120),
648
+ reducedFrom: reduction.reducedFrom,
649
+ source: "SKILL.md"
650
+ });
651
+ } else {
652
+ results.push({
653
+ id: "CONT-005",
654
+ category: "CONT",
655
+ severity: "HIGH",
656
+ title: "Promotional/advertising content",
657
+ message: `Line ${skill.bodyStartLine + i}: Contains ad-like content.`,
658
+ line: skill.bodyStartLine + i,
659
+ snippet: line.trim().slice(0, 120),
660
+ source: "SKILL.md"
661
+ });
662
+ }
518
663
  break;
519
664
  }
520
665
  }
@@ -993,70 +1138,6 @@ function dedup(results) {
993
1138
  });
994
1139
  }
995
1140
 
996
- // src/types.ts
997
- var SEVERITY_SCORES = {
998
- CRITICAL: 25,
999
- HIGH: 10,
1000
- MEDIUM: 3,
1001
- LOW: 1
1002
- };
1003
- function computeGrade(score) {
1004
- if (score >= 90) return "A";
1005
- if (score >= 75) return "B";
1006
- if (score >= 60) return "C";
1007
- if (score >= 40) return "D";
1008
- return "F";
1009
- }
1010
- var REDUCE_MAP = {
1011
- CRITICAL: "HIGH",
1012
- HIGH: "MEDIUM",
1013
- MEDIUM: "LOW",
1014
- LOW: "LOW"
1015
- };
1016
- function reduceSeverity(original, reason) {
1017
- let reduced = REDUCE_MAP[original];
1018
- if (original === "CRITICAL" && reduced === "LOW") {
1019
- reduced = "MEDIUM";
1020
- }
1021
- return {
1022
- severity: reduced,
1023
- reducedFrom: original,
1024
- annotation: `[reduced: ${reason}]`
1025
- };
1026
- }
1027
- var DEFAULT_CONFIG = {
1028
- policy: "balanced",
1029
- overrides: {},
1030
- ignore: []
1031
- };
1032
- function getHookAction(policy, severity) {
1033
- const matrix = {
1034
- strict: {
1035
- CRITICAL: "deny",
1036
- HIGH: "deny",
1037
- MEDIUM: "ask",
1038
- LOW: "report"
1039
- },
1040
- balanced: {
1041
- CRITICAL: "deny",
1042
- HIGH: "ask",
1043
- MEDIUM: "report",
1044
- LOW: "report"
1045
- },
1046
- permissive: {
1047
- CRITICAL: "ask",
1048
- HIGH: "report",
1049
- MEDIUM: "report",
1050
- LOW: "report"
1051
- }
1052
- };
1053
- const row = matrix[policy];
1054
- if (!row) {
1055
- return matrix.balanced[severity];
1056
- }
1057
- return row[severity];
1058
- }
1059
-
1060
1141
  // src/checks/code-safety.ts
1061
1142
  var EVAL_PATTERNS = [
1062
1143
  /\beval\s*\(/,
@@ -1533,8 +1614,8 @@ import { homedir } from "os";
1533
1614
 
1534
1615
  // src/ioc/indicators.ts
1535
1616
  var DEFAULT_IOC = {
1536
- version: "2026.03.06",
1537
- updated: "2026-03-06",
1617
+ version: "2026.03.16",
1618
+ updated: "2026-03-16",
1538
1619
  c2_ips: [
1539
1620
  "91.92.242.30",
1540
1621
  "91.92.242.39",
@@ -1543,36 +1624,65 @@ var DEFAULT_IOC = {
1543
1624
  "45.155.205.233"
1544
1625
  ],
1545
1626
  malicious_hashes: {
1546
- // NOTE: Never add the SHA-256 of an empty file (e3b0c44298fc...b855)
1627
+ // NOTE: Never add the SHA256 of an empty file (e3b0c44298fc...b855)
1547
1628
  // as it causes false positives on any empty file.
1548
1629
  "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "clawhavoc-exfiltrator"
1549
1630
  },
1550
- malicious_domains: [
1551
- "webhook.site",
1552
- "requestbin.com",
1553
- "pipedream.com",
1554
- "pipedream.net",
1555
- "hookbin.com",
1556
- "beeceptor.com",
1557
- "ngrok.io",
1558
- "ngrok-free.app",
1559
- "serveo.net",
1560
- "localtunnel.me",
1561
- "bore.pub",
1562
- "interact.sh",
1563
- "oast.fun",
1564
- "oastify.com",
1565
- "dnslog.cn",
1566
- "ceye.io",
1567
- "burpcollaborator.net",
1568
- "pastebin.com",
1569
- "paste.ee",
1570
- "hastebin.com",
1571
- "ghostbin.com",
1572
- "evil.com",
1573
- "malware.com",
1574
- "exploit.in"
1575
- ],
1631
+ malicious_domains: {
1632
+ exfiltration: [
1633
+ "webhook.site",
1634
+ "requestbin.com",
1635
+ "requestcatcher.com",
1636
+ "pipedream.com",
1637
+ "pipedream.net",
1638
+ "hookbin.com",
1639
+ "beeceptor.com",
1640
+ "postb.in",
1641
+ "webhook.lol",
1642
+ "requestinspector.com",
1643
+ "mockbin.org"
1644
+ ],
1645
+ tunnel: [
1646
+ "ngrok.io",
1647
+ "ngrok-free.app",
1648
+ "serveo.net",
1649
+ "localtunnel.me",
1650
+ "bore.pub",
1651
+ "localhost.run",
1652
+ "loca.lt",
1653
+ "telebit.cloud",
1654
+ "playit.gg",
1655
+ "portmap.io",
1656
+ "pagekite.me"
1657
+ ],
1658
+ oast: [
1659
+ "interact.sh",
1660
+ "oast.fun",
1661
+ "oastify.com",
1662
+ "dnslog.cn",
1663
+ "ceye.io",
1664
+ "burpcollaborator.net",
1665
+ "canarytokens.com",
1666
+ "requestrepo.com"
1667
+ ],
1668
+ paste: [
1669
+ "pastebin.com",
1670
+ "paste.ee",
1671
+ "hastebin.com",
1672
+ "ghostbin.com",
1673
+ "dpaste.org",
1674
+ "rentry.co",
1675
+ "0bin.net",
1676
+ "privatebin.net",
1677
+ "paste.mozilla.org"
1678
+ ],
1679
+ c2: [
1680
+ "evil.com",
1681
+ "malware.com",
1682
+ "exploit.in",
1683
+ "darkweb.onion"
1684
+ ]
1685
+ },
1576
1686
  typosquat: {
1577
1687
  known_patterns: [
1578
1688
  "clawhub1",
@@ -1632,10 +1742,21 @@ function mergeIOC(base, ext) {
1632
1742
  Object.assign(base.malicious_hashes, ext.malicious_hashes);
1633
1743
  }
1634
1744
  if (ext.malicious_domains) {
1635
- base.malicious_domains = dedupe([
1636
- ...base.malicious_domains,
1637
- ...ext.malicious_domains
1638
- ]);
1745
+ const categories = [
1746
+ "exfiltration",
1747
+ "tunnel",
1748
+ "oast",
1749
+ "paste",
1750
+ "c2"
1751
+ ];
1752
+ for (const cat of categories) {
1753
+ if (ext.malicious_domains[cat]) {
1754
+ base.malicious_domains[cat] = dedupe([
1755
+ ...base.malicious_domains[cat],
1756
+ ...ext.malicious_domains[cat]
1757
+ ]);
1758
+ }
1759
+ }
1639
1760
  }
1640
1761
  if (ext.typosquat) {
1641
1762
  if (ext.typosquat.known_patterns) {
@@ -1660,6 +1781,22 @@ function mergeIOC(base, ext) {
1660
1781
  if (ext.version) base.version = ext.version;
1661
1782
  if (ext.updated) base.updated = ext.updated;
1662
1783
  }
1784
+ function getAllDomains(ioc) {
1785
+ const { exfiltration, tunnel, oast, paste, c2 } = ioc.malicious_domains;
1786
+ return [...exfiltration, ...tunnel, ...oast, ...paste, ...c2];
1787
+ }
1788
+ function getDomainCategory(ioc, domain) {
1789
+ const d = domain.toLowerCase();
1790
+ const categories = ["exfiltration", "tunnel", "oast", "paste", "c2"];
1791
+ for (const cat of categories) {
1792
+ if (ioc.malicious_domains[cat].some(
1793
+ (entry) => d === entry || d.endsWith("." + entry)
1794
+ )) {
1795
+ return cat;
1796
+ }
1797
+ }
1798
+ return void 0;
1799
+ }
1663
1800
  function dedupe(arr) {
1664
1801
  return [...new Set(arr)];
1665
1802
  }
@@ -1668,6 +1805,24 @@ function dedupe(arr) {
1668
1805
  var FALLBACK_SUSPICIOUS_DOMAINS = [
1669
1806
  "darkweb.onion"
1670
1807
  ];
1808
+ function isSensitiveDomainCombo(line) {
1809
+ if (/curl\b[^\n]*(?:-d|--data|--data-binary|--data-raw|--data-urlencode)\s+@/i.test(line)) {
1810
+ return true;
1811
+ }
1812
+ if (/curl\b[^\n]*(?:-F|--form)\s+[^\s=]+=@/i.test(line)) {
1813
+ return true;
1814
+ }
1815
+ if (/wget\b[^\n]*--post-file/i.test(line)) {
1816
+ return true;
1817
+ }
1818
+ if (/\|\s*(?:sh|bash|zsh|python|node)\b/i.test(line)) {
1819
+ return true;
1820
+ }
1821
+ if (/(?:\.env|\.ssh|id_rsa|\.aws|credentials|\.netrc|\.git-credentials)/i.test(line)) {
1822
+ return true;
1823
+ }
1824
+ return false;
1825
+ }
1671
1826
  var MCP_SERVER_PATTERN = /\bmcp[-_]?server\b/i;
1672
1827
  var NPX_Y_PATTERN = /\bnpx\s+-y\s+/;
1673
1828
  var NPM_INSTALL_PATTERN = /\bnpm\s+install\b/;
@@ -1692,7 +1847,10 @@ var supplyChainChecks = {
1692
1847
  const results = [];
1693
1848
  const allText = getAllText(skill);
1694
1849
  const ioc = loadIOC();
1695
- const suspiciousDomains = ioc.malicious_domains.length > 0 ? ioc.malicious_domains : FALLBACK_SUSPICIOUS_DOMAINS;
1850
+ const suspiciousDomains = getAllDomains(ioc);
1851
+ if (suspiciousDomains.length === 0) {
1852
+ suspiciousDomains.push(...FALLBACK_SUSPICIOUS_DOMAINS);
1853
+ }
1696
1854
  for (let i = 0; i < allText.length; i++) {
1697
1855
  const { line, lineNum, source } = allText[i];
1698
1856
  if (MCP_SERVER_PATTERN.test(line)) {
@@ -1855,15 +2013,46 @@ var supplyChainChecks = {
1855
2013
  const hostname = extractHostname(url);
1856
2014
  for (const domain of suspiciousDomains) {
1857
2015
  if (hostnameMatchesDomain(hostname, domain)) {
2016
+ const category = getDomainCategory(ioc, domain);
2017
+ const categoryLabel = category ? ` (${category})` : "";
2018
+ let severity = "HIGH";
2019
+ let reducedFrom;
2020
+ let msgSuffix = "";
2021
+ if (isSensitiveDomainCombo(line)) {
2022
+ severity = "CRITICAL";
2023
+ msgSuffix = " [escalated: combined with sensitive operation]";
2024
+ } else {
2025
+ const srcLines = getLinesForSource(skill, source);
2026
+ const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
2027
+ const inCodeBlock = localIdx >= 0 && isInCodeBlock(srcLines, localIdx);
2028
+ if (inCodeBlock) {
2029
+ severity = "MEDIUM";
2030
+ reducedFrom = "HIGH";
2031
+ msgSuffix = " [reduced: in code block]";
2032
+ } else {
2033
+ const allLines = getAllLines(skill);
2034
+ const globalIdx = findGlobalLineIndex(allLines, source, lineNum);
2035
+ const isDoc = source === "SKILL.md" && globalIdx >= 0 && isInDocumentationContext(
2036
+ allLines.map((l) => l.line),
2037
+ globalIdx
2038
+ );
2039
+ if (isDoc) {
2040
+ severity = "LOW";
2041
+ reducedFrom = "HIGH";
2042
+ msgSuffix = " [reduced: in documentation context]";
2043
+ }
2044
+ }
2045
+ }
1858
2046
  results.push({
1859
2047
  id: "SUPPLY-007",
1860
2048
  category: "SUPPLY",
1861
- severity: "CRITICAL",
1862
- title: "Suspicious domain detected",
1863
- message: `${source}:${lineNum}: References suspicious domain "${domain}".`,
2049
+ severity,
2050
+ title: `Suspicious domain${categoryLabel} detected`,
2051
+ message: `${source}:${lineNum}: References suspicious domain "${domain}".${msgSuffix}`,
1864
2052
  line: lineNum,
1865
2053
  snippet: url,
1866
- source
2054
+ source,
2055
+ reducedFrom
1867
2056
  });
1868
2057
  break;
1869
2058
  }