ferret-scan 2.1.2 → 2.3.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.
- package/CHANGELOG.md +35 -0
- package/README.md +15 -11
- package/bin/ferret.js +109 -13
- package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
- package/dist/__tests__/AgentMonitor.test.js +235 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
- package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
- package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
- package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
- package/dist/__tests__/IndicatorMatcher.test.js +245 -0
- package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
- package/dist/__tests__/MarketplaceScanner.test.js +212 -0
- package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
- package/dist/__tests__/RuleGenerator.test.js +207 -0
- package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
- package/dist/__tests__/ThreatFeed.test.js +359 -0
- package/dist/__tests__/WatchMode.test.d.ts +6 -0
- package/dist/__tests__/WatchMode.test.js +104 -0
- package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
- package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerFull.test.js +138 -0
- package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
- package/dist/__tests__/atlas.test.d.ts +6 -0
- package/dist/__tests__/atlas.test.js +319 -0
- package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalog.test.js +200 -0
- package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
- package/dist/__tests__/baseline.test.d.ts +6 -0
- package/dist/__tests__/baseline.test.js +321 -0
- package/dist/__tests__/baselineExtra.test.d.ts +6 -0
- package/dist/__tests__/baselineExtra.test.js +317 -0
- package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
- package/dist/__tests__/capabilityMapping.test.js +49 -0
- package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
- package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
- package/dist/__tests__/complianceExtra.test.d.ts +6 -0
- package/dist/__tests__/complianceExtra.test.js +121 -0
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/configLoader.test.d.ts +6 -0
- package/dist/__tests__/configLoader.test.js +225 -0
- package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
- package/dist/__tests__/configLoaderExtra.test.js +186 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
- package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
- package/dist/__tests__/customRules.extra.test.d.ts +6 -0
- package/dist/__tests__/customRules.extra.test.js +245 -0
- package/dist/__tests__/customRules.test.d.ts +7 -0
- package/dist/__tests__/customRules.test.js +347 -0
- package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
- package/dist/__tests__/dependencyRisk.test.js +248 -0
- package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
- package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
- package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
- package/dist/__tests__/featureExitCodes.test.js +332 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
- package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
- package/dist/__tests__/fixer.extra.test.d.ts +6 -0
- package/dist/__tests__/fixer.extra.test.js +135 -0
- package/dist/__tests__/fixerApply.test.d.ts +6 -0
- package/dist/__tests__/fixerApply.test.js +132 -0
- package/dist/__tests__/gitHooks.test.d.ts +7 -0
- package/dist/__tests__/gitHooks.test.js +188 -0
- package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
- package/dist/__tests__/htmlReporter.extra.test.js +126 -0
- package/dist/__tests__/interactiveTui.test.d.ts +6 -0
- package/dist/__tests__/interactiveTui.test.js +180 -0
- package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
- package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiMore.test.js +194 -0
- package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiSession.test.js +173 -0
- package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysis.test.js +229 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
- package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
- package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
- package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
- package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
- package/dist/__tests__/llmGroqTPM.test.js +89 -0
- package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
- package/dist/__tests__/llmProviderRetry.test.js +172 -0
- package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
- package/dist/__tests__/mcpValidator.extra.test.js +270 -0
- package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
- package/dist/__tests__/patternMatcherExtra.test.js +198 -0
- package/dist/__tests__/patternsCommon.test.d.ts +6 -0
- package/dist/__tests__/patternsCommon.test.js +107 -0
- package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
- package/dist/__tests__/policyEnforcement.test.js +510 -0
- package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
- package/dist/__tests__/quarantineExtra.test.js +214 -0
- package/dist/__tests__/redactionExtra.test.d.ts +6 -0
- package/dist/__tests__/redactionExtra.test.js +228 -0
- package/dist/__tests__/scanDiff.test.d.ts +7 -0
- package/dist/__tests__/scanDiff.test.js +266 -0
- package/dist/__tests__/scanFull.test.d.ts +6 -0
- package/dist/__tests__/scanFull.test.js +158 -0
- package/dist/__tests__/scannerDampening.test.d.ts +6 -0
- package/dist/__tests__/scannerDampening.test.js +160 -0
- package/dist/__tests__/scannerExtra.test.d.ts +6 -0
- package/dist/__tests__/scannerExtra.test.js +194 -0
- package/dist/__tests__/scannerMitre.test.d.ts +5 -0
- package/dist/__tests__/scannerMitre.test.js +141 -0
- package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
- package/dist/__tests__/scannerSSRF.test.js +149 -0
- package/dist/__tests__/schemas.test.d.ts +6 -0
- package/dist/__tests__/schemas.test.js +125 -0
- package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
- package/dist/__tests__/webhooks.extra.test.js +144 -0
- package/dist/__tests__/webhooks.test.d.ts +6 -0
- package/dist/__tests__/webhooks.test.js +154 -0
- package/dist/analyzers/AstAnalyzer.d.ts +5 -1
- package/dist/analyzers/AstAnalyzer.js +25 -4
- package/dist/features/customRules.js +22 -29
- package/dist/features/ignoreComments.js +5 -5
- package/dist/features/mcpTrustScore.d.ts +17 -0
- package/dist/features/mcpTrustScore.js +74 -0
- package/dist/features/mcpValidator.d.ts +2 -0
- package/dist/features/mcpValidator.js +13 -0
- package/dist/features/policyEnforcement.d.ts +22 -22
- package/dist/features/policyEnforcement.js +3 -2
- package/dist/intelligence/ThreatFeed.js +207 -62
- package/dist/remediation/Fixer.js +56 -30
- package/dist/remediation/Quarantine.js +79 -11
- package/dist/reporters/ConsoleReporter.js +10 -0
- package/dist/reporters/HtmlReporter.js +5 -0
- package/dist/reporters/SarifReporter.d.ts +1 -0
- package/dist/reporters/SarifReporter.js +1 -0
- package/dist/rules/ai-specific.js +8 -8
- package/dist/rules/backdoors.js +12 -12
- package/dist/rules/correlationRules.js +6 -6
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +10 -1
- package/dist/rules/injection.js +8 -8
- package/dist/rules/patterns/common.d.ts +34 -0
- package/dist/rules/patterns/common.js +48 -0
- package/dist/scanner/IAnalyzer.d.ts +19 -0
- package/dist/scanner/IAnalyzer.js +5 -0
- package/dist/scanner/PatternMatcher.js +19 -2
- package/dist/scanner/Scanner.js +64 -125
- package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
- package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
- package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
- package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +1 -1
- package/dist/utils/baseline.d.ts +15 -2
- package/dist/utils/baseline.js +50 -19
- package/dist/utils/contentCache.d.ts +39 -0
- package/dist/utils/contentCache.js +77 -0
- package/dist/utils/glob.d.ts +50 -0
- package/dist/utils/glob.js +84 -0
- package/dist/utils/pathSecurity.js +1 -0
- package/dist/utils/safeRegex.d.ts +55 -0
- package/dist/utils/safeRegex.js +130 -0
- package/dist/utils/schemas.d.ts +70 -64
- package/dist/utils/schemas.js +13 -0
- package/package.json +34 -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,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
|
|
@@ -6,7 +6,11 @@ import type { SemanticFinding, DiscoveredFile, Rule } from '../types.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Analyze a single file for semantic patterns
|
|
8
8
|
*/
|
|
9
|
-
export declare function analyzeFile(file: DiscoveredFile, content: string, rules: Rule[]
|
|
9
|
+
export declare function analyzeFile(file: DiscoveredFile, content: string, rules: Rule[], opts?: {
|
|
10
|
+
maxMs?: number;
|
|
11
|
+
maxNodes?: number;
|
|
12
|
+
maxBlockMs?: number;
|
|
13
|
+
}): Promise<SemanticFinding[]>;
|
|
10
14
|
/**
|
|
11
15
|
* Check if semantic analysis should be performed
|
|
12
16
|
*/
|
|
@@ -133,11 +133,19 @@ function extractSemanticContext(tsLib, sourceFile) {
|
|
|
133
133
|
return context;
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
|
-
* Find security patterns in AST
|
|
136
|
+
* Find security patterns in AST, with optional time and node-count guards.
|
|
137
137
|
*/
|
|
138
|
-
function findSecurityPatterns(tsLib, sourceFile, patterns) {
|
|
138
|
+
function findSecurityPatterns(tsLib, sourceFile, patterns, opts) {
|
|
139
139
|
const matches = [];
|
|
140
|
+
let nodeCount = 0;
|
|
141
|
+
const deadline = opts?.deadline;
|
|
142
|
+
const maxNodes = opts?.maxNodes ?? 50_000;
|
|
140
143
|
function visit(node) {
|
|
144
|
+
nodeCount++;
|
|
145
|
+
if (nodeCount > maxNodes)
|
|
146
|
+
return;
|
|
147
|
+
if (deadline !== undefined && Date.now() > deadline)
|
|
148
|
+
return;
|
|
141
149
|
for (const pattern of patterns) {
|
|
142
150
|
const match = matchSemanticPattern(tsLib, node, pattern, sourceFile);
|
|
143
151
|
if (match) {
|
|
@@ -277,8 +285,11 @@ function createContextLines(sourceFile, node, contextLines = 3) {
|
|
|
277
285
|
/**
|
|
278
286
|
* Analyze a single file for semantic patterns
|
|
279
287
|
*/
|
|
280
|
-
export async function analyzeFile(file, content, rules) {
|
|
288
|
+
export async function analyzeFile(file, content, rules, opts) {
|
|
281
289
|
const findings = [];
|
|
290
|
+
const maxMs = opts?.maxMs ?? 2000;
|
|
291
|
+
const maxNodes = opts?.maxNodes ?? 50_000;
|
|
292
|
+
const perBlockMs = Math.min(maxMs, opts?.maxBlockMs ?? 500);
|
|
282
293
|
try {
|
|
283
294
|
// Get rules with semantic patterns
|
|
284
295
|
const semanticRules = rules.filter(rule => rule.semanticPatterns && rule.semanticPatterns.length > 0);
|
|
@@ -296,16 +307,26 @@ export async function analyzeFile(file, content, rules) {
|
|
|
296
307
|
// Analyze the entire file for TypeScript/JavaScript files
|
|
297
308
|
codeBlocksToAnalyze = [{ code: content, language: file.type, line: 1 }];
|
|
298
309
|
}
|
|
310
|
+
const fileDeadline = Date.now() + maxMs;
|
|
299
311
|
// Analyze each code block
|
|
300
312
|
for (const codeBlock of codeBlocksToAnalyze) {
|
|
313
|
+
if (Date.now() > fileDeadline) {
|
|
314
|
+
logger.warn(`AST analysis file deadline (${maxMs}ms) reached for ${file.relativePath}; skipping remaining code blocks`);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
301
317
|
try {
|
|
302
318
|
const sourceFile = createAST(tsLib, codeBlock.code, `${file.relativePath}_block_${codeBlock.line}.${codeBlock.language}`);
|
|
303
319
|
const semanticContext = extractSemanticContext(tsLib, sourceFile);
|
|
320
|
+
// Per-block deadline: min of (remaining file budget, per-block cap).
|
|
321
|
+
const blockDeadline = Math.min(fileDeadline, Date.now() + perBlockMs);
|
|
304
322
|
// Check each semantic rule
|
|
305
323
|
for (const rule of semanticRules) {
|
|
306
324
|
if (!rule.semanticPatterns)
|
|
307
325
|
continue;
|
|
308
|
-
const patternMatches = findSecurityPatterns(tsLib, sourceFile, rule.semanticPatterns
|
|
326
|
+
const patternMatches = findSecurityPatterns(tsLib, sourceFile, rule.semanticPatterns, {
|
|
327
|
+
deadline: blockDeadline,
|
|
328
|
+
maxNodes,
|
|
329
|
+
});
|
|
309
330
|
for (const match of patternMatches) {
|
|
310
331
|
const position = getPositionFromNode(match.node, sourceFile);
|
|
311
332
|
const astNodeInfo = createASTNodeInfo(tsLib, match.node, sourceFile);
|
|
@@ -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
|
|
143
|
+
// Compile regex patterns — reject unsafe patterns via compileSafePattern
|
|
143
144
|
const patterns = [];
|
|
144
145
|
for (const pattern of def.patterns) {
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
}
|
|
@@ -8,16 +8,16 @@ import logger from '../utils/logger.js';
|
|
|
8
8
|
*/
|
|
9
9
|
const COMMENT_PATTERNS = {
|
|
10
10
|
default: [
|
|
11
|
-
/\/\/\s*ferret-(ignore|
|
|
12
|
-
/\/\*\s*ferret-(ignore|
|
|
13
|
-
/#\s*ferret-(ignore|
|
|
11
|
+
/\/\/\s*ferret-(ignore-next-line|ignore-line|ignore|disable|enable)(?:\s+([^\n]+))?/gi,
|
|
12
|
+
/\/\*\s*ferret-(ignore-next-line|ignore-line|ignore|disable|enable)(?:\s+([^*]+))?\s*\*\//gi,
|
|
13
|
+
/#\s*ferret-(ignore-next-line|ignore-line|ignore|disable|enable)(?:\s+([^\n]+))?/gi,
|
|
14
14
|
],
|
|
15
15
|
html: [
|
|
16
16
|
// Non-greedy capture so rule ids like "INJ-001" (with hyphens) work correctly.
|
|
17
|
-
/<!--\s*ferret-(ignore|
|
|
17
|
+
/<!--\s*ferret-(ignore-next-line|ignore-line|ignore|disable|enable)(?:\s+(.+?))?\s*-->/gi,
|
|
18
18
|
],
|
|
19
19
|
sql: [
|
|
20
|
-
/--\s*ferret-(ignore|
|
|
20
|
+
/--\s*ferret-(ignore-next-line|ignore-line|ignore|disable|enable)(?:\s+([^\n]+))?/gi,
|
|
21
21
|
],
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
@@ -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
|
}
|