@skillsmith/core 0.11.2 → 0.11.3

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 (131) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +6 -7
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/analysis/McpReferenceExtractor.d.ts +59 -5
  5. package/dist/src/analysis/McpReferenceExtractor.d.ts.map +1 -1
  6. package/dist/src/analysis/McpReferenceExtractor.hardening.test.d.ts +2 -0
  7. package/dist/src/analysis/McpReferenceExtractor.hardening.test.d.ts.map +1 -0
  8. package/dist/src/analysis/McpReferenceExtractor.hardening.test.js +278 -0
  9. package/dist/src/analysis/McpReferenceExtractor.hardening.test.js.map +1 -0
  10. package/dist/src/analysis/McpReferenceExtractor.js +276 -20
  11. package/dist/src/analysis/McpReferenceExtractor.js.map +1 -1
  12. package/dist/src/analysis/McpReferenceExtractor.test.js +4 -0
  13. package/dist/src/analysis/McpReferenceExtractor.test.js.map +1 -1
  14. package/dist/src/config/config-atomic-write.d.ts +58 -0
  15. package/dist/src/config/config-atomic-write.d.ts.map +1 -0
  16. package/dist/src/config/config-atomic-write.js +151 -0
  17. package/dist/src/config/config-atomic-write.js.map +1 -0
  18. package/dist/src/config/config-atomic-write.test.d.ts +9 -0
  19. package/dist/src/config/config-atomic-write.test.d.ts.map +1 -0
  20. package/dist/src/config/config-atomic-write.test.js +92 -0
  21. package/dist/src/config/config-atomic-write.test.js.map +1 -0
  22. package/dist/src/config/device-identity.d.ts +16 -0
  23. package/dist/src/config/device-identity.d.ts.map +1 -1
  24. package/dist/src/config/device-identity.js +82 -14
  25. package/dist/src/config/device-identity.js.map +1 -1
  26. package/dist/src/config/device-identity.test.js +158 -1
  27. package/dist/src/config/device-identity.test.js.map +1 -1
  28. package/dist/src/config/index.d.ts +14 -0
  29. package/dist/src/config/index.d.ts.map +1 -1
  30. package/dist/src/config/index.js +31 -15
  31. package/dist/src/config/index.js.map +1 -1
  32. package/dist/src/exports/services.d.ts +2 -1
  33. package/dist/src/exports/services.d.ts.map +1 -1
  34. package/dist/src/exports/services.js +1 -0
  35. package/dist/src/exports/services.js.map +1 -1
  36. package/dist/src/index.d.ts +2 -1
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +5 -1
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/indexer/SwarmIndexer.d.ts +1 -1
  41. package/dist/src/indexer/SwarmIndexer.js +2 -2
  42. package/dist/src/indexer/SwarmIndexer.js.map +1 -1
  43. package/dist/src/install/paths.d.ts +1 -1
  44. package/dist/src/install/paths.d.ts.map +1 -1
  45. package/dist/src/install/paths.js +2 -0
  46. package/dist/src/install/paths.js.map +1 -1
  47. package/dist/src/install/paths.test.js +10 -1
  48. package/dist/src/install/paths.test.js.map +1 -1
  49. package/dist/src/scripts/__tests__/scan-imported-skills.test.js +5 -0
  50. package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
  51. package/dist/src/security/audit-types.d.ts +1 -1
  52. package/dist/src/security/audit-types.d.ts.map +1 -1
  53. package/dist/src/security/audit-types.js.map +1 -1
  54. package/dist/src/security/scanner/SecurityScanner.exec.d.ts +7 -0
  55. package/dist/src/security/scanner/SecurityScanner.exec.d.ts.map +1 -1
  56. package/dist/src/security/scanner/SecurityScanner.exec.js +10 -74
  57. package/dist/src/security/scanner/SecurityScanner.exec.js.map +1 -1
  58. package/dist/src/security/scanner/SecurityScanner.formatters.d.ts +14 -1
  59. package/dist/src/security/scanner/SecurityScanner.formatters.d.ts.map +1 -1
  60. package/dist/src/security/scanner/SecurityScanner.formatters.js +33 -0
  61. package/dist/src/security/scanner/SecurityScanner.formatters.js.map +1 -1
  62. package/dist/src/security/scanner/SecurityScanner.formatters.test.d.ts +9 -0
  63. package/dist/src/security/scanner/SecurityScanner.formatters.test.d.ts.map +1 -0
  64. package/dist/src/security/scanner/SecurityScanner.formatters.test.js +65 -0
  65. package/dist/src/security/scanner/SecurityScanner.formatters.test.js.map +1 -0
  66. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
  67. package/dist/src/security/scanner/SecurityScanner.helpers.js +15 -1
  68. package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
  69. package/dist/src/security/scanner/confusables.d.ts +28 -0
  70. package/dist/src/security/scanner/confusables.d.ts.map +1 -0
  71. package/dist/src/security/scanner/confusables.js +87 -0
  72. package/dist/src/security/scanner/confusables.js.map +1 -0
  73. package/dist/src/security/scanner/confusables.test.d.ts +10 -0
  74. package/dist/src/security/scanner/confusables.test.d.ts.map +1 -0
  75. package/dist/src/security/scanner/confusables.test.js +44 -0
  76. package/dist/src/security/scanner/confusables.test.js.map +1 -0
  77. package/dist/src/security/scanner/index.d.ts +7 -1
  78. package/dist/src/security/scanner/index.d.ts.map +1 -1
  79. package/dist/src/security/scanner/index.js +13 -0
  80. package/dist/src/security/scanner/index.js.map +1 -1
  81. package/dist/src/security/scanner/types.d.ts +14 -1
  82. package/dist/src/security/scanner/types.d.ts.map +1 -1
  83. package/dist/src/security/scanner/typosquat-reference-list.d.ts +54 -0
  84. package/dist/src/security/scanner/typosquat-reference-list.d.ts.map +1 -0
  85. package/dist/src/security/scanner/typosquat-reference-list.js +55 -0
  86. package/dist/src/security/scanner/typosquat-reference-list.js.map +1 -0
  87. package/dist/src/security/scanner/typosquat-reference-list.test.d.ts +5 -0
  88. package/dist/src/security/scanner/typosquat-reference-list.test.d.ts.map +1 -0
  89. package/dist/src/security/scanner/typosquat-reference-list.test.js +52 -0
  90. package/dist/src/security/scanner/typosquat-reference-list.test.js.map +1 -0
  91. package/dist/src/security/scanner/typosquat.d.ts +100 -0
  92. package/dist/src/security/scanner/typosquat.d.ts.map +1 -0
  93. package/dist/src/security/scanner/typosquat.js +256 -0
  94. package/dist/src/security/scanner/typosquat.js.map +1 -0
  95. package/dist/src/security/scanner/typosquat.test.d.ts +12 -0
  96. package/dist/src/security/scanner/typosquat.test.d.ts.map +1 -0
  97. package/dist/src/security/scanner/typosquat.test.js +140 -0
  98. package/dist/src/security/scanner/typosquat.test.js.map +1 -0
  99. package/dist/src/security/scanner/weights.d.ts.map +1 -1
  100. package/dist/src/security/scanner/weights.js +10 -0
  101. package/dist/src/security/scanner/weights.js.map +1 -1
  102. package/dist/src/services/skill-installation.helpers.d.ts +21 -0
  103. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  104. package/dist/src/services/skill-installation.helpers.js +46 -2
  105. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  106. package/dist/src/services/skill-installation.helpers.test.d.ts +2 -0
  107. package/dist/src/services/skill-installation.helpers.test.d.ts.map +1 -0
  108. package/dist/src/services/skill-installation.helpers.test.js +80 -0
  109. package/dist/src/services/skill-installation.helpers.test.js.map +1 -0
  110. package/dist/src/sync/inventory-collector.d.ts +15 -4
  111. package/dist/src/sync/inventory-collector.d.ts.map +1 -1
  112. package/dist/src/sync/inventory-collector.js +73 -24
  113. package/dist/src/sync/inventory-collector.js.map +1 -1
  114. package/dist/src/sync/inventory-collector.test.d.ts +15 -1
  115. package/dist/src/sync/inventory-collector.test.d.ts.map +1 -1
  116. package/dist/src/sync/inventory-collector.test.js +75 -5
  117. package/dist/src/sync/inventory-collector.test.js.map +1 -1
  118. package/dist/src/telemetry/tracer-imports.d.ts +1 -1
  119. package/dist/src/telemetry/tracer-imports.js +1 -1
  120. package/dist/src/telemetry/tracer.js +1 -1
  121. package/dist/tests/SecurityScanner.scoring.test.js +60 -0
  122. package/dist/tests/SecurityScanner.scoring.test.js.map +1 -1
  123. package/dist/tests/SwarmIndexer.test.js +4 -5
  124. package/dist/tests/SwarmIndexer.test.js.map +1 -1
  125. package/dist/tests/security/scanner-regression-guard.test.js +1 -0
  126. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  127. package/dist/tests/services/aidefence-feedback.test.js +1 -0
  128. package/dist/tests/services/aidefence-feedback.test.js.map +1 -1
  129. package/dist/tests/skill-scanner/allowlist.test.js +1 -0
  130. package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
  131. package/package.json +13 -8
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,mBAAmB,EACnB,yBAAyB,EACzB,YAAY,EACZ,uBAAuB,GACxB,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGjE,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAG3F,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAG/D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,mBAAmB,EACnB,yBAAyB,EACzB,YAAY,EACZ,uBAAuB,GACxB,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGjE,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAK3F,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAI1D,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,6BAA6B,EAC7B,+BAA+B,EAC/B,kCAAkC,GACnC,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACL,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,+BAA+B,CAAA;AACtC,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,GACnC,MAAM,+BAA+B,CAAA;AAGtC,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAGjE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAG/D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA"}
@@ -9,6 +9,19 @@ export { DEFAULT_ALLOWED_DOMAINS, SENSITIVE_PATH_PATTERNS, JAILBREAK_PATTERNS, S
9
9
  export { SEVERITY_WEIGHTS, CATEGORY_WEIGHTS } from './weights.js';
10
10
  // Regex utilities (for testing/extending)
11
11
  export { MAX_LINE_LENGTH_FOR_REGEX, safeRegexTest, safeRegexCheck } from './regex-utils.js';
12
+ // Obfuscation-defeat primitive (SMI-4703: reused by
13
+ // packages/doc-retrieval-mcp/src/security/memory-injection-scanner.ts's
14
+ // normalization pipeline rather than reimplemented there)
15
+ export { stripInvisible } from './SecurityScanner.exec.js';
16
+ // Confusable/homoglyph normalization primitives (SMI-595 extraction, for testing/extending;
17
+ // also reused by SMI-4703's memory-injection-scanner.ts confusable-fold step)
18
+ export { CONFUSABLES, isFullwidthLatin, isMathAlphanumeric, confusableSkeleton, } from './confusables.js';
19
+ // Typosquat detector (SMI-595)
20
+ export { BRAND_ALIASES, levenshteinDistance, scanTyposquat, detectTyposquat, applyTyposquatEnforcementMode, resolveTyposquatEnforcementMode, DEFAULT_TYPOSQUAT_ENFORCEMENT_MODE, } from './typosquat.js';
21
+ // Typosquat reference-name list builder (SMI-595)
22
+ export { buildTyposquatReferenceList, DEFAULT_TOP_INSTALLED_LIMIT, } from './typosquat-reference-list.js';
23
+ // describeSignals (SMI-595, for testing/extending)
24
+ export { describeSignals } from './SecurityScanner.formatters.js';
12
25
  // Main class
13
26
  export { SecurityScanner, default } from './SecurityScanner.js';
14
27
  // Hostile-update comparator (SMI-5535, R0 Wave 2A)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/security/scanner/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,mCAAmC;AACnC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,mBAAmB,EACnB,yBAAyB,EACzB,YAAY,EACZ,uBAAuB,GACxB,MAAM,eAAe,CAAA;AAEtB,kCAAkC;AAClC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEjE,0CAA0C;AAC1C,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE3F,aAAa;AACb,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAE/D,mDAAmD;AACnD,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/security/scanner/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,mCAAmC;AACnC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,mBAAmB,EACnB,yBAAyB,EACzB,YAAY,EACZ,uBAAuB,GACxB,MAAM,eAAe,CAAA;AAEtB,kCAAkC;AAClC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEjE,0CAA0C;AAC1C,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE3F,oDAAoD;AACpD,wEAAwE;AACxE,0DAA0D;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE1D,4FAA4F;AAC5F,8EAA8E;AAC9E,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,+BAA+B;AAC/B,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,6BAA6B,EAC7B,+BAA+B,EAC/B,kCAAkC,GACnC,MAAM,gBAAgB,CAAA;AAEvB,kDAAkD;AAClD,OAAO,EACL,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,+BAA+B,CAAA;AAOtC,mDAAmD;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAEjE,aAAa;AACb,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAE/D,mDAAmD;AACnD,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA"}
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Types of security findings that can be detected
8
8
  */
9
- export type SecurityFindingType = 'url' | 'sensitive_path' | 'jailbreak' | 'suspicious_pattern' | 'social_engineering' | 'prompt_leaking' | 'data_exfiltration' | 'privilege_escalation' | 'ai_defence' | 'ssrf' | 'pii' | 'code_execution' | 'obfuscated_directive';
9
+ export type SecurityFindingType = 'url' | 'sensitive_path' | 'jailbreak' | 'suspicious_pattern' | 'social_engineering' | 'prompt_leaking' | 'data_exfiltration' | 'privilege_escalation' | 'ai_defence' | 'ssrf' | 'pii' | 'code_execution' | 'obfuscated_directive' | 'typosquat';
10
10
  /**
11
11
  * Severity levels for security findings
12
12
  */
@@ -53,6 +53,7 @@ export interface RiskScoreBreakdown {
53
53
  pii: number;
54
54
  codeExecution: number;
55
55
  obfuscatedDirective: number;
56
+ typosquat: number;
56
57
  }
57
58
  /**
58
59
  * Comprehensive scan report with risk scoring
@@ -89,6 +90,16 @@ export interface HostileUpdateVerdict {
89
90
  /** One concrete human-readable sentence citing the deciding signal. */
90
91
  reason: string;
91
92
  }
93
+ /**
94
+ * SMI-595: Rollout mode for the typosquat detector.
95
+ * - `off` — detector output is discarded entirely (no findings surface).
96
+ * - `warn` — shadow mode (default): findings surface but are capped at
97
+ * `medium` severity regardless of the raw detector's confidence, so a
98
+ * false positive on this early-stage heuristic can't alone quarantine
99
+ * an install.
100
+ * - `block` — findings surface at their raw (uncapped) severity.
101
+ */
102
+ export type TyposquatEnforcementMode = 'off' | 'warn' | 'block';
92
103
  /**
93
104
  * Configuration options for the security scanner
94
105
  */
@@ -98,5 +109,7 @@ export interface ScannerOptions {
98
109
  maxContentLength?: number;
99
110
  /** Risk score threshold for failing a scan (default: 40) */
100
111
  riskThreshold?: number;
112
+ /** SMI-595: typosquat detector enforcement mode (default: 'warn'). */
113
+ typosquatEnforcementMode?: TyposquatEnforcementMode;
101
114
  }
102
115
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAC3B,KAAK,GACL,gBAAgB,GAChB,WAAW,GACX,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,mBAAmB,GACnB,sBAAsB,GACtB,YAAY,GACZ,MAAM,GACN,KAAK,GACL,gBAAgB,GAChB,sBAAsB,CAAA;AAE1B;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAA;AAErE;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEzD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAA;IACzB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,qEAAqE;IACrE,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B,0IAA0I;IAC1I,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,SAAS,EAAE,IAAI,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,aAAa,EAAE,kBAAkB,CAAA;CAClC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAA;IAC5C,8EAA8E;IAC9E,WAAW,EAAE,eAAe,EAAE,CAAA;IAC9B,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAC3B,KAAK,GACL,gBAAgB,GAChB,WAAW,GACX,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,mBAAmB,GACnB,sBAAsB,GACtB,YAAY,GACZ,MAAM,GACN,KAAK,GACL,gBAAgB,GAChB,sBAAsB,GACtB,WAAW,CAAA;AAEf;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAA;AAErE;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEzD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAA;IACzB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,qEAAqE;IACrE,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B,0IAA0I;IAC1I,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,SAAS,EAAE,IAAI,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,aAAa,EAAE,kBAAkB,CAAA;CAClC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAA;IAC5C,8EAA8E;IAC9E,WAAW,EAAE,eAAe,EAAE,CAAA;IAC9B,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;AAE/D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,wBAAwB,CAAC,EAAE,wBAAwB,CAAA;CACpD"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Typosquat reference-name list builder
3
+ * @module @skillsmith/core/security/scanner/typosquat-reference-list
4
+ *
5
+ * SMI-595 §2: a name-comparison detector is only as good as its reference
6
+ * list; a hand-maintained list goes stale the moment a new popular skill
7
+ * ships. The reference list is built from two sources:
8
+ *
9
+ * 1. Every skill published by an owner in `HIGH_TRUST_OWNERS`
10
+ * (`packages/core/src/scripts/github-import/signal-of-intent.ts`).
11
+ * 2. The top-N installed skills by install count (N configurable, default
12
+ * 200) — covers popular community skills that aren't from a
13
+ * `HIGH_TRUST_OWNERS` publisher but are popular enough to be worth
14
+ * impersonating.
15
+ *
16
+ * Plus `BRAND_ALIASES` (`typosquat.ts` §3) — brand names not derivable from
17
+ * either source above.
18
+ *
19
+ * This module is a pure builder: it accepts the two source arrays already
20
+ * fetched by the caller (rather than querying a database or the GitHub API
21
+ * itself), so it stays cheaply unit-testable. Wiring a *live* caller that
22
+ * sources real `HIGH_TRUST_OWNERS`-published skill names and real
23
+ * install-count data, refreshed on the existing metadata-refresh cadence
24
+ * (`.claude/development/deployment-guide.md`'s "Metadata Refresh (every 4h
25
+ * :30)" job — no new cron), is a follow-up integration point, not built out
26
+ * this wave.
27
+ */
28
+ /** A minimal reference-skill shape: only the fields this builder needs. */
29
+ export interface ReferenceSkillEntry {
30
+ author: string;
31
+ name: string;
32
+ }
33
+ /** An installed skill together with its install count, for the top-N filter. */
34
+ export interface InstalledSkillEntry extends ReferenceSkillEntry {
35
+ installCount: number;
36
+ }
37
+ /** SMI-595 §2: default top-N cutoff for installed skills folded into the list. */
38
+ export declare const DEFAULT_TOP_INSTALLED_LIMIT = 200;
39
+ export interface BuildTyposquatReferenceListOptions {
40
+ /** Skills published by an owner in `HIGH_TRUST_OWNERS`. */
41
+ highTrustOwnerSkills?: ReferenceSkillEntry[];
42
+ /** All installed skills with a known install count; only the top N (by
43
+ * count, descending) are folded in. */
44
+ installedSkills?: InstalledSkillEntry[];
45
+ /** Max number of installed skills to fold in. Default: `DEFAULT_TOP_INSTALLED_LIMIT` (200). */
46
+ topInstalledLimit?: number;
47
+ }
48
+ /**
49
+ * Build the lowercase reference-name set used by `scanTyposquat()`. Combines
50
+ * HIGH_TRUST_OWNERS-published skill names, the top-N installed skills by
51
+ * install count, and the bare brand tokens from `BRAND_ALIASES`.
52
+ */
53
+ export declare function buildTyposquatReferenceList(options?: BuildTyposquatReferenceListOptions): ReadonlySet<string>;
54
+ //# sourceMappingURL=typosquat-reference-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typosquat-reference-list.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/typosquat-reference-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,kFAAkF;AAClF,eAAO,MAAM,2BAA2B,MAAM,CAAA;AAE9C,MAAM,WAAW,kCAAkC;IACjD,2DAA2D;IAC3D,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAA;IAC5C;4CACwC;IACxC,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAA;IACvC,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,WAAW,CAAC,MAAM,CAAC,CAyBrB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Typosquat reference-name list builder
3
+ * @module @skillsmith/core/security/scanner/typosquat-reference-list
4
+ *
5
+ * SMI-595 §2: a name-comparison detector is only as good as its reference
6
+ * list; a hand-maintained list goes stale the moment a new popular skill
7
+ * ships. The reference list is built from two sources:
8
+ *
9
+ * 1. Every skill published by an owner in `HIGH_TRUST_OWNERS`
10
+ * (`packages/core/src/scripts/github-import/signal-of-intent.ts`).
11
+ * 2. The top-N installed skills by install count (N configurable, default
12
+ * 200) — covers popular community skills that aren't from a
13
+ * `HIGH_TRUST_OWNERS` publisher but are popular enough to be worth
14
+ * impersonating.
15
+ *
16
+ * Plus `BRAND_ALIASES` (`typosquat.ts` §3) — brand names not derivable from
17
+ * either source above.
18
+ *
19
+ * This module is a pure builder: it accepts the two source arrays already
20
+ * fetched by the caller (rather than querying a database or the GitHub API
21
+ * itself), so it stays cheaply unit-testable. Wiring a *live* caller that
22
+ * sources real `HIGH_TRUST_OWNERS`-published skill names and real
23
+ * install-count data, refreshed on the existing metadata-refresh cadence
24
+ * (`.claude/development/deployment-guide.md`'s "Metadata Refresh (every 4h
25
+ * :30)" job — no new cron), is a follow-up integration point, not built out
26
+ * this wave.
27
+ */
28
+ import { BRAND_ALIASES } from './typosquat.js';
29
+ /** SMI-595 §2: default top-N cutoff for installed skills folded into the list. */
30
+ export const DEFAULT_TOP_INSTALLED_LIMIT = 200;
31
+ /**
32
+ * Build the lowercase reference-name set used by `scanTyposquat()`. Combines
33
+ * HIGH_TRUST_OWNERS-published skill names, the top-N installed skills by
34
+ * install count, and the bare brand tokens from `BRAND_ALIASES`.
35
+ */
36
+ export function buildTyposquatReferenceList(options = {}) {
37
+ const { highTrustOwnerSkills = [], installedSkills = [], topInstalledLimit = DEFAULT_TOP_INSTALLED_LIMIT, } = options;
38
+ const names = new Set();
39
+ for (const entry of highTrustOwnerSkills) {
40
+ if (entry.name)
41
+ names.add(entry.name.toLowerCase());
42
+ }
43
+ const topInstalled = [...installedSkills]
44
+ .sort((a, b) => b.installCount - a.installCount)
45
+ .slice(0, Math.max(0, topInstalledLimit));
46
+ for (const entry of topInstalled) {
47
+ if (entry.name)
48
+ names.add(entry.name.toLowerCase());
49
+ }
50
+ for (const brand of Object.keys(BRAND_ALIASES)) {
51
+ names.add(brand.toLowerCase());
52
+ }
53
+ return names;
54
+ }
55
+ //# sourceMappingURL=typosquat-reference-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typosquat-reference-list.js","sourceRoot":"","sources":["../../../../src/security/scanner/typosquat-reference-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAa9C,kFAAkF;AAClF,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAA;AAY9C;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CACzC,UAA8C,EAAE;IAEhD,MAAM,EACJ,oBAAoB,GAAG,EAAE,EACzB,eAAe,GAAG,EAAE,EACpB,iBAAiB,GAAG,2BAA2B,GAChD,GAAG,OAAO,CAAA;IAEX,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAE/B,KAAK,MAAM,KAAK,IAAI,oBAAoB,EAAE,CAAC;QACzC,IAAI,KAAK,CAAC,IAAI;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,MAAM,YAAY,GAAG,CAAC,GAAG,eAAe,CAAC;SACtC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;SAC/C,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAA;IAC3C,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,IAAI;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/C,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IAChC,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SMI-595 §2: typosquat-reference-list.ts — reference-list builder tests.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=typosquat-reference-list.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typosquat-reference-list.test.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/typosquat-reference-list.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SMI-595 §2: typosquat-reference-list.ts — reference-list builder tests.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { buildTyposquatReferenceList, DEFAULT_TOP_INSTALLED_LIMIT, } from './typosquat-reference-list.js';
6
+ import { BRAND_ALIASES } from './typosquat.js';
7
+ describe('buildTyposquatReferenceList (SMI-595 §2)', () => {
8
+ it('folds in HIGH_TRUST_OWNERS-published skill names, lowercased', () => {
9
+ const list = buildTyposquatReferenceList({
10
+ highTrustOwnerSkills: [{ author: 'anthropics', name: 'Claude-Helper' }],
11
+ });
12
+ expect(list.has('claude-helper')).toBe(true);
13
+ });
14
+ it('folds in the top-N installed skills by install count', () => {
15
+ const list = buildTyposquatReferenceList({
16
+ installedSkills: [
17
+ { author: 'a', name: 'popular-skill', installCount: 1000 },
18
+ { author: 'b', name: 'niche-skill', installCount: 1 },
19
+ ],
20
+ topInstalledLimit: 1,
21
+ });
22
+ expect(list.has('popular-skill')).toBe(true);
23
+ expect(list.has('niche-skill')).toBe(false); // cut off by the limit
24
+ });
25
+ it('respects the default top-N limit of 200', () => {
26
+ expect(DEFAULT_TOP_INSTALLED_LIMIT).toBe(200);
27
+ const installedSkills = Array.from({ length: 250 }, (_, i) => ({
28
+ author: 'a',
29
+ name: `skill-${i}`,
30
+ installCount: 250 - i, // skill-0 has the highest count
31
+ }));
32
+ const list = buildTyposquatReferenceList({ installedSkills });
33
+ expect(list.has('skill-0')).toBe(true); // rank 1, well within top 200
34
+ expect(list.has('skill-249')).toBe(false); // rank 250, cut off
35
+ });
36
+ it('always includes the BRAND_ALIASES keys, even with no other sources', () => {
37
+ const list = buildTyposquatReferenceList();
38
+ for (const brand of Object.keys(BRAND_ALIASES)) {
39
+ expect(list.has(brand)).toBe(true);
40
+ }
41
+ });
42
+ it('deduplicates a name that appears in both sources', () => {
43
+ const list = buildTyposquatReferenceList({
44
+ highTrustOwnerSkills: [{ author: 'anthropics', name: 'shared-skill' }],
45
+ installedSkills: [{ author: 'anthropics', name: 'shared-skill', installCount: 5 }],
46
+ });
47
+ // A Set naturally dedupes; just confirm the entry is present exactly once
48
+ // (Set semantics already guarantee this, this is a readability check).
49
+ expect([...list].filter((n) => n === 'shared-skill')).toHaveLength(1);
50
+ });
51
+ });
52
+ //# sourceMappingURL=typosquat-reference-list.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typosquat-reference-list.test.js","sourceRoot":"","sources":["../../../../src/security/scanner/typosquat-reference-list.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Typosquat / impersonation detector for skill names
3
+ * @module @skillsmith/core/security/scanner/typosquat
4
+ *
5
+ * SMI-595: no Levenshtein-distance / character-substitution / homoglyph
6
+ * name-comparison detector against a popular-skill-name list existed
7
+ * anywhere in this codebase prior to this module — content-based scanning
8
+ * (`SecurityScanner`) cannot catch a name-only impersonation attempt where
9
+ * the skill's actual content is benign (e.g. `anthopic/claude-helper` or
10
+ * `аnthropic/skill` with a Cyrillic `а`).
11
+ *
12
+ * Three independent checks, deliberately NOT conflated:
13
+ *
14
+ * 1. **Exact-skeleton impersonation** — fires ONLY on exact confusable-folded
15
+ * equality to a reference name, never on substring/affix match. This is a
16
+ * deliberately narrow rule: `anthropic-community-mcp`, `unofficial-claude-
17
+ * tools`, `awesome-gemini-skills` are normal, non-malicious community
18
+ * naming conventions that happen to contain a brand token and must NOT be
19
+ * blocked by this rule.
20
+ * 2. **Levenshtein edit-distance ≤2** — catches near-miss variants
21
+ * (`anthropc`, `anthropic1`, `anthropci`) that don't fold to an exact
22
+ * skeleton match but are still suspiciously close to a reference name.
23
+ * 3. **Authority-claiming affix** — INDEPENDENT of #1: a candidate containing
24
+ * a reference brand token PLUS an affix that claims official status
25
+ * (`-official`, `-verified`, `-authentic`, `-genuine`) is flagged
26
+ * regardless of whether it passes the exact-skeleton check, because the
27
+ * affix itself is the impersonation vector. Benign functional affixes
28
+ * (`-mcp`, `-tools`, `-community`) do NOT trigger this — they simply
29
+ * aren't in the curated affix list.
30
+ *
31
+ * Consumer-surface decision (Wave 1 Step 6): the install-time scanner path
32
+ * (risk-score wiring — see types.ts/weights.ts/SecurityScanner.helpers.ts) is
33
+ * the primary *enforcement* surface (blocks/warns before a skill lands on
34
+ * disk). `skill_audit` (existing MCP tool) is the recommended primary
35
+ * *consumer-facing* surface for on-demand querying of typosquat status on an
36
+ * already-installed skill — live-wiring that tool to this detector is
37
+ * deferred to a filed follow-up (SMI-5711), out of this wave's scope.
38
+ *
39
+ * Integration note: `SecurityScanner.scan()` takes `content` only, not a
40
+ * skill name/author — this wave does NOT change that signature or thread a
41
+ * live reference list through `scripts/skill-scanner/scanner.ts`'s scan
42
+ * pipeline (that would require sourcing real HIGH_TRUST_OWNERS-published
43
+ * skill data and real install-count data, which is a live-data/infra
44
+ * integration, not a detector-design concern). This module is a
45
+ * self-contained, fully-tested unit ready to be wired into that pipeline as a
46
+ * follow-up.
47
+ */
48
+ import type { SecurityFinding, TyposquatEnforcementMode } from './types.js';
49
+ export type { TyposquatEnforcementMode };
50
+ /**
51
+ * Brand names that are NOT derivable from `HIGH_TRUST_OWNERS`' GitHub owner
52
+ * slugs (`signal-of-intent.ts`) or from the installed-skill corpus at all —
53
+ * the product/brand a typosquat would actually target (`anthropic`, `claude`,
54
+ * `gemini`) differs from the GitHub org slug that publishes it (`anthropics`,
55
+ * `google-gemini`). Folded into the reference list
56
+ * (`typosquat-reference-list.ts`) as bare-brand reference entries, and also
57
+ * used as the curated "brand token" corpus for the authority-claiming-affix
58
+ * check below (§1 rule 3) — a narrower, independent check from the
59
+ * exact-skeleton rule (§1 rule 1).
60
+ *
61
+ * Values cross-checked against `HIGH_TRUST_OWNERS`
62
+ * (`packages/core/src/scripts/github-import/signal-of-intent.ts`) — kept in
63
+ * sync manually, same convention as that file's own documented
64
+ * cross-boundary-import constraint.
65
+ */
66
+ export declare const BRAND_ALIASES: Readonly<Record<string, string>>;
67
+ /**
68
+ * Standard iterative-DP Levenshtein edit distance (insertion/deletion/
69
+ * substitution). Inputs here are short skill-name strings, so an O(n*m) table
70
+ * is more than fast enough — no early-exit optimization needed.
71
+ */
72
+ export declare function levenshteinDistance(a: string, b: string): number;
73
+ /**
74
+ * Run all three checks against a single candidate skill name/id. Returns
75
+ * RAW findings (uncapped severity) — callers apply
76
+ * `applyTyposquatEnforcementMode()` before surfacing them, per the
77
+ * `typosquatEnforcementMode` rollout config (§6).
78
+ *
79
+ * `referenceNames` should already be lowercase (as produced by
80
+ * `buildTyposquatReferenceList()` in `typosquat-reference-list.ts`).
81
+ */
82
+ export declare function scanTyposquat(candidateName: string, referenceNames: ReadonlySet<string>): SecurityFinding[];
83
+ /** SMI-595: default rollout mode — shadow mode, matching the
84
+ * `concurrency-audit-pr.yml` precedent (shadow for a period, then promote). */
85
+ export declare const DEFAULT_TYPOSQUAT_ENFORCEMENT_MODE: TyposquatEnforcementMode;
86
+ /** Resolve the effective enforcement mode, applying the default when unset. */
87
+ export declare function resolveTyposquatEnforcementMode(mode?: TyposquatEnforcementMode): TyposquatEnforcementMode;
88
+ /**
89
+ * Apply the rollout mode to raw typosquat findings:
90
+ * - `off` — discard all findings.
91
+ * - `warn` — cap severity at `medium` regardless of the raw detector's
92
+ * confidence (confidence is left untouched — only severity is capped).
93
+ * - `block` — pass findings through at their raw severity.
94
+ */
95
+ export declare function applyTyposquatEnforcementMode(findings: SecurityFinding[], mode?: TyposquatEnforcementMode): SecurityFinding[];
96
+ /**
97
+ * Convenience one-shot: detect + apply the enforcement mode in a single call.
98
+ */
99
+ export declare function detectTyposquat(candidateName: string, referenceNames: ReadonlySet<string>, mode?: TyposquatEnforcementMode): SecurityFinding[];
100
+ //# sourceMappingURL=typosquat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typosquat.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/typosquat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAG3E,YAAY,EAAE,wBAAwB,EAAE,CAAA;AAMxC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO1D,CAAA;AAmDD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAyBhE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,GAClC,eAAe,EAAE,CAgEnB;AAMD;gFACgF;AAChF,eAAO,MAAM,kCAAkC,EAAE,wBAAiC,CAAA;AAElF,+EAA+E;AAC/E,wBAAgB,+BAA+B,CAC7C,IAAI,CAAC,EAAE,wBAAwB,GAC9B,wBAAwB,CAE1B;AAID;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,eAAe,EAAE,EAC3B,IAAI,GAAE,wBAA6D,GAClE,eAAe,EAAE,CAQnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,EACnC,IAAI,GAAE,wBAA6D,GAClE,eAAe,EAAE,CAEnB"}
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Typosquat / impersonation detector for skill names
3
+ * @module @skillsmith/core/security/scanner/typosquat
4
+ *
5
+ * SMI-595: no Levenshtein-distance / character-substitution / homoglyph
6
+ * name-comparison detector against a popular-skill-name list existed
7
+ * anywhere in this codebase prior to this module — content-based scanning
8
+ * (`SecurityScanner`) cannot catch a name-only impersonation attempt where
9
+ * the skill's actual content is benign (e.g. `anthopic/claude-helper` or
10
+ * `аnthropic/skill` with a Cyrillic `а`).
11
+ *
12
+ * Three independent checks, deliberately NOT conflated:
13
+ *
14
+ * 1. **Exact-skeleton impersonation** — fires ONLY on exact confusable-folded
15
+ * equality to a reference name, never on substring/affix match. This is a
16
+ * deliberately narrow rule: `anthropic-community-mcp`, `unofficial-claude-
17
+ * tools`, `awesome-gemini-skills` are normal, non-malicious community
18
+ * naming conventions that happen to contain a brand token and must NOT be
19
+ * blocked by this rule.
20
+ * 2. **Levenshtein edit-distance ≤2** — catches near-miss variants
21
+ * (`anthropc`, `anthropic1`, `anthropci`) that don't fold to an exact
22
+ * skeleton match but are still suspiciously close to a reference name.
23
+ * 3. **Authority-claiming affix** — INDEPENDENT of #1: a candidate containing
24
+ * a reference brand token PLUS an affix that claims official status
25
+ * (`-official`, `-verified`, `-authentic`, `-genuine`) is flagged
26
+ * regardless of whether it passes the exact-skeleton check, because the
27
+ * affix itself is the impersonation vector. Benign functional affixes
28
+ * (`-mcp`, `-tools`, `-community`) do NOT trigger this — they simply
29
+ * aren't in the curated affix list.
30
+ *
31
+ * Consumer-surface decision (Wave 1 Step 6): the install-time scanner path
32
+ * (risk-score wiring — see types.ts/weights.ts/SecurityScanner.helpers.ts) is
33
+ * the primary *enforcement* surface (blocks/warns before a skill lands on
34
+ * disk). `skill_audit` (existing MCP tool) is the recommended primary
35
+ * *consumer-facing* surface for on-demand querying of typosquat status on an
36
+ * already-installed skill — live-wiring that tool to this detector is
37
+ * deferred to a filed follow-up (SMI-5711), out of this wave's scope.
38
+ *
39
+ * Integration note: `SecurityScanner.scan()` takes `content` only, not a
40
+ * skill name/author — this wave does NOT change that signature or thread a
41
+ * live reference list through `scripts/skill-scanner/scanner.ts`'s scan
42
+ * pipeline (that would require sourcing real HIGH_TRUST_OWNERS-published
43
+ * skill data and real install-count data, which is a live-data/infra
44
+ * integration, not a detector-design concern). This module is a
45
+ * self-contained, fully-tested unit ready to be wired into that pipeline as a
46
+ * follow-up.
47
+ */
48
+ import { confusableSkeleton } from './confusables.js';
49
+ // ============================================================================
50
+ // Brand aliases (§3)
51
+ // ============================================================================
52
+ /**
53
+ * Brand names that are NOT derivable from `HIGH_TRUST_OWNERS`' GitHub owner
54
+ * slugs (`signal-of-intent.ts`) or from the installed-skill corpus at all —
55
+ * the product/brand a typosquat would actually target (`anthropic`, `claude`,
56
+ * `gemini`) differs from the GitHub org slug that publishes it (`anthropics`,
57
+ * `google-gemini`). Folded into the reference list
58
+ * (`typosquat-reference-list.ts`) as bare-brand reference entries, and also
59
+ * used as the curated "brand token" corpus for the authority-claiming-affix
60
+ * check below (§1 rule 3) — a narrower, independent check from the
61
+ * exact-skeleton rule (§1 rule 1).
62
+ *
63
+ * Values cross-checked against `HIGH_TRUST_OWNERS`
64
+ * (`packages/core/src/scripts/github-import/signal-of-intent.ts`) — kept in
65
+ * sync manually, same convention as that file's own documented
66
+ * cross-boundary-import constraint.
67
+ */
68
+ export const BRAND_ALIASES = {
69
+ anthropic: 'anthropics',
70
+ claude: 'anthropics',
71
+ gemini: 'google-gemini',
72
+ copilot: 'microsoft',
73
+ vercel: 'vercel-labs',
74
+ salesforce: 'SalesforceCommerceCloud',
75
+ };
76
+ // ============================================================================
77
+ // Authority-claiming affixes (§1 rule 3 / Change #5)
78
+ // ============================================================================
79
+ /**
80
+ * A small, curated "claims official status" affix list. Deliberately narrow —
81
+ * benign functional affixes (`mcp`, `tools`, `community`, `helper`, ...)
82
+ * simply aren't in this list, so they never trigger this check.
83
+ */
84
+ const AUTHORITY_CLAIMING_AFFIXES = new Set([
85
+ 'official',
86
+ 'verified',
87
+ 'authentic',
88
+ 'genuine',
89
+ ]);
90
+ /** Minimum reference-name length considered for the Levenshtein check (§1 rule 2).
91
+ * Guards against noisy false positives on very short brand tokens where an
92
+ * edit distance of 2 covers a large fraction of unrelated short words. */
93
+ const MIN_LEVENSHTEIN_REFERENCE_LENGTH = 6;
94
+ /** Maximum Levenshtein edit distance considered a typosquat variant. */
95
+ const MAX_LEVENSHTEIN_DISTANCE = 2;
96
+ // ============================================================================
97
+ // Folding / tokenization helpers
98
+ // ============================================================================
99
+ /**
100
+ * Fold a candidate/reference name through the confusable-skeleton map, then
101
+ * NFKC, then lowercase — the comparison key used by the exact-skeleton and
102
+ * Levenshtein checks (§1 rules 1 and 2).
103
+ */
104
+ function foldSkillName(raw) {
105
+ return confusableSkeleton(raw).normalize('NFKC').toLowerCase();
106
+ }
107
+ /** Split a name into lowercase alphanumeric tokens on any separator. */
108
+ function tokenize(raw) {
109
+ return raw
110
+ .toLowerCase()
111
+ .split(/[^a-z0-9]+/i)
112
+ .filter((t) => t.length > 0);
113
+ }
114
+ // ============================================================================
115
+ // Levenshtein distance
116
+ // ============================================================================
117
+ /**
118
+ * Standard iterative-DP Levenshtein edit distance (insertion/deletion/
119
+ * substitution). Inputs here are short skill-name strings, so an O(n*m) table
120
+ * is more than fast enough — no early-exit optimization needed.
121
+ */
122
+ export function levenshteinDistance(a, b) {
123
+ if (a === b)
124
+ return 0;
125
+ const aLen = a.length;
126
+ const bLen = b.length;
127
+ if (aLen === 0)
128
+ return bLen;
129
+ if (bLen === 0)
130
+ return aLen;
131
+ let prevRow = new Array(bLen + 1);
132
+ let currRow = new Array(bLen + 1);
133
+ for (let j = 0; j <= bLen; j++)
134
+ prevRow[j] = j;
135
+ for (let i = 1; i <= aLen; i++) {
136
+ currRow[0] = i;
137
+ for (let j = 1; j <= bLen; j++) {
138
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
139
+ currRow[j] = Math.min(prevRow[j] + 1, // deletion
140
+ currRow[j - 1] + 1, // insertion
141
+ prevRow[j - 1] + cost // substitution
142
+ );
143
+ }
144
+ ;
145
+ [prevRow, currRow] = [currRow, prevRow];
146
+ }
147
+ return prevRow[bLen];
148
+ }
149
+ // ============================================================================
150
+ // Detector
151
+ // ============================================================================
152
+ /**
153
+ * Run all three checks against a single candidate skill name/id. Returns
154
+ * RAW findings (uncapped severity) — callers apply
155
+ * `applyTyposquatEnforcementMode()` before surfacing them, per the
156
+ * `typosquatEnforcementMode` rollout config (§6).
157
+ *
158
+ * `referenceNames` should already be lowercase (as produced by
159
+ * `buildTyposquatReferenceList()` in `typosquat-reference-list.ts`).
160
+ */
161
+ export function scanTyposquat(candidateName, referenceNames) {
162
+ const findings = [];
163
+ const candidateLower = candidateName.toLowerCase();
164
+ const foldedCandidate = foldSkillName(candidateName);
165
+ // ---- Rule 1: exact-skeleton impersonation --------------------------------
166
+ // Fires ONLY when folding changed something meaningful (a confusable
167
+ // substitution / fullwidth / math-alphanumeric fold occurred) AND the
168
+ // folded form exactly equals a reference name. A candidate whose lowercase
169
+ // form already equals the reference (pure case difference, or the
170
+ // candidate IS the reference) is not an impersonation attempt.
171
+ for (const reference of referenceNames) {
172
+ if (foldedCandidate === reference && candidateLower !== reference) {
173
+ findings.push({
174
+ type: 'typosquat',
175
+ severity: 'critical',
176
+ confidence: 'high',
177
+ message: `Skill name folds to the same confusable-skeleton as a well-known reference name ("${reference}") but is not byte-identical to it.`,
178
+ location: candidateName,
179
+ category: 'typosquat:impersonation-exact-skeleton',
180
+ });
181
+ break;
182
+ }
183
+ }
184
+ // ---- Rule 2: Levenshtein edit-distance <= 2 ------------------------------
185
+ // Skipped once rule 1 already fired for this candidate (distance 0 in
186
+ // folded space is rule 1's job, not rule 2's — no double-counting).
187
+ if (findings.length === 0) {
188
+ for (const reference of referenceNames) {
189
+ if (reference.length < MIN_LEVENSHTEIN_REFERENCE_LENGTH)
190
+ continue;
191
+ // Cheap length-delta pre-filter before the O(n*m) DP.
192
+ if (Math.abs(foldedCandidate.length - reference.length) > MAX_LEVENSHTEIN_DISTANCE)
193
+ continue;
194
+ const distance = levenshteinDistance(foldedCandidate, reference);
195
+ if (distance > 0 && distance <= MAX_LEVENSHTEIN_DISTANCE) {
196
+ findings.push({
197
+ type: 'typosquat',
198
+ severity: 'high',
199
+ confidence: 'medium',
200
+ message: `Skill name is an edit-distance-${distance} variant of a well-known reference name ("${reference}").`,
201
+ location: candidateName,
202
+ category: 'typosquat:levenshtein',
203
+ });
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ // ---- Rule 3: authority-claiming affix (independent of rule 1) -----------
209
+ const tokens = tokenize(candidateName);
210
+ const hasBrandToken = tokens.some((t) => Object.prototype.hasOwnProperty.call(BRAND_ALIASES, t));
211
+ const hasAuthorityAffix = tokens.some((t) => AUTHORITY_CLAIMING_AFFIXES.has(t));
212
+ if (hasBrandToken && hasAuthorityAffix) {
213
+ findings.push({
214
+ type: 'typosquat',
215
+ severity: 'critical',
216
+ confidence: 'high',
217
+ message: `Skill name combines a known brand token with an affix that claims official/verified status.`,
218
+ location: candidateName,
219
+ category: 'typosquat:authority-affix',
220
+ });
221
+ }
222
+ return findings;
223
+ }
224
+ // ============================================================================
225
+ // Enforcement mode (§6)
226
+ // ============================================================================
227
+ /** SMI-595: default rollout mode — shadow mode, matching the
228
+ * `concurrency-audit-pr.yml` precedent (shadow for a period, then promote). */
229
+ export const DEFAULT_TYPOSQUAT_ENFORCEMENT_MODE = 'warn';
230
+ /** Resolve the effective enforcement mode, applying the default when unset. */
231
+ export function resolveTyposquatEnforcementMode(mode) {
232
+ return mode ?? DEFAULT_TYPOSQUAT_ENFORCEMENT_MODE;
233
+ }
234
+ const DOWNGRADE_TO_MEDIUM = new Set(['high', 'critical']);
235
+ /**
236
+ * Apply the rollout mode to raw typosquat findings:
237
+ * - `off` — discard all findings.
238
+ * - `warn` — cap severity at `medium` regardless of the raw detector's
239
+ * confidence (confidence is left untouched — only severity is capped).
240
+ * - `block` — pass findings through at their raw severity.
241
+ */
242
+ export function applyTyposquatEnforcementMode(findings, mode = DEFAULT_TYPOSQUAT_ENFORCEMENT_MODE) {
243
+ if (mode === 'off')
244
+ return [];
245
+ if (mode === 'warn') {
246
+ return findings.map((finding) => DOWNGRADE_TO_MEDIUM.has(finding.severity) ? { ...finding, severity: 'medium' } : finding);
247
+ }
248
+ return findings;
249
+ }
250
+ /**
251
+ * Convenience one-shot: detect + apply the enforcement mode in a single call.
252
+ */
253
+ export function detectTyposquat(candidateName, referenceNames, mode = DEFAULT_TYPOSQUAT_ENFORCEMENT_MODE) {
254
+ return applyTyposquatEnforcementMode(scanTyposquat(candidateName, referenceNames), mode);
255
+ }
256
+ //# sourceMappingURL=typosquat.js.map