@indicated/vibeguard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude/settings.local.json +5 -0
  2. package/.github/workflows/ci.yml +65 -0
  3. package/.github/workflows/release.yml +85 -0
  4. package/PROGRESS.md +192 -0
  5. package/README.md +183 -0
  6. package/dist/api/license.d.ts +13 -0
  7. package/dist/api/license.d.ts.map +1 -0
  8. package/dist/api/license.js +138 -0
  9. package/dist/api/license.js.map +1 -0
  10. package/dist/api/rules.d.ts +13 -0
  11. package/dist/api/rules.d.ts.map +1 -0
  12. package/dist/api/rules.js +57 -0
  13. package/dist/api/rules.js.map +1 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.d.ts.map +1 -0
  16. package/dist/cli/commands/init.js +145 -0
  17. package/dist/cli/commands/init.js.map +1 -0
  18. package/dist/cli/commands/login.d.ts +4 -0
  19. package/dist/cli/commands/login.d.ts.map +1 -0
  20. package/dist/cli/commands/login.js +121 -0
  21. package/dist/cli/commands/login.js.map +1 -0
  22. package/dist/cli/commands/mcp.d.ts +3 -0
  23. package/dist/cli/commands/mcp.d.ts.map +1 -0
  24. package/dist/cli/commands/mcp.js +14 -0
  25. package/dist/cli/commands/mcp.js.map +1 -0
  26. package/dist/cli/commands/rules.d.ts +3 -0
  27. package/dist/cli/commands/rules.d.ts.map +1 -0
  28. package/dist/cli/commands/rules.js +52 -0
  29. package/dist/cli/commands/rules.js.map +1 -0
  30. package/dist/cli/commands/scan.d.ts +3 -0
  31. package/dist/cli/commands/scan.d.ts.map +1 -0
  32. package/dist/cli/commands/scan.js +114 -0
  33. package/dist/cli/commands/scan.js.map +1 -0
  34. package/dist/cli/config.d.ts +4 -0
  35. package/dist/cli/config.d.ts.map +1 -0
  36. package/dist/cli/config.js +88 -0
  37. package/dist/cli/config.js.map +1 -0
  38. package/dist/cli/index.d.ts +3 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +25 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/output.d.ts +15 -0
  43. package/dist/cli/output.d.ts.map +1 -0
  44. package/dist/cli/output.js +152 -0
  45. package/dist/cli/output.js.map +1 -0
  46. package/dist/mcp/server.d.ts +2 -0
  47. package/dist/mcp/server.d.ts.map +1 -0
  48. package/dist/mcp/server.js +188 -0
  49. package/dist/mcp/server.js.map +1 -0
  50. package/dist/scanner/index.d.ts +15 -0
  51. package/dist/scanner/index.d.ts.map +1 -0
  52. package/dist/scanner/index.js +207 -0
  53. package/dist/scanner/index.js.map +1 -0
  54. package/dist/scanner/parsers/javascript.d.ts +12 -0
  55. package/dist/scanner/parsers/javascript.d.ts.map +1 -0
  56. package/dist/scanner/parsers/javascript.js +266 -0
  57. package/dist/scanner/parsers/javascript.js.map +1 -0
  58. package/dist/scanner/parsers/python.d.ts +3 -0
  59. package/dist/scanner/parsers/python.d.ts.map +1 -0
  60. package/dist/scanner/parsers/python.js +108 -0
  61. package/dist/scanner/parsers/python.js.map +1 -0
  62. package/dist/scanner/rules/definitions.d.ts +5 -0
  63. package/dist/scanner/rules/definitions.d.ts.map +1 -0
  64. package/dist/scanner/rules/definitions.js +584 -0
  65. package/dist/scanner/rules/definitions.js.map +1 -0
  66. package/dist/scanner/rules/loader.d.ts +8 -0
  67. package/dist/scanner/rules/loader.d.ts.map +1 -0
  68. package/dist/scanner/rules/loader.js +45 -0
  69. package/dist/scanner/rules/loader.js.map +1 -0
  70. package/dist/scanner/rules/matcher.d.ts +11 -0
  71. package/dist/scanner/rules/matcher.d.ts.map +1 -0
  72. package/dist/scanner/rules/matcher.js +53 -0
  73. package/dist/scanner/rules/matcher.js.map +1 -0
  74. package/dist/types.d.ts +33 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +3 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +48 -0
  79. package/src/api/license.ts +120 -0
  80. package/src/api/rules.ts +70 -0
  81. package/src/cli/commands/init.ts +123 -0
  82. package/src/cli/commands/login.ts +92 -0
  83. package/src/cli/commands/mcp.ts +12 -0
  84. package/src/cli/commands/rules.ts +58 -0
  85. package/src/cli/commands/scan.ts +94 -0
  86. package/src/cli/config.ts +54 -0
  87. package/src/cli/index.ts +28 -0
  88. package/src/cli/output.ts +159 -0
  89. package/src/mcp/server.ts +195 -0
  90. package/src/scanner/index.ts +195 -0
  91. package/src/scanner/parsers/javascript.ts +285 -0
  92. package/src/scanner/parsers/python.ts +126 -0
  93. package/src/scanner/rules/definitions.ts +592 -0
  94. package/src/scanner/rules/loader.ts +59 -0
  95. package/src/scanner/rules/matcher.ts +68 -0
  96. package/src/types.ts +36 -0
  97. package/test-samples/secure.js +52 -0
  98. package/test-samples/vulnerable.js +56 -0
  99. package/test-samples/vulnerable.py +39 -0
  100. package/tests/helpers.ts +43 -0
  101. package/tests/rules/critical.test.ts +186 -0
  102. package/tests/rules/definitions.test.ts +167 -0
  103. package/tests/rules/high.test.ts +377 -0
  104. package/tests/rules/low.test.ts +172 -0
  105. package/tests/rules/medium.test.ts +224 -0
  106. package/tests/scanner/scanner.test.ts +161 -0
  107. package/tsconfig.json +19 -0
  108. package/vibe-coding-security-checklist.md +245 -0
  109. package/vitest.config.ts +15 -0
@@ -0,0 +1,195 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { glob } from 'glob';
4
+ import { Finding, ScanResult, SecurityRule, Config } from '../types';
5
+ import { loadRules, filterRules } from './rules/loader';
6
+ import { parseJavaScript, scanWithAST, scanWithPatterns } from './parsers/javascript';
7
+ import { scanPythonWithPatterns } from './parsers/python';
8
+
9
+ const SUPPORTED_EXTENSIONS: Record<string, string> = {
10
+ '.js': 'javascript',
11
+ '.jsx': 'javascript',
12
+ '.ts': 'typescript',
13
+ '.tsx': 'typescript',
14
+ '.mjs': 'javascript',
15
+ '.cjs': 'javascript',
16
+ '.py': 'python',
17
+ };
18
+
19
+ const DEFAULT_EXCLUDE = [
20
+ '**/node_modules/**',
21
+ '**/dist/**',
22
+ '**/build/**',
23
+ '**/.git/**',
24
+ '**/coverage/**',
25
+ '**/__pycache__/**',
26
+ '**/venv/**',
27
+ '**/.venv/**',
28
+ '**/env/**',
29
+ '**/*.min.js',
30
+ '**/*.bundle.js',
31
+ ];
32
+
33
+ export class Scanner {
34
+ private rules: SecurityRule[] = [];
35
+ private config: Config;
36
+
37
+ constructor(config: Config = {}) {
38
+ this.config = config;
39
+ }
40
+
41
+ async initialize(licenseKey?: string): Promise<void> {
42
+ const allRules = await loadRules(licenseKey);
43
+ this.rules = filterRules(allRules, {
44
+ enabled: this.config.rules?.enabled,
45
+ disabled: this.config.rules?.disabled,
46
+ });
47
+ }
48
+
49
+ async scan(targets: string[]): Promise<ScanResult> {
50
+ const startTime = Date.now();
51
+ const findings: Finding[] = [];
52
+ const files: string[] = [];
53
+
54
+ for (const target of targets) {
55
+ const targetPath = path.resolve(target);
56
+ const stat = fs.statSync(targetPath);
57
+
58
+ if (stat.isDirectory()) {
59
+ const globPattern = path.join(targetPath, '**/*');
60
+ const exclude = [...DEFAULT_EXCLUDE, ...(this.config.exclude || [])];
61
+
62
+ const matchedFiles = await glob(globPattern, {
63
+ ignore: exclude,
64
+ nodir: true,
65
+ });
66
+
67
+ files.push(...matchedFiles.filter(f => this.isSupportedFile(f)));
68
+ } else if (stat.isFile() && this.isSupportedFile(targetPath)) {
69
+ files.push(targetPath);
70
+ }
71
+ }
72
+
73
+ for (const file of files) {
74
+ const fileFindings = await this.scanFile(file);
75
+ findings.push(...fileFindings);
76
+ }
77
+
78
+ return {
79
+ files: files.length,
80
+ findings: this.sortFindings(findings),
81
+ duration: Date.now() - startTime,
82
+ };
83
+ }
84
+
85
+ async scanStaged(): Promise<ScanResult> {
86
+ const startTime = Date.now();
87
+ const findings: Finding[] = [];
88
+
89
+ // Get staged files from git
90
+ const { execSync } = await import('child_process');
91
+ let stagedFiles: string[] = [];
92
+
93
+ try {
94
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
95
+ encoding: 'utf-8',
96
+ });
97
+ stagedFiles = output
98
+ .split('\n')
99
+ .filter(f => f.trim() && this.isSupportedFile(f))
100
+ .map(f => path.resolve(f));
101
+ } catch {
102
+ // Not in a git repo or git not available
103
+ return {
104
+ files: 0,
105
+ findings: [],
106
+ duration: Date.now() - startTime,
107
+ };
108
+ }
109
+
110
+ for (const file of stagedFiles) {
111
+ if (fs.existsSync(file)) {
112
+ const fileFindings = await this.scanFile(file);
113
+ findings.push(...fileFindings);
114
+ }
115
+ }
116
+
117
+ return {
118
+ files: stagedFiles.length,
119
+ findings: this.sortFindings(findings),
120
+ duration: Date.now() - startTime,
121
+ };
122
+ }
123
+
124
+ private async scanFile(filePath: string): Promise<Finding[]> {
125
+ const findings: Finding[] = [];
126
+ const ext = path.extname(filePath);
127
+ const language = SUPPORTED_EXTENSIONS[ext];
128
+
129
+ if (!language) return findings;
130
+
131
+ let code: string;
132
+ try {
133
+ code = fs.readFileSync(filePath, 'utf-8');
134
+ } catch {
135
+ return findings;
136
+ }
137
+
138
+ // Filter rules by language
139
+ const languageRules = this.rules.filter(rule =>
140
+ rule.languages.includes(language as 'javascript' | 'typescript' | 'python')
141
+ );
142
+
143
+ if (language === 'javascript' || language === 'typescript') {
144
+ // Try AST-based scanning
145
+ const ast = parseJavaScript(code, filePath);
146
+ if (ast) {
147
+ const astFindings = scanWithAST(ast, languageRules, {
148
+ code,
149
+ lines: code.split('\n'),
150
+ filePath,
151
+ });
152
+ findings.push(...astFindings);
153
+ }
154
+
155
+ // Also run pattern-based scanning
156
+ const patternFindings = scanWithPatterns(code, languageRules, filePath);
157
+ findings.push(...patternFindings);
158
+ } else if (language === 'python') {
159
+ const patternFindings = scanPythonWithPatterns(code, languageRules, filePath);
160
+ findings.push(...patternFindings);
161
+ }
162
+
163
+ // Deduplicate findings
164
+ return this.deduplicateFindings(findings);
165
+ }
166
+
167
+ private isSupportedFile(filePath: string): boolean {
168
+ const ext = path.extname(filePath);
169
+ return ext in SUPPORTED_EXTENSIONS;
170
+ }
171
+
172
+ private sortFindings(findings: Finding[]): Finding[] {
173
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
174
+ return findings.sort((a, b) => {
175
+ const severityDiff =
176
+ severityOrder[a.rule.severity] - severityOrder[b.rule.severity];
177
+ if (severityDiff !== 0) return severityDiff;
178
+ return a.file.localeCompare(b.file) || a.line - b.line;
179
+ });
180
+ }
181
+
182
+ private deduplicateFindings(findings: Finding[]): Finding[] {
183
+ const seen = new Set<string>();
184
+ return findings.filter(f => {
185
+ const key = `${f.rule.id}:${f.file}:${f.line}`;
186
+ if (seen.has(key)) return false;
187
+ seen.add(key);
188
+ return true;
189
+ });
190
+ }
191
+
192
+ getRules(): SecurityRule[] {
193
+ return this.rules;
194
+ }
195
+ }
@@ -0,0 +1,285 @@
1
+ import * as parser from '@babel/parser';
2
+ import traverse, { NodePath } from '@babel/traverse';
3
+ import * as t from '@babel/types';
4
+ import { Finding, SecurityRule } from '../../types';
5
+
6
+ interface ASTContext {
7
+ code: string;
8
+ lines: string[];
9
+ filePath: string;
10
+ }
11
+
12
+ export function parseJavaScript(code: string, filePath: string): t.File | null {
13
+ try {
14
+ return parser.parse(code, {
15
+ sourceType: 'module',
16
+ plugins: [
17
+ 'jsx',
18
+ 'typescript',
19
+ 'decorators-legacy',
20
+ 'classProperties',
21
+ 'optionalChaining',
22
+ 'nullishCoalescingOperator',
23
+ ],
24
+ });
25
+ } catch {
26
+ // If parsing fails, return null and rely on regex patterns only
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export function scanWithAST(
32
+ ast: t.File,
33
+ rules: SecurityRule[],
34
+ context: ASTContext
35
+ ): Finding[] {
36
+ const findings: Finding[] = [];
37
+
38
+ const astMatchers: Record<string, (path: NodePath) => Finding | null> = {
39
+ 'eval-usage': (path: NodePath) => {
40
+ if (
41
+ path.isCallExpression() &&
42
+ t.isIdentifier(path.node.callee) &&
43
+ path.node.callee.name === 'eval'
44
+ ) {
45
+ const rule = rules.find(r => r.id === 'eval-usage');
46
+ if (rule) {
47
+ const loc = path.node.loc;
48
+ return {
49
+ rule,
50
+ file: context.filePath,
51
+ line: loc?.start.line || 0,
52
+ column: loc?.start.column || 0,
53
+ code: context.lines[(loc?.start.line || 1) - 1] || '',
54
+ message: rule.description,
55
+ };
56
+ }
57
+ }
58
+ return null;
59
+ },
60
+
61
+ 'sql-injection': (path: NodePath) => {
62
+ if (path.isTemplateLiteral() || path.isBinaryExpression()) {
63
+ const parent = path.parentPath;
64
+ if (
65
+ parent?.isCallExpression() &&
66
+ t.isMemberExpression(parent.node.callee)
67
+ ) {
68
+ const callee = parent.node.callee;
69
+ const methodName = t.isIdentifier(callee.property) ? callee.property.name : '';
70
+
71
+ if (['query', 'exec', 'execute', 'raw'].includes(methodName)) {
72
+ // Check if there's string concatenation or template literal with expressions
73
+ if (
74
+ path.isTemplateLiteral() &&
75
+ path.node.expressions.length > 0
76
+ ) {
77
+ const codeSnippet = context.code.substring(
78
+ path.node.start || 0,
79
+ path.node.end || 0
80
+ ).toLowerCase();
81
+
82
+ if (
83
+ codeSnippet.includes('select') ||
84
+ codeSnippet.includes('insert') ||
85
+ codeSnippet.includes('update') ||
86
+ codeSnippet.includes('delete') ||
87
+ codeSnippet.includes('where')
88
+ ) {
89
+ const rule = rules.find(r => r.id === 'sql-injection');
90
+ if (rule) {
91
+ const loc = path.node.loc;
92
+ return {
93
+ rule,
94
+ file: context.filePath,
95
+ line: loc?.start.line || 0,
96
+ column: loc?.start.column || 0,
97
+ code: context.lines[(loc?.start.line || 1) - 1] || '',
98
+ message: rule.description,
99
+ };
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return null;
107
+ },
108
+
109
+ 'xss-innerhtml': (path: NodePath) => {
110
+ if (path.isAssignmentExpression()) {
111
+ const left = path.node.left;
112
+ if (
113
+ t.isMemberExpression(left) &&
114
+ t.isIdentifier(left.property) &&
115
+ left.property.name === 'innerHTML'
116
+ ) {
117
+ const rule = rules.find(r => r.id === 'xss-innerhtml');
118
+ if (rule) {
119
+ const loc = path.node.loc;
120
+ return {
121
+ rule,
122
+ file: context.filePath,
123
+ line: loc?.start.line || 0,
124
+ column: loc?.start.column || 0,
125
+ code: context.lines[(loc?.start.line || 1) - 1] || '',
126
+ message: rule.description,
127
+ };
128
+ }
129
+ }
130
+ }
131
+
132
+ // Check for dangerouslySetInnerHTML in JSX
133
+ if (path.isJSXAttribute()) {
134
+ const name = path.node.name;
135
+ if (t.isJSXIdentifier(name) && name.name === 'dangerouslySetInnerHTML') {
136
+ const rule = rules.find(r => r.id === 'xss-innerhtml');
137
+ if (rule) {
138
+ const loc = path.node.loc;
139
+ return {
140
+ rule,
141
+ file: context.filePath,
142
+ line: loc?.start.line || 0,
143
+ column: loc?.start.column || 0,
144
+ code: context.lines[(loc?.start.line || 1) - 1] || '',
145
+ message: rule.description,
146
+ };
147
+ }
148
+ }
149
+ }
150
+
151
+ return null;
152
+ },
153
+
154
+ 'missing-auth': (path: NodePath) => {
155
+ // Detect Express/Next.js API routes without auth checks
156
+ if (path.isCallExpression()) {
157
+ const callee = path.node.callee;
158
+ if (
159
+ t.isMemberExpression(callee) &&
160
+ t.isIdentifier(callee.property) &&
161
+ ['get', 'post', 'put', 'delete', 'patch'].includes(callee.property.name)
162
+ ) {
163
+ const args = path.node.arguments;
164
+ if (args.length >= 2) {
165
+ // Get the full call expression code to check for middleware
166
+ const callCode = context.code.substring(
167
+ path.node.start || 0,
168
+ path.node.end || 0
169
+ ).toLowerCase();
170
+
171
+ // Check if any argument mentions auth-related terms (middleware)
172
+ const hasAuthMiddleware =
173
+ callCode.includes('auth') ||
174
+ callCode.includes('session') ||
175
+ callCode.includes('token') ||
176
+ callCode.includes('jwt') ||
177
+ callCode.includes('middleware') ||
178
+ callCode.includes('isauthenticated') ||
179
+ callCode.includes('requireauth') ||
180
+ callCode.includes('protect') ||
181
+ callCode.includes('verify');
182
+
183
+ if (!hasAuthMiddleware) {
184
+ // Check route path for sensitive endpoints
185
+ const routePath = args[0];
186
+ if (t.isStringLiteral(routePath)) {
187
+ const pathValue = routePath.value.toLowerCase();
188
+ if (
189
+ pathValue.includes('/api/') ||
190
+ pathValue.includes('/user') ||
191
+ pathValue.includes('/admin') ||
192
+ pathValue.includes('/account') ||
193
+ pathValue.includes('/profile')
194
+ ) {
195
+ const rule = rules.find(r => r.id === 'missing-auth-route');
196
+ if (rule) {
197
+ const loc = path.node.loc;
198
+ return {
199
+ rule,
200
+ file: context.filePath,
201
+ line: loc?.start.line || 0,
202
+ column: loc?.start.column || 0,
203
+ code: context.lines[(loc?.start.line || 1) - 1] || '',
204
+ message: `API route ${routePath.value} may be missing authentication`,
205
+ };
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ return null;
214
+ },
215
+ };
216
+
217
+ traverse(ast, {
218
+ enter(path) {
219
+ for (const matcherKey of Object.keys(astMatchers)) {
220
+ const finding = astMatchers[matcherKey](path);
221
+ if (finding) {
222
+ // Avoid duplicates
223
+ const isDuplicate = findings.some(
224
+ f =>
225
+ f.rule.id === finding.rule.id &&
226
+ f.line === finding.line &&
227
+ f.file === finding.file
228
+ );
229
+ if (!isDuplicate) {
230
+ findings.push(finding);
231
+ }
232
+ }
233
+ }
234
+ },
235
+ });
236
+
237
+ return findings;
238
+ }
239
+
240
+ export function scanWithPatterns(
241
+ code: string,
242
+ rules: SecurityRule[],
243
+ filePath: string
244
+ ): Finding[] {
245
+ const findings: Finding[] = [];
246
+ const lines = code.split('\n');
247
+
248
+ for (const rule of rules) {
249
+ if (!rule.patterns) continue;
250
+
251
+ for (const pattern of rule.patterns) {
252
+ let match;
253
+ const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g'));
254
+
255
+ while ((match = regex.exec(code)) !== null) {
256
+ // Calculate line number from match index
257
+ const beforeMatch = code.substring(0, match.index);
258
+ const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
259
+ const lineStart = beforeMatch.lastIndexOf('\n') + 1;
260
+ const column = match.index - lineStart;
261
+
262
+ // Avoid duplicates
263
+ const isDuplicate = findings.some(
264
+ f =>
265
+ f.rule.id === rule.id &&
266
+ f.line === lineNumber &&
267
+ f.file === filePath
268
+ );
269
+
270
+ if (!isDuplicate) {
271
+ findings.push({
272
+ rule,
273
+ file: filePath,
274
+ line: lineNumber,
275
+ column,
276
+ code: lines[lineNumber - 1] || '',
277
+ message: rule.description,
278
+ });
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ return findings;
285
+ }
@@ -0,0 +1,126 @@
1
+ import { Finding, SecurityRule } from '../../types';
2
+
3
+ // Python-specific patterns in addition to shared patterns
4
+ const pythonPatterns: { ruleId: string; pattern: RegExp }[] = [
5
+ // SQL injection patterns
6
+ {
7
+ ruleId: 'sql-injection',
8
+ pattern: /cursor\.execute\s*\(\s*f?['"`](?:SELECT|INSERT|UPDATE|DELETE)[^'"`]*\{[^}]+\}/i,
9
+ },
10
+ {
11
+ ruleId: 'sql-injection',
12
+ pattern: /\.execute\s*\(\s*['"`](?:SELECT|INSERT|UPDATE|DELETE)[^'"`]*['"]\s*%\s*/i,
13
+ },
14
+ {
15
+ ruleId: 'sql-injection',
16
+ pattern: /\.execute\s*\(\s*['"`](?:SELECT|INSERT|UPDATE|DELETE)[^'"`]*['"`]\s*\+/i,
17
+ },
18
+ // Eval usage
19
+ {
20
+ ruleId: 'eval-usage',
21
+ pattern: /\beval\s*\([^)]*(?:input|request|data|params|args)/i,
22
+ },
23
+ {
24
+ ruleId: 'eval-usage',
25
+ pattern: /\bexec\s*\([^)]*(?:input|request|data|params|args)/i,
26
+ },
27
+ // Hardcoded secrets
28
+ {
29
+ ruleId: 'hardcoded-secret',
30
+ pattern: /(?:api[_-]?key|secret[_-]?key|password|token)\s*=\s*['"][^'"]{8,}['"]/i,
31
+ },
32
+ // Flask/Django without CSRF
33
+ {
34
+ ruleId: 'permissive-cors',
35
+ pattern: /CORS\s*\(\s*app\s*(?:,\s*resources\s*=\s*\{[^}]*\*[^}]*\})?/,
36
+ },
37
+ // Debug mode in production
38
+ {
39
+ ruleId: 'verbose-errors',
40
+ pattern: /app\.run\s*\([^)]*debug\s*=\s*True/i,
41
+ },
42
+ {
43
+ ruleId: 'verbose-errors',
44
+ pattern: /DEBUG\s*=\s*True/,
45
+ },
46
+ ];
47
+
48
+ export function scanPythonWithPatterns(
49
+ code: string,
50
+ rules: SecurityRule[],
51
+ filePath: string
52
+ ): Finding[] {
53
+ const findings: Finding[] = [];
54
+ const lines = code.split('\n');
55
+
56
+ // First, scan with rule-defined patterns
57
+ for (const rule of rules) {
58
+ if (!rule.patterns || !rule.languages.includes('python')) continue;
59
+
60
+ for (const pattern of rule.patterns) {
61
+ let match;
62
+ const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g'));
63
+
64
+ while ((match = regex.exec(code)) !== null) {
65
+ const beforeMatch = code.substring(0, match.index);
66
+ const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
67
+ const lineStart = beforeMatch.lastIndexOf('\n') + 1;
68
+ const column = match.index - lineStart;
69
+
70
+ const isDuplicate = findings.some(
71
+ f =>
72
+ f.rule.id === rule.id &&
73
+ f.line === lineNumber &&
74
+ f.file === filePath
75
+ );
76
+
77
+ if (!isDuplicate) {
78
+ findings.push({
79
+ rule,
80
+ file: filePath,
81
+ line: lineNumber,
82
+ column,
83
+ code: lines[lineNumber - 1] || '',
84
+ message: rule.description,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // Then scan with Python-specific patterns
92
+ for (const { ruleId, pattern } of pythonPatterns) {
93
+ const rule = rules.find(r => r.id === ruleId);
94
+ if (!rule) continue;
95
+
96
+ let match;
97
+ const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g'));
98
+
99
+ while ((match = regex.exec(code)) !== null) {
100
+ const beforeMatch = code.substring(0, match.index);
101
+ const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
102
+ const lineStart = beforeMatch.lastIndexOf('\n') + 1;
103
+ const column = match.index - lineStart;
104
+
105
+ const isDuplicate = findings.some(
106
+ f =>
107
+ f.rule.id === ruleId &&
108
+ f.line === lineNumber &&
109
+ f.file === filePath
110
+ );
111
+
112
+ if (!isDuplicate) {
113
+ findings.push({
114
+ rule,
115
+ file: filePath,
116
+ line: lineNumber,
117
+ column,
118
+ code: lines[lineNumber - 1] || '',
119
+ message: rule.description,
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ return findings;
126
+ }