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
package/dist/scanner/Scanner.js
CHANGED
|
@@ -9,18 +9,18 @@ import { discoverFiles } from './FileDiscovery.js';
|
|
|
9
9
|
import { matchRules } from './PatternMatcher.js';
|
|
10
10
|
import { getRulesForScan } from '../rules/index.js';
|
|
11
11
|
import { loadCustomRules, loadCustomRulesSource } from '../features/customRules.js';
|
|
12
|
-
import { analyzeFile as analyzeFileSemantics, shouldAnalyze as shouldAnalyzeSemantics, getMemoryUsage } from '../analyzers/AstAnalyzer.js';
|
|
13
12
|
import { analyzeCorrelations, shouldAnalyzeCorrelations } from '../analyzers/CorrelationAnalyzer.js';
|
|
14
|
-
import { loadThreatDatabase } from '../intelligence/ThreatFeed.js';
|
|
15
|
-
import { matchIndicators, shouldMatchIndicators } from '../intelligence/IndicatorMatcher.js';
|
|
16
|
-
import { analyzeEntropy, entropyFindingsToFindings } from '../features/entropyAnalysis.js';
|
|
17
|
-
import { validateMcpConfigContent, mcpAssessmentsToFindings } from '../features/mcpValidator.js';
|
|
18
|
-
import { analyzeDependencies, dependencyAssessmentsToFindings } from '../features/dependencyRisk.js';
|
|
19
|
-
import { analyzeCapabilitiesContent, capabilityProfileToFindings } from '../features/capabilityMapping.js';
|
|
20
13
|
import { parseIgnoreComments, shouldIgnoreFinding } from '../features/ignoreComments.js';
|
|
21
14
|
import { annotateFindingsWithMitreAtlas, setMitreAtlasTechniqueCatalog } from '../mitre/atlas.js';
|
|
22
15
|
import { loadMitreAtlasTechniqueCatalog } from '../mitre/atlasCatalog.js';
|
|
23
|
-
import { createLlmProvider
|
|
16
|
+
import { createLlmProvider } from '../features/llmAnalysis.js';
|
|
17
|
+
import { EntropyAnalyzer } from './analyzers/EntropyAnalyzer.js';
|
|
18
|
+
import { McpAnalyzer } from './analyzers/McpAnalyzer.js';
|
|
19
|
+
import { DependencyAnalyzer } from './analyzers/DependencyAnalyzer.js';
|
|
20
|
+
import { CapabilityAnalyzer } from './analyzers/CapabilityAnalyzer.js';
|
|
21
|
+
import { LlmAnalyzer } from './analyzers/LlmAnalyzer.js';
|
|
22
|
+
import { SemanticAnalyzer } from './analyzers/SemanticAnalyzer.js';
|
|
23
|
+
import { ThreatIntelAnalyzer } from './analyzers/ThreatIntelAnalyzer.js';
|
|
24
24
|
import logger from '../utils/logger.js';
|
|
25
25
|
import ora from 'ora';
|
|
26
26
|
function looksLikeDocumentationPath(filePath) {
|
|
@@ -242,14 +242,27 @@ async function loadCustomRulesForScan(config) {
|
|
|
242
242
|
}
|
|
243
243
|
return rules.filter(r => r.enabled && config.categories.includes(r.category) && config.severities.includes(r.severity));
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Build the ordered list of analyzers for a scan.
|
|
247
|
+
*/
|
|
248
|
+
function buildAnalyzers(llmProvider, llmRuntime) {
|
|
249
|
+
return [
|
|
250
|
+
new EntropyAnalyzer(),
|
|
251
|
+
new McpAnalyzer(),
|
|
252
|
+
new DependencyAnalyzer(),
|
|
253
|
+
new CapabilityAnalyzer(),
|
|
254
|
+
new LlmAnalyzer(llmProvider, llmRuntime),
|
|
255
|
+
new SemanticAnalyzer(),
|
|
256
|
+
new ThreatIntelAnalyzer(),
|
|
257
|
+
];
|
|
258
|
+
}
|
|
245
259
|
/**
|
|
246
260
|
* Scan a single file
|
|
247
261
|
*/
|
|
248
|
-
async function scanFile(file, config, rules,
|
|
262
|
+
async function scanFile(file, config, rules, analyzers) {
|
|
249
263
|
try {
|
|
250
264
|
const content = await readFile(file.path, 'utf-8');
|
|
251
265
|
const allFindings = [];
|
|
252
|
-
const fileErrors = [];
|
|
253
266
|
let ignoreState;
|
|
254
267
|
if (config.ignoreComments && content.includes('ferret-')) {
|
|
255
268
|
const parsed = parseIgnoreComments(content, file.type);
|
|
@@ -262,124 +275,20 @@ async function scanFile(file, config, rules, llmProvider, llmRuntime) {
|
|
|
262
275
|
contextLines: config.contextLines,
|
|
263
276
|
});
|
|
264
277
|
allFindings.push(...patternFindings);
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
allFindings.push(...converted);
|
|
271
|
-
}
|
|
272
|
-
catch (entropyError) {
|
|
273
|
-
const entropyMessage = entropyError instanceof Error ? entropyError.message : String(entropyError);
|
|
274
|
-
logger.warn(`Entropy analysis error for ${file.relativePath}: ${entropyMessage}`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
// MCP configuration validation if enabled
|
|
278
|
-
if (config.mcpValidation && file.component === 'mcp' && file.type === 'json') {
|
|
279
|
-
const mcpResult = validateMcpConfigContent(content);
|
|
280
|
-
if (mcpResult.valid && mcpResult.assessments.length > 0) {
|
|
281
|
-
const mcpFindings = mcpAssessmentsToFindings(mcpResult.assessments, file.path);
|
|
282
|
-
// Normalize relative path (feature module uses basename)
|
|
283
|
-
for (const f of mcpFindings) {
|
|
284
|
-
f.relativePath = file.relativePath;
|
|
285
|
-
}
|
|
286
|
-
allFindings.push(...mcpFindings);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Dependency analysis if enabled
|
|
290
|
-
if (config.dependencyAnalysis && basename(file.path).toLowerCase() === 'package.json') {
|
|
291
|
-
try {
|
|
292
|
-
const depResult = analyzeDependencies(file.path, config.dependencyAudit);
|
|
293
|
-
const depFindings = dependencyAssessmentsToFindings(depResult);
|
|
294
|
-
for (const f of depFindings) {
|
|
295
|
-
f.relativePath = file.relativePath;
|
|
296
|
-
}
|
|
297
|
-
allFindings.push(...depFindings);
|
|
298
|
-
}
|
|
299
|
-
catch (depError) {
|
|
300
|
-
const depMessage = depError instanceof Error ? depError.message : String(depError);
|
|
301
|
-
logger.warn(`Dependency analysis error for ${file.relativePath}: ${depMessage}`);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// Capability mapping if enabled (best-effort, JSON-only)
|
|
305
|
-
if (config.capabilityMapping && file.type === 'json') {
|
|
306
|
-
try {
|
|
307
|
-
const profile = analyzeCapabilitiesContent(file.path, content);
|
|
308
|
-
if (profile) {
|
|
309
|
-
const capFindings = capabilityProfileToFindings(profile);
|
|
310
|
-
for (const f of capFindings) {
|
|
311
|
-
f.relativePath = file.relativePath;
|
|
312
|
-
}
|
|
313
|
-
allFindings.push(...capFindings);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
catch (capError) {
|
|
317
|
-
const capMessage = capError instanceof Error ? capError.message : String(capError);
|
|
318
|
-
logger.warn(`Capability mapping error for ${file.relativePath}: ${capMessage}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// LLM-assisted analysis (optional; networked)
|
|
322
|
-
if (config.llmAnalysis && llmProvider && !llmRuntime.disabled && llmRuntime.analyzed < config.llm.maxFiles) {
|
|
323
|
-
const llmResult = await analyzeWithLlm(llmProvider, config.llm, file, content, allFindings);
|
|
324
|
-
if (llmResult.ran) {
|
|
325
|
-
llmRuntime.analyzed += 1;
|
|
326
|
-
}
|
|
327
|
-
allFindings.push(...llmResult.findings);
|
|
328
|
-
if (llmResult.error) {
|
|
329
|
-
fileErrors.push(`LLM analysis: ${llmResult.error}`);
|
|
330
|
-
if (!llmRuntime.disabled && /\bHTTP 429\b/i.test(llmResult.error)) {
|
|
331
|
-
llmRuntime.disabled = true;
|
|
332
|
-
llmRuntime.disabledReason = 'rate limited (HTTP 429)';
|
|
333
|
-
logger.warn('LLM disabled for remainder of scan due to rate limiting (HTTP 429)');
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// Semantic analysis if enabled and applicable
|
|
338
|
-
if (config.semanticAnalysis && shouldAnalyzeSemantics(file, config)) {
|
|
339
|
-
// Monitor memory usage
|
|
340
|
-
const memBefore = getMemoryUsage();
|
|
341
|
-
if (memBefore.used > 1000) { // More than 1GB used
|
|
342
|
-
logger.warn(`High memory usage (${memBefore.used}MB) - skipping semantic analysis for ${file.relativePath}`);
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
try {
|
|
346
|
-
logger.debug(`Running semantic analysis on ${file.relativePath}`);
|
|
347
|
-
const semanticFindings = await analyzeFileSemantics(file, content, rules);
|
|
348
|
-
// Convert SemanticFinding to Finding for compatibility
|
|
349
|
-
allFindings.push(...semanticFindings);
|
|
350
|
-
const memAfter = getMemoryUsage();
|
|
351
|
-
logger.debug(`Semantic analysis memory: ${memAfter.used - memBefore.used}MB delta`);
|
|
352
|
-
}
|
|
353
|
-
catch (semanticError) {
|
|
354
|
-
const semanticMessage = semanticError instanceof Error ? semanticError.message : String(semanticError);
|
|
355
|
-
logger.warn(`Semantic analysis error for ${file.relativePath}: ${semanticMessage}`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
// Threat intelligence matching if enabled
|
|
360
|
-
if (config.threatIntel && shouldMatchIndicators(file, config)) {
|
|
278
|
+
// Run each analyzer in order via the registry
|
|
279
|
+
const ctx = { file, content, config, rules, existingFindings: allFindings };
|
|
280
|
+
for (const analyzer of analyzers) {
|
|
281
|
+
if (!analyzer.shouldRun(ctx))
|
|
282
|
+
continue;
|
|
361
283
|
try {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
minConfidence: 50,
|
|
366
|
-
enablePatternMatching: true,
|
|
367
|
-
maxMatchesPerFile: 50
|
|
368
|
-
});
|
|
369
|
-
allFindings.push(...threatFindings);
|
|
370
|
-
logger.debug(`Found ${threatFindings.length} threat intelligence matches`);
|
|
284
|
+
const found = await analyzer.analyze(ctx);
|
|
285
|
+
allFindings.push(...found);
|
|
286
|
+
ctx.existingFindings = allFindings;
|
|
371
287
|
}
|
|
372
|
-
catch (
|
|
373
|
-
|
|
374
|
-
logger.warn(`Threat intelligence error for ${file.relativePath}: ${threatMessage}`);
|
|
288
|
+
catch (err) {
|
|
289
|
+
logger.warn(`${analyzer.name} error for ${file.relativePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
375
290
|
}
|
|
376
291
|
}
|
|
377
|
-
if (fileErrors.length > 0 && ignoreState) {
|
|
378
|
-
return { findings: allFindings, errors: fileErrors, ignoreState };
|
|
379
|
-
}
|
|
380
|
-
if (fileErrors.length > 0) {
|
|
381
|
-
return { findings: allFindings, errors: fileErrors };
|
|
382
|
-
}
|
|
383
292
|
if (ignoreState) {
|
|
384
293
|
return { findings: allFindings, ignoreState };
|
|
385
294
|
}
|
|
@@ -406,6 +315,30 @@ function isLocalEndpoint(urlStr) {
|
|
|
406
315
|
return false;
|
|
407
316
|
}
|
|
408
317
|
}
|
|
318
|
+
function buildMcpTrustSummary(trustFindings) {
|
|
319
|
+
const summary = { total: 0, high: 0, medium: 0, low: 0, critical: 0, lowestScore: 100 };
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
for (const f of trustFindings) {
|
|
322
|
+
const server = String(f.metadata?.['serverName'] ?? f.file);
|
|
323
|
+
if (seen.has(server))
|
|
324
|
+
continue;
|
|
325
|
+
seen.add(server);
|
|
326
|
+
summary.total++;
|
|
327
|
+
const score = typeof f.metadata?.['trustScore'] === 'number'
|
|
328
|
+
? f.metadata['trustScore']
|
|
329
|
+
: (f.severity === 'CRITICAL' ? 20 : 45);
|
|
330
|
+
summary.lowestScore = Math.min(summary.lowestScore, score);
|
|
331
|
+
if (score >= 80)
|
|
332
|
+
summary.high++;
|
|
333
|
+
else if (score >= 60)
|
|
334
|
+
summary.medium++;
|
|
335
|
+
else if (score >= 40)
|
|
336
|
+
summary.low++;
|
|
337
|
+
else
|
|
338
|
+
summary.critical++;
|
|
339
|
+
}
|
|
340
|
+
return summary;
|
|
341
|
+
}
|
|
409
342
|
/**
|
|
410
343
|
* Main scan function
|
|
411
344
|
*/
|
|
@@ -459,6 +392,8 @@ export async function scan(config) {
|
|
|
459
392
|
'Review privacy/compliance requirements before enabling this feature.');
|
|
460
393
|
}
|
|
461
394
|
}
|
|
395
|
+
// Build analyzer registry (uses llmProvider + llmRuntime by reference)
|
|
396
|
+
const analyzers = buildAnalyzers(llmProvider, llmRuntime);
|
|
462
397
|
// Discover files with spinner
|
|
463
398
|
let spinner = null;
|
|
464
399
|
if (showProgress) {
|
|
@@ -520,7 +455,7 @@ export async function scan(config) {
|
|
|
520
455
|
lastYield = Date.now();
|
|
521
456
|
}
|
|
522
457
|
}
|
|
523
|
-
const result = await scanFile(file, config, rulesForScan,
|
|
458
|
+
const result = await scanFile(file, config, rulesForScan, analyzers);
|
|
524
459
|
if (result.errors && result.errors.length > 0) {
|
|
525
460
|
for (const err of result.errors) {
|
|
526
461
|
errors.push({
|
|
@@ -588,6 +523,9 @@ export async function scan(config) {
|
|
|
588
523
|
const sortedFindings = sortFindings(filteredFindings);
|
|
589
524
|
const endTime = new Date();
|
|
590
525
|
const duration = endTime.getTime() - startTime.getTime();
|
|
526
|
+
// Build MCP trust summary from trust-score findings emitted by mcpValidator
|
|
527
|
+
const mcpTrustFindings = sortedFindings.filter(f => f.metadata?.['issueType'] === 'trust-score');
|
|
528
|
+
const mcpTrustSummary = mcpTrustFindings.length > 0 ? buildMcpTrustSummary(mcpTrustFindings) : undefined;
|
|
591
529
|
const result = {
|
|
592
530
|
success: true,
|
|
593
531
|
startTime,
|
|
@@ -604,6 +542,7 @@ export async function scan(config) {
|
|
|
604
542
|
summary: calculateSummary(sortedFindings),
|
|
605
543
|
errors,
|
|
606
544
|
ignoredFindings,
|
|
545
|
+
...(mcpTrustSummary !== undefined ? { mcpTrustSummary } : {}),
|
|
607
546
|
};
|
|
608
547
|
logger.info(`Scan complete: ${result.summary.total} findings in ${result.analyzedFiles} files (${duration}ms)`);
|
|
609
548
|
return result;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
export declare class CapabilityAnalyzer implements IAnalyzer {
|
|
4
|
+
readonly name = "CapabilityAnalyzer";
|
|
5
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
6
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=CapabilityAnalyzer.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { analyzeCapabilitiesContent, capabilityProfileToFindings } from '../../features/capabilityMapping.js';
|
|
2
|
+
export class CapabilityAnalyzer {
|
|
3
|
+
name = 'CapabilityAnalyzer';
|
|
4
|
+
shouldRun(ctx) {
|
|
5
|
+
return ctx.config.capabilityMapping && ctx.file.type === 'json';
|
|
6
|
+
}
|
|
7
|
+
async analyze(ctx) {
|
|
8
|
+
const profile = analyzeCapabilitiesContent(ctx.file.path, ctx.content);
|
|
9
|
+
if (!profile) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const capFindings = capabilityProfileToFindings(profile);
|
|
13
|
+
for (const f of capFindings) {
|
|
14
|
+
f.relativePath = ctx.file.relativePath;
|
|
15
|
+
}
|
|
16
|
+
return capFindings;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=CapabilityAnalyzer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
export declare class DependencyAnalyzer implements IAnalyzer {
|
|
4
|
+
readonly name = "DependencyAnalyzer";
|
|
5
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
6
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=DependencyAnalyzer.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { analyzeDependencies, dependencyAssessmentsToFindings } from '../../features/dependencyRisk.js';
|
|
3
|
+
export class DependencyAnalyzer {
|
|
4
|
+
name = 'DependencyAnalyzer';
|
|
5
|
+
shouldRun(ctx) {
|
|
6
|
+
return (ctx.config.dependencyAnalysis &&
|
|
7
|
+
basename(ctx.file.path).toLowerCase() === 'package.json');
|
|
8
|
+
}
|
|
9
|
+
async analyze(ctx) {
|
|
10
|
+
const depResult = analyzeDependencies(ctx.file.path, ctx.config.dependencyAudit);
|
|
11
|
+
const depFindings = dependencyAssessmentsToFindings(depResult);
|
|
12
|
+
for (const f of depFindings) {
|
|
13
|
+
f.relativePath = ctx.file.relativePath;
|
|
14
|
+
}
|
|
15
|
+
return depFindings;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=DependencyAnalyzer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
export declare class EntropyAnalyzer implements IAnalyzer {
|
|
4
|
+
readonly name = "EntropyAnalyzer";
|
|
5
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
6
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=EntropyAnalyzer.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { analyzeEntropy, entropyFindingsToFindings } from '../../features/entropyAnalysis.js';
|
|
2
|
+
export class EntropyAnalyzer {
|
|
3
|
+
name = 'EntropyAnalyzer';
|
|
4
|
+
shouldRun(ctx) {
|
|
5
|
+
return ctx.config.entropyAnalysis;
|
|
6
|
+
}
|
|
7
|
+
async analyze(ctx) {
|
|
8
|
+
const entropyFindings = analyzeEntropy(ctx.content, ctx.file);
|
|
9
|
+
return entropyFindingsToFindings(entropyFindings, ctx.file, ctx.content);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=EntropyAnalyzer.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
import { type LlmProvider } from '../../features/llmAnalysis.js';
|
|
4
|
+
export interface LlmRuntime {
|
|
5
|
+
analyzed: number;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
disabledReason?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class LlmAnalyzer implements IAnalyzer {
|
|
10
|
+
private readonly llmProvider;
|
|
11
|
+
private readonly llmRuntime;
|
|
12
|
+
readonly name = "LlmAnalyzer";
|
|
13
|
+
constructor(llmProvider: LlmProvider | null, llmRuntime: LlmRuntime);
|
|
14
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
15
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=LlmAnalyzer.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { analyzeWithLlm } from '../../features/llmAnalysis.js';
|
|
2
|
+
import logger from '../../utils/logger.js';
|
|
3
|
+
export class LlmAnalyzer {
|
|
4
|
+
llmProvider;
|
|
5
|
+
llmRuntime;
|
|
6
|
+
name = 'LlmAnalyzer';
|
|
7
|
+
constructor(llmProvider, llmRuntime) {
|
|
8
|
+
this.llmProvider = llmProvider;
|
|
9
|
+
this.llmRuntime = llmRuntime;
|
|
10
|
+
}
|
|
11
|
+
shouldRun(ctx) {
|
|
12
|
+
return (ctx.config.llmAnalysis &&
|
|
13
|
+
this.llmProvider !== null &&
|
|
14
|
+
!this.llmRuntime.disabled &&
|
|
15
|
+
this.llmRuntime.analyzed < ctx.config.llm.maxFiles);
|
|
16
|
+
}
|
|
17
|
+
async analyze(ctx) {
|
|
18
|
+
// shouldRun already guards llmProvider non-null, but TypeScript needs the cast
|
|
19
|
+
const provider = this.llmProvider;
|
|
20
|
+
const llmResult = await analyzeWithLlm(provider, ctx.config.llm, ctx.file, ctx.content, ctx.existingFindings);
|
|
21
|
+
if (llmResult.ran) {
|
|
22
|
+
this.llmRuntime.analyzed += 1;
|
|
23
|
+
}
|
|
24
|
+
if (llmResult.error) {
|
|
25
|
+
if (!this.llmRuntime.disabled && /\bHTTP 429\b/i.test(llmResult.error)) {
|
|
26
|
+
this.llmRuntime.disabled = true;
|
|
27
|
+
this.llmRuntime.disabledReason = 'rate limited (HTTP 429)';
|
|
28
|
+
logger.warn('LLM disabled for remainder of scan due to rate limiting (HTTP 429)');
|
|
29
|
+
}
|
|
30
|
+
// Re-throw so the caller's catch block records it as a file error
|
|
31
|
+
throw new Error(`LLM analysis: ${llmResult.error}`);
|
|
32
|
+
}
|
|
33
|
+
return llmResult.findings;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=LlmAnalyzer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
export declare class McpAnalyzer implements IAnalyzer {
|
|
4
|
+
readonly name = "McpAnalyzer";
|
|
5
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
6
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=McpAnalyzer.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { validateMcpConfigContent, mcpAssessmentsToFindings } from '../../features/mcpValidator.js';
|
|
2
|
+
export class McpAnalyzer {
|
|
3
|
+
name = 'McpAnalyzer';
|
|
4
|
+
shouldRun(ctx) {
|
|
5
|
+
return ctx.config.mcpValidation && ctx.file.component === 'mcp' && ctx.file.type === 'json';
|
|
6
|
+
}
|
|
7
|
+
async analyze(ctx) {
|
|
8
|
+
const mcpResult = validateMcpConfigContent(ctx.content);
|
|
9
|
+
if (!mcpResult.valid || mcpResult.assessments.length === 0) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const mcpFindings = mcpAssessmentsToFindings(mcpResult.assessments, ctx.file.path);
|
|
13
|
+
for (const f of mcpFindings) {
|
|
14
|
+
f.relativePath = ctx.file.relativePath;
|
|
15
|
+
}
|
|
16
|
+
return mcpFindings;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=McpAnalyzer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
export declare class SemanticAnalyzer implements IAnalyzer {
|
|
4
|
+
readonly name = "SemanticAnalyzer";
|
|
5
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
6
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=SemanticAnalyzer.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { analyzeFile as analyzeFileSemantics, shouldAnalyze as shouldAnalyzeSemantics, getMemoryUsage, } from '../../analyzers/AstAnalyzer.js';
|
|
2
|
+
import logger from '../../utils/logger.js';
|
|
3
|
+
export class SemanticAnalyzer {
|
|
4
|
+
name = 'SemanticAnalyzer';
|
|
5
|
+
shouldRun(ctx) {
|
|
6
|
+
return ctx.config.semanticAnalysis && shouldAnalyzeSemantics(ctx.file, ctx.config);
|
|
7
|
+
}
|
|
8
|
+
async analyze(ctx) {
|
|
9
|
+
const memBefore = getMemoryUsage();
|
|
10
|
+
if (memBefore.used > 1000) {
|
|
11
|
+
logger.warn(`High memory usage (${memBefore.used}MB) - skipping semantic analysis for ${ctx.file.relativePath}`);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
logger.debug(`Running semantic analysis on ${ctx.file.relativePath}`);
|
|
15
|
+
const semanticFindings = await analyzeFileSemantics(ctx.file, ctx.content, ctx.rules);
|
|
16
|
+
const memAfter = getMemoryUsage();
|
|
17
|
+
logger.debug(`Semantic analysis memory: ${memAfter.used - memBefore.used}MB delta`);
|
|
18
|
+
return semanticFindings;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=SemanticAnalyzer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '../../types.js';
|
|
2
|
+
import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
|
|
3
|
+
export declare class ThreatIntelAnalyzer implements IAnalyzer {
|
|
4
|
+
readonly name = "ThreatIntelAnalyzer";
|
|
5
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
6
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=ThreatIntelAnalyzer.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadThreatDatabase } from '../../intelligence/ThreatFeed.js';
|
|
2
|
+
import { matchIndicators, shouldMatchIndicators } from '../../intelligence/IndicatorMatcher.js';
|
|
3
|
+
import logger from '../../utils/logger.js';
|
|
4
|
+
export class ThreatIntelAnalyzer {
|
|
5
|
+
name = 'ThreatIntelAnalyzer';
|
|
6
|
+
shouldRun(ctx) {
|
|
7
|
+
return ctx.config.threatIntel && shouldMatchIndicators(ctx.file, ctx.config);
|
|
8
|
+
}
|
|
9
|
+
async analyze(ctx) {
|
|
10
|
+
const threatDB = loadThreatDatabase();
|
|
11
|
+
logger.debug(`Running threat intelligence matching on ${ctx.file.relativePath}`);
|
|
12
|
+
const threatFindings = matchIndicators(threatDB, ctx.file, ctx.content, {
|
|
13
|
+
minConfidence: 50,
|
|
14
|
+
enablePatternMatching: true,
|
|
15
|
+
maxMatchesPerFile: 50,
|
|
16
|
+
});
|
|
17
|
+
logger.debug(`Found ${threatFindings.length} threat intelligence matches`);
|
|
18
|
+
return threatFindings;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=ThreatIntelAnalyzer.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -217,6 +217,23 @@ export interface ScanResult {
|
|
|
217
217
|
errors: ScanError[];
|
|
218
218
|
/** Findings that were suppressed via inline ignore directives */
|
|
219
219
|
ignoredFindings?: number;
|
|
220
|
+
/** Aggregate MCP server trust score summary (present when MCP configs were scanned) */
|
|
221
|
+
mcpTrustSummary?: McpTrustSummary;
|
|
222
|
+
}
|
|
223
|
+
/** Aggregated MCP server trust scores across a scan */
|
|
224
|
+
export interface McpTrustSummary {
|
|
225
|
+
/** Total number of MCP servers evaluated */
|
|
226
|
+
total: number;
|
|
227
|
+
/** Servers with HIGH trust (score 80–100) */
|
|
228
|
+
high: number;
|
|
229
|
+
/** Servers with MEDIUM trust (score 60–79) */
|
|
230
|
+
medium: number;
|
|
231
|
+
/** Servers with LOW trust (score 40–59) */
|
|
232
|
+
low: number;
|
|
233
|
+
/** Servers with CRITICAL trust (score 0–39) */
|
|
234
|
+
critical: number;
|
|
235
|
+
/** Lowest individual trust score seen */
|
|
236
|
+
lowestScore: number;
|
|
220
237
|
}
|
|
221
238
|
/** Summary statistics for a scan */
|
|
222
239
|
export interface ScanSummary {
|
|
@@ -304,6 +321,12 @@ export interface ScannerConfig {
|
|
|
304
321
|
verbose: boolean;
|
|
305
322
|
/** CI mode (simplified output) */
|
|
306
323
|
ci: boolean;
|
|
324
|
+
/** Maximum wall-clock ms for semantic AST analysis of a single code block (default: 2000) */
|
|
325
|
+
maxSemanticAnalysisMs?: number;
|
|
326
|
+
/** Maximum AST node count before aborting semantic analysis of a single code block (default: 50000) */
|
|
327
|
+
maxAstNodes?: number;
|
|
328
|
+
/** Per-code-block deadline in ms within the file-scoped budget (default: 500) */
|
|
329
|
+
maxBlockMs?: number;
|
|
307
330
|
}
|
|
308
331
|
/** Supported output formats */
|
|
309
332
|
export type OutputFormat = 'console' | 'json' | 'sarif' | 'html' | 'csv' | 'atlas';
|
package/dist/types.js
CHANGED
package/dist/utils/baseline.d.ts
CHANGED
|
@@ -14,21 +14,34 @@ export interface BaselineFinding {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
expiresDate?: string;
|
|
16
16
|
}
|
|
17
|
+
export interface BaselineIntegrity {
|
|
18
|
+
algorithm: 'sha256';
|
|
19
|
+
hash: string;
|
|
20
|
+
}
|
|
17
21
|
export interface Baseline {
|
|
18
22
|
version: string;
|
|
19
23
|
createdDate: string;
|
|
20
24
|
lastUpdated: string;
|
|
21
25
|
description?: string;
|
|
22
26
|
findings: BaselineFinding[];
|
|
27
|
+
integrity?: BaselineIntegrity;
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute integrity hash of a baseline (excluding the integrity field itself)
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeBaselineIntegrity(baseline: Omit<Baseline, 'integrity'>): BaselineIntegrity;
|
|
33
|
+
/**
|
|
34
|
+
* Verify that a loaded baseline has not been tampered with
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifyBaselineIntegrity(baseline: Baseline): boolean;
|
|
24
37
|
/**
|
|
25
38
|
* Load baseline from file
|
|
26
39
|
*/
|
|
27
|
-
export declare function loadBaseline(baselinePath: string): Baseline | null
|
|
40
|
+
export declare function loadBaseline(baselinePath: string): Promise<Baseline | null>;
|
|
28
41
|
/**
|
|
29
42
|
* Save baseline to file
|
|
30
43
|
*/
|
|
31
|
-
export declare function saveBaseline(baseline: Baseline, baselinePath: string): void
|
|
44
|
+
export declare function saveBaseline(baseline: Baseline, baselinePath: string): Promise<void>;
|
|
32
45
|
/**
|
|
33
46
|
* Create a new baseline from scan results
|
|
34
47
|
*/
|