ferret-scan 2.2.0 → 2.4.0

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 (159) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +104 -8
  4. package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
  5. package/dist/__tests__/AgentMonitor.test.js +235 -0
  6. package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
  7. package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
  8. package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
  9. package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
  10. package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
  11. package/dist/__tests__/IndicatorMatcher.test.js +245 -0
  12. package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
  13. package/dist/__tests__/MarketplaceScanner.test.js +212 -0
  14. package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
  15. package/dist/__tests__/RuleGenerator.test.js +207 -0
  16. package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
  17. package/dist/__tests__/ThreatFeed.test.js +359 -0
  18. package/dist/__tests__/WatchMode.test.d.ts +6 -0
  19. package/dist/__tests__/WatchMode.test.js +104 -0
  20. package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
  21. package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
  22. package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
  23. package/dist/__tests__/astAnalyzerFull.test.js +138 -0
  24. package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
  25. package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
  26. package/dist/__tests__/atlas.test.d.ts +6 -0
  27. package/dist/__tests__/atlas.test.js +319 -0
  28. package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
  29. package/dist/__tests__/atlasCatalog.test.js +200 -0
  30. package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
  31. package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
  32. package/dist/__tests__/baseline.test.d.ts +6 -0
  33. package/dist/__tests__/baseline.test.js +321 -0
  34. package/dist/__tests__/baselineExtra.test.d.ts +6 -0
  35. package/dist/__tests__/baselineExtra.test.js +317 -0
  36. package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
  37. package/dist/__tests__/capabilityMapping.test.js +49 -0
  38. package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
  39. package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
  40. package/dist/__tests__/complianceExtra.test.d.ts +6 -0
  41. package/dist/__tests__/complianceExtra.test.js +121 -0
  42. package/dist/__tests__/config.test.js +1 -1
  43. package/dist/__tests__/configLoader.test.d.ts +6 -0
  44. package/dist/__tests__/configLoader.test.js +225 -0
  45. package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
  46. package/dist/__tests__/configLoaderExtra.test.js +186 -0
  47. package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
  48. package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
  49. package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
  50. package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
  51. package/dist/__tests__/customRules.extra.test.d.ts +6 -0
  52. package/dist/__tests__/customRules.extra.test.js +245 -0
  53. package/dist/__tests__/customRules.test.d.ts +7 -0
  54. package/dist/__tests__/customRules.test.js +347 -0
  55. package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
  56. package/dist/__tests__/dependencyRisk.test.js +248 -0
  57. package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
  58. package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
  59. package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
  60. package/dist/__tests__/featureExitCodes.test.js +332 -0
  61. package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
  62. package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
  63. package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
  64. package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
  65. package/dist/__tests__/fixer.extra.test.d.ts +6 -0
  66. package/dist/__tests__/fixer.extra.test.js +135 -0
  67. package/dist/__tests__/fixerApply.test.d.ts +6 -0
  68. package/dist/__tests__/fixerApply.test.js +132 -0
  69. package/dist/__tests__/gitHooks.test.d.ts +7 -0
  70. package/dist/__tests__/gitHooks.test.js +188 -0
  71. package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
  72. package/dist/__tests__/htmlReporter.extra.test.js +126 -0
  73. package/dist/__tests__/interactiveTui.test.d.ts +6 -0
  74. package/dist/__tests__/interactiveTui.test.js +180 -0
  75. package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
  76. package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
  77. package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
  78. package/dist/__tests__/interactiveTuiMore.test.js +194 -0
  79. package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
  80. package/dist/__tests__/interactiveTuiSession.test.js +173 -0
  81. package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
  82. package/dist/__tests__/llmAnalysis.test.js +229 -0
  83. package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
  84. package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
  85. package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
  86. package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
  87. package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
  88. package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
  89. package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
  90. package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
  91. package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
  92. package/dist/__tests__/llmGroqTPM.test.js +89 -0
  93. package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
  94. package/dist/__tests__/llmProviderRetry.test.js +172 -0
  95. package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
  96. package/dist/__tests__/mcpValidator.extra.test.js +270 -0
  97. package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
  98. package/dist/__tests__/patternMatcherExtra.test.js +198 -0
  99. package/dist/__tests__/patternsCommon.test.d.ts +6 -0
  100. package/dist/__tests__/patternsCommon.test.js +107 -0
  101. package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
  102. package/dist/__tests__/policyEnforcement.test.js +510 -0
  103. package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
  104. package/dist/__tests__/quarantineExtra.test.js +214 -0
  105. package/dist/__tests__/redactionExtra.test.d.ts +6 -0
  106. package/dist/__tests__/redactionExtra.test.js +228 -0
  107. package/dist/__tests__/scanDiff.test.d.ts +7 -0
  108. package/dist/__tests__/scanDiff.test.js +266 -0
  109. package/dist/__tests__/scanFull.test.d.ts +6 -0
  110. package/dist/__tests__/scanFull.test.js +158 -0
  111. package/dist/__tests__/scannerDampening.test.d.ts +6 -0
  112. package/dist/__tests__/scannerDampening.test.js +160 -0
  113. package/dist/__tests__/scannerExtra.test.d.ts +6 -0
  114. package/dist/__tests__/scannerExtra.test.js +194 -0
  115. package/dist/__tests__/scannerMitre.test.d.ts +5 -0
  116. package/dist/__tests__/scannerMitre.test.js +141 -0
  117. package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
  118. package/dist/__tests__/scannerSSRF.test.js +149 -0
  119. package/dist/__tests__/schemas.test.d.ts +6 -0
  120. package/dist/__tests__/schemas.test.js +125 -0
  121. package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
  122. package/dist/__tests__/webhooks.extra.test.js +144 -0
  123. package/dist/__tests__/webhooks.test.d.ts +6 -0
  124. package/dist/__tests__/webhooks.test.js +154 -0
  125. package/dist/features/customRules.js +22 -29
  126. package/dist/features/mcpTrustScore.d.ts +17 -0
  127. package/dist/features/mcpTrustScore.js +74 -0
  128. package/dist/features/mcpValidator.d.ts +2 -0
  129. package/dist/features/mcpValidator.js +13 -0
  130. package/dist/features/policyEnforcement.d.ts +22 -22
  131. package/dist/intelligence/ThreatFeed.js +207 -62
  132. package/dist/remediation/Quarantine.js +24 -6
  133. package/dist/reporters/ConsoleReporter.js +10 -0
  134. package/dist/reporters/HtmlReporter.js +5 -0
  135. package/dist/reporters/SarifReporter.d.ts +1 -0
  136. package/dist/reporters/SarifReporter.js +1 -0
  137. package/dist/scanner/IAnalyzer.d.ts +19 -0
  138. package/dist/scanner/IAnalyzer.js +5 -0
  139. package/dist/scanner/Scanner.js +64 -125
  140. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  141. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  142. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  143. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  144. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  145. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  146. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  147. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  148. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  149. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  150. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  151. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  152. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  153. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  154. package/dist/types.d.ts +17 -0
  155. package/dist/types.js +1 -1
  156. package/dist/utils/safeRegex.d.ts +12 -51
  157. package/dist/utils/safeRegex.js +45 -62
  158. package/dist/utils/schemas.d.ts +64 -64
  159. package/package.json +25 -19
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Additional Webhook Tests
3
+ * Covers sendWebhook with slack/discord/teams includeDetails formatting
4
+ */
5
+ import { sendWebhook } from '../features/webhooks.js';
6
+ function makeFinding(overrides = {}) {
7
+ return {
8
+ ruleId: 'INJ-001',
9
+ ruleName: 'Test Rule',
10
+ severity: 'HIGH',
11
+ category: 'injection',
12
+ file: '/test.md',
13
+ relativePath: 'test.md',
14
+ line: 5,
15
+ match: 'bad content',
16
+ context: [],
17
+ remediation: 'fix it',
18
+ timestamp: new Date(),
19
+ riskScore: 50,
20
+ ...overrides,
21
+ };
22
+ }
23
+ function makeScanResult(findings = [], overrides = {}) {
24
+ return {
25
+ success: true,
26
+ startTime: new Date(),
27
+ endTime: new Date(),
28
+ duration: 1000,
29
+ scannedPaths: ['/project'],
30
+ totalFiles: 5,
31
+ analyzedFiles: 4,
32
+ skippedFiles: 1,
33
+ findings,
34
+ findingsBySeverity: {
35
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
36
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
37
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
38
+ LOW: findings.filter(f => f.severity === 'LOW'),
39
+ INFO: findings.filter(f => f.severity === 'INFO'),
40
+ },
41
+ findingsByCategory: {},
42
+ overallRiskScore: 50,
43
+ summary: {
44
+ critical: findings.filter(f => f.severity === 'CRITICAL').length,
45
+ high: findings.filter(f => f.severity === 'HIGH').length,
46
+ medium: findings.filter(f => f.severity === 'MEDIUM').length,
47
+ low: findings.filter(f => f.severity === 'LOW').length,
48
+ info: 0,
49
+ total: findings.length,
50
+ },
51
+ errors: [],
52
+ ...overrides,
53
+ };
54
+ }
55
+ describe('sendWebhook with includeDetails', () => {
56
+ let originalFetch;
57
+ beforeEach(() => {
58
+ originalFetch = globalThis.fetch;
59
+ globalThis.fetch = jest.fn().mockResolvedValue({
60
+ ok: true,
61
+ status: 200,
62
+ text: () => Promise.resolve(''),
63
+ });
64
+ });
65
+ afterEach(() => {
66
+ globalThis.fetch = originalFetch;
67
+ });
68
+ it('sends slack with includeDetails and CRITICAL findings', async () => {
69
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'CRITICAL' })]), {
70
+ url: 'https://hooks.slack.com/services/xxx',
71
+ type: 'slack',
72
+ includeDetails: true,
73
+ });
74
+ expect(result.success).toBe(true);
75
+ expect(globalThis.fetch).toHaveBeenCalled();
76
+ const body = JSON.parse(globalThis.fetch.mock.calls[0][1]?.body);
77
+ expect(body.attachments).toBeDefined();
78
+ });
79
+ it('sends discord with includeDetails', async () => {
80
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'HIGH' })]), {
81
+ url: 'https://discord.com/api/webhooks/x/y',
82
+ type: 'discord',
83
+ includeDetails: true,
84
+ });
85
+ expect(result.success).toBe(true);
86
+ const body = JSON.parse(globalThis.fetch.mock.calls[0][1]?.body);
87
+ expect(body.embeds).toBeDefined();
88
+ });
89
+ it('sends teams with includeDetails', async () => {
90
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'MEDIUM' })]), {
91
+ url: 'https://org.webhook.office.com/webhook',
92
+ type: 'teams',
93
+ includeDetails: true,
94
+ });
95
+ expect(result.success).toBe(true);
96
+ const body = JSON.parse(globalThis.fetch.mock.calls[0][1]?.body);
97
+ expect(body['@type']).toBe('MessageCard');
98
+ });
99
+ it('sends generic webhook without error', async () => {
100
+ const result = await sendWebhook(makeScanResult([makeFinding()]), {
101
+ url: 'https://custom-webhook.example.com/hook',
102
+ type: 'generic',
103
+ includeDetails: true,
104
+ headers: { 'X-Custom-Token': 'abc123' },
105
+ });
106
+ expect(result.success).toBe(true);
107
+ const [, options] = globalThis.fetch.mock.calls[0];
108
+ expect(options.headers['X-Custom-Token']).toBe('abc123');
109
+ });
110
+ it('sends with medium severity findings triggering yellow color', async () => {
111
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'MEDIUM' })]), {
112
+ url: 'https://hooks.slack.com/services/xxx',
113
+ type: 'slack',
114
+ includeDetails: false,
115
+ });
116
+ expect(result.success).toBe(true);
117
+ });
118
+ it('sends with no findings (green color path)', async () => {
119
+ const result = await sendWebhook(makeScanResult([]), {
120
+ url: 'https://hooks.slack.com/services/xxx',
121
+ type: 'slack',
122
+ includeDetails: false,
123
+ });
124
+ expect(result.success).toBe(true);
125
+ });
126
+ it('respects custom timeout option', async () => {
127
+ const result = await sendWebhook(makeScanResult(), {
128
+ url: 'https://hooks.slack.com/services/xxx',
129
+ type: 'slack',
130
+ timeout: 5000,
131
+ });
132
+ expect(result.success).toBe(true);
133
+ });
134
+ it('skips sending when all findings are below minSeverity', async () => {
135
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'LOW' }), makeFinding({ severity: 'INFO' })]), {
136
+ url: 'https://hooks.slack.com/services/xxx',
137
+ type: 'slack',
138
+ minSeverity: 'HIGH',
139
+ });
140
+ expect(result.success).toBe(true);
141
+ expect(globalThis.fetch).not.toHaveBeenCalled();
142
+ });
143
+ });
144
+ //# sourceMappingURL=webhooks.extra.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Webhooks Tests
3
+ * Tests for detectWebhookType and sendWebhook (mocking fetch).
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=webhooks.test.d.ts.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Webhooks Tests
3
+ * Tests for detectWebhookType and sendWebhook (mocking fetch).
4
+ */
5
+ import { detectWebhookType, sendWebhook, } from '../features/webhooks.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ function makeFinding(overrides = {}) {
10
+ return {
11
+ ruleId: 'INJ-001',
12
+ ruleName: 'Test Rule',
13
+ severity: 'HIGH',
14
+ category: 'injection',
15
+ file: '/test.md',
16
+ relativePath: 'test.md',
17
+ line: 5,
18
+ match: 'bad content',
19
+ context: [],
20
+ remediation: 'fix it',
21
+ timestamp: new Date(),
22
+ riskScore: 50,
23
+ ...overrides,
24
+ };
25
+ }
26
+ function makeScanResult(findings = []) {
27
+ return {
28
+ success: true,
29
+ startTime: new Date(),
30
+ endTime: new Date(),
31
+ duration: 1000,
32
+ scannedPaths: ['/project'],
33
+ totalFiles: 5,
34
+ analyzedFiles: 4,
35
+ skippedFiles: 1,
36
+ findings,
37
+ findingsBySeverity: {
38
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
39
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
40
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
41
+ LOW: findings.filter(f => f.severity === 'LOW'),
42
+ INFO: findings.filter(f => f.severity === 'INFO'),
43
+ },
44
+ findingsByCategory: {},
45
+ overallRiskScore: 50,
46
+ summary: {
47
+ critical: 0,
48
+ high: findings.filter(f => f.severity === 'HIGH').length,
49
+ medium: 0, low: 0, info: 0,
50
+ total: findings.length,
51
+ },
52
+ errors: [],
53
+ };
54
+ }
55
+ function makeWebhookConfig(overrides = {}) {
56
+ return {
57
+ url: 'https://hooks.example.com/webhook',
58
+ type: 'generic',
59
+ ...overrides,
60
+ };
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // detectWebhookType
64
+ // ---------------------------------------------------------------------------
65
+ describe('detectWebhookType', () => {
66
+ it('detects Slack URL', () => {
67
+ expect(detectWebhookType('https://hooks.slack.com/services/xxx')).toBe('slack');
68
+ });
69
+ it('detects Discord URL', () => {
70
+ expect(detectWebhookType('https://discord.com/api/webhooks/123/abc')).toBe('discord');
71
+ });
72
+ it('detects Teams from webhook.office.com', () => {
73
+ expect(detectWebhookType('https://myorg.webhook.office.com/webhookb2/xxx')).toBe('teams');
74
+ });
75
+ it('detects Teams from outlook.office.com', () => {
76
+ expect(detectWebhookType('https://myorg.outlook.office.com/webhook/xxx')).toBe('teams');
77
+ });
78
+ it('returns generic for unknown URLs', () => {
79
+ expect(detectWebhookType('https://my-custom-webhook.example.com/hook')).toBe('generic');
80
+ });
81
+ it('returns generic for empty string', () => {
82
+ expect(detectWebhookType('')).toBe('generic');
83
+ });
84
+ });
85
+ // ---------------------------------------------------------------------------
86
+ // sendWebhook — with mocked fetch
87
+ // ---------------------------------------------------------------------------
88
+ describe('sendWebhook', () => {
89
+ let originalFetch;
90
+ beforeEach(() => {
91
+ originalFetch = globalThis.fetch;
92
+ });
93
+ afterEach(() => {
94
+ globalThis.fetch = originalFetch;
95
+ });
96
+ function mockFetch(status, ok, body = '') {
97
+ globalThis.fetch = jest.fn().mockResolvedValue({
98
+ ok,
99
+ status,
100
+ text: () => Promise.resolve(body),
101
+ });
102
+ }
103
+ it('returns success when fetch succeeds with 200', async () => {
104
+ mockFetch(200, true);
105
+ const result = await sendWebhook(makeScanResult(), makeWebhookConfig());
106
+ expect(result.success).toBe(true);
107
+ expect(result.statusCode).toBe(200);
108
+ });
109
+ it('returns failure when fetch returns non-ok status', async () => {
110
+ mockFetch(500, false, 'Internal Server Error');
111
+ const result = await sendWebhook(makeScanResult(), makeWebhookConfig());
112
+ expect(result.success).toBe(false);
113
+ expect(result.statusCode).toBe(500);
114
+ });
115
+ it('returns failure when fetch throws', async () => {
116
+ globalThis.fetch = jest.fn().mockRejectedValue(new Error('network error'));
117
+ const result = await sendWebhook(makeScanResult(), makeWebhookConfig());
118
+ expect(result.success).toBe(false);
119
+ expect(result.error).toContain('network error');
120
+ });
121
+ it('sends to slack type without error', async () => {
122
+ mockFetch(200, true);
123
+ const result = await sendWebhook(makeScanResult([makeFinding()]), makeWebhookConfig({ type: 'slack', url: 'https://hooks.slack.com/services/xxx' }));
124
+ expect(result.success).toBe(true);
125
+ });
126
+ it('sends to discord type without error', async () => {
127
+ mockFetch(200, true);
128
+ const result = await sendWebhook(makeScanResult([makeFinding()]), makeWebhookConfig({ type: 'discord', url: 'https://discord.com/api/webhooks/x/y' }));
129
+ expect(result.success).toBe(true);
130
+ });
131
+ it('sends to teams type without error', async () => {
132
+ mockFetch(200, true);
133
+ const result = await sendWebhook(makeScanResult([makeFinding()]), makeWebhookConfig({ type: 'teams', url: 'https://org.webhook.office.com/webhook' }));
134
+ expect(result.success).toBe(true);
135
+ });
136
+ it('skips when minSeverity not met and findings exist', async () => {
137
+ mockFetch(200, true);
138
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'LOW' })]), makeWebhookConfig({ minSeverity: 'HIGH' }));
139
+ // Should return success=true without sending
140
+ expect(result.success).toBe(true);
141
+ });
142
+ it('sends when minSeverity is met', async () => {
143
+ mockFetch(200, true);
144
+ const result = await sendWebhook(makeScanResult([makeFinding({ severity: 'CRITICAL' })]), makeWebhookConfig({ minSeverity: 'HIGH' }));
145
+ expect(result.success).toBe(true);
146
+ expect(globalThis.fetch).toHaveBeenCalled();
147
+ });
148
+ it('includes details when includeDetails is true', async () => {
149
+ mockFetch(200, true);
150
+ const result = await sendWebhook(makeScanResult([makeFinding()]), makeWebhookConfig({ includeDetails: true }));
151
+ expect(result.success).toBe(true);
152
+ });
153
+ });
154
+ //# sourceMappingURL=webhooks.test.js.map
@@ -7,6 +7,7 @@ import { createHash } from 'node:crypto';
7
7
  import { resolve, extname } from 'node:path';
8
8
  import { parse as parseYaml } from 'yaml';
9
9
  import { z } from 'zod';
10
+ import { compileSafePattern } from '../utils/safeRegex.js';
10
11
  import logger from '../utils/logger.js';
11
12
  /**
12
13
  * Schema for custom rule definition in YAML/JSON
@@ -139,14 +140,15 @@ function parseCustomRulesContent(content, sourceExt, sourceLabel) {
139
140
  * Convert custom rule definition to Rule object
140
141
  */
141
142
  function definitionToRule(def) {
142
- // Compile regex patterns with error handling
143
+ // Compile regex patterns reject unsafe patterns via compileSafePattern
143
144
  const patterns = [];
144
145
  for (const pattern of def.patterns) {
145
- try {
146
- patterns.push(new RegExp(pattern, 'gi'));
146
+ const compiled = compileSafePattern(pattern, 'gi');
147
+ if (compiled === null) {
148
+ logger.warn(`Unsafe or invalid regex pattern in rule ${def.id} (skipped): ${pattern}`);
147
149
  }
148
- catch {
149
- logger.warn(`Invalid regex pattern in rule ${def.id}: ${pattern}`);
150
+ else {
151
+ patterns.push(compiled);
150
152
  }
151
153
  }
152
154
  if (patterns.length === 0) {
@@ -154,33 +156,27 @@ function definitionToRule(def) {
154
156
  }
155
157
  // Compile exclude patterns
156
158
  const excludePatterns = def.excludePatterns?.map((p) => {
157
- try {
158
- return new RegExp(p, 'gi');
159
- }
160
- catch {
161
- logger.warn(`Invalid exclude pattern in rule ${def.id}: ${p}`);
162
- return null;
159
+ const compiled = compileSafePattern(p, 'gi');
160
+ if (compiled === null) {
161
+ logger.warn(`Unsafe or invalid exclude pattern in rule ${def.id} (skipped): ${p}`);
163
162
  }
163
+ return compiled;
164
164
  }).filter((p) => p !== null);
165
165
  // Compile require context patterns
166
166
  const requireContext = def.requireContext?.map((p) => {
167
- try {
168
- return new RegExp(p, 'gi');
169
- }
170
- catch {
171
- logger.warn(`Invalid requireContext pattern in rule ${def.id}: ${p}`);
172
- return null;
167
+ const compiled = compileSafePattern(p, 'gi');
168
+ if (compiled === null) {
169
+ logger.warn(`Unsafe or invalid requireContext pattern in rule ${def.id} (skipped): ${p}`);
173
170
  }
171
+ return compiled;
174
172
  }).filter((p) => p !== null);
175
173
  // Compile exclude context patterns
176
174
  const excludeContext = def.excludeContext?.map((p) => {
177
- try {
178
- return new RegExp(p, 'gi');
179
- }
180
- catch {
181
- logger.warn(`Invalid excludeContext pattern in rule ${def.id}: ${p}`);
182
- return null;
175
+ const compiled = compileSafePattern(p, 'gi');
176
+ if (compiled === null) {
177
+ logger.warn(`Unsafe or invalid excludeContext pattern in rule ${def.id} (skipped): ${p}`);
183
178
  }
179
+ return compiled;
184
180
  }).filter((p) => p !== null);
185
181
  const rule = {
186
182
  id: def.id,
@@ -458,14 +454,11 @@ export function validateCustomRulesFile(filePath) {
458
454
  warnings,
459
455
  };
460
456
  }
461
- // Validate regex patterns
457
+ // Validate regex patterns — also screen for ReDoS risks
462
458
  for (const rule of result.data.rules) {
463
459
  for (const pattern of rule.patterns) {
464
- try {
465
- void new RegExp(pattern, 'gi');
466
- }
467
- catch {
468
- errors.push(`Rule ${rule.id}: Invalid regex pattern "${pattern}"`);
460
+ if (compileSafePattern(pattern, 'gi') === null) {
461
+ errors.push(`Rule ${rule.id}: Unsafe or invalid regex pattern "${pattern}"`);
469
462
  }
470
463
  }
471
464
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * MCP Server Trust Scoring
3
+ * Evaluates the security posture of an MCP server configuration.
4
+ */
5
+ export interface McpTrustResult {
6
+ score: number;
7
+ trustLevel: 'HIGH' | 'MEDIUM' | 'LOW' | 'CRITICAL';
8
+ flags: string[];
9
+ }
10
+ /**
11
+ * Score an MCP server configuration entry.
12
+ *
13
+ * @param serverConfig - A single MCP server config object (value from `mcpServers` map)
14
+ * @returns Trust score (0-100), trust level, and list of flags
15
+ */
16
+ export declare function scoreMcpServer(serverConfig: unknown): McpTrustResult;
17
+ //# sourceMappingURL=mcpTrustScore.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MCP Server Trust Scoring
3
+ * Evaluates the security posture of an MCP server configuration.
4
+ */
5
+ // Known suspicious package names / name fragments
6
+ const SUSPICIOUS_NAMES = [
7
+ 'shadow', 'stealer', 'exfil', 'beacon', 'c2-', '-c2',
8
+ 'keylog', 'implant', 'dropper', 'exploit',
9
+ ];
10
+ /**
11
+ * Score an MCP server configuration entry.
12
+ *
13
+ * @param serverConfig - A single MCP server config object (value from `mcpServers` map)
14
+ * @returns Trust score (0-100), trust level, and list of flags
15
+ */
16
+ export function scoreMcpServer(serverConfig) {
17
+ const flags = [];
18
+ let score = 100;
19
+ if (typeof serverConfig !== 'object' || serverConfig === null) {
20
+ return { score: 0, trustLevel: 'CRITICAL', flags: ['Invalid config object'] };
21
+ }
22
+ const cfg = serverConfig;
23
+ // Insecure transport
24
+ const transport = cfg['transport'];
25
+ if (transport === 'http' || transport === 'sse') {
26
+ score -= 30;
27
+ flags.push(`Insecure transport: '${transport}' — prefer stdio or wss`);
28
+ }
29
+ // Plain HTTP URL
30
+ const url = typeof cfg['url'] === 'string' ? cfg['url'] : '';
31
+ if (url.startsWith('http://')) {
32
+ score -= 25;
33
+ flags.push('Plain HTTP URL — credentials and tool calls are transmitted in cleartext');
34
+ }
35
+ // Unpinned npx command
36
+ const command = typeof cfg['command'] === 'string' ? cfg['command'] : '';
37
+ if (command === 'npx' || command.endsWith('/npx')) {
38
+ const args = Array.isArray(cfg['args']) ? cfg['args'] : [];
39
+ const firstArg = args[0] ?? '';
40
+ if (firstArg && !firstArg.includes('@') && !firstArg.startsWith('-')) {
41
+ score -= 20;
42
+ flags.push(`Unpinned npx package '${firstArg}' — pin to a specific version to prevent rug pulls`);
43
+ }
44
+ }
45
+ // Dangerous flags in args
46
+ const args = Array.isArray(cfg['args']) ? cfg['args'] : [];
47
+ for (const arg of args) {
48
+ if (typeof arg === 'string' && (arg.includes('--allow-all') || arg.includes('--dangerously-skip'))) {
49
+ score -= 30;
50
+ flags.push(`Dangerous arg '${arg}' — bypasses MCP safety checks`);
51
+ }
52
+ }
53
+ // Suspicious name
54
+ const name = typeof cfg['name'] === 'string' ? cfg['name'].toLowerCase() : '';
55
+ const pkg = args.find(a => typeof a === 'string' && !a.startsWith('-')) ?? '';
56
+ const combined = `${name} ${pkg}`.toLowerCase();
57
+ for (const pattern of SUSPICIOUS_NAMES) {
58
+ if (combined.includes(pattern)) {
59
+ score -= 50;
60
+ flags.push(`Name matches suspicious pattern '${pattern}'`);
61
+ break;
62
+ }
63
+ }
64
+ const clampedScore = Math.max(0, Math.min(100, score));
65
+ return {
66
+ score: clampedScore,
67
+ trustLevel: clampedScore >= 80 ? 'HIGH'
68
+ : clampedScore >= 60 ? 'MEDIUM'
69
+ : clampedScore >= 40 ? 'LOW'
70
+ : 'CRITICAL',
71
+ flags,
72
+ };
73
+ }
74
+ //# sourceMappingURL=mcpTrustScore.js.map
@@ -3,6 +3,7 @@
3
3
  * Validates .mcp.json files for dangerous permissions, untrusted sources, etc.
4
4
  */
5
5
  import type { Finding, Severity } from '../types.js';
6
+ import { type McpTrustResult } from './mcpTrustScore.js';
6
7
  /**
7
8
  * Risk assessment for MCP servers
8
9
  */
@@ -18,6 +19,7 @@ export interface McpRiskAssessment {
18
19
  capabilities: string[];
19
20
  command?: string | undefined;
20
21
  url?: string | undefined;
22
+ trustScore?: McpTrustResult | undefined;
21
23
  }
22
24
  /**
23
25
  * Validate MCP configuration JSON content
@@ -7,6 +7,7 @@
7
7
  import { readFileSync, existsSync } from 'node:fs';
8
8
  import { resolve, basename } from 'node:path';
9
9
  import { z } from 'zod';
10
+ import { scoreMcpServer } from './mcpTrustScore.js';
10
11
  /**
11
12
  * MCP Server configuration schema
12
13
  */
@@ -282,6 +283,18 @@ export function validateMcpConfigContent(content) {
282
283
  for (const [name, config] of Object.entries(servers)) {
283
284
  if (typeof config === 'object' && config !== null) {
284
285
  const assessment = analyzeServer(name, config);
286
+ // Augment with trust score; surface CRITICAL/LOW trust as issues
287
+ assessment.trustScore = scoreMcpServer({ ...config, name });
288
+ if (assessment.trustScore.trustLevel === 'CRITICAL' || assessment.trustScore.trustLevel === 'LOW') {
289
+ for (const flag of assessment.trustScore.flags) {
290
+ assessment.issues.push({
291
+ type: 'trust-score',
292
+ severity: assessment.trustScore.trustLevel === 'CRITICAL' ? 'CRITICAL' : 'HIGH',
293
+ description: `Trust score ${assessment.trustScore.score}/100: ${flag}`,
294
+ remediation: 'Review MCP server configuration and address the flagged concern.',
295
+ });
296
+ }
297
+ }
285
298
  assessments.push(assessment);
286
299
  }
287
300
  }
@@ -24,17 +24,17 @@ declare const PolicyRuleSchema: z.ZodObject<{
24
24
  minRiskScore: z.ZodOptional<z.ZodNumber>;
25
25
  maxFindings: z.ZodOptional<z.ZodNumber>;
26
26
  }, "strip", z.ZodTypeAny, {
27
- categories?: string[] | undefined;
28
27
  ruleIds?: string[] | undefined;
29
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
28
+ categories?: string[] | undefined;
30
29
  filePatterns?: string[] | undefined;
30
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
31
31
  minRiskScore?: number | undefined;
32
32
  maxFindings?: number | undefined;
33
33
  }, {
34
- categories?: string[] | undefined;
35
34
  ruleIds?: string[] | undefined;
36
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
35
+ categories?: string[] | undefined;
37
36
  filePatterns?: string[] | undefined;
37
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
38
38
  minRiskScore?: number | undefined;
39
39
  maxFindings?: number | undefined;
40
40
  }>;
@@ -44,10 +44,10 @@ declare const PolicyRuleSchema: z.ZodObject<{
44
44
  enabled: boolean;
45
45
  action: "warn" | "ignore" | "block";
46
46
  conditions: {
47
- categories?: string[] | undefined;
48
47
  ruleIds?: string[] | undefined;
49
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
48
+ categories?: string[] | undefined;
50
49
  filePatterns?: string[] | undefined;
50
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
51
51
  minRiskScore?: number | undefined;
52
52
  maxFindings?: number | undefined;
53
53
  };
@@ -56,10 +56,10 @@ declare const PolicyRuleSchema: z.ZodObject<{
56
56
  }, {
57
57
  id: string;
58
58
  conditions: {
59
- categories?: string[] | undefined;
60
59
  ruleIds?: string[] | undefined;
61
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
60
+ categories?: string[] | undefined;
62
61
  filePatterns?: string[] | undefined;
62
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
63
63
  minRiskScore?: number | undefined;
64
64
  maxFindings?: number | undefined;
65
65
  };
@@ -88,17 +88,17 @@ declare const PolicyConfigSchema: z.ZodObject<{
88
88
  minRiskScore: z.ZodOptional<z.ZodNumber>;
89
89
  maxFindings: z.ZodOptional<z.ZodNumber>;
90
90
  }, "strip", z.ZodTypeAny, {
91
- categories?: string[] | undefined;
92
91
  ruleIds?: string[] | undefined;
93
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
92
+ categories?: string[] | undefined;
94
93
  filePatterns?: string[] | undefined;
94
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
95
95
  minRiskScore?: number | undefined;
96
96
  maxFindings?: number | undefined;
97
97
  }, {
98
- categories?: string[] | undefined;
99
98
  ruleIds?: string[] | undefined;
100
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
99
+ categories?: string[] | undefined;
101
100
  filePatterns?: string[] | undefined;
101
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
102
102
  minRiskScore?: number | undefined;
103
103
  maxFindings?: number | undefined;
104
104
  }>;
@@ -108,10 +108,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
108
108
  enabled: boolean;
109
109
  action: "warn" | "ignore" | "block";
110
110
  conditions: {
111
- categories?: string[] | undefined;
112
111
  ruleIds?: string[] | undefined;
113
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
112
+ categories?: string[] | undefined;
114
113
  filePatterns?: string[] | undefined;
114
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
115
115
  minRiskScore?: number | undefined;
116
116
  maxFindings?: number | undefined;
117
117
  };
@@ -120,10 +120,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
120
120
  }, {
121
121
  id: string;
122
122
  conditions: {
123
- categories?: string[] | undefined;
124
123
  ruleIds?: string[] | undefined;
125
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
124
+ categories?: string[] | undefined;
126
125
  filePatterns?: string[] | undefined;
126
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
127
127
  minRiskScore?: number | undefined;
128
128
  maxFindings?: number | undefined;
129
129
  };
@@ -182,10 +182,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
182
182
  enabled: boolean;
183
183
  action: "warn" | "ignore" | "block";
184
184
  conditions: {
185
- categories?: string[] | undefined;
186
185
  ruleIds?: string[] | undefined;
187
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
186
+ categories?: string[] | undefined;
188
187
  filePatterns?: string[] | undefined;
188
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
189
189
  minRiskScore?: number | undefined;
190
190
  maxFindings?: number | undefined;
191
191
  };
@@ -198,10 +198,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
198
198
  rules: {
199
199
  id: string;
200
200
  conditions: {
201
- categories?: string[] | undefined;
202
201
  ruleIds?: string[] | undefined;
203
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
202
+ categories?: string[] | undefined;
204
203
  filePatterns?: string[] | undefined;
204
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
205
205
  minRiskScore?: number | undefined;
206
206
  maxFindings?: number | undefined;
207
207
  };
@@ -313,10 +313,10 @@ declare const _default: {
313
313
  enabled: boolean;
314
314
  action: "warn" | "ignore" | "block";
315
315
  conditions: {
316
- categories?: string[] | undefined;
317
316
  ruleIds?: string[] | undefined;
318
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
317
+ categories?: string[] | undefined;
319
318
  filePatterns?: string[] | undefined;
319
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
320
320
  minRiskScore?: number | undefined;
321
321
  maxFindings?: number | undefined;
322
322
  };