agent-security-scanner-mcp 3.3.0 → 3.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.
@@ -2,6 +2,9 @@
2
2
  import { z } from "zod";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { detectLanguage, runAnalyzer, generateFix } from '../utils.js';
5
+ import { deduplicateFindings } from '../dedup.js';
6
+ import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from '../context.js';
7
+ import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
5
8
 
6
9
  export const fixSecuritySchema = {
7
10
  file_path: z.string().describe("Path to the file to fix"),
@@ -47,24 +50,48 @@ export async function fixSecurity({ file_path, verbosity }) {
47
50
  };
48
51
  }
49
52
 
50
- const issues = runAnalyzer(file_path);
53
+ // Load project configuration
54
+ const config = loadConfig(file_path);
51
55
 
52
- if (issues.error || !Array.isArray(issues) || issues.length === 0) {
56
+ // Check file exclusion
57
+ if (shouldExcludeFile(file_path, config)) {
58
+ return {
59
+ content: [{ type: "text", text: JSON.stringify({ file: file_path, message: "File excluded by configuration", fixes_applied: 0 }) }]
60
+ };
61
+ }
62
+
63
+ const rawIssues = runAnalyzer(file_path);
64
+
65
+ if (rawIssues.error || !Array.isArray(rawIssues) || rawIssues.length === 0) {
53
66
  return {
54
67
  content: [{
55
68
  type: "text",
56
69
  text: JSON.stringify({
57
- message: issues.error ? "Error scanning file" : "No security issues found",
58
- details: issues
70
+ message: rawIssues.error ? "Error scanning file" : "No security issues found",
71
+ details: rawIssues
59
72
  })
60
73
  }]
61
74
  };
62
75
  }
63
76
 
77
+ // Cross-engine deduplication
78
+ const dedupedIssues = deduplicateFindings(rawIssues);
79
+
64
80
  // Read and fix the file
65
81
  const content = readFileSync(file_path, 'utf-8');
66
82
  const lines = content.split('\n');
67
83
  const language = detectLanguage(file_path);
84
+
85
+ // Context-aware filtering (suppress known module imports)
86
+ const contextFiltered = applyContextFilter(dedupedIssues, file_path, language);
87
+
88
+ // Framework-aware severity adjustment
89
+ const frameworks = detectFrameworks(file_path, language);
90
+ const frameworkAdjusted = applyFrameworkAdjustments(contextFiltered, frameworks);
91
+
92
+ // Apply .scannerrc configuration (rule suppression, severity/confidence thresholds)
93
+ const issues = applyConfig(frameworkAdjusted, file_path, config);
94
+
68
95
  const fixes = [];
69
96
 
70
97
  // Apply fixes (process in reverse order to preserve line numbers)
@@ -0,0 +1,151 @@
1
+ // src/tools/scan-diff.js
2
+ import { z } from "zod";
3
+ import { execFileSync } from "child_process";
4
+ import { existsSync } from "fs";
5
+ import { scanSecurity } from './scan-security.js';
6
+
7
+ export const scanDiffSchema = {
8
+ base_ref: z.string().optional().describe("Base git ref (default: HEAD~1)"),
9
+ target_ref: z.string().optional().describe("Target git ref (default: HEAD)"),
10
+ verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level")
11
+ };
12
+
13
+ // Parse unified diff output to extract changed files and line ranges
14
+ function parseDiffOutput(diffOutput) {
15
+ const changes = new Map(); // filePath -> Set<lineNumber>
16
+ let currentFile = null;
17
+
18
+ for (const line of diffOutput.split('\n')) {
19
+ // Match diff header: +++ b/path/to/file
20
+ const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
21
+ if (fileMatch) {
22
+ currentFile = fileMatch[1];
23
+ if (!changes.has(currentFile)) {
24
+ changes.set(currentFile, new Set());
25
+ }
26
+ continue;
27
+ }
28
+
29
+ // Match hunk header: @@ -old,count +new,count @@
30
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
31
+ if (hunkMatch && currentFile) {
32
+ const start = parseInt(hunkMatch[1], 10);
33
+ const count = parseInt(hunkMatch[2] || '1', 10);
34
+ const fileChanges = changes.get(currentFile);
35
+ for (let i = start; i < start + count; i++) {
36
+ fileChanges.add(i);
37
+ }
38
+ }
39
+ }
40
+
41
+ return changes;
42
+ }
43
+
44
+ export async function scanDiff({ base_ref, target_ref, verbosity }) {
45
+ const base = base_ref || 'HEAD~1';
46
+ const target = target_ref || 'HEAD';
47
+
48
+ // Get diff output
49
+ let diffOutput;
50
+ try {
51
+ diffOutput = execFileSync('git', ['diff', '--unified=0', `${base}...${target}`], {
52
+ encoding: 'utf-8',
53
+ timeout: 30000,
54
+ maxBuffer: 10 * 1024 * 1024
55
+ });
56
+ } catch (err) {
57
+ // Try without three-dot notation (for uncommitted changes)
58
+ try {
59
+ diffOutput = execFileSync('git', ['diff', '--unified=0', base, target], {
60
+ encoding: 'utf-8',
61
+ timeout: 30000,
62
+ maxBuffer: 10 * 1024 * 1024
63
+ });
64
+ } catch (err2) {
65
+ return {
66
+ content: [{ type: "text", text: JSON.stringify({ error: `Git diff failed: ${err2.message}` }) }]
67
+ };
68
+ }
69
+ }
70
+
71
+ if (!diffOutput.trim()) {
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify({ message: "No changes found between refs", base, target, issues_count: 0 }) }]
74
+ };
75
+ }
76
+
77
+ // Parse diff to get changed files and lines
78
+ const changes = parseDiffOutput(diffOutput);
79
+ const allIssues = [];
80
+ const scannedFiles = [];
81
+
82
+ // Scan each changed file
83
+ for (const [filePath, changedLines] of changes) {
84
+ if (!existsSync(filePath)) continue;
85
+
86
+ const result = await scanSecurity({ file_path: filePath, verbosity: 'full' });
87
+ const parsed = JSON.parse(result.content[0].text);
88
+
89
+ if (parsed.issues && Array.isArray(parsed.issues)) {
90
+ // Filter to only issues on changed lines
91
+ const diffIssues = parsed.issues.filter(issue => {
92
+ const issueLine = (issue.line || 0) + 1; // convert 0-indexed to 1-indexed
93
+ return changedLines.has(issueLine);
94
+ });
95
+
96
+ for (const issue of diffIssues) {
97
+ allIssues.push({ ...issue, file: filePath });
98
+ }
99
+ }
100
+ scannedFiles.push(filePath);
101
+ }
102
+
103
+ // Format based on verbosity
104
+ const level = verbosity || 'compact';
105
+
106
+ if (level === 'minimal') {
107
+ const bySeverity = { error: 0, warning: 0, info: 0 };
108
+ allIssues.forEach(i => bySeverity[i.severity] = (bySeverity[i.severity] || 0) + 1);
109
+ return {
110
+ content: [{ type: "text", text: JSON.stringify({
111
+ base, target,
112
+ files_scanned: scannedFiles.length,
113
+ total: allIssues.length,
114
+ critical: bySeverity.error,
115
+ warning: bySeverity.warning,
116
+ info: bySeverity.info,
117
+ message: allIssues.length > 0
118
+ ? `Found ${allIssues.length} new issue(s) in changed code.`
119
+ : "No new security issues in changed code."
120
+ }) }]
121
+ };
122
+ }
123
+
124
+ if (level === 'compact') {
125
+ return {
126
+ content: [{ type: "text", text: JSON.stringify({
127
+ base, target,
128
+ files_scanned: scannedFiles.length,
129
+ issues_count: allIssues.length,
130
+ issues: allIssues.map(i => ({
131
+ file: i.file,
132
+ line: (i.line || 0) + 1,
133
+ ruleId: i.ruleId,
134
+ severity: i.severity,
135
+ message: i.message
136
+ }))
137
+ }, null, 2) }]
138
+ };
139
+ }
140
+
141
+ // full
142
+ return {
143
+ content: [{ type: "text", text: JSON.stringify({
144
+ base, target,
145
+ files_scanned: scannedFiles.length,
146
+ issues_count: allIssues.length,
147
+ issues: allIssues,
148
+ changed_files: Array.from(changes.keys())
149
+ }, null, 2) }]
150
+ };
151
+ }
@@ -0,0 +1,308 @@
1
+ // src/tools/scan-project.js
2
+ import { z } from "zod";
3
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
4
+ import { join, resolve, relative, extname, basename } from "path";
5
+ import { execFileSync } from "child_process";
6
+ import { scanSecurity } from './scan-security.js';
7
+ import { matchGlob, loadConfig, shouldExcludeFile } from '../config.js';
8
+ import { detectLanguage } from '../utils.js';
9
+
10
+ export const scanProjectSchema = {
11
+ directory_path: z.string().describe("Path to the directory to scan"),
12
+ recursive: z.boolean().optional().describe("Scan subdirectories recursively (default: true)"),
13
+ include_patterns: z.array(z.string()).optional().describe("Glob patterns to include (e.g. ['**/*.py', '**/*.js'])"),
14
+ exclude_patterns: z.array(z.string()).optional().describe("Glob patterns to exclude (e.g. ['*test*', 'vendor/**'])"),
15
+ diff_only: z.boolean().optional().describe("Only scan git-changed files"),
16
+ cross_file: z.boolean().optional().describe("Enable cross-file taint analysis (max 50 files)"),
17
+ verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level")
18
+ };
19
+
20
+ // Scannable file extensions
21
+ const SCANNABLE_EXTENSIONS = new Set([
22
+ '.py', '.js', '.ts', '.tsx', '.jsx', '.java', '.go', '.rb', '.php',
23
+ '.rs', '.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.cs',
24
+ '.tf', '.hcl', '.sql',
25
+ ]);
26
+
27
+ // Parse .gitignore into patterns
28
+ function parseGitignore(dirPath) {
29
+ const gitignorePath = join(dirPath, '.gitignore');
30
+ if (!existsSync(gitignorePath)) return [];
31
+
32
+ try {
33
+ const content = readFileSync(gitignorePath, 'utf-8');
34
+ return content.split('\n')
35
+ .map(line => line.trim())
36
+ .filter(line => line && !line.startsWith('#'))
37
+ .map(line => {
38
+ // Normalize: remove trailing slash for directories
39
+ if (line.endsWith('/')) return line.slice(0, -1) + '/**';
40
+ return line;
41
+ });
42
+ } catch {
43
+ return [];
44
+ }
45
+ }
46
+
47
+ // Check if a file path matches gitignore patterns
48
+ function isGitignored(filePath, patterns) {
49
+ const normalized = filePath.replace(/\\/g, '/');
50
+ return patterns.some(pattern => matchGlob(normalized, pattern));
51
+ }
52
+
53
+ // Recursively walk a directory, respecting exclusions
54
+ function walkDirectory(dirPath, options = {}) {
55
+ const { recursive = true, includePatterns = [], excludePatterns = [], gitignorePatterns = [], config } = options;
56
+ const files = [];
57
+
58
+ function walk(currentDir) {
59
+ let entries;
60
+ try {
61
+ entries = readdirSync(currentDir);
62
+ } catch {
63
+ return;
64
+ }
65
+
66
+ for (const entry of entries) {
67
+ // Skip hidden directories/files
68
+ if (entry.startsWith('.')) continue;
69
+
70
+ const fullPath = join(currentDir, entry);
71
+ const relativePath = relative(dirPath, fullPath);
72
+
73
+ let stat;
74
+ try {
75
+ stat = statSync(fullPath);
76
+ } catch {
77
+ continue;
78
+ }
79
+
80
+ if (stat.isDirectory()) {
81
+ // Skip common non-source directories
82
+ if (['node_modules', 'vendor', 'dist', 'build', '__pycache__', '.git',
83
+ 'venv', 'env', '.venv', 'target', 'coverage'].includes(entry)) continue;
84
+
85
+ // Skip gitignored directories
86
+ if (isGitignored(relativePath, gitignorePatterns)) continue;
87
+
88
+ if (recursive) walk(fullPath);
89
+ } else if (stat.isFile()) {
90
+ const ext = extname(entry).toLowerCase();
91
+ const base = basename(entry).toLowerCase();
92
+
93
+ // Check extension or special filenames
94
+ if (!SCANNABLE_EXTENSIONS.has(ext) && base !== 'dockerfile') continue;
95
+
96
+ // Check gitignore
97
+ if (isGitignored(relativePath, gitignorePatterns)) continue;
98
+
99
+ // Check config exclusions
100
+ if (config && shouldExcludeFile(relativePath, config)) continue;
101
+
102
+ // Check include patterns (if specified, only include matching files)
103
+ if (includePatterns.length > 0) {
104
+ const matches = includePatterns.some(p => matchGlob(relativePath, p));
105
+ if (!matches) continue;
106
+ }
107
+
108
+ // Check exclude patterns (if specified, skip matching files)
109
+ if (excludePatterns.length > 0) {
110
+ const excluded = excludePatterns.some(p => matchGlob(relativePath, p) || relativePath.includes(p) || entry.includes(p));
111
+ if (excluded) continue;
112
+ }
113
+
114
+ files.push(fullPath);
115
+ }
116
+ }
117
+ }
118
+
119
+ walk(dirPath);
120
+ return files;
121
+ }
122
+
123
+ // Get git-changed files in a directory
124
+ function getGitChangedFiles(dirPath) {
125
+ try {
126
+ const output = execFileSync('git', ['diff', '--name-only', 'HEAD'], {
127
+ cwd: dirPath, encoding: 'utf-8', timeout: 10000
128
+ });
129
+ const untrackedOutput = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
130
+ cwd: dirPath, encoding: 'utf-8', timeout: 10000
131
+ });
132
+ const allFiles = [...output.trim().split('\n'), ...untrackedOutput.trim().split('\n')]
133
+ .filter(f => f.trim())
134
+ .map(f => resolve(dirPath, f))
135
+ .filter(f => existsSync(f));
136
+ return [...new Set(allFiles)];
137
+ } catch {
138
+ return [];
139
+ }
140
+ }
141
+
142
+ // Calculate security grade based on findings
143
+ function calculateGrade(totalIssues, totalFiles, errorCount) {
144
+ if (totalFiles === 0) return 'A';
145
+ const density = totalIssues / totalFiles;
146
+
147
+ if (errorCount === 0 && density === 0) return 'A';
148
+ if (errorCount === 0 && density < 0.5) return 'B';
149
+ if (errorCount <= 2 && density < 1.5) return 'C';
150
+ if (errorCount <= 5 && density < 3) return 'D';
151
+ return 'F';
152
+ }
153
+
154
+ export async function scanProject({ directory_path, recursive, include_patterns, exclude_patterns, diff_only, cross_file, verbosity }) {
155
+ const dirPath = resolve(directory_path);
156
+
157
+ if (!existsSync(dirPath)) {
158
+ return {
159
+ content: [{ type: "text", text: JSON.stringify({ error: "Directory not found" }) }]
160
+ };
161
+ }
162
+
163
+ // Load config from directory
164
+ const config = loadConfig(join(dirPath, 'dummy.js'));
165
+ const gitignorePatterns = parseGitignore(dirPath);
166
+
167
+ // Get files to scan
168
+ let files;
169
+ if (diff_only) {
170
+ files = getGitChangedFiles(dirPath);
171
+ } else {
172
+ files = walkDirectory(dirPath, {
173
+ recursive: recursive !== false,
174
+ includePatterns: include_patterns || [],
175
+ excludePatterns: exclude_patterns || [],
176
+ gitignorePatterns,
177
+ config
178
+ });
179
+ }
180
+
181
+ // Filter to scannable extensions
182
+ files = files.filter(f => {
183
+ const ext = extname(f).toLowerCase();
184
+ const base = basename(f).toLowerCase();
185
+ return SCANNABLE_EXTENSIONS.has(ext) || base === 'dockerfile';
186
+ });
187
+
188
+ if (files.length === 0) {
189
+ return {
190
+ content: [{ type: "text", text: JSON.stringify({
191
+ directory: dirPath,
192
+ message: "No scannable files found",
193
+ files_scanned: 0,
194
+ grade: 'A'
195
+ }) }]
196
+ };
197
+ }
198
+
199
+ // Scan each file
200
+ const allIssues = [];
201
+ const byFile = {};
202
+ const bySeverity = { error: 0, warning: 0, info: 0 };
203
+ const byCategory = {};
204
+
205
+ for (const filePath of files) {
206
+ const result = await scanSecurity({ file_path: filePath, verbosity: 'full' });
207
+ const parsed = JSON.parse(result.content[0].text);
208
+
209
+ if (parsed.issues && Array.isArray(parsed.issues)) {
210
+ const relativePath = relative(dirPath, filePath);
211
+ byFile[relativePath] = parsed.issues.length;
212
+
213
+ for (const issue of parsed.issues) {
214
+ allIssues.push({ ...issue, file: relativePath });
215
+ bySeverity[issue.severity] = (bySeverity[issue.severity] || 0) + 1;
216
+ const category = issue.ruleId?.split('.')[0] || 'other';
217
+ byCategory[category] = (byCategory[category] || 0) + 1;
218
+ }
219
+ }
220
+ }
221
+
222
+ // Cross-file taint analysis (opt-in, max 50 files)
223
+ let crossFileIssues = [];
224
+ if (cross_file && files.length <= 50) {
225
+ try {
226
+ const { runCrossFileAnalyzer } = await import('../utils.js');
227
+ if (typeof runCrossFileAnalyzer === 'function') {
228
+ const crossResults = runCrossFileAnalyzer(files);
229
+ if (Array.isArray(crossResults)) {
230
+ crossFileIssues = crossResults;
231
+ for (const issue of crossFileIssues) {
232
+ const relativePath = relative(dirPath, issue.file || '');
233
+ allIssues.push({ ...issue, file: relativePath });
234
+ bySeverity[issue.severity] = (bySeverity[issue.severity] || 0) + 1;
235
+ }
236
+ }
237
+ }
238
+ } catch {
239
+ // Cross-file analysis not available
240
+ }
241
+ }
242
+
243
+ const grade = calculateGrade(allIssues.length, files.length, bySeverity.error);
244
+ const level = verbosity || 'compact';
245
+
246
+ if (level === 'minimal') {
247
+ return {
248
+ content: [{ type: "text", text: JSON.stringify({
249
+ directory: dirPath,
250
+ files_scanned: files.length,
251
+ total: allIssues.length,
252
+ critical: bySeverity.error,
253
+ warning: bySeverity.warning,
254
+ info: bySeverity.info,
255
+ grade,
256
+ message: allIssues.length > 0
257
+ ? `Found ${allIssues.length} issue(s) across ${files.length} files. Grade: ${grade}`
258
+ : `No issues found in ${files.length} files. Grade: ${grade}`
259
+ }) }]
260
+ };
261
+ }
262
+
263
+ if (level === 'compact') {
264
+ // Show top issues per file, sorted by severity
265
+ const topIssues = allIssues
266
+ .sort((a, b) => {
267
+ const order = { error: 0, warning: 1, info: 2 };
268
+ return (order[a.severity] || 2) - (order[b.severity] || 2);
269
+ })
270
+ .slice(0, 50)
271
+ .map(i => ({
272
+ file: i.file,
273
+ line: (i.line || 0) + 1,
274
+ ruleId: i.ruleId,
275
+ severity: i.severity,
276
+ message: i.message
277
+ }));
278
+
279
+ return {
280
+ content: [{ type: "text", text: JSON.stringify({
281
+ directory: dirPath,
282
+ files_scanned: files.length,
283
+ issues_count: allIssues.length,
284
+ grade,
285
+ by_severity: bySeverity,
286
+ by_category: byCategory,
287
+ cross_file_issues: crossFileIssues.length > 0 ? crossFileIssues.length : undefined,
288
+ issues: topIssues
289
+ }, null, 2) }]
290
+ };
291
+ }
292
+
293
+ // full
294
+ return {
295
+ content: [{ type: "text", text: JSON.stringify({
296
+ directory: dirPath,
297
+ files_scanned: files.length,
298
+ issues_count: allIssues.length,
299
+ grade,
300
+ by_severity: bySeverity,
301
+ by_category: byCategory,
302
+ by_file: byFile,
303
+ cross_file_issues: crossFileIssues.length > 0 ? crossFileIssues : undefined,
304
+ issues: allIssues,
305
+ scanned_files: files.map(f => relative(dirPath, f))
306
+ }, null, 2) }]
307
+ };
308
+ }
@@ -2,11 +2,15 @@
2
2
  import { z } from "zod";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { detectLanguage, runAnalyzer, generateFix, toSarif } from '../utils.js';
5
+ import { deduplicateFindings } from '../dedup.js';
6
+ import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from '../context.js';
7
+ import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
5
8
 
6
9
  export const scanSecuritySchema = {
7
10
  file_path: z.string().describe("Path to the file to scan"),
8
11
  output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration"),
9
- verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)")
12
+ verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)"),
13
+ engine: z.enum(['auto', 'ast', 'regex']).optional().describe("Analysis engine: 'auto' (default, AST with regex fallback), 'ast' (tree-sitter only), 'regex' (regex only)")
10
14
  };
11
15
 
12
16
  // Verbosity formatters
@@ -35,6 +39,7 @@ function formatCompact(file_path, language, issues) {
35
39
  line: i.line + 1,
36
40
  ruleId: i.ruleId,
37
41
  severity: i.severity,
42
+ confidence: i.confidence || 'MEDIUM',
38
43
  message: i.message,
39
44
  fix: i.suggested_fix?.fixed ? i.suggested_fix.fixed.trim() : null
40
45
  }))
@@ -50,26 +55,49 @@ function formatFull(file_path, language, issues) {
50
55
  };
51
56
  }
52
57
 
53
- export async function scanSecurity({ file_path, output_format, verbosity }) {
58
+ export async function scanSecurity({ file_path, output_format, verbosity, engine }) {
54
59
  if (!existsSync(file_path)) {
55
60
  return {
56
61
  content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
57
62
  };
58
63
  }
59
64
 
60
- const issues = runAnalyzer(file_path);
65
+ // Load project configuration
66
+ const config = loadConfig(file_path);
61
67
 
62
- if (issues.error) {
68
+ // Check file exclusion
69
+ if (shouldExcludeFile(file_path, config)) {
63
70
  return {
64
- content: [{ type: "text", text: JSON.stringify(issues) }]
71
+ content: [{ type: "text", text: JSON.stringify({ file: file_path, message: "File excluded by configuration", issues_count: 0 }) }]
65
72
  };
66
73
  }
67
74
 
75
+ const rawIssues = runAnalyzer(file_path, engine || 'auto');
76
+
77
+ if (rawIssues.error) {
78
+ return {
79
+ content: [{ type: "text", text: JSON.stringify(rawIssues) }]
80
+ };
81
+ }
82
+
83
+ // Cross-engine deduplication
84
+ const dedupedIssues = deduplicateFindings(rawIssues);
85
+
68
86
  // Read file content for fix suggestions
69
87
  const content = readFileSync(file_path, 'utf-8');
70
88
  const lines = content.split('\n');
71
89
  const language = detectLanguage(file_path);
72
90
 
91
+ // Context-aware filtering (suppress known module imports)
92
+ const contextFiltered = applyContextFilter(dedupedIssues, file_path, language);
93
+
94
+ // Framework-aware severity adjustment
95
+ const frameworks = detectFrameworks(file_path, language);
96
+ const frameworkAdjusted = applyFrameworkAdjustments(contextFiltered, frameworks);
97
+
98
+ // Apply .scannerrc configuration (rule suppression, severity/confidence thresholds)
99
+ const issues = applyConfig(frameworkAdjusted, file_path, config);
100
+
73
101
  // Enhance issues with fix suggestions
74
102
  const enhancedIssues = issues.map(issue => {
75
103
  const line = lines[issue.line] || '';