@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.
- package/dist/gates/context-window-artifacts.js +2 -1
- package/dist/gates/deprecated-apis.js +3 -1
- package/dist/gates/duplication-drift.js +2 -1
- package/dist/gates/frontend-secret-exposure.d.ts +41 -9
- package/dist/gates/frontend-secret-exposure.js +315 -135
- package/dist/gates/hallucinated-imports.js +3 -1
- package/dist/gates/inconsistent-error-handling.js +2 -1
- package/dist/gates/phantom-apis.js +3 -1
- package/dist/gates/promise-safety-helpers.js +5 -5
- package/dist/gates/promise-safety.js +5 -4
- package/dist/gates/runner.js +2 -2
- package/dist/gates/runner.test.js +5 -5
- package/dist/gates/security-patterns.js +2 -1
- package/dist/gates/side-effect-analysis.js +2 -1
- package/dist/gates/test-quality.js +10 -8
- package/dist/inference/index.js +3 -1
- package/dist/inference/sidecar-provider.js +1 -1
- package/dist/inference/types.d.ts +5 -1
- package/dist/inference/types.js +33 -17
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +1 -1
- package/package.json +6 -6
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
16
|
-
private
|
|
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
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
137
|
+
cfg;
|
|
138
|
+
secretNamePatterns;
|
|
139
|
+
safePrefixPatterns;
|
|
140
|
+
frontendPathPatterns;
|
|
141
|
+
serverPathPatterns;
|
|
38
142
|
constructor(config = {}) {
|
|
39
|
-
super('frontend-secret-exposure', 'Frontend Secret Exposure
|
|
40
|
-
this.
|
|
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 ??
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
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:
|
|
59
|
-
ignore: [
|
|
189
|
+
patterns: scanPatterns,
|
|
190
|
+
ignore: [
|
|
191
|
+
...(context.ignore || []),
|
|
192
|
+
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
193
|
+
'**/.next/**', '**/coverage/**', '**/out/**',
|
|
194
|
+
],
|
|
60
195
|
});
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
Logger.info(`Frontend Secret Exposure Gate:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
+
// Unreadable — skip silently
|
|
72
214
|
}
|
|
73
|
-
if (!this.isClientBundled(file, content))
|
|
74
|
-
continue;
|
|
75
|
-
findings.push(...this.findEnvExposures(file, content));
|
|
76
215
|
}
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
89
|
-
if (
|
|
90
|
-
return
|
|
91
|
-
return
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
244
|
+
if (this.safePrefixPatterns.some(p => p.test(varName)))
|
|
98
245
|
return false;
|
|
99
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
for (
|
|
134
|
-
const
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 -
|
|
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 -
|
|
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 -
|
|
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 +
|
|
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 +
|
|
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:
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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;
|
package/dist/gates/runner.js
CHANGED
|
@@ -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 ? '
|
|
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' : '
|
|
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
|
|
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('
|
|
38
|
-
expect(report.stats.deep?.model).toBe('
|
|
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
|
|
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('
|
|
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:
|
|
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:
|
|
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/**'],
|
package/dist/inference/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export function createProvider(options) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
// Default: local sidecar
|
|
21
|
-
|
|
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
|
}
|
|
@@ -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' | '
|
|
68
|
+
export type ModelTier = 'deep' | 'lite' | 'legacy';
|
|
65
69
|
/**
|
|
66
70
|
* Model info for download/caching.
|
|
67
71
|
*/
|
package/dist/inference/types.js
CHANGED
|
@@ -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-
|
|
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
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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", "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
2868
|
+
tier?: "deep" | "lite" | "legacy" | "cloud" | undefined;
|
|
2869
2869
|
model?: string | undefined;
|
|
2870
2870
|
total_ms?: number | undefined;
|
|
2871
2871
|
files_analyzed?: number | undefined;
|
package/dist/types/index.js
CHANGED
|
@@ -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', '
|
|
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.
|
|
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.
|
|
63
|
-
"@rigour-labs/brain-linux-arm64": "4.3.
|
|
64
|
-
"@rigour-labs/brain-darwin-x64": "4.3.
|
|
65
|
-
"@rigour-labs/brain-win-x64": "4.3.
|
|
66
|
-
"@rigour-labs/brain-linux-x64": "4.3.
|
|
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",
|