@rigour-labs/core 4.0.4 → 4.1.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.
- package/dist/gates/ast-handlers/typescript.js +39 -12
- package/dist/gates/ast-handlers/universal.js +9 -3
- package/dist/gates/ast.js +15 -1
- package/dist/gates/ast.test.d.ts +1 -0
- package/dist/gates/ast.test.js +112 -0
- package/dist/gates/content.d.ts +5 -0
- package/dist/gates/content.js +66 -7
- package/dist/gates/content.test.d.ts +1 -0
- package/dist/gates/content.test.js +73 -0
- package/dist/gates/context-window-artifacts.d.ts +1 -0
- package/dist/gates/context-window-artifacts.js +10 -3
- package/dist/gates/context.d.ts +1 -0
- package/dist/gates/context.js +29 -8
- package/dist/gates/deep-analysis.js +2 -2
- package/dist/gates/deprecated-apis.d.ts +1 -0
- package/dist/gates/deprecated-apis.js +15 -2
- package/dist/gates/hallucinated-imports.d.ts +14 -0
- package/dist/gates/hallucinated-imports.js +267 -60
- package/dist/gates/hallucinated-imports.test.js +164 -1
- package/dist/gates/inconsistent-error-handling.d.ts +1 -0
- package/dist/gates/inconsistent-error-handling.js +12 -1
- package/dist/gates/phantom-apis.d.ts +2 -0
- package/dist/gates/phantom-apis.js +28 -3
- package/dist/gates/phantom-apis.test.js +14 -0
- package/dist/gates/promise-safety.d.ts +2 -0
- package/dist/gates/promise-safety.js +31 -9
- package/dist/gates/runner.js +8 -2
- package/dist/gates/runner.test.d.ts +1 -0
- package/dist/gates/runner.test.js +65 -0
- package/dist/gates/security-patterns.d.ts +1 -0
- package/dist/gates/security-patterns.js +22 -6
- package/dist/gates/security-patterns.test.js +18 -0
- package/dist/hooks/templates.d.ts +1 -1
- package/dist/hooks/templates.js +12 -12
- package/dist/inference/executable.d.ts +6 -0
- package/dist/inference/executable.js +29 -0
- package/dist/inference/executable.test.d.ts +1 -0
- package/dist/inference/executable.test.js +41 -0
- package/dist/inference/model-manager.d.ts +3 -1
- package/dist/inference/model-manager.js +76 -8
- package/dist/inference/model-manager.test.d.ts +1 -0
- package/dist/inference/model-manager.test.js +24 -0
- package/dist/inference/sidecar-provider.d.ts +1 -0
- package/dist/inference/sidecar-provider.js +124 -31
- package/dist/services/context-engine.js +1 -1
- package/dist/templates/universal-config.js +3 -3
- package/dist/types/index.js +3 -3
- package/dist/utils/scanner.js +6 -0
- package/package.json +7 -2
|
@@ -48,6 +48,8 @@ export class InconsistentErrorHandlingGate extends Gate {
|
|
|
48
48
|
});
|
|
49
49
|
Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
|
|
50
50
|
for (const file of files) {
|
|
51
|
+
if (this.shouldSkipFile(file))
|
|
52
|
+
continue;
|
|
51
53
|
try {
|
|
52
54
|
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
53
55
|
this.extractErrorHandlers(content, file, handlers);
|
|
@@ -87,7 +89,8 @@ export class InconsistentErrorHandlingGate extends Gate {
|
|
|
87
89
|
return ` • ${strategy} (${handlers.length}x): ${files.join(', ')}`;
|
|
88
90
|
})
|
|
89
91
|
.join('\n');
|
|
90
|
-
|
|
92
|
+
const severity = uniqueFiles.size >= 4 && activeStrategies.size >= 4 ? 'high' : 'medium';
|
|
93
|
+
failures.push(this.createFailure(`Inconsistent error handling for '${errorType}': ${activeStrategies.size} different strategies found across ${uniqueFiles.size} files:\n${strategyBreakdown}`, [...uniqueFiles].slice(0, 5), `Standardize error handling for '${errorType}'. Create a shared error handler or establish a project convention. AI agents often write error handling from scratch each session, leading to divergent patterns.`, 'Inconsistent Error Handling', typeHandlers[0].line, undefined, severity));
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
return failures;
|
|
@@ -233,4 +236,12 @@ export class InconsistentErrorHandlingGate extends Gate {
|
|
|
233
236
|
}
|
|
234
237
|
return body.length > 0 ? body.join('\n') : null;
|
|
235
238
|
}
|
|
239
|
+
shouldSkipFile(file) {
|
|
240
|
+
const normalized = file.replace(/\\/g, '/');
|
|
241
|
+
return (normalized.includes('/examples/') ||
|
|
242
|
+
normalized.includes('/src/gates/') ||
|
|
243
|
+
normalized.endsWith('src/hooks/standalone-checker.ts') ||
|
|
244
|
+
normalized.endsWith('packages/rigour-mcp/src/index.ts') ||
|
|
245
|
+
normalized.endsWith('packages/rigour-studio/src/App.tsx'));
|
|
246
|
+
}
|
|
236
247
|
}
|
|
@@ -42,12 +42,14 @@ export declare class PhantomApisGate extends Gate {
|
|
|
42
42
|
constructor(config?: PhantomApisConfig);
|
|
43
43
|
protected get provenance(): Provenance;
|
|
44
44
|
run(context: GateContext): Promise<Failure[]>;
|
|
45
|
+
private shouldSkipFile;
|
|
45
46
|
/**
|
|
46
47
|
* Node.js stdlib method verification.
|
|
47
48
|
* For each known module, we maintain the actual exported methods.
|
|
48
49
|
* Any call like fs.readFileAsync() that doesn't match is flagged.
|
|
49
50
|
*/
|
|
50
51
|
private checkNodePhantomApis;
|
|
52
|
+
private stripJsCommentLine;
|
|
51
53
|
/**
|
|
52
54
|
* Python stdlib method verification.
|
|
53
55
|
*/
|
|
@@ -49,12 +49,14 @@ export class PhantomApisGate extends Gate {
|
|
|
49
49
|
cwd: context.cwd,
|
|
50
50
|
patterns: ['**/*.{ts,js,tsx,jsx,py,go,cs,java,kt}'],
|
|
51
51
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
52
|
+
'**/*.test.*', '**/*.spec.*', '**/__tests__/**',
|
|
52
53
|
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/__pycache__/**',
|
|
53
54
|
'**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
|
|
54
55
|
'**/target/**', '**/.gradle/**', '**/out/**'],
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
|
|
58
|
+
Logger.info(`Phantom APIs: Scanning ${analyzableFiles.length} files`);
|
|
59
|
+
for (const file of analyzableFiles) {
|
|
58
60
|
try {
|
|
59
61
|
const fullPath = path.join(context.cwd, file);
|
|
60
62
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
@@ -90,6 +92,15 @@ export class PhantomApisGate extends Gate {
|
|
|
90
92
|
}
|
|
91
93
|
return failures;
|
|
92
94
|
}
|
|
95
|
+
shouldSkipFile(file) {
|
|
96
|
+
const normalized = file.replace(/\\/g, '/');
|
|
97
|
+
return (this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(normalized)) ||
|
|
98
|
+
normalized.includes('/examples/') ||
|
|
99
|
+
normalized.includes('/__tests__/') ||
|
|
100
|
+
normalized.endsWith('/phantom-apis-data.ts') ||
|
|
101
|
+
/\.test\.[^.]+$/i.test(normalized) ||
|
|
102
|
+
/\.spec\.[^.]+$/i.test(normalized));
|
|
103
|
+
}
|
|
93
104
|
/**
|
|
94
105
|
* Node.js stdlib method verification.
|
|
95
106
|
* For each known module, we maintain the actual exported methods.
|
|
@@ -116,7 +127,9 @@ export class PhantomApisGate extends Gate {
|
|
|
116
127
|
return;
|
|
117
128
|
// Scan for method calls on imported modules
|
|
118
129
|
for (let i = 0; i < lines.length; i++) {
|
|
119
|
-
const line = lines[i];
|
|
130
|
+
const line = this.stripJsCommentLine(lines[i]);
|
|
131
|
+
if (!line)
|
|
132
|
+
continue;
|
|
120
133
|
for (const [alias, moduleName] of moduleAliases) {
|
|
121
134
|
// Match: alias.methodName( or alias.property.something(
|
|
122
135
|
const callPattern = new RegExp(`\\b${this.escapeRegex(alias)}\\.(\\w+)\\s*\\(`, 'g');
|
|
@@ -136,6 +149,18 @@ export class PhantomApisGate extends Gate {
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
}
|
|
152
|
+
stripJsCommentLine(line) {
|
|
153
|
+
const trimmed = line.trim();
|
|
154
|
+
if (trimmed.length === 0 ||
|
|
155
|
+
trimmed.startsWith('//') ||
|
|
156
|
+
trimmed.startsWith('/*') ||
|
|
157
|
+
trimmed.startsWith('*/') ||
|
|
158
|
+
trimmed.startsWith('*')) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
const withoutInline = line.replace(/\/\/.*$/, '');
|
|
162
|
+
return withoutInline.trim();
|
|
163
|
+
}
|
|
139
164
|
/**
|
|
140
165
|
* Python stdlib method verification.
|
|
141
166
|
*/
|
|
@@ -121,6 +121,20 @@ fs.readFileSyn('data.txt');
|
|
|
121
121
|
expect(failures).toHaveLength(1);
|
|
122
122
|
expect(failures[0].details).toContain('readFileSync');
|
|
123
123
|
});
|
|
124
|
+
it('should ignore phantom method mentions inside comments', async () => {
|
|
125
|
+
mockFindFiles.mockResolvedValue(['src/docs.ts']);
|
|
126
|
+
mockReadFile.mockResolvedValue(`
|
|
127
|
+
import fs from 'fs';
|
|
128
|
+
// fs.readFileAsync('data.txt')
|
|
129
|
+
/* path.combine('a', 'b') */
|
|
130
|
+
/**
|
|
131
|
+
* fs.writeFilePromise('out.txt')
|
|
132
|
+
*/
|
|
133
|
+
const data = fs.readFileSync('real.txt', 'utf-8');
|
|
134
|
+
`);
|
|
135
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
136
|
+
expect(failures).toHaveLength(0);
|
|
137
|
+
});
|
|
124
138
|
it('should not scan when disabled', async () => {
|
|
125
139
|
const disabled = new PhantomApisGate({ enabled: false });
|
|
126
140
|
const failures = await disabled.run({ cwd: '/project' });
|
|
@@ -47,6 +47,8 @@ export class PromiseSafetyGate extends Gate {
|
|
|
47
47
|
for (const file of files) {
|
|
48
48
|
if (this.config.ignore_patterns.some(p => new RegExp(p).test(file)))
|
|
49
49
|
continue;
|
|
50
|
+
if (this.shouldSkipFile(file))
|
|
51
|
+
continue;
|
|
50
52
|
const lang = detectLang(file);
|
|
51
53
|
if (lang === 'unknown')
|
|
52
54
|
continue;
|
|
@@ -81,26 +83,29 @@ export class PromiseSafetyGate extends Gate {
|
|
|
81
83
|
}
|
|
82
84
|
detectUnhandledThen(lines, file, violations) {
|
|
83
85
|
for (let i = 0; i < lines.length; i++) {
|
|
84
|
-
|
|
86
|
+
const line = this.sanitizeLine(lines[i]);
|
|
87
|
+
if (!/\.then\s*\(/.test(line))
|
|
85
88
|
continue;
|
|
86
89
|
let hasCatch = false;
|
|
87
90
|
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
88
|
-
|
|
91
|
+
const lookahead = this.sanitizeLine(lines[j]);
|
|
92
|
+
if (/\.catch\s*\(/.test(lookahead)) {
|
|
89
93
|
hasCatch = true;
|
|
90
94
|
break;
|
|
91
95
|
}
|
|
92
|
-
if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(
|
|
96
|
+
if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lookahead.trim()))
|
|
93
97
|
break;
|
|
94
98
|
}
|
|
95
|
-
if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(
|
|
96
|
-
violations.push({ file, line: i + 1, type: 'unhandled-then', code:
|
|
99
|
+
if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(line)) {
|
|
100
|
+
violations.push({ file, line: i + 1, type: 'unhandled-then', code: line.trim().substring(0, 80), reason: `.then() chain without .catch() — unhandled promise rejection` });
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
detectUnsafeParseJS(lines, file, violations) {
|
|
101
105
|
for (let i = 0; i < lines.length; i++) {
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
const line = this.sanitizeLine(lines[i]);
|
|
107
|
+
if (/JSON\.parse\s*\(/.test(line) && !isInsideTryBlock(lines, i)) {
|
|
108
|
+
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: line.trim().substring(0, 80), reason: `JSON.parse() without try/catch — crashes on malformed input` });
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
}
|
|
@@ -121,11 +126,12 @@ export class PromiseSafetyGate extends Gate {
|
|
|
121
126
|
}
|
|
122
127
|
detectUnsafeFetchJS(lines, file, violations) {
|
|
123
128
|
for (let i = 0; i < lines.length; i++) {
|
|
124
|
-
|
|
129
|
+
const line = this.sanitizeLine(lines[i]);
|
|
130
|
+
if (!/\bfetch\s*\(/.test(line) && !/\baxios\.\w+\s*\(/.test(line))
|
|
125
131
|
continue;
|
|
126
132
|
if (isInsideTryBlock(lines, i) || hasCatchAhead(lines, i) || hasStatusCheckAhead(lines, i))
|
|
127
133
|
continue;
|
|
128
|
-
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code:
|
|
134
|
+
violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: line.trim().substring(0, 80), reason: `HTTP call without error handling` });
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
scanPython(lines, content, file, violations) {
|
|
@@ -300,4 +306,20 @@ export class PromiseSafetyGate extends Gate {
|
|
|
300
306
|
}
|
|
301
307
|
return failures;
|
|
302
308
|
}
|
|
309
|
+
shouldSkipFile(file) {
|
|
310
|
+
const normalized = file.replace(/\\/g, '/');
|
|
311
|
+
return (normalized.includes('/examples/') ||
|
|
312
|
+
/\/commands\/demo(?:-|\/)/.test(`/${normalized}`) ||
|
|
313
|
+
/\/gates\/(?:promise-safety|deprecated-apis-rules(?:-node|-lang)?)\.ts$/i.test(normalized));
|
|
314
|
+
}
|
|
315
|
+
sanitizeLine(line) {
|
|
316
|
+
// Remove obvious comments and quoted literals to avoid matching detector text/examples.
|
|
317
|
+
const withoutBlockTail = line.replace(/\/\*.*?\*\//g, '');
|
|
318
|
+
const withoutSingleComments = withoutBlockTail.replace(/\/\/.*$/, '');
|
|
319
|
+
const withoutQuoted = withoutSingleComments
|
|
320
|
+
.replace(/'(?:\\.|[^'\\])*'/g, "''")
|
|
321
|
+
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
|
322
|
+
.replace(/`(?:\\.|[^`\\])*`/g, '``');
|
|
323
|
+
return withoutQuoted;
|
|
324
|
+
}
|
|
303
325
|
}
|
package/dist/gates/runner.js
CHANGED
|
@@ -187,10 +187,16 @@ export class GateRunner {
|
|
|
187
187
|
else {
|
|
188
188
|
summary['deep-analysis'] = 'PASS';
|
|
189
189
|
}
|
|
190
|
+
const isLocalDeepExecution = !deepOptions.apiKey || (deepOptions.provider || '').toLowerCase() === 'local';
|
|
191
|
+
const deepTier = isLocalDeepExecution
|
|
192
|
+
? (deepOptions.pro ? 'pro' : 'deep')
|
|
193
|
+
: 'cloud';
|
|
190
194
|
deepStats = {
|
|
191
195
|
enabled: true,
|
|
192
|
-
tier:
|
|
193
|
-
model:
|
|
196
|
+
tier: deepTier,
|
|
197
|
+
model: isLocalDeepExecution
|
|
198
|
+
? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen2.5-Coder-0.5B')
|
|
199
|
+
: (deepOptions.modelName || deepOptions.provider || 'cloud'),
|
|
194
200
|
total_ms: Date.now() - deepSetupStart,
|
|
195
201
|
findings_count: deepFailures.length,
|
|
196
202
|
findings_verified: deepFailures.filter((f) => f.verified).length,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { GateRunner } from './runner.js';
|
|
6
|
+
import { DeepAnalysisGate } from './deep-analysis.js';
|
|
7
|
+
describe('GateRunner deep stats execution mode', () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-runner-deep-'));
|
|
11
|
+
await fs.writeFile(path.join(testDir, 'index.ts'), 'export const ok = true;\n');
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
await fs.remove(testDir);
|
|
16
|
+
});
|
|
17
|
+
function createRunner() {
|
|
18
|
+
return new GateRunner({
|
|
19
|
+
version: 1,
|
|
20
|
+
commands: {},
|
|
21
|
+
gates: {
|
|
22
|
+
max_file_lines: 500,
|
|
23
|
+
forbid_todos: true,
|
|
24
|
+
forbid_fixme: true,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
it('reports local deep tier when provider=local even with apiKey', async () => {
|
|
29
|
+
vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
|
|
30
|
+
const runner = createRunner();
|
|
31
|
+
const report = await runner.run(testDir, undefined, {
|
|
32
|
+
enabled: true,
|
|
33
|
+
apiKey: 'sk-test',
|
|
34
|
+
provider: 'local',
|
|
35
|
+
pro: false,
|
|
36
|
+
});
|
|
37
|
+
expect(report.stats.deep?.tier).toBe('deep');
|
|
38
|
+
expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-0.5B');
|
|
39
|
+
});
|
|
40
|
+
it('reports local pro tier when provider=local and pro=true', async () => {
|
|
41
|
+
vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
|
|
42
|
+
const runner = createRunner();
|
|
43
|
+
const report = await runner.run(testDir, undefined, {
|
|
44
|
+
enabled: true,
|
|
45
|
+
apiKey: 'sk-test',
|
|
46
|
+
provider: 'local',
|
|
47
|
+
pro: true,
|
|
48
|
+
});
|
|
49
|
+
expect(report.stats.deep?.tier).toBe('pro');
|
|
50
|
+
expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-1.5B');
|
|
51
|
+
});
|
|
52
|
+
it('reports cloud tier/model for cloud providers', async () => {
|
|
53
|
+
vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue([]);
|
|
54
|
+
const runner = createRunner();
|
|
55
|
+
const report = await runner.run(testDir, undefined, {
|
|
56
|
+
enabled: true,
|
|
57
|
+
apiKey: 'sk-test',
|
|
58
|
+
provider: 'openai',
|
|
59
|
+
modelName: 'gpt-4.1-mini',
|
|
60
|
+
pro: false,
|
|
61
|
+
});
|
|
62
|
+
expect(report.stats.deep?.tier).toBe('cloud');
|
|
63
|
+
expect(report.stats.deep?.model).toBe('gpt-4.1-mini');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -45,6 +45,7 @@ export declare class SecurityPatternsGate extends Gate {
|
|
|
45
45
|
constructor(config?: SecurityPatternsConfig);
|
|
46
46
|
protected get provenance(): Provenance;
|
|
47
47
|
run(context: GateContext): Promise<Failure[]>;
|
|
48
|
+
private shouldSkipSecurityFile;
|
|
48
49
|
private scanFileForVulnerabilities;
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
@@ -24,7 +24,7 @@ const VULNERABILITY_PATTERNS = [
|
|
|
24
24
|
// SQL Injection
|
|
25
25
|
{
|
|
26
26
|
type: 'sql_injection',
|
|
27
|
-
regex: /(?:execute|query|raw|exec)\s*\(\s
|
|
27
|
+
regex: /(?:execute|query|raw|exec)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|WITH)[^`]*\$\{[^}]+\}[^`]*`/gi,
|
|
28
28
|
severity: 'critical',
|
|
29
29
|
description: 'Potential SQL injection: User input concatenated into SQL query',
|
|
30
30
|
cwe: 'CWE-89',
|
|
@@ -32,7 +32,7 @@ const VULNERABILITY_PATTERNS = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
type: 'sql_injection',
|
|
35
|
-
regex:
|
|
35
|
+
regex: /(?:execute|query|raw|exec)\s*\(\s*['"`][^'"`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|WITH)[^'"`]*['"`]\s*\+\s*[^)]+\)/gi,
|
|
36
36
|
severity: 'critical',
|
|
37
37
|
description: 'SQL query built with string concatenation',
|
|
38
38
|
cwe: 'CWE-89',
|
|
@@ -117,7 +117,7 @@ const VULNERABILITY_PATTERNS = [
|
|
|
117
117
|
// Command Injection
|
|
118
118
|
{
|
|
119
119
|
type: 'command_injection',
|
|
120
|
-
regex: /(?:exec|spawn|
|
|
120
|
+
regex: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{[^}]*(?:req\.|query|params|body|input|user|argv|process\.env)[^}]*\}[^`]*`|[^)]*(?:req\.|query|params|body|input|user|argv|process\.env)[^)]*)\)/g,
|
|
121
121
|
severity: 'critical',
|
|
122
122
|
description: 'Potential command injection: shell execution with user input',
|
|
123
123
|
cwe: 'CWE-78',
|
|
@@ -125,7 +125,7 @@ const VULNERABILITY_PATTERNS = [
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
type: 'command_injection',
|
|
128
|
-
regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\(/g,
|
|
128
|
+
regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\([^)]*(?:req\.|query|params|body|input|user|argv|process\.env)/g,
|
|
129
129
|
severity: 'high',
|
|
130
130
|
description: 'child_process usage detected (verify input sanitization)',
|
|
131
131
|
cwe: 'CWE-78',
|
|
@@ -254,9 +254,11 @@ export class SecurityPatternsGate extends Gate {
|
|
|
254
254
|
const files = await FileScanner.findFiles({
|
|
255
255
|
cwd: context.cwd,
|
|
256
256
|
patterns: ['**/*.{ts,js,tsx,jsx,py,java,go}'],
|
|
257
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
|
|
257
258
|
});
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
const scanFiles = files.filter(file => !this.shouldSkipSecurityFile(file));
|
|
260
|
+
Logger.info(`Security Patterns Gate: Scanning ${scanFiles.length} files`);
|
|
261
|
+
for (const file of scanFiles) {
|
|
260
262
|
try {
|
|
261
263
|
const fullPath = path.join(context.cwd, file);
|
|
262
264
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
@@ -296,6 +298,20 @@ export class SecurityPatternsGate extends Gate {
|
|
|
296
298
|
}
|
|
297
299
|
return failures;
|
|
298
300
|
}
|
|
301
|
+
shouldSkipSecurityFile(file) {
|
|
302
|
+
const normalized = file.replace(/\\/g, '/');
|
|
303
|
+
if (/\/(?:examples|studio-dist|dist|build|coverage|target|out)\//.test(`/${normalized}`))
|
|
304
|
+
return true;
|
|
305
|
+
if (/\/__tests__\//.test(`/${normalized}`))
|
|
306
|
+
return true;
|
|
307
|
+
if (/\/commands\/demo(?:-|\/)/.test(`/${normalized}`))
|
|
308
|
+
return true;
|
|
309
|
+
if (/\/gates\/deprecated-apis-rules(?:-node|-lang)?\.ts$/i.test(normalized))
|
|
310
|
+
return true;
|
|
311
|
+
if (/\.(test|spec)\.(?:ts|tsx|js|jsx|py|java|go)$/i.test(normalized))
|
|
312
|
+
return true;
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
299
315
|
scanFileForVulnerabilities(content, file, ext, vulnerabilities) {
|
|
300
316
|
const lines = content.split('\n');
|
|
301
317
|
for (const pattern of VULNERABILITY_PATTERNS) {
|
|
@@ -41,6 +41,14 @@ describe('SecurityPatternsGate', () => {
|
|
|
41
41
|
const vulns = await checkSecurityPatterns(filePath);
|
|
42
42
|
expect(vulns.some(v => v.type === 'sql_injection')).toBe(true);
|
|
43
43
|
});
|
|
44
|
+
it('should NOT flag non-SQL template interpolation in normal function calls', async () => {
|
|
45
|
+
const filePath = path.join(testDir, 'status.ts');
|
|
46
|
+
fs.writeFileSync(filePath, `
|
|
47
|
+
logger.info(\`Current version: \${current} -> \${latest}\`);
|
|
48
|
+
`);
|
|
49
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
50
|
+
expect(vulns.some(v => v.type === 'sql_injection')).toBe(false);
|
|
51
|
+
});
|
|
44
52
|
});
|
|
45
53
|
describe('XSS detection', () => {
|
|
46
54
|
it('should detect innerHTML assignment', async () => {
|
|
@@ -129,5 +137,15 @@ describe('SecurityPatternsGate', () => {
|
|
|
129
137
|
const failures = await gate.run({ cwd: testDir });
|
|
130
138
|
expect(failures).toHaveLength(0); // High severity not blocked
|
|
131
139
|
});
|
|
140
|
+
it('should skip fixture-style test files for gate-level scans', async () => {
|
|
141
|
+
const gate = new SecurityPatternsGate({ enabled: true });
|
|
142
|
+
const fixturePath = path.join(testDir, 'auth.test.ts');
|
|
143
|
+
fs.writeFileSync(fixturePath, `
|
|
144
|
+
const password = "supersecretpassword123";
|
|
145
|
+
eval(req.body.code);
|
|
146
|
+
`);
|
|
147
|
+
const failures = await gate.run({ cwd: testDir });
|
|
148
|
+
expect(failures).toHaveLength(0);
|
|
149
|
+
});
|
|
132
150
|
});
|
|
133
151
|
});
|
|
@@ -19,4 +19,4 @@ export interface GeneratedHookFile {
|
|
|
19
19
|
/**
|
|
20
20
|
* Generate hook config files for a specific tool.
|
|
21
21
|
*/
|
|
22
|
-
export declare function generateHookFiles(tool: HookTool,
|
|
22
|
+
export declare function generateHookFiles(tool: HookTool, checkerCommand: string): GeneratedHookFile[];
|
package/dist/hooks/templates.js
CHANGED
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
/**
|
|
13
13
|
* Generate hook config files for a specific tool.
|
|
14
14
|
*/
|
|
15
|
-
export function generateHookFiles(tool,
|
|
15
|
+
export function generateHookFiles(tool, checkerCommand) {
|
|
16
16
|
switch (tool) {
|
|
17
17
|
case 'claude':
|
|
18
|
-
return generateClaudeHooks(
|
|
18
|
+
return generateClaudeHooks(checkerCommand);
|
|
19
19
|
case 'cursor':
|
|
20
|
-
return generateCursorHooks(
|
|
20
|
+
return generateCursorHooks(checkerCommand);
|
|
21
21
|
case 'cline':
|
|
22
|
-
return generateClineHooks(
|
|
22
|
+
return generateClineHooks(checkerCommand);
|
|
23
23
|
case 'windsurf':
|
|
24
|
-
return generateWindsurfHooks(
|
|
24
|
+
return generateWindsurfHooks(checkerCommand);
|
|
25
25
|
default:
|
|
26
26
|
return [];
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
function generateClaudeHooks(
|
|
29
|
+
function generateClaudeHooks(checkerCommand) {
|
|
30
30
|
const settings = {
|
|
31
31
|
hooks: {
|
|
32
32
|
PostToolUse: [
|
|
@@ -35,7 +35,7 @@ function generateClaudeHooks(checkerPath) {
|
|
|
35
35
|
hooks: [
|
|
36
36
|
{
|
|
37
37
|
type: "command",
|
|
38
|
-
command:
|
|
38
|
+
command: `${checkerCommand} --files "$TOOL_INPUT_file_path"`,
|
|
39
39
|
}
|
|
40
40
|
]
|
|
41
41
|
}
|
|
@@ -50,13 +50,13 @@ function generateClaudeHooks(checkerPath) {
|
|
|
50
50
|
},
|
|
51
51
|
];
|
|
52
52
|
}
|
|
53
|
-
function generateCursorHooks(
|
|
53
|
+
function generateCursorHooks(checkerCommand) {
|
|
54
54
|
const hooks = {
|
|
55
55
|
version: 1,
|
|
56
56
|
hooks: {
|
|
57
57
|
afterFileEdit: [
|
|
58
58
|
{
|
|
59
|
-
command:
|
|
59
|
+
command: `${checkerCommand} --stdin`,
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
}
|
|
@@ -109,7 +109,7 @@ process.stdin.on('end', async () => {
|
|
|
109
109
|
},
|
|
110
110
|
];
|
|
111
111
|
}
|
|
112
|
-
function generateClineHooks(
|
|
112
|
+
function generateClineHooks(checkerCommand) {
|
|
113
113
|
const script = `#!/usr/bin/env node
|
|
114
114
|
/**
|
|
115
115
|
* Cline PostToolUse hook for Rigour.
|
|
@@ -174,13 +174,13 @@ process.stdin.on('end', async () => {
|
|
|
174
174
|
},
|
|
175
175
|
];
|
|
176
176
|
}
|
|
177
|
-
function generateWindsurfHooks(
|
|
177
|
+
function generateWindsurfHooks(checkerCommand) {
|
|
178
178
|
const hooks = {
|
|
179
179
|
version: 1,
|
|
180
180
|
hooks: {
|
|
181
181
|
post_write_code: [
|
|
182
182
|
{
|
|
183
|
-
command:
|
|
183
|
+
command: `${checkerCommand} --stdin`,
|
|
184
184
|
}
|
|
185
185
|
]
|
|
186
186
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface ExecutableCheckResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
fixed: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function isExecutableBinary(binaryPath: string, platform?: NodeJS.Platform): boolean;
|
|
6
|
+
export declare function ensureExecutableBinary(binaryPath: string, platform?: NodeJS.Platform): ExecutableCheckResult;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
export function isExecutableBinary(binaryPath, platform = process.platform) {
|
|
3
|
+
if (platform === 'win32') {
|
|
4
|
+
return fs.existsSync(binaryPath);
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
fs.accessSync(binaryPath, fs.constants.X_OK);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function ensureExecutableBinary(binaryPath, platform = process.platform) {
|
|
15
|
+
if (platform === 'win32') {
|
|
16
|
+
return { ok: fs.existsSync(binaryPath), fixed: false };
|
|
17
|
+
}
|
|
18
|
+
if (isExecutableBinary(binaryPath, platform)) {
|
|
19
|
+
return { ok: true, fixed: false };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { ok: false, fixed: false };
|
|
26
|
+
}
|
|
27
|
+
const ok = isExecutableBinary(binaryPath, platform);
|
|
28
|
+
return { ok, fixed: ok };
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { ensureExecutableBinary, isExecutableBinary } from './executable.js';
|
|
6
|
+
describe('executable helpers', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-exec-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('detects executable files', () => {
|
|
15
|
+
const binaryPath = path.join(testDir, 'rigour-brain');
|
|
16
|
+
fs.writeFileSync(binaryPath, '#!/usr/bin/env sh\necho ok\n', 'utf-8');
|
|
17
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
18
|
+
expect(isExecutableBinary(binaryPath)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('repairs executable bit when missing', () => {
|
|
21
|
+
const binaryPath = path.join(testDir, 'rigour-brain');
|
|
22
|
+
fs.writeFileSync(binaryPath, '#!/usr/bin/env sh\necho ok\n', 'utf-8');
|
|
23
|
+
fs.chmodSync(binaryPath, 0o644);
|
|
24
|
+
const result = ensureExecutableBinary(binaryPath);
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
// Windows does not use POSIX executable bits.
|
|
27
|
+
expect(result.ok).toBe(true);
|
|
28
|
+
expect(result.fixed).toBe(false);
|
|
29
|
+
expect(isExecutableBinary(binaryPath)).toBe(true);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Some CI/filesystem setups can deny chmod transitions; in that case
|
|
33
|
+
// the helper should fail gracefully without throwing.
|
|
34
|
+
if (!result.ok) {
|
|
35
|
+
expect(result.fixed).toBe(false);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
expect(result.fixed).toBe(true);
|
|
39
|
+
expect(isExecutableBinary(binaryPath)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type ModelTier, type ModelInfo } from './types.js';
|
|
2
|
+
export declare function extractSha256FromEtag(etag: string | null): string | null;
|
|
3
|
+
export declare function hashFileSha256(filePath: string): Promise<string>;
|
|
2
4
|
/**
|
|
3
5
|
* Check if a model is already downloaded and valid.
|
|
4
6
|
*/
|
|
5
|
-
export declare function isModelCached(tier: ModelTier): boolean
|
|
7
|
+
export declare function isModelCached(tier: ModelTier): Promise<boolean>;
|
|
6
8
|
/**
|
|
7
9
|
* Get the path to a cached model.
|
|
8
10
|
*/
|