@rigour-labs/core 4.3.2 → 4.3.3

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.
@@ -36,9 +36,10 @@ export class ContextWindowArtifactsGate extends Gate {
36
36
  if (!this.config.enabled)
37
37
  return [];
38
38
  const failures = [];
39
+ const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx,py}'];
39
40
  const files = await FileScanner.findFiles({
40
41
  cwd: context.cwd,
41
- patterns: ['**/*.{ts,js,tsx,jsx,py}'],
42
+ patterns: scanPatterns,
42
43
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.min.*'],
43
44
  });
44
45
  Logger.info(`Context Window Artifacts: Scanning ${files.length} files`);
@@ -48,9 +48,11 @@ export class DeprecatedApisGate extends Gate {
48
48
  return [];
49
49
  const failures = [];
50
50
  const deprecated = [];
51
+ const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'];
52
+ const scanPatterns = context.patterns || defaultPatterns;
51
53
  const files = await FileScanner.findFiles({
52
54
  cwd: context.cwd,
53
- patterns: ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'],
55
+ patterns: scanPatterns,
54
56
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
55
57
  '**/*.test.*', '**/*.spec.*', '**/__tests__/**',
56
58
  '**/.venv/**', '**/venv/**', '**/vendor/**', '**/__pycache__/**',
@@ -33,9 +33,10 @@ export class DuplicationDriftGate extends Gate {
33
33
  return [];
34
34
  const failures = [];
35
35
  const functions = [];
36
+ const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx,py}'];
36
37
  const files = await FileScanner.findFiles({
37
38
  cwd: context.cwd,
38
- patterns: ['**/*.{ts,js,tsx,jsx,py}'],
39
+ patterns: scanPatterns,
39
40
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
40
41
  });
41
42
  Logger.info(`Duplication Drift: Scanning ${files.length} files`);
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Frontend Secret Exposure Gate
3
+ *
4
+ * Detects server-side secret keys (Stripe, OpenAI, AWS, etc.) referenced
5
+ * in frontend-accessible files where they would be bundled into client JS.
6
+ *
7
+ * The "vibe-coded startup" attack pattern:
8
+ * process.env.STRIPE_SECRET_KEY inside a React component
9
+ * → bundled by webpack/Vite → visible in DevTools → $2,500 in fraudulent charges
10
+ *
11
+ * Two detection modes:
12
+ * 1. Env-var name detection: process.env.VARNAME / import.meta.env.VARNAME
13
+ * where VARNAME matches secret_env_name_patterns and lacks a safe public prefix.
14
+ * 2. Literal key detection: actual live API key values embedded in source
15
+ * (sk_live_..., sk-proj-..., AKIA..., ghp_..., etc.)
16
+ *
17
+ * @since v4.4.0
18
+ * @see CWE-312 Cleartext Storage of Sensitive Information
19
+ */
1
20
  import { Gate, GateContext } from './base.js';
2
21
  import { Failure, Provenance } from '../types/index.js';
3
22
  export interface FrontendSecretExposureConfig {
@@ -12,16 +31,29 @@ export interface FrontendSecretExposureConfig {
12
31
  allowlist_env_names?: string[];
13
32
  }
14
33
  export declare class FrontendSecretExposureGate extends Gate {
15
- private config;
16
- private severityOrder;
34
+ private cfg;
35
+ private secretNamePatterns;
36
+ private safePrefixPatterns;
37
+ private frontendPathPatterns;
38
+ private serverPathPatterns;
17
39
  constructor(config?: FrontendSecretExposureConfig);
18
40
  protected get provenance(): Provenance;
19
41
  run(context: GateContext): Promise<Failure[]>;
20
- private shouldSkipFile;
21
- private isClientBundled;
22
- private isServerOnlyContent;
23
- private findEnvExposures;
24
- private collectMatches;
25
- private isSecretLikeEnvName;
26
- private matchesAnyPattern;
42
+ /** Classify a file path as frontend, server, or ambiguous based on config patterns. */
43
+ private classifyFile;
44
+ /** Test/fixture files are excluded — they legitimately contain dummy keys. */
45
+ private isTestFile;
46
+ /**
47
+ * Check whether a var name looks like a server-side secret:
48
+ * - Matches at least one secret_env_name_pattern
49
+ * - Does NOT start with a safe public prefix
50
+ * - Is NOT in the user allowlist
51
+ */
52
+ private isSecretVar;
53
+ /** Scan one file for env-var references and literal key values. */
54
+ private scanFile;
55
+ private makeEnvRefExposure;
56
+ /** Filters out trivial dummy/placeholder text to reduce false positives. */
57
+ private isDummyValue;
58
+ private toFailures;
27
59
  }
@@ -1,174 +1,354 @@
1
+ /**
2
+ * Frontend Secret Exposure Gate
3
+ *
4
+ * Detects server-side secret keys (Stripe, OpenAI, AWS, etc.) referenced
5
+ * in frontend-accessible files where they would be bundled into client JS.
6
+ *
7
+ * The "vibe-coded startup" attack pattern:
8
+ * process.env.STRIPE_SECRET_KEY inside a React component
9
+ * → bundled by webpack/Vite → visible in DevTools → $2,500 in fraudulent charges
10
+ *
11
+ * Two detection modes:
12
+ * 1. Env-var name detection: process.env.VARNAME / import.meta.env.VARNAME
13
+ * where VARNAME matches secret_env_name_patterns and lacks a safe public prefix.
14
+ * 2. Literal key detection: actual live API key values embedded in source
15
+ * (sk_live_..., sk-proj-..., AKIA..., ghp_..., etc.)
16
+ *
17
+ * @since v4.4.0
18
+ * @see CWE-312 Cleartext Storage of Sensitive Information
19
+ */
1
20
  import { Gate } from './base.js';
2
21
  import { FileScanner } from '../utils/scanner.js';
3
22
  import { Logger } from '../utils/logger.js';
4
23
  import fs from 'fs-extra';
5
24
  import path from 'path';
6
- const DEFAULT_SECRET_ENV_NAME_PATTERNS = [
7
- '(?:^|_)(?:secret|private)(?:_|$)',
8
- '(?:^|_)(?:token|api[_-]?key|access[_-]?key|client[_-]?secret|signing|webhook)(?:_|$)',
9
- '(?:^|_)(?:db[_-]?url|database[_-]?url|connection[_-]?string)(?:_|$)',
10
- ];
11
- const DEFAULT_SAFE_PUBLIC_PREFIXES = [
12
- 'NEXT_PUBLIC_',
13
- 'VITE_',
14
- 'PUBLIC_',
15
- 'NUXT_PUBLIC_',
16
- 'REACT_APP_',
17
- ];
18
- const DEFAULT_FRONTEND_PATH_PATTERNS = [
19
- '(^|/)pages/(?!api/)',
20
- '(^|/)components/',
21
- '(^|/)src/components/',
22
- '(^|/)src/views/',
23
- '(^|/)src/app/',
24
- '(^|/)app/(?!api/)',
25
- '(^|/)views/',
26
- '(^|/)public/',
27
- ];
28
- const DEFAULT_SERVER_PATH_PATTERNS = [
29
- '(^|/)pages/api/',
30
- '(^|/)src/pages/api/',
31
- '(^|/)app/api/',
32
- '(^|/)src/app/api/',
33
- '\\.server\\.(?:ts|tsx|js|jsx|mjs|cjs)$',
25
+ /**
26
+ * Literal API key patterns — actual values that are always dangerous in frontend code.
27
+ * These bypass the env-name heuristic and fire regardless of variable naming.
28
+ */
29
+ const LITERAL_KEY_PATTERNS = [
30
+ {
31
+ label: 'Stripe Live Secret Key',
32
+ regex: /\bsk_live_[a-zA-Z0-9]{24,}\b/g,
33
+ severity: 'critical',
34
+ hint: 'Live Stripe secret key (sk_live_...) exposed in client bundle. ' +
35
+ 'Attackers can charge cards without a frontend. Move to server-side only.',
36
+ cwe: 'CWE-312',
37
+ },
38
+ {
39
+ label: 'Stripe Live Restricted Key',
40
+ regex: /\brk_live_[a-zA-Z0-9]{24,}\b/g,
41
+ severity: 'critical',
42
+ hint: 'Live Stripe restricted key (rk_live_...) must never appear in frontend code.',
43
+ cwe: 'CWE-312',
44
+ },
45
+ {
46
+ label: 'OpenAI API Key',
47
+ regex: /\bsk-proj-[a-zA-Z0-9_-]{40,}\b/g,
48
+ severity: 'critical',
49
+ hint: 'OpenAI project API key (sk-proj-...) exposed in client bundle. ' +
50
+ 'Attackers can make unlimited API calls at your expense.',
51
+ cwe: 'CWE-312',
52
+ },
53
+ {
54
+ label: 'OpenAI Legacy API Key',
55
+ regex: /\bsk-[a-zA-Z0-9]{48,}\b/g,
56
+ severity: 'critical',
57
+ hint: 'OpenAI API key (sk-...) exposed in client bundle. Move to server-side proxy.',
58
+ cwe: 'CWE-312',
59
+ },
60
+ {
61
+ label: 'AWS Access Key ID',
62
+ regex: /\bAKIA[A-Z2-7]{16}\b/g,
63
+ severity: 'critical',
64
+ hint: 'AWS Access Key ID exposed in client bundle. This grants AWS API access.',
65
+ cwe: 'CWE-312',
66
+ },
67
+ {
68
+ label: 'GitHub Personal Access Token',
69
+ regex: /\bghp_[a-zA-Z0-9]{36,}\b/g,
70
+ severity: 'critical',
71
+ hint: 'GitHub PAT (ghp_...) in frontend code. Grants repo access to anyone who views bundle.',
72
+ cwe: 'CWE-312',
73
+ },
74
+ {
75
+ label: 'GitHub Fine-Grained PAT',
76
+ regex: /\bgithub_pat_[a-zA-Z0-9_]{55,}\b/g,
77
+ severity: 'critical',
78
+ hint: 'GitHub fine-grained PAT (github_pat_...) must never appear in frontend code.',
79
+ cwe: 'CWE-312',
80
+ },
81
+ {
82
+ label: 'Anthropic API Key',
83
+ regex: /\bsk-ant-api\d{2}-[a-zA-Z0-9_-]{90,}\b/g,
84
+ severity: 'critical',
85
+ hint: 'Anthropic API key exposed in client bundle. Move to a server-side proxy.',
86
+ cwe: 'CWE-312',
87
+ },
88
+ {
89
+ label: 'SendGrid API Key',
90
+ regex: /\bSG\.[a-zA-Z0-9_-]{22,}\.[a-zA-Z0-9_-]{43,}\b/g,
91
+ severity: 'critical',
92
+ hint: 'SendGrid API key exposed in client bundle. Attackers can send spam as you.',
93
+ cwe: 'CWE-312',
94
+ },
34
95
  ];
96
+ /** Severity ordering for threshold comparison */
97
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
98
+ /**
99
+ * Extracts the env var name from a single-reference line.
100
+ * process.env.VARNAME → "VARNAME"
101
+ * process.env["VARNAME"] → "VARNAME"
102
+ * import.meta.env.VARNAME → "VARNAME"
103
+ * Returns null if no match.
104
+ */
105
+ function extractEnvVarName(line, checkProcessEnv, checkImportMetaEnv) {
106
+ if (checkProcessEnv) {
107
+ const dot = line.match(/process\.env\.([A-Z][A-Z0-9_]*)/);
108
+ if (dot)
109
+ return dot[1];
110
+ const bracket = line.match(/process\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/);
111
+ if (bracket)
112
+ return bracket[1];
113
+ }
114
+ if (checkImportMetaEnv) {
115
+ const vite = line.match(/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/);
116
+ if (vite)
117
+ return vite[1];
118
+ }
119
+ return null;
120
+ }
121
+ /**
122
+ * Finds all var names from destructuring:
123
+ * const { STRIPE_SECRET_KEY, OPENAI_KEY } = process.env
124
+ */
125
+ function extractDestructuredEnvVars(line) {
126
+ if (!/=\s*process\.env\b/.test(line))
127
+ return [];
128
+ const brace = line.match(/\{\s*([^}]+)\s*\}/);
129
+ if (!brace)
130
+ return [];
131
+ return brace[1]
132
+ .split(',')
133
+ .map(s => s.split(':')[0].trim()) // handle "SECRET_KEY: renamed"
134
+ .filter(s => /^[A-Z_][A-Z0-9_]*$/.test(s));
135
+ }
35
136
  export class FrontendSecretExposureGate extends Gate {
36
- config;
37
- severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
137
+ cfg;
138
+ secretNamePatterns;
139
+ safePrefixPatterns;
140
+ frontendPathPatterns;
141
+ serverPathPatterns;
38
142
  constructor(config = {}) {
39
- super('frontend-secret-exposure', 'Frontend Secret Exposure Detection');
40
- this.config = {
143
+ super('frontend-secret-exposure', 'Frontend Secret Exposure');
144
+ this.cfg = {
41
145
  enabled: config.enabled ?? true,
42
146
  block_on_severity: config.block_on_severity ?? 'high',
43
147
  check_process_env: config.check_process_env ?? true,
44
148
  check_import_meta_env: config.check_import_meta_env ?? true,
45
- secret_env_name_patterns: config.secret_env_name_patterns ?? DEFAULT_SECRET_ENV_NAME_PATTERNS,
46
- safe_public_prefixes: config.safe_public_prefixes ?? DEFAULT_SAFE_PUBLIC_PREFIXES,
47
- frontend_path_patterns: config.frontend_path_patterns ?? DEFAULT_FRONTEND_PATH_PATTERNS,
48
- server_path_patterns: config.server_path_patterns ?? DEFAULT_SERVER_PATH_PATTERNS,
149
+ secret_env_name_patterns: config.secret_env_name_patterns ?? [
150
+ '(?:^|_)(?:secret|private)(?:_|$)',
151
+ '(?:^|_)(?:token|api[_-]?key|access[_-]?key|client[_-]?secret|signing|webhook)(?:_|$)',
152
+ '(?:^|_)(?:db[_-]?url|database[_-]?url|connection[_-]?string)(?:_|$)',
153
+ ],
154
+ safe_public_prefixes: config.safe_public_prefixes ?? [
155
+ 'NEXT_PUBLIC_', 'VITE_', 'PUBLIC_', 'NUXT_PUBLIC_', 'REACT_APP_',
156
+ ],
157
+ frontend_path_patterns: config.frontend_path_patterns ?? [
158
+ '(^|/)pages/(?!api/)',
159
+ '(^|/)components/',
160
+ '(^|/)src/components/',
161
+ '(^|/)src/views/',
162
+ '(^|/)src/app/',
163
+ '(^|/)app/(?!api/)',
164
+ '(^|/)views/',
165
+ '(^|/)public/',
166
+ ],
167
+ server_path_patterns: config.server_path_patterns ?? [
168
+ '(^|/)pages/api/',
169
+ '(^|/)src/pages/api/',
170
+ '(^|/)app/api/',
171
+ '(^|/)src/app/api/',
172
+ '\\.server\\.(?:ts|tsx|js|jsx|mjs|cjs)$',
173
+ ],
49
174
  allowlist_env_names: config.allowlist_env_names ?? [],
50
175
  };
176
+ this.secretNamePatterns = this.cfg.secret_env_name_patterns.map(p => new RegExp(p, 'i'));
177
+ // Escape prefix strings to safe regex literals
178
+ this.safePrefixPatterns = this.cfg.safe_public_prefixes.map(prefix => new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
179
+ this.frontendPathPatterns = this.cfg.frontend_path_patterns.map(p => new RegExp(p));
180
+ this.serverPathPatterns = this.cfg.server_path_patterns.map(p => new RegExp(p));
51
181
  }
52
182
  get provenance() { return 'security'; }
53
183
  async run(context) {
54
- if (!this.config.enabled)
184
+ if (!this.cfg.enabled)
55
185
  return [];
186
+ const scanPatterns = context.patterns || ['**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte}'];
56
187
  const files = await FileScanner.findFiles({
57
188
  cwd: context.cwd,
58
- patterns: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
59
- ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
189
+ patterns: scanPatterns,
190
+ ignore: [
191
+ ...(context.ignore || []),
192
+ '**/node_modules/**', '**/dist/**', '**/build/**',
193
+ '**/.next/**', '**/coverage/**', '**/out/**',
194
+ ],
60
195
  });
61
- const scanFiles = files.filter(file => !this.shouldSkipFile(file));
62
- const findings = [];
63
- Logger.info(`Frontend Secret Exposure Gate: Scanning ${scanFiles.length} files`);
64
- for (const file of scanFiles) {
65
- const fullPath = path.join(context.cwd, file);
66
- let content = '';
196
+ // Skip test/fixture files they routinely use dummy keys
197
+ const sourceFiles = files.filter(f => !this.isTestFile(f));
198
+ Logger.info(`Frontend Secret Exposure Gate: scanning ${sourceFiles.length} files`);
199
+ const exposures = [];
200
+ for (const file of sourceFiles) {
201
+ const fileContext = this.classifyFile(file);
202
+ if (fileContext === 'server')
203
+ continue;
67
204
  try {
68
- content = await fs.readFile(fullPath, 'utf-8');
205
+ const fullPath = path.join(context.cwd, file);
206
+ const content = await fs.readFile(fullPath, 'utf-8');
207
+ // Skip files guarded by Next.js server-only import
208
+ if (/import\s+['"]server-only['"]/.test(content))
209
+ continue;
210
+ this.scanFile(content, file, fileContext, exposures);
69
211
  }
70
212
  catch {
71
- continue;
213
+ // Unreadable — skip silently
72
214
  }
73
- if (!this.isClientBundled(file, content))
74
- continue;
75
- findings.push(...this.findEnvExposures(file, content));
76
215
  }
77
- findings.sort((a, b) => this.severityOrder[a.severity] - this.severityOrder[b.severity]);
78
- const threshold = this.severityOrder[this.config.block_on_severity];
79
- return findings
80
- .filter(f => this.severityOrder[f.severity] <= threshold)
81
- .map(f => this.createFailure(`Potential frontend secret exposure: ${f.source}.${f.envVar} is referenced in client-bundled code.`, [f.file], 'Move secret usage to server-only code (API route/server action) and expose only public-safe values.', 'Security: Frontend Secret Exposure', f.line, f.line, f.severity));
216
+ return this.toFailures(exposures);
82
217
  }
83
- shouldSkipFile(file) {
84
- const normalized = file.replace(/\\/g, '/');
85
- if (/\.(test|spec)\.(?:ts|tsx|js|jsx|mjs|cjs)$/i.test(normalized))
86
- return true;
87
- if (/\/(?:__tests__|tests|test|__test__|e2e|fixtures|mocks)\//.test(`/${normalized}`))
88
- return true;
89
- if (/\/(?:examples|studio-dist)\//.test(`/${normalized}`))
90
- return true;
91
- return false;
218
+ /** Classify a file path as frontend, server, or ambiguous based on config patterns. */
219
+ classifyFile(file) {
220
+ const n = file.replace(/\\/g, '/');
221
+ // Server paths take priority over frontend paths
222
+ if (this.serverPathPatterns.some(p => p.test(n)))
223
+ return 'server';
224
+ if (this.frontendPathPatterns.some(p => p.test(n)))
225
+ return 'frontend';
226
+ return 'ambiguous';
227
+ }
228
+ /** Test/fixture files are excluded — they legitimately contain dummy keys. */
229
+ isTestFile(file) {
230
+ const n = file.replace(/\\/g, '/');
231
+ return /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(n)
232
+ || /\/__tests__\//.test(n)
233
+ || /\/(?:fixtures|mocks|stubs)\//.test(n);
92
234
  }
93
- isClientBundled(file, content) {
94
- const normalized = file.replace(/\\/g, '/');
95
- if (this.matchesAnyPattern(normalized, this.config.server_path_patterns))
235
+ /**
236
+ * Check whether a var name looks like a server-side secret:
237
+ * - Matches at least one secret_env_name_pattern
238
+ * - Does NOT start with a safe public prefix
239
+ * - Is NOT in the user allowlist
240
+ */
241
+ isSecretVar(varName) {
242
+ if (this.cfg.allowlist_env_names.includes(varName))
96
243
  return false;
97
- if (this.isServerOnlyContent(content))
244
+ if (this.safePrefixPatterns.some(p => p.test(varName)))
98
245
  return false;
99
- if (/^\s*['"]use client['"]\s*;?/m.test(content))
100
- return true;
101
- if (this.matchesAnyPattern(normalized, this.config.frontend_path_patterns))
102
- return true;
103
- return false;
104
- }
105
- isServerOnlyContent(content) {
106
- if (/from\s+['"]server-only['"]/.test(content))
107
- return true;
108
- if (/import\s+['"]server-only['"]/.test(content))
109
- return true;
110
- if (/export\s+async\s+function\s+getServerSideProps\s*\(/.test(content))
111
- return true;
112
- if (/export\s+async\s+function\s+getStaticProps\s*\(/.test(content))
113
- return true;
114
- if (/['"]use server['"]/.test(content))
115
- return true;
116
- return false;
117
- }
118
- findEnvExposures(file, content) {
119
- const findings = [];
120
- if (this.config.check_process_env) {
121
- const processEnvRegex = /process\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
122
- findings.push(...this.collectMatches(file, content, processEnvRegex, 'process.env'));
123
- }
124
- if (this.config.check_import_meta_env) {
125
- const importMetaRegex = /import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
126
- findings.push(...this.collectMatches(file, content, importMetaRegex, 'import.meta.env'));
127
- }
128
- return findings;
246
+ return this.secretNamePatterns.some(p => p.test(varName));
129
247
  }
130
- collectMatches(file, content, regex, source) {
131
- const matches = [];
132
- const scanRegex = new RegExp(regex.source, 'g');
133
- for (const match of content.matchAll(scanRegex)) {
134
- const envVar = match[1];
135
- if (!this.isSecretLikeEnvName(envVar))
248
+ /** Scan one file for env-var references and literal key values. */
249
+ scanFile(content, file, fileContext, out) {
250
+ const lines = content.split('\n');
251
+ for (let i = 0; i < lines.length; i++) {
252
+ const line = lines[i];
253
+ const lineNum = i + 1;
254
+ // Skip comment lines to avoid flagging docs/examples
255
+ const trimmed = line.trimStart();
256
+ if (trimmed.startsWith('//') ||
257
+ trimmed.startsWith('*') ||
258
+ trimmed.startsWith('#') ||
259
+ trimmed.startsWith('<!--'))
136
260
  continue;
137
- const startIndex = match.index ?? 0;
138
- const beforeMatch = content.slice(0, startIndex);
139
- const line = beforeMatch.split('\n').length;
140
- matches.push({
141
- file,
142
- line,
143
- envVar,
144
- source,
145
- severity: 'high',
146
- });
147
- }
148
- return matches;
149
- }
150
- isSecretLikeEnvName(envVar) {
151
- if (this.config.allowlist_env_names.includes(envVar))
152
- return false;
153
- if (this.config.safe_public_prefixes.some(prefix => envVar.startsWith(prefix)))
154
- return false;
155
- return this.config.secret_env_name_patterns.some(pattern => {
156
- try {
157
- return new RegExp(pattern, 'i').test(envVar);
261
+ // 1. Env-var name detection (process.env / import.meta.env)
262
+ if (this.cfg.check_process_env || this.cfg.check_import_meta_env) {
263
+ const single = extractEnvVarName(line, this.cfg.check_process_env, this.cfg.check_import_meta_env);
264
+ if (single && this.isSecretVar(single)) {
265
+ out.push(this.makeEnvRefExposure(file, lineNum, line, single, fileContext));
266
+ }
267
+ // Destructuring: const { STRIPE_SECRET_KEY } = process.env
268
+ if (this.cfg.check_process_env) {
269
+ for (const varName of extractDestructuredEnvVars(line)) {
270
+ if (this.isSecretVar(varName)) {
271
+ out.push(this.makeEnvRefExposure(file, lineNum, line, varName, fileContext));
272
+ }
273
+ }
274
+ }
158
275
  }
159
- catch {
160
- return false;
276
+ // 2. Literal API key detection
277
+ for (const pattern of LITERAL_KEY_PATTERNS) {
278
+ pattern.regex.lastIndex = 0;
279
+ let m;
280
+ while ((m = pattern.regex.exec(line)) !== null) {
281
+ // Skip dummy/placeholder values and test-mode keys
282
+ if (this.isDummyValue(m[0]))
283
+ continue;
284
+ if (/(?:sk_test_|pk_test_|_test_|_sandbox_)/i.test(m[0]))
285
+ continue;
286
+ out.push({
287
+ file,
288
+ line: lineNum,
289
+ lineText: line.trim().slice(0, 80),
290
+ varName: m[0].slice(0, 24) + (m[0].length > 24 ? '...' : ''),
291
+ kind: 'literal',
292
+ serviceLabel: pattern.label,
293
+ hint: pattern.hint,
294
+ cwe: pattern.cwe,
295
+ severity: pattern.severity,
296
+ fileContext,
297
+ });
298
+ }
161
299
  }
162
- });
300
+ }
163
301
  }
164
- matchesAnyPattern(value, patterns) {
165
- return patterns.some(pattern => {
166
- try {
167
- return new RegExp(pattern, 'i').test(value);
168
- }
169
- catch {
302
+ makeEnvRefExposure(file, line, lineText, varName, fileContext) {
303
+ // Definite frontend → critical; ambiguous → high (may be a shared util)
304
+ const severity = fileContext === 'frontend' ? 'critical' : 'high';
305
+ return {
306
+ file,
307
+ line,
308
+ lineText: lineText.trim().slice(0, 80),
309
+ varName,
310
+ kind: 'env-ref',
311
+ serviceLabel: 'Secret Env Var',
312
+ hint: `\`${varName}\` looks like a server-side secret but is referenced in a ` +
313
+ `client-bundled file. Use an API route (Next.js /api, Remix action, ` +
314
+ `Nuxt server route) to proxy calls instead of exposing the secret to the browser.`,
315
+ cwe: 'CWE-312',
316
+ severity,
317
+ fileContext,
318
+ };
319
+ }
320
+ /** Filters out trivial dummy/placeholder text to reduce false positives. */
321
+ isDummyValue(text) {
322
+ return /(?:example|placeholder|your[_-]|xxx+|dummy|fake|changeme)/i.test(text);
323
+ }
324
+ toFailures(exposures) {
325
+ if (exposures.length === 0)
326
+ return [];
327
+ const blockThreshold = SEVERITY_ORDER[this.cfg.block_on_severity];
328
+ // Deduplicate: same file + line + varName from parallel patterns
329
+ const seen = new Set();
330
+ const unique = exposures.filter(e => {
331
+ const key = `${e.file}:${e.line}:${e.varName}`;
332
+ if (seen.has(key))
170
333
  return false;
171
- }
334
+ seen.add(key);
335
+ return true;
172
336
  });
337
+ // Sort critical first
338
+ unique.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
339
+ const failures = [];
340
+ for (const exp of unique) {
341
+ if (SEVERITY_ORDER[exp.severity] > blockThreshold)
342
+ continue;
343
+ const ctx = exp.fileContext === 'frontend' ? '[definite frontend]' : '[possible frontend]';
344
+ const kind = exp.kind === 'literal' ? 'literal key' : 'env var ref';
345
+ failures.push(this.createFailure(`[${exp.cwe}] ${exp.serviceLabel} (${kind}) in ${ctx} file — ` +
346
+ `\`${exp.varName}\` at line ${exp.line}`, [exp.file], exp.hint, `Security: Frontend Secret Exposure — ${exp.serviceLabel}`, exp.line, exp.line, exp.severity));
347
+ }
348
+ if (unique.length > 0 && failures.length === 0) {
349
+ Logger.info(`Frontend Secret Exposure: ${unique.length} issue(s) below ` +
350
+ `${this.cfg.block_on_severity} threshold`);
351
+ }
352
+ return failures;
173
353
  }
174
354
  }
@@ -47,9 +47,11 @@ export class HallucinatedImportsGate extends Gate {
47
47
  return [];
48
48
  const failures = [];
49
49
  const hallucinated = [];
50
+ const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
51
+ const scanPatterns = context.patterns || defaultPatterns;
50
52
  const files = await FileScanner.findFiles({
51
53
  cwd: context.cwd,
52
- patterns: ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'],
54
+ patterns: scanPatterns,
53
55
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
54
56
  '**/examples/**',
55
57
  '**/studio-dist/**', '**/.next/**', '**/coverage/**',
@@ -41,9 +41,10 @@ export class InconsistentErrorHandlingGate extends Gate {
41
41
  return [];
42
42
  const failures = [];
43
43
  const handlers = [];
44
+ const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx}'];
44
45
  const files = await FileScanner.findFiles({
45
46
  cwd: context.cwd,
46
- patterns: ['**/*.{ts,js,tsx,jsx}'],
47
+ patterns: scanPatterns,
47
48
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
48
49
  });
49
50
  Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
@@ -45,9 +45,11 @@ export class PhantomApisGate extends Gate {
45
45
  return [];
46
46
  const failures = [];
47
47
  const phantoms = [];
48
+ const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'];
49
+ const scanPatterns = context.patterns || defaultPatterns;
48
50
  const files = await FileScanner.findFiles({
49
51
  cwd: context.cwd,
50
- patterns: ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'],
52
+ patterns: scanPatterns,
51
53
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
52
54
  '**/*.test.*', '**/*.spec.*', '**/__tests__/**',
53
55
  '**/.venv/**', '**/venv/**', '**/vendor/**', '**/__pycache__/**',
@@ -39,7 +39,7 @@ export function extractIndentedBody(content, startIdx) {
39
39
  }
40
40
  export function isInsideTryBlock(lines, lineIdx) {
41
41
  let braceDepth = 0;
42
- for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
42
+ for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
43
43
  const prevLine = stripStrings(lines[j]);
44
44
  for (const ch of prevLine) {
45
45
  if (ch === '}')
@@ -56,7 +56,7 @@ export function isInsideTryBlock(lines, lineIdx) {
56
56
  }
57
57
  export function isInsidePythonTry(lines, lineIdx) {
58
58
  const lineIndent = lines[lineIdx].length - lines[lineIdx].trimStart().length;
59
- for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
59
+ for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
60
60
  const trimmed = lines[j].trim();
61
61
  if (trimmed === '')
62
62
  continue;
@@ -71,7 +71,7 @@ export function isInsidePythonTry(lines, lineIdx) {
71
71
  return false;
72
72
  }
73
73
  export function isInsideRubyRescue(lines, lineIdx) {
74
- for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
74
+ for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
75
75
  const trimmed = lines[j].trim();
76
76
  if (trimmed === 'begin')
77
77
  return true;
@@ -83,14 +83,14 @@ export function isInsideRubyRescue(lines, lineIdx) {
83
83
  return false;
84
84
  }
85
85
  export function hasCatchAhead(lines, idx) {
86
- for (let j = idx; j < Math.min(idx + 10, lines.length); j++) {
86
+ for (let j = idx; j < Math.min(idx + 50, lines.length); j++) {
87
87
  if (/\.catch\s*\(/.test(lines[j]))
88
88
  return true;
89
89
  }
90
90
  return false;
91
91
  }
92
92
  export function hasStatusCheckAhead(lines, idx) {
93
- for (let j = idx; j < Math.min(idx + 10, lines.length); j++) {
93
+ for (let j = idx; j < Math.min(idx + 50, lines.length); j++) {
94
94
  if (/\.\s*ok\b/.test(lines[j]) || /\.status(?:Text)?\b/.test(lines[j]))
95
95
  return true;
96
96
  }
@@ -36,9 +36,10 @@ export class PromiseSafetyGate extends Gate {
36
36
  return [];
37
37
  const violations = [];
38
38
  const allPatterns = Object.values(LANG_GLOBS).flat();
39
+ const scanPatterns = context.patterns || allPatterns;
39
40
  const files = await FileScanner.findFiles({
40
41
  cwd: context.cwd,
41
- patterns: allPatterns,
42
+ patterns: scanPatterns,
42
43
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
43
44
  '**/*.test.*', '**/*.spec.*', '**/vendor/**', '**/__pycache__/**',
44
45
  '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**', '**/venv/**', '**/.venv/**'],
@@ -87,7 +88,7 @@ export class PromiseSafetyGate extends Gate {
87
88
  if (!/\.then\s*\(/.test(line))
88
89
  continue;
89
90
  let hasCatch = false;
90
- for (let j = i; j < Math.min(i + 10, lines.length); j++) {
91
+ for (let j = i; j < Math.min(i + 50, lines.length); j++) {
91
92
  const lookahead = this.sanitizeLine(lines[j]);
92
93
  if (/\.catch\s*\(/.test(lookahead)) {
93
94
  hasCatch = true;
@@ -209,7 +210,7 @@ export class PromiseSafetyGate extends Gate {
209
210
  if (!httpPatterns.test(lines[i]) || isInsidePythonTry(lines, i))
210
211
  continue;
211
212
  let hasCheck = false;
212
- for (let j = i; j < Math.min(i + 10, lines.length); j++) {
213
+ for (let j = i; j < Math.min(i + 50, lines.length); j++) {
213
214
  if (/raise_for_status|status_code/.test(lines[j])) {
214
215
  hasCheck = true;
215
216
  break;
@@ -317,7 +318,7 @@ export class PromiseSafetyGate extends Gate {
317
318
  for (let i = 0; i < lines.length; i++) {
318
319
  if (/\.(?:GetAsync|PostAsync|SendAsync)\s*\(/.test(lines[i]) && !isInsideTryBlock(lines, i)) {
319
320
  let hasCheck = false;
320
- for (let j = i; j < Math.min(i + 10, lines.length); j++) {
321
+ for (let j = i; j < Math.min(i + 50, lines.length); j++) {
321
322
  if (/EnsureSuccess|IsSuccessStatusCode|StatusCode/.test(lines[j])) {
322
323
  hasCheck = true;
323
324
  break;
@@ -199,13 +199,13 @@ export class GateRunner {
199
199
  }
200
200
  const isLocalDeepExecution = !deepOptions.apiKey || (deepOptions.provider || '').toLowerCase() === 'local';
201
201
  const deepTier = isLocalDeepExecution
202
- ? (deepOptions.pro ? 'pro' : 'deep')
202
+ ? (deepOptions.pro ? 'deep' : 'lite')
203
203
  : 'cloud';
204
204
  deepStats = {
205
205
  enabled: true,
206
206
  tier: deepTier,
207
207
  model: isLocalDeepExecution
208
- ? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen2.5-Coder-0.5B')
208
+ ? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen3.5-0.8B')
209
209
  : (deepOptions.modelName || deepOptions.provider || 'cloud'),
210
210
  total_ms: Date.now() - deepSetupStart,
211
211
  findings_count: deepFailures.length,
@@ -25,7 +25,7 @@ describe('GateRunner deep stats execution mode', () => {
25
25
  },
26
26
  });
27
27
  }
28
- it('reports local deep tier when provider=local even with apiKey', async () => {
28
+ it('reports local lite tier when provider=local even with apiKey', async () => {
29
29
  vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
30
30
  const runner = createRunner();
31
31
  const report = await runner.run(testDir, undefined, {
@@ -34,10 +34,10 @@ describe('GateRunner deep stats execution mode', () => {
34
34
  provider: 'local',
35
35
  pro: false,
36
36
  });
37
- expect(report.stats.deep?.tier).toBe('deep');
38
- expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-0.5B');
37
+ expect(report.stats.deep?.tier).toBe('lite');
38
+ expect(report.stats.deep?.model).toBe('Qwen3.5-0.8B');
39
39
  });
40
- it('reports local pro tier when provider=local and pro=true', async () => {
40
+ it('reports local deep tier when provider=local and pro=true', async () => {
41
41
  vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
42
42
  const runner = createRunner();
43
43
  const report = await runner.run(testDir, undefined, {
@@ -46,7 +46,7 @@ describe('GateRunner deep stats execution mode', () => {
46
46
  provider: 'local',
47
47
  pro: true,
48
48
  });
49
- expect(report.stats.deep?.tier).toBe('pro');
49
+ expect(report.stats.deep?.tier).toBe('deep');
50
50
  expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-1.5B');
51
51
  });
52
52
  it('reports cloud tier/model for cloud providers', async () => {
@@ -251,9 +251,10 @@ export class SecurityPatternsGate extends Gate {
251
251
  }
252
252
  const failures = [];
253
253
  const vulnerabilities = [];
254
+ const scanPatterns = context.patterns || ['**/*.{ts,js,tsx,jsx,py,java,go}'];
254
255
  const files = await FileScanner.findFiles({
255
256
  cwd: context.cwd,
256
- patterns: ['**/*.{ts,js,tsx,jsx,py,java,go}'],
257
+ patterns: scanPatterns,
257
258
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
258
259
  });
259
260
  const scanFiles = files.filter(file => !this.shouldSkipSecurityFile(file));
@@ -73,9 +73,10 @@ export class SideEffectAnalysisGate extends Gate {
73
73
  if (!this.cfg.enabled)
74
74
  return [];
75
75
  const violations = [];
76
+ const scanPatterns = context.patterns || FILE_GLOBS;
76
77
  const files = await FileScanner.findFiles({
77
78
  cwd: context.cwd,
78
- patterns: FILE_GLOBS,
79
+ patterns: scanPatterns,
79
80
  ignore: [
80
81
  ...(context.ignore || []),
81
82
  '**/node_modules/**', '**/dist/**', '**/build/**',
@@ -50,16 +50,18 @@ export class TestQualityGate extends Gate {
50
50
  return [];
51
51
  const failures = [];
52
52
  const issues = [];
53
+ const defaultPatterns = [
54
+ '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}',
55
+ '**/__tests__/**/*.{ts,js,tsx,jsx}',
56
+ '**/test_*.py', '**/*_test.py', '**/tests/**/*.py',
57
+ '**/*_test.go',
58
+ '**/*Test.java', '**/*Tests.java', '**/src/test/**/*.java',
59
+ '**/*Test.kt', '**/*Tests.kt', '**/src/test/**/*.kt',
60
+ ];
61
+ const scanPatterns = context.patterns || defaultPatterns;
53
62
  const files = await FileScanner.findFiles({
54
63
  cwd: context.cwd,
55
- patterns: [
56
- '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}',
57
- '**/__tests__/**/*.{ts,js,tsx,jsx}',
58
- '**/test_*.py', '**/*_test.py', '**/tests/**/*.py',
59
- '**/*_test.go',
60
- '**/*Test.java', '**/*Tests.java', '**/src/test/**/*.java',
61
- '**/*Test.kt', '**/*Tests.kt', '**/src/test/**/*.kt',
62
- ],
64
+ patterns: scanPatterns,
63
65
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
64
66
  '**/.venv/**', '**/venv/**', '**/vendor/**',
65
67
  '**/target/**', '**/.gradle/**', '**/out/**'],
@@ -18,6 +18,8 @@ export function createProvider(options) {
18
18
  });
19
19
  }
20
20
  // Default: local sidecar
21
- const tier = options.pro ? 'pro' : 'deep';
21
+ // deep = Qwen2.5-Coder-1.5B (full power, company-hosted)
22
+ // lite = Qwen3.5-0.8B (lightweight, default CLI sidecar)
23
+ const tier = options.pro ? 'deep' : 'lite';
22
24
  return new SidecarProvider(tier);
23
25
  }
@@ -27,7 +27,7 @@ export class SidecarProvider {
27
27
  modelPath = null;
28
28
  tier;
29
29
  threads;
30
- constructor(tier = 'deep', threads = 4) {
30
+ constructor(tier = 'lite', threads = 4) {
31
31
  this.tier = tier;
32
32
  this.threads = threads;
33
33
  }
@@ -60,8 +60,12 @@ export interface DeepAnalysisResult {
60
60
  }
61
61
  /**
62
62
  * Available model tiers.
63
+ *
64
+ * - deep: Qwen2.5-Coder-1.5B fine-tuned — full power, company-hosted
65
+ * - lite: Qwen3.5-0.8B fine-tuned — lightweight, ships as default CLI sidecar
66
+ * - legacy: Qwen2.5-Coder-0.5B fine-tuned — previous default, reproducibility
63
67
  */
64
- export type ModelTier = 'deep' | 'pro';
68
+ export type ModelTier = 'deep' | 'lite' | 'legacy';
65
69
  /**
66
70
  * Model info for download/caching.
67
71
  */
@@ -8,20 +8,28 @@ export const MODEL_VERSION = '1';
8
8
  export const MODELS = {
9
9
  deep: {
10
10
  tier: 'deep',
11
- name: 'Rigour-Deep-v1 (Qwen2.5-Coder-0.5B fine-tuned)',
11
+ name: 'Rigour-Deep-v1 (Qwen2.5-Coder-1.5B fine-tuned)',
12
12
  filename: `rigour-deep-v${MODEL_VERSION}-q4_k_m.gguf`,
13
13
  url: `https://huggingface.co/rigour-labs/rigour-deep-v1-gguf/resolve/main/rigour-deep-v${MODEL_VERSION}-q4_k_m.gguf`,
14
- sizeBytes: 350_000_000,
15
- sizeHuman: '350MB',
16
- },
17
- pro: {
18
- tier: 'pro',
19
- name: 'Rigour-Pro-v1 (Qwen2.5-Coder-1.5B fine-tuned)',
20
- filename: `rigour-pro-v${MODEL_VERSION}-q4_k_m.gguf`,
21
- url: `https://huggingface.co/rigour-labs/rigour-pro-v1-gguf/resolve/main/rigour-pro-v${MODEL_VERSION}-q4_k_m.gguf`,
22
14
  sizeBytes: 900_000_000,
23
15
  sizeHuman: '900MB',
24
16
  },
17
+ lite: {
18
+ tier: 'lite',
19
+ name: 'Rigour-Lite-v1 (Qwen3.5-0.8B fine-tuned)',
20
+ filename: `rigour-lite-v${MODEL_VERSION}-q4_k_m.gguf`,
21
+ url: `https://huggingface.co/rigour-labs/rigour-lite-v1-gguf/resolve/main/rigour-lite-v${MODEL_VERSION}-q4_k_m.gguf`,
22
+ sizeBytes: 500_000_000,
23
+ sizeHuman: '500MB',
24
+ },
25
+ legacy: {
26
+ tier: 'legacy',
27
+ name: 'Rigour-Legacy-v1 (Qwen2.5-Coder-0.5B fine-tuned)',
28
+ filename: `rigour-legacy-v${MODEL_VERSION}-q4_k_m.gguf`,
29
+ url: `https://huggingface.co/rigour-labs/rigour-legacy-v1-gguf/resolve/main/rigour-legacy-v${MODEL_VERSION}-q4_k_m.gguf`,
30
+ sizeBytes: 350_000_000,
31
+ sizeHuman: '350MB',
32
+ },
25
33
  };
26
34
  /**
27
35
  * Fallback stock models — used when fine-tuned model is not yet
@@ -30,18 +38,26 @@ export const MODELS = {
30
38
  export const FALLBACK_MODELS = {
31
39
  deep: {
32
40
  tier: 'deep',
33
- name: 'Qwen2.5-Coder-0.5B-Instruct (stock)',
34
- filename: 'qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
35
- url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
36
- sizeBytes: 350_000_000,
37
- sizeHuman: '350MB',
38
- },
39
- pro: {
40
- tier: 'pro',
41
41
  name: 'Qwen2.5-Coder-1.5B-Instruct (stock)',
42
42
  filename: 'qwen2.5-coder-1.5b-instruct-q4_k_m.gguf',
43
43
  url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf',
44
44
  sizeBytes: 900_000_000,
45
45
  sizeHuman: '900MB',
46
46
  },
47
+ lite: {
48
+ tier: 'lite',
49
+ name: 'Qwen3.5-0.8B (stock)',
50
+ filename: 'qwen3.5-0.8b-q4_k_m.gguf',
51
+ url: 'https://huggingface.co/Qwen/Qwen3.5-0.8B-GGUF/resolve/main/qwen3.5-0.8b-q4_k_m.gguf',
52
+ sizeBytes: 500_000_000,
53
+ sizeHuman: '500MB',
54
+ },
55
+ legacy: {
56
+ tier: 'legacy',
57
+ name: 'Qwen2.5-Coder-0.5B-Instruct (stock)',
58
+ filename: 'qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
59
+ url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-0.5b-instruct-q4_k_m.gguf',
60
+ sizeBytes: 350_000_000,
61
+ sizeHuman: '350MB',
62
+ },
47
63
  };
@@ -2748,7 +2748,7 @@ export declare const ReportSchema: z.ZodObject<{
2748
2748
  }>>;
2749
2749
  deep: z.ZodOptional<z.ZodObject<{
2750
2750
  enabled: z.ZodBoolean;
2751
- tier: z.ZodOptional<z.ZodEnum<["deep", "pro", "cloud"]>>;
2751
+ tier: z.ZodOptional<z.ZodEnum<["deep", "lite", "legacy", "cloud"]>>;
2752
2752
  model: z.ZodOptional<z.ZodString>;
2753
2753
  total_ms: z.ZodOptional<z.ZodNumber>;
2754
2754
  files_analyzed: z.ZodOptional<z.ZodNumber>;
@@ -2756,7 +2756,7 @@ export declare const ReportSchema: z.ZodObject<{
2756
2756
  findings_verified: z.ZodOptional<z.ZodNumber>;
2757
2757
  }, "strip", z.ZodTypeAny, {
2758
2758
  enabled: boolean;
2759
- tier?: "deep" | "pro" | "cloud" | undefined;
2759
+ tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
2760
2760
  model?: string | undefined;
2761
2761
  total_ms?: number | undefined;
2762
2762
  files_analyzed?: number | undefined;
@@ -2764,7 +2764,7 @@ export declare const ReportSchema: z.ZodObject<{
2764
2764
  findings_verified?: number | undefined;
2765
2765
  }, {
2766
2766
  enabled: boolean;
2767
- tier?: "deep" | "pro" | "cloud" | undefined;
2767
+ tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
2768
2768
  model?: string | undefined;
2769
2769
  total_ms?: number | undefined;
2770
2770
  files_analyzed?: number | undefined;
@@ -2775,7 +2775,7 @@ export declare const ReportSchema: z.ZodObject<{
2775
2775
  duration_ms: number;
2776
2776
  deep?: {
2777
2777
  enabled: boolean;
2778
- tier?: "deep" | "pro" | "cloud" | undefined;
2778
+ tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
2779
2779
  model?: string | undefined;
2780
2780
  total_ms?: number | undefined;
2781
2781
  files_analyzed?: number | undefined;
@@ -2798,7 +2798,7 @@ export declare const ReportSchema: z.ZodObject<{
2798
2798
  duration_ms: number;
2799
2799
  deep?: {
2800
2800
  enabled: boolean;
2801
- tier?: "deep" | "pro" | "cloud" | undefined;
2801
+ tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
2802
2802
  model?: string | undefined;
2803
2803
  total_ms?: number | undefined;
2804
2804
  files_analyzed?: number | undefined;
@@ -2823,7 +2823,7 @@ export declare const ReportSchema: z.ZodObject<{
2823
2823
  duration_ms: number;
2824
2824
  deep?: {
2825
2825
  enabled: boolean;
2826
- tier?: "deep" | "pro" | "cloud" | undefined;
2826
+ tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
2827
2827
  model?: string | undefined;
2828
2828
  total_ms?: number | undefined;
2829
2829
  files_analyzed?: number | undefined;
@@ -2865,7 +2865,7 @@ export declare const ReportSchema: z.ZodObject<{
2865
2865
  duration_ms: number;
2866
2866
  deep?: {
2867
2867
  enabled: boolean;
2868
- tier?: "deep" | "pro" | "cloud" | undefined;
2868
+ tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
2869
2869
  model?: string | undefined;
2870
2870
  total_ms?: number | undefined;
2871
2871
  files_analyzed?: number | undefined;
@@ -383,7 +383,7 @@ export const ReportSchema = z.object({
383
383
  }).optional(),
384
384
  deep: z.object({
385
385
  enabled: z.boolean(),
386
- tier: z.enum(['deep', 'pro', 'cloud']).optional(),
386
+ tier: z.enum(['deep', 'lite', 'legacy', 'cloud']).optional(),
387
387
  model: z.string().optional(),
388
388
  total_ms: z.number().optional(),
389
389
  files_analyzed: z.number().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "4.3.2",
3
+ "version": "4.3.3",
4
4
  "description": "Deterministic quality gate engine for AI-generated code. AST analysis, drift detection, and Fix Packet generation across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -59,11 +59,11 @@
59
59
  "@xenova/transformers": "^2.17.2",
60
60
  "better-sqlite3": "^11.0.0",
61
61
  "openai": "^4.104.0",
62
- "@rigour-labs/brain-darwin-arm64": "4.3.2",
63
- "@rigour-labs/brain-linux-arm64": "4.3.2",
64
- "@rigour-labs/brain-darwin-x64": "4.3.2",
65
- "@rigour-labs/brain-win-x64": "4.3.2",
66
- "@rigour-labs/brain-linux-x64": "4.3.2"
62
+ "@rigour-labs/brain-darwin-arm64": "4.3.3",
63
+ "@rigour-labs/brain-linux-arm64": "4.3.3",
64
+ "@rigour-labs/brain-darwin-x64": "4.3.3",
65
+ "@rigour-labs/brain-win-x64": "4.3.3",
66
+ "@rigour-labs/brain-linux-x64": "4.3.3"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/better-sqlite3": "^7.6.12",