agent-security-scanner-mcp 3.7.0 → 3.9.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.
@@ -0,0 +1,249 @@
1
+ // src/tools/import-resolver.js
2
+ // Import graph resolution — resolves local imports to file paths and builds dependency graphs
3
+
4
+ import { existsSync, readFileSync, statSync } from 'fs';
5
+ import { resolve, dirname, join, extname, relative, isAbsolute } from 'path';
6
+ import { createHash } from 'crypto';
7
+ import { extractImports, detectLanguage } from '../utils.js';
8
+
9
+ // In-memory cache: absPath -> { content_hash, imports, resolvedImports }
10
+ const importGraphCache = new Map();
11
+
12
+ /**
13
+ * Clear the import graph cache (for testing).
14
+ */
15
+ export function clearImportGraphCache() {
16
+ importGraphCache.clear();
17
+ }
18
+
19
+ /**
20
+ * Check if an import specifier is a local/relative import.
21
+ * @param {string} importSpec - The import string (e.g., './helpers', 'express')
22
+ * @param {string} language - The language: 'javascript', 'typescript', 'python', 'go'
23
+ * @returns {boolean}
24
+ */
25
+ export function isLocalImport(importSpec, language) {
26
+ switch (language) {
27
+ case 'javascript':
28
+ case 'typescript':
29
+ return importSpec.startsWith('./') || importSpec.startsWith('../');
30
+ case 'python':
31
+ return importSpec.startsWith('.');
32
+ case 'go':
33
+ return importSpec.startsWith('./') || importSpec.startsWith('../');
34
+ default:
35
+ return importSpec.startsWith('./') || importSpec.startsWith('../');
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Resolve a local import specifier to an actual file path.
41
+ * @param {string} importSpec - The import string (e.g., './routes/users')
42
+ * @param {string} fromFile - Absolute path of the file containing the import
43
+ * @param {string} language - The language
44
+ * @returns {string|null} Absolute resolved path, or null if unresolvable/package
45
+ */
46
+ export function resolveImportPath(importSpec, fromFile, language) {
47
+ if (!isLocalImport(importSpec, language)) {
48
+ return null;
49
+ }
50
+
51
+ const fromDir = dirname(fromFile);
52
+
53
+ switch (language) {
54
+ case 'javascript':
55
+ case 'typescript': {
56
+ const base = resolve(fromDir, importSpec);
57
+ // Try exact path first
58
+ if (existsSync(base) && !isDirectory(base)) return base;
59
+ // Try with extensions
60
+ const jsExtensions = ['.js', '.ts', '.tsx', '.jsx', '.mjs'];
61
+ for (const ext of jsExtensions) {
62
+ const withExt = base + ext;
63
+ if (existsSync(withExt)) return withExt;
64
+ }
65
+ // Try as directory with index file
66
+ const indexFiles = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', 'index.mjs'];
67
+ for (const idx of indexFiles) {
68
+ const indexPath = join(base, idx);
69
+ if (existsSync(indexPath)) return indexPath;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ case 'python': {
75
+ // Python relative imports: . = current package, .. = parent package
76
+ // Convert dot-prefix to path
77
+ let dotCount = 0;
78
+ let rest = importSpec;
79
+ while (rest.startsWith('.')) {
80
+ dotCount++;
81
+ rest = rest.slice(1);
82
+ }
83
+
84
+ let targetDir = fromDir;
85
+ // Each dot beyond the first goes up one directory
86
+ for (let i = 1; i < dotCount; i++) {
87
+ targetDir = dirname(targetDir);
88
+ }
89
+
90
+ if (!rest) {
91
+ // Just dots — refers to __init__.py in the target dir
92
+ const initPath = join(targetDir, '__init__.py');
93
+ if (existsSync(initPath)) return initPath;
94
+ return null;
95
+ }
96
+
97
+ // Convert module.sub to module/sub
98
+ const modulePath = rest.replace(/\./g, '/');
99
+ const base = join(targetDir, modulePath);
100
+
101
+ // Try as .py file
102
+ const asPy = base + '.py';
103
+ if (existsSync(asPy)) return asPy;
104
+ // Try as package (__init__.py)
105
+ const asInit = join(base, '__init__.py');
106
+ if (existsSync(asInit)) return asInit;
107
+ // Try as sibling .py (same directory)
108
+ const sibling = join(fromDir, modulePath + '.py');
109
+ if (sibling !== asPy && existsSync(sibling)) return sibling;
110
+
111
+ return null;
112
+ }
113
+
114
+ case 'go': {
115
+ const base = resolve(fromDir, importSpec);
116
+ if (existsSync(base) && !isDirectory(base)) return base;
117
+ const withGo = base + '.go';
118
+ if (existsSync(withGo)) return withGo;
119
+ return null;
120
+ }
121
+
122
+ default:
123
+ return null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Build an import graph starting from an entry file using BFS traversal.
129
+ * @param {string} filePath - Absolute path to the entry file
130
+ * @param {string} projectRoot - Absolute path to the project root
131
+ * @param {{ maxDepth?: number }} [options]
132
+ * @returns {{ files: Record<string, object>, edges: Array, cycles: Array, unresolved: Array, summary: object }}
133
+ */
134
+ export function resolveImportGraph(filePath, projectRoot, options = {}) {
135
+ const { maxDepth = 3 } = options;
136
+ const MAX_FILES = 100;
137
+
138
+ const absEntry = isAbsolute(filePath) ? filePath : resolve(filePath);
139
+ const absRoot = isAbsolute(projectRoot) ? projectRoot : resolve(projectRoot);
140
+
141
+ const files = {}; // absPath -> { imports, resolvedImports, content_hash }
142
+ const edges = []; // [{ from, to, importSpec }]
143
+ const cycles = []; // [[from, to]]
144
+ const unresolved = []; // [{ file, importSpec }]
145
+
146
+ // BFS
147
+ // Queue entries: { absPath, depth }
148
+ const queue = [{ absPath: absEntry, depth: 0 }];
149
+ const visited = new Set();
150
+
151
+ while (queue.length > 0 && Object.keys(files).length < MAX_FILES) {
152
+ const { absPath, depth } = queue.shift();
153
+
154
+ if (visited.has(absPath)) continue;
155
+ visited.add(absPath);
156
+
157
+ if (!existsSync(absPath)) continue;
158
+
159
+ const language = detectLanguage(absPath);
160
+ let content;
161
+ try {
162
+ content = readFileSync(absPath, 'utf-8');
163
+ } catch {
164
+ continue;
165
+ }
166
+
167
+ const contentHash = createHash('sha256').update(content).digest('hex').slice(0, 16);
168
+
169
+ // Check cache
170
+ const cached = importGraphCache.get(absPath);
171
+ let rawImports, resolvedImports;
172
+
173
+ if (cached && cached.content_hash === contentHash) {
174
+ rawImports = cached.imports;
175
+ resolvedImports = cached.resolvedImports;
176
+ } else {
177
+ rawImports = extractImports(content, language);
178
+ resolvedImports = [];
179
+
180
+ for (const spec of rawImports) {
181
+ if (!isLocalImport(spec, language)) continue;
182
+
183
+ const resolved = resolveImportPath(spec, absPath, language);
184
+ if (resolved) {
185
+ // Guard: resolved path must stay within projectRoot
186
+ const relToRoot = relative(absRoot, resolved);
187
+ if (relToRoot.startsWith('..') || isAbsolute(relToRoot)) {
188
+ unresolved.push({ file: absPath, importSpec: spec });
189
+ continue;
190
+ }
191
+ resolvedImports.push({ spec, resolvedPath: resolved });
192
+ } else {
193
+ if (isLocalImport(spec, language)) {
194
+ unresolved.push({ file: absPath, importSpec: spec });
195
+ }
196
+ }
197
+ }
198
+
199
+ // Update cache
200
+ importGraphCache.set(absPath, {
201
+ content_hash: contentHash,
202
+ imports: rawImports,
203
+ resolvedImports,
204
+ });
205
+ }
206
+
207
+ files[absPath] = {
208
+ imports: rawImports,
209
+ resolvedImports,
210
+ content_hash: contentHash,
211
+ };
212
+
213
+ // Build edges and enqueue resolved imports
214
+ for (const { spec, resolvedPath } of resolvedImports) {
215
+ edges.push({ from: absPath, to: resolvedPath, importSpec: spec });
216
+
217
+ if (visited.has(resolvedPath)) {
218
+ // Cycle detected
219
+ cycles.push([absPath, resolvedPath]);
220
+ } else if (depth < maxDepth) {
221
+ queue.push({ absPath: resolvedPath, depth: depth + 1 });
222
+ }
223
+ }
224
+ }
225
+
226
+ const maxDepthReached = queue.some(item => item.depth >= maxDepth);
227
+
228
+ return {
229
+ files,
230
+ edges,
231
+ cycles,
232
+ unresolved,
233
+ summary: {
234
+ total_files: Object.keys(files).length,
235
+ total_edges: edges.length,
236
+ has_cycles: cycles.length > 0,
237
+ max_depth_reached: maxDepthReached || false,
238
+ },
239
+ };
240
+ }
241
+
242
+ // Helper: check if a path is a directory
243
+ function isDirectory(p) {
244
+ try {
245
+ return statSync(p).isDirectory();
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
@@ -0,0 +1,365 @@
1
+ // src/tools/project-context.js
2
+ // Project security profile discovery — detects frameworks, middleware, and security libraries
3
+
4
+ import { existsSync, readFileSync } from 'fs';
5
+ import { join, basename } from 'path';
6
+ import { createHash } from 'crypto';
7
+
8
+ // Cache: projectRoot -> { contentHash, result }
9
+ const projectContextCache = new Map();
10
+
11
+ /**
12
+ * Clear the project context cache (for testing).
13
+ */
14
+ export function clearProjectContextCache() {
15
+ projectContextCache.clear();
16
+ }
17
+
18
+ // Framework detection: package name → { name, ecosystem }
19
+ const FRAMEWORK_MAP = {
20
+ // Node.js
21
+ 'express': { name: 'Express', ecosystem: 'node' },
22
+ 'koa': { name: 'Koa', ecosystem: 'node' },
23
+ 'fastify': { name: 'Fastify', ecosystem: 'node' },
24
+ '@hapi/hapi': { name: 'Hapi', ecosystem: 'node' },
25
+ 'hapi': { name: 'Hapi', ecosystem: 'node' },
26
+ '@nestjs/core': { name: 'NestJS', ecosystem: 'node' },
27
+ 'next': { name: 'Next.js', ecosystem: 'node' },
28
+ 'nuxt': { name: 'Nuxt', ecosystem: 'node' },
29
+ // Python
30
+ 'django': { name: 'Django', ecosystem: 'python' },
31
+ 'flask': { name: 'Flask', ecosystem: 'python' },
32
+ 'fastapi': { name: 'FastAPI', ecosystem: 'python' },
33
+ 'tornado': { name: 'Tornado', ecosystem: 'python' },
34
+ 'sanic': { name: 'Sanic', ecosystem: 'python' },
35
+ 'starlette': { name: 'Starlette', ecosystem: 'python' },
36
+ // Ruby
37
+ 'rails': { name: 'Rails', ecosystem: 'ruby' },
38
+ 'sinatra': { name: 'Sinatra', ecosystem: 'ruby' },
39
+ // Go (module paths)
40
+ 'github.com/gin-gonic/gin': { name: 'Gin', ecosystem: 'go' },
41
+ 'github.com/labstack/echo': { name: 'Echo', ecosystem: 'go' },
42
+ 'github.com/gofiber/fiber': { name: 'Fiber', ecosystem: 'go' },
43
+ // Java
44
+ 'spring-boot-starter-web': { name: 'Spring Boot', ecosystem: 'java' },
45
+ 'spring-boot-starter-security': { name: 'Spring Security', ecosystem: 'java' },
46
+ };
47
+
48
+ // Security middleware/library detection: package name → capability category
49
+ const SECURITY_MAP = {
50
+ // Node.js security middleware
51
+ 'helmet': 'http-headers',
52
+ 'cors': 'cors',
53
+ 'csurf': 'csrf',
54
+ 'express-rate-limit': 'rate-limiting',
55
+ 'rate-limiter-flexible': 'rate-limiting',
56
+ 'dompurify': 'xss-sanitization',
57
+ 'isomorphic-dompurify': 'xss-sanitization',
58
+ 'xss': 'xss-sanitization',
59
+ 'sanitize-html': 'xss-sanitization',
60
+ 'express-validator': 'input-validation',
61
+ 'joi': 'input-validation',
62
+ 'zod': 'input-validation',
63
+ 'yup': 'input-validation',
64
+ 'hpp': 'http-parameter-pollution',
65
+ 'express-mongo-sanitize': 'nosql-injection',
66
+ 'sqlstring': 'sql-sanitization',
67
+ // Node.js auth
68
+ 'passport': 'authentication',
69
+ 'jsonwebtoken': 'jwt-auth',
70
+ 'express-jwt': 'jwt-auth',
71
+ 'bcrypt': 'password-hashing',
72
+ 'bcryptjs': 'password-hashing',
73
+ 'argon2': 'password-hashing',
74
+ 'oauth2-server': 'oauth',
75
+ // Python security
76
+ 'django-cors-headers': 'cors',
77
+ 'flask-cors': 'cors',
78
+ 'flask-wtf': 'csrf',
79
+ 'flask-limiter': 'rate-limiting',
80
+ 'bleach': 'xss-sanitization',
81
+ 'python-jose': 'jwt-auth',
82
+ 'pyjwt': 'jwt-auth',
83
+ 'passlib': 'password-hashing',
84
+ 'django-ratelimit': 'rate-limiting',
85
+ 'django-csp': 'csp',
86
+ 'django-axes': 'brute-force-protection',
87
+ 'cerberus': 'input-validation',
88
+ 'marshmallow': 'input-validation',
89
+ 'pydantic': 'input-validation',
90
+ // Ruby security
91
+ 'rack-cors': 'cors',
92
+ 'devise': 'authentication',
93
+ 'pundit': 'authorization',
94
+ 'brakeman': 'sast',
95
+ 'rack-attack': 'rate-limiting',
96
+ 'secure_headers': 'http-headers',
97
+ // Go security
98
+ 'github.com/gorilla/csrf': 'csrf',
99
+ 'golang.org/x/crypto': 'crypto',
100
+ 'github.com/rs/cors': 'cors',
101
+ 'github.com/ulule/limiter': 'rate-limiting',
102
+ 'github.com/golang-jwt/jwt': 'jwt-auth',
103
+ // Java security
104
+ 'spring-security-core': 'authentication',
105
+ 'spring-security-web': 'csrf',
106
+ 'spring-security-oauth2': 'oauth',
107
+ 'owasp-java-html-sanitizer': 'xss-sanitization',
108
+ };
109
+
110
+ // Test framework detection
111
+ const TEST_FRAMEWORK_MAP = {
112
+ 'jest': 'jest', 'vitest': 'vitest', 'mocha': 'mocha', 'chai': 'chai',
113
+ 'jasmine': 'jasmine', 'ava': 'ava', 'tap': 'tap',
114
+ 'pytest': 'pytest', 'unittest': 'unittest', 'nose2': 'nose2',
115
+ 'rspec': 'rspec', 'minitest': 'minitest',
116
+ 'testing': 'go-testing',
117
+ 'junit': 'junit', 'testng': 'testng',
118
+ };
119
+
120
+ // Dependency file parsers
121
+
122
+ function parsePackageJson(projectRoot) {
123
+ const filePath = join(projectRoot, 'package.json');
124
+ if (!existsSync(filePath)) return null;
125
+ try {
126
+ const pkg = JSON.parse(readFileSync(filePath, 'utf-8'));
127
+ const allDeps = {
128
+ ...pkg.dependencies,
129
+ ...pkg.devDependencies,
130
+ };
131
+ return { language: 'javascript', deps: Object.keys(allDeps), file: 'package.json' };
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function parseRequirementsTxt(projectRoot) {
138
+ const filePath = join(projectRoot, 'requirements.txt');
139
+ if (!existsSync(filePath)) return null;
140
+ try {
141
+ const content = readFileSync(filePath, 'utf-8');
142
+ const deps = content.split('\n')
143
+ .map(line => line.trim())
144
+ .filter(line => line && !line.startsWith('#') && !line.startsWith('-'))
145
+ .map(line => line.split(/[>=<!~\[;]/)[0].trim().toLowerCase());
146
+ return { language: 'python', deps, file: 'requirements.txt' };
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function parsePyprojectToml(projectRoot) {
153
+ const filePath = join(projectRoot, 'pyproject.toml');
154
+ if (!existsSync(filePath)) return null;
155
+ try {
156
+ const content = readFileSync(filePath, 'utf-8');
157
+ // Simple extraction of dependencies from pyproject.toml
158
+ const deps = [];
159
+ const depRegex = /["']([a-zA-Z0-9_-]+)(?:[>=<!~\[].*)?["']/g;
160
+ const inDeps = content.match(/dependencies\s*=\s*\[([^\]]*)\]/s);
161
+ if (inDeps) {
162
+ let match;
163
+ while ((match = depRegex.exec(inDeps[1])) !== null) {
164
+ deps.push(match[1].toLowerCase());
165
+ }
166
+ }
167
+ return deps.length > 0 ? { language: 'python', deps, file: 'pyproject.toml' } : null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function parseGoMod(projectRoot) {
174
+ const filePath = join(projectRoot, 'go.mod');
175
+ if (!existsSync(filePath)) return null;
176
+ try {
177
+ const content = readFileSync(filePath, 'utf-8');
178
+ const deps = [];
179
+ const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
180
+ if (requireBlock) {
181
+ const lines = requireBlock[1].split('\n');
182
+ for (const line of lines) {
183
+ const trimmed = line.trim();
184
+ if (trimmed && !trimmed.startsWith('//')) {
185
+ const parts = trimmed.split(/\s+/);
186
+ if (parts[0]) deps.push(parts[0]);
187
+ }
188
+ }
189
+ }
190
+ // Also match single-line requires
191
+ const singleRequires = content.matchAll(/require\s+(\S+)\s+/g);
192
+ for (const match of singleRequires) {
193
+ deps.push(match[1]);
194
+ }
195
+ return deps.length > 0 ? { language: 'go', deps, file: 'go.mod' } : null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ function parseGemfile(projectRoot) {
202
+ const filePath = join(projectRoot, 'Gemfile');
203
+ if (!existsSync(filePath)) return null;
204
+ try {
205
+ const content = readFileSync(filePath, 'utf-8');
206
+ const deps = [];
207
+ const gemRegex = /gem\s+['"]([a-zA-Z0-9_-]+)['"]/g;
208
+ let match;
209
+ while ((match = gemRegex.exec(content)) !== null) {
210
+ deps.push(match[1].toLowerCase());
211
+ }
212
+ return deps.length > 0 ? { language: 'ruby', deps, file: 'Gemfile' } : null;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ function parsePomXml(projectRoot) {
219
+ const filePath = join(projectRoot, 'pom.xml');
220
+ if (!existsSync(filePath)) return null;
221
+ try {
222
+ const content = readFileSync(filePath, 'utf-8');
223
+ const deps = [];
224
+ const artifactRegex = /<artifactId>([^<]+)<\/artifactId>/g;
225
+ let match;
226
+ while ((match = artifactRegex.exec(content)) !== null) {
227
+ deps.push(match[1].toLowerCase());
228
+ }
229
+ return deps.length > 0 ? { language: 'java', deps, file: 'pom.xml' } : null;
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+
235
+ // Dependency files to check for cache hashing
236
+ const DEP_FILES = ['package.json', 'requirements.txt', 'pyproject.toml', 'go.mod', 'Gemfile', 'pom.xml'];
237
+
238
+ /**
239
+ * Hash the contents of dependency files in a project root for cache invalidation.
240
+ */
241
+ function hashDependencyFiles(projectRoot) {
242
+ const hash = createHash('sha256');
243
+ let hasContent = false;
244
+ for (const depFile of DEP_FILES) {
245
+ const filePath = join(projectRoot, depFile);
246
+ if (existsSync(filePath)) {
247
+ try {
248
+ hash.update(depFile + ':' + readFileSync(filePath, 'utf-8'));
249
+ hasContent = true;
250
+ } catch {
251
+ // ignore unreadable files
252
+ }
253
+ }
254
+ }
255
+ return hasContent ? hash.digest('hex').slice(0, 16) : null;
256
+ }
257
+
258
+ /**
259
+ * Discover project security context from dependency files (with caching).
260
+ * Results are cached by projectRoot and invalidated when dependency file contents change.
261
+ * @param {string} projectRoot - Path to project root directory
262
+ * @returns {{ language: string, framework: string|null, security_middleware: string[], sanitizers: string[], auth_libraries: string[], test_frameworks: string[], dependency_file: string|null, raw_security_deps: Record<string, string> }}
263
+ */
264
+ export function discoverProjectContext(projectRoot) {
265
+ const contentHash = hashDependencyFiles(projectRoot);
266
+ const cached = projectContextCache.get(projectRoot);
267
+ if (cached && cached.contentHash === contentHash) {
268
+ return cached.result;
269
+ }
270
+
271
+ const result = _discoverProjectContextUncached(projectRoot);
272
+ projectContextCache.set(projectRoot, { contentHash, result });
273
+ return result;
274
+ }
275
+
276
+ /**
277
+ * Internal: discover project security context without caching.
278
+ */
279
+ function _discoverProjectContextUncached(projectRoot) {
280
+ const parsers = [
281
+ parsePackageJson,
282
+ parseRequirementsTxt,
283
+ parsePyprojectToml,
284
+ parseGoMod,
285
+ parseGemfile,
286
+ parsePomXml,
287
+ ];
288
+
289
+ let parsed = null;
290
+ for (const parser of parsers) {
291
+ parsed = parser(projectRoot);
292
+ if (parsed) break;
293
+ }
294
+
295
+ if (!parsed) {
296
+ return {
297
+ language: 'unknown',
298
+ framework: null,
299
+ security_middleware: [],
300
+ sanitizers: [],
301
+ auth_libraries: [],
302
+ test_frameworks: [],
303
+ dependency_file: null,
304
+ raw_security_deps: {},
305
+ };
306
+ }
307
+
308
+ const { language, deps, file } = parsed;
309
+
310
+ // Detect framework
311
+ let framework = null;
312
+ for (const dep of deps) {
313
+ const depLower = dep.toLowerCase();
314
+ if (FRAMEWORK_MAP[depLower]) {
315
+ framework = FRAMEWORK_MAP[depLower].name;
316
+ break;
317
+ }
318
+ // Partial match for Java spring artifacts
319
+ if (depLower.includes('spring-boot-starter-web')) {
320
+ framework = 'Spring Boot';
321
+ break;
322
+ }
323
+ }
324
+
325
+ // Detect security packages by category
326
+ const securityMiddleware = [];
327
+ const sanitizers = [];
328
+ const authLibraries = [];
329
+ const testFrameworks = [];
330
+ const rawSecurityDeps = {};
331
+
332
+ for (const dep of deps) {
333
+ const depLower = dep.toLowerCase();
334
+
335
+ // Security middleware
336
+ const capability = SECURITY_MAP[depLower] || SECURITY_MAP[dep];
337
+ if (capability) {
338
+ rawSecurityDeps[dep] = capability;
339
+
340
+ if (['xss-sanitization', 'sql-sanitization', 'nosql-injection'].includes(capability)) {
341
+ sanitizers.push(dep);
342
+ } else if (['authentication', 'jwt-auth', 'password-hashing', 'oauth', 'authorization'].includes(capability)) {
343
+ authLibraries.push(dep);
344
+ } else {
345
+ securityMiddleware.push(dep);
346
+ }
347
+ }
348
+
349
+ // Test frameworks
350
+ if (TEST_FRAMEWORK_MAP[depLower]) {
351
+ testFrameworks.push(TEST_FRAMEWORK_MAP[depLower]);
352
+ }
353
+ }
354
+
355
+ return {
356
+ language,
357
+ framework,
358
+ security_middleware: securityMiddleware,
359
+ sanitizers,
360
+ auth_libraries: authLibraries,
361
+ test_frameworks: testFrameworks,
362
+ dependency_file: file,
363
+ raw_security_deps: rawSecurityDeps,
364
+ };
365
+ }