@rigour-labs/core 6.2.3 → 6.2.5
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/deep/code-context.d.ts +28 -0
- package/dist/deep/code-context.js +93 -0
- package/dist/deep/code-context.test.d.ts +1 -0
- package/dist/deep/code-context.test.js +48 -0
- package/dist/deep/code-pass.d.ts +15 -0
- package/dist/deep/code-pass.js +44 -0
- package/dist/deep/code-review-prompt.d.ts +17 -0
- package/dist/deep/code-review-prompt.js +30 -0
- package/dist/deep/code-review-prompt.test.d.ts +1 -0
- package/dist/deep/code-review-prompt.test.js +20 -0
- package/dist/deep/code-verifier.d.ts +19 -0
- package/dist/deep/code-verifier.js +48 -0
- package/dist/deep/code-verifier.test.d.ts +1 -0
- package/dist/deep/code-verifier.test.js +46 -0
- package/dist/deep/fact-extractor.d.ts +7 -1
- package/dist/deep/fact-extractor.js +16 -8
- package/dist/deep/fact-extractor.test.js +34 -1
- package/dist/deep/facts-pass.d.ts +24 -0
- package/dist/deep/facts-pass.js +69 -0
- package/dist/deep/index.d.ts +6 -1
- package/dist/deep/index.js +5 -1
- package/dist/deep/parse-findings.d.ts +12 -0
- package/dist/deep/parse-findings.js +73 -0
- package/dist/deep/parse-findings.test.d.ts +1 -0
- package/dist/deep/parse-findings.test.js +25 -0
- package/dist/deep/prompts.d.ts +1 -1
- package/dist/deep/prompts.js +7 -8
- package/dist/deep/prompts.test.js +15 -0
- package/dist/gates/context.js +7 -0
- package/dist/gates/deep-analysis.d.ts +31 -8
- package/dist/gates/deep-analysis.js +165 -306
- package/dist/gates/deep-analysis.test.d.ts +1 -0
- package/dist/gates/deep-analysis.test.js +86 -0
- package/dist/gates/deep-runner.d.ts +15 -0
- package/dist/gates/deep-runner.js +65 -0
- package/dist/gates/dependency.js +4 -0
- package/dist/gates/hallucinated-imports/index.js +13 -9
- package/dist/gates/hallucinated-imports.test.js +10 -0
- package/dist/gates/js-style-context.d.ts +3 -0
- package/dist/gates/js-style-context.js +65 -0
- package/dist/gates/js-style-context.test.d.ts +1 -0
- package/dist/gates/js-style-context.test.js +35 -0
- package/dist/gates/logic-drift-git-base.d.ts +7 -0
- package/dist/gates/logic-drift-git-base.js +41 -0
- package/dist/gates/logic-drift.d.ts +3 -3
- package/dist/gates/logic-drift.js +39 -7
- package/dist/gates/logic-drift.test.d.ts +1 -0
- package/dist/gates/logic-drift.test.js +45 -0
- package/dist/gates/runner.js +5 -40
- package/dist/gates/runner.test.js +37 -25
- package/dist/gates/scoped-gates.test.d.ts +1 -0
- package/dist/gates/scoped-gates.test.js +52 -0
- package/dist/gates/security-command-execution.d.ts +5 -0
- package/dist/gates/security-command-execution.js +123 -0
- package/dist/gates/security-patterns-data.js +1 -17
- package/dist/gates/security-patterns.d.ts +0 -5
- package/dist/gates/security-patterns.js +11 -19
- package/dist/gates/security-patterns.test.js +37 -2
- package/dist/gates/style-drift.d.ts +4 -4
- package/dist/gates/style-drift.js +15 -16
- package/dist/gates/style-drift.test.d.ts +1 -0
- package/dist/gates/style-drift.test.js +26 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/inference/http-download.d.ts +22 -0
- package/dist/inference/http-download.js +132 -0
- package/dist/inference/http-download.test.d.ts +1 -0
- package/dist/inference/http-download.test.js +109 -0
- package/dist/inference/index.d.ts +5 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/llama-engine.d.ts +27 -0
- package/dist/inference/llama-engine.js +152 -0
- package/dist/inference/llama-engine.test.d.ts +1 -0
- package/dist/inference/llama-engine.test.js +51 -0
- package/dist/inference/llama-process.d.ts +72 -0
- package/dist/inference/llama-process.js +126 -0
- package/dist/inference/llama-process.test.d.ts +1 -0
- package/dist/inference/llama-process.test.js +61 -0
- package/dist/inference/model-download.test.d.ts +1 -0
- package/dist/inference/model-download.test.js +73 -0
- package/dist/inference/model-manager.d.ts +14 -1
- package/dist/inference/model-manager.js +65 -80
- package/dist/inference/sidecar-provider.d.ts +19 -10
- package/dist/inference/sidecar-provider.js +115 -343
- package/dist/templates/universal-config.js +0 -2
- package/dist/types/index.d.ts +58 -14
- package/dist/types/index.js +9 -2
- package/dist/utils/deep-status.d.ts +7 -0
- package/dist/utils/deep-status.js +11 -0
- package/dist/utils/diff.d.ts +3 -0
- package/dist/utils/diff.js +52 -0
- package/dist/utils/diff.test.d.ts +1 -0
- package/dist/utils/diff.test.js +38 -0
- package/dist/utils/scanner.js +13 -2
- package/dist/utils/scanner.test.js +22 -8
- package/dist/utils/scope.d.ts +15 -0
- package/dist/utils/scope.js +51 -0
- package/dist/utils/scope.test.d.ts +1 -0
- package/dist/utils/scope.test.js +38 -0
- package/package.json +6 -6
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { ContextGate } from './context.js';
|
|
6
|
+
import { DependencyGate } from './dependency.js';
|
|
7
|
+
import { LogicDriftGate } from './logic-drift.js';
|
|
8
|
+
import { StyleDriftGate } from './style-drift.js';
|
|
9
|
+
describe('gates on a scoped run', () => {
|
|
10
|
+
let cwd;
|
|
11
|
+
beforeEach(async () => { cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-scoped-')); });
|
|
12
|
+
afterEach(async () => { await fs.remove(cwd); });
|
|
13
|
+
it('context-drift reports only findings that cite a file in scope', async () => {
|
|
14
|
+
await fs.outputFile(path.join(cwd, 'src/a.ts'), 'export const a = process.env.API_URL_PROD;\n');
|
|
15
|
+
await fs.outputFile(path.join(cwd, 'src/b.ts'), 'export const b = process.env.API_URL_STAGING;\n');
|
|
16
|
+
const gate = new ContextGate({ context: { enabled: true } });
|
|
17
|
+
const record = { anchors: [{ type: 'env', id: 'API_URL', confidence: 1 }] };
|
|
18
|
+
const unscoped = await gate.run({ cwd, record });
|
|
19
|
+
const scoped = await gate.run({ cwd, record, patterns: ['src/a.ts'] });
|
|
20
|
+
expect(unscoped.flatMap(f => f.files)).toEqual(expect.arrayContaining(['src/a.ts', 'src/b.ts']));
|
|
21
|
+
expect(scoped.length).toBeGreaterThan(0);
|
|
22
|
+
expect(scoped.every(f => f.files?.includes('src/a.ts'))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('dependency-guardian checks the manifest only when package.json is in scope', async () => {
|
|
25
|
+
await fs.outputJson(path.join(cwd, 'package.json'), { dependencies: { 'left-pad': '1.0.0' } });
|
|
26
|
+
await fs.outputFile(path.join(cwd, 'src/a.ts'), "import pad from 'left-pad';\n");
|
|
27
|
+
const gate = new DependencyGate({ gates: { dependencies: { forbid: ['left-pad'] } } });
|
|
28
|
+
expect(await gate.run({ cwd, patterns: ['src/a.ts'] })).toEqual([]);
|
|
29
|
+
const withManifest = await gate.run({ cwd, patterns: ['package.json'] });
|
|
30
|
+
expect(withManifest.some(f => f.title === 'Forbidden Dependency')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('logic-drift never creates or rewrites its baseline from a scoped run', async () => {
|
|
33
|
+
const baseline = path.join(cwd, '.rigour/logic-baseline.json');
|
|
34
|
+
await fs.outputFile(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score >= 10; }\n');
|
|
35
|
+
await fs.outputFile(path.join(cwd, 'src/other.ts'), 'export function other(n: number) { return n > 1; }\n');
|
|
36
|
+
const gate = new LogicDriftGate();
|
|
37
|
+
expect(await gate.run({ cwd, patterns: ['src/rules.ts'] })).toEqual([]);
|
|
38
|
+
expect(await fs.pathExists(baseline)).toBe(false);
|
|
39
|
+
await gate.run({ cwd });
|
|
40
|
+
const before = await fs.readFile(baseline, 'utf-8');
|
|
41
|
+
await fs.outputFile(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score > 10; }\n');
|
|
42
|
+
const scoped = await gate.run({ cwd, patterns: ['src/rules.ts'] });
|
|
43
|
+
expect(scoped.some(f => f.details.includes("'>=' to '>'"))).toBe(true);
|
|
44
|
+
expect(await fs.readFile(baseline, 'utf-8')).toBe(before);
|
|
45
|
+
});
|
|
46
|
+
it('style-drift does not create its baseline from a scoped run', async () => {
|
|
47
|
+
await fs.outputFile(path.join(cwd, 'src/a.ts'), 'export function doThing() { return 1; }\n');
|
|
48
|
+
const gate = new StyleDriftGate();
|
|
49
|
+
expect(await gate.run({ cwd, patterns: ['src/a.ts'] })).toEqual([]);
|
|
50
|
+
expect(await fs.pathExists(path.join(cwd, '.rigour/style-baseline.json'))).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const SHELL_MODULES = new Set(['child_process', 'node:child_process']);
|
|
3
|
+
const EXEC_METHODS = new Set(['exec', 'execSync']);
|
|
4
|
+
const SPAWN_METHODS = new Set(['spawn', 'spawnSync']);
|
|
5
|
+
const USER_INPUT = /\b(?:req|request|query|params|body|input|user|argv)\b|process\.env/;
|
|
6
|
+
function shellModule(node) {
|
|
7
|
+
return ts.isStringLiteral(node) && SHELL_MODULES.has(node.text);
|
|
8
|
+
}
|
|
9
|
+
function isShellRequire(node) {
|
|
10
|
+
return ts.isCallExpression(node)
|
|
11
|
+
&& ts.isIdentifier(node.expression)
|
|
12
|
+
&& node.expression.text === 'require'
|
|
13
|
+
&& node.arguments.length === 1
|
|
14
|
+
&& shellModule(node.arguments[0]);
|
|
15
|
+
}
|
|
16
|
+
function collectBindings(source) {
|
|
17
|
+
const bindings = { functions: new Map(), namespaces: new Set() };
|
|
18
|
+
for (const statement of source.statements) {
|
|
19
|
+
if (ts.isImportDeclaration(statement) && shellModule(statement.moduleSpecifier)) {
|
|
20
|
+
const clause = statement.importClause;
|
|
21
|
+
if (clause?.name)
|
|
22
|
+
bindings.namespaces.add(clause.name.text);
|
|
23
|
+
const named = clause?.namedBindings;
|
|
24
|
+
if (named && ts.isNamespaceImport(named))
|
|
25
|
+
bindings.namespaces.add(named.name.text);
|
|
26
|
+
if (named && ts.isNamedImports(named)) {
|
|
27
|
+
for (const element of named.elements) {
|
|
28
|
+
const exported = element.propertyName?.text ?? element.name.text;
|
|
29
|
+
if (EXEC_METHODS.has(exported) || SPAWN_METHODS.has(exported)) {
|
|
30
|
+
bindings.functions.set(element.name.text, exported);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!ts.isVariableStatement(statement))
|
|
36
|
+
continue;
|
|
37
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
38
|
+
const initializer = declaration.initializer;
|
|
39
|
+
if (!initializer)
|
|
40
|
+
continue;
|
|
41
|
+
if (isShellRequire(initializer)) {
|
|
42
|
+
if (ts.isIdentifier(declaration.name))
|
|
43
|
+
bindings.namespaces.add(declaration.name.text);
|
|
44
|
+
if (ts.isObjectBindingPattern(declaration.name)) {
|
|
45
|
+
for (const element of declaration.name.elements) {
|
|
46
|
+
const exported = element.propertyName?.getText(source) ?? element.name.getText(source);
|
|
47
|
+
if ((EXEC_METHODS.has(exported) || SPAWN_METHODS.has(exported)) && ts.isIdentifier(element.name)) {
|
|
48
|
+
bindings.functions.set(element.name.text, exported);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (ts.isPropertyAccessExpression(initializer) && isShellRequire(initializer.expression)
|
|
54
|
+
&& ts.isIdentifier(declaration.name)) {
|
|
55
|
+
bindings.functions.set(declaration.name.text, initializer.name.text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return bindings;
|
|
60
|
+
}
|
|
61
|
+
function shadowsImportedName(node, name) {
|
|
62
|
+
for (let scope = node.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
|
|
63
|
+
if (ts.isFunctionLike(scope) && scope.parameters.some(param => ts.isIdentifier(param.name) && param.name.text === name)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (!ts.isBlock(scope))
|
|
67
|
+
continue;
|
|
68
|
+
for (const statement of scope.statements) {
|
|
69
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name)
|
|
70
|
+
return true;
|
|
71
|
+
if (!ts.isVariableStatement(statement))
|
|
72
|
+
continue;
|
|
73
|
+
if (statement.declarationList.declarations.some(decl => ts.isIdentifier(decl.name) && decl.name.text === name)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function shellMethod(call, bindings) {
|
|
81
|
+
const expression = call.expression;
|
|
82
|
+
if (ts.isIdentifier(expression)) {
|
|
83
|
+
return shadowsImportedName(call, expression.text) ? undefined : bindings.functions.get(expression.text);
|
|
84
|
+
}
|
|
85
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
86
|
+
const receiver = expression.expression;
|
|
87
|
+
if (isShellRequire(receiver))
|
|
88
|
+
return expression.name.text;
|
|
89
|
+
if (ts.isIdentifier(receiver) && bindings.namespaces.has(receiver.text)
|
|
90
|
+
&& !shadowsImportedName(call, receiver.text))
|
|
91
|
+
return expression.name.text;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function shellEnabled(call) {
|
|
96
|
+
return call.arguments.some(arg => ts.isObjectLiteralExpression(arg)
|
|
97
|
+
&& arg.properties.some(prop => ts.isPropertyAssignment(prop)
|
|
98
|
+
&& (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
|
|
99
|
+
&& prop.name.text === 'shell'
|
|
100
|
+
&& prop.initializer.kind === ts.SyntaxKind.TrueKeyword));
|
|
101
|
+
}
|
|
102
|
+
function taintedArgument(call, source) {
|
|
103
|
+
return call.arguments.some(arg => USER_INPUT.test(arg.getText(source)));
|
|
104
|
+
}
|
|
105
|
+
export function findUnsafeShellCalls(content, file) {
|
|
106
|
+
const kind = file.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
107
|
+
const source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, kind);
|
|
108
|
+
const bindings = collectBindings(source);
|
|
109
|
+
const calls = [];
|
|
110
|
+
const visit = (node) => {
|
|
111
|
+
if (ts.isCallExpression(node)) {
|
|
112
|
+
const method = shellMethod(node, bindings);
|
|
113
|
+
const executesShell = method && (EXEC_METHODS.has(method) || (SPAWN_METHODS.has(method) && shellEnabled(node)));
|
|
114
|
+
if (executesShell && taintedArgument(node, source)) {
|
|
115
|
+
calls.push({ line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
116
|
+
text: node.getText(source).slice(0, 120) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
ts.forEachChild(node, visit);
|
|
120
|
+
};
|
|
121
|
+
visit(source);
|
|
122
|
+
return calls;
|
|
123
|
+
}
|
|
@@ -98,23 +98,7 @@ export const VULNERABILITY_PATTERNS = [
|
|
|
98
98
|
cwe: 'CWE-338',
|
|
99
99
|
languages: ['ts', 'js', 'tsx', 'jsx']
|
|
100
100
|
},
|
|
101
|
-
//
|
|
102
|
-
{
|
|
103
|
-
type: 'command_injection',
|
|
104
|
-
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,
|
|
105
|
-
severity: 'critical',
|
|
106
|
-
description: 'Potential command injection: shell execution with user input',
|
|
107
|
-
cwe: 'CWE-78',
|
|
108
|
-
languages: ['ts', 'js']
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
type: 'command_injection',
|
|
112
|
-
regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\([^)]*(?:req\.|query|params|body|input|user|argv|process\.env)/g,
|
|
113
|
-
severity: 'high',
|
|
114
|
-
description: 'child_process usage detected (verify input sanitization)',
|
|
115
|
-
cwe: 'CWE-78',
|
|
116
|
-
languages: ['ts', 'js']
|
|
117
|
-
},
|
|
101
|
+
// JavaScript shell calls are checked with receiver-aware syntax analysis.
|
|
118
102
|
// Python subprocess command injection
|
|
119
103
|
{
|
|
120
104
|
type: 'command_injection',
|
|
@@ -61,11 +61,6 @@ export declare class SecurityPatternsGate extends Gate {
|
|
|
61
61
|
* Known sanitizers: DOMPurify.sanitize(), sanitize(), xss(), escapeHtml(), htmlEncode().
|
|
62
62
|
*/
|
|
63
63
|
private isSanitizedAssignment;
|
|
64
|
-
/**
|
|
65
|
-
* Check if a shell execution call is using { shell: false } option (safe pattern).
|
|
66
|
-
* Also, spawn() without { shell: true } is safe by default — don't flag it.
|
|
67
|
-
*/
|
|
68
|
-
private isSafeShellCall;
|
|
69
64
|
}
|
|
70
65
|
/**
|
|
71
66
|
* Quick helper to check a single file for security issues
|
|
@@ -16,6 +16,7 @@ import { Gate } from './base.js';
|
|
|
16
16
|
import { FileScanner } from '../utils/scanner.js';
|
|
17
17
|
import { Logger } from '../utils/logger.js';
|
|
18
18
|
import { VULNERABILITY_PATTERNS } from './security-patterns-data.js';
|
|
19
|
+
import { findUnsafeShellCalls } from './security-command-execution.js';
|
|
19
20
|
import fs from 'fs-extra';
|
|
20
21
|
import path from 'path';
|
|
21
22
|
export class SecurityPatternsGate extends Gate {
|
|
@@ -127,7 +128,16 @@ export class SecurityPatternsGate extends Gate {
|
|
|
127
128
|
return false;
|
|
128
129
|
}
|
|
129
130
|
scanFileForVulnerabilities(content, file, ext, vulnerabilities) {
|
|
130
|
-
|
|
131
|
+
if (ext === 'ts' || ext === 'js') {
|
|
132
|
+
for (const call of findUnsafeShellCalls(content, file)) {
|
|
133
|
+
vulnerabilities.push({
|
|
134
|
+
type: 'command_injection', severity: 'critical', file,
|
|
135
|
+
line: call.line, match: call.text,
|
|
136
|
+
description: 'Potential command injection: shell execution with user input',
|
|
137
|
+
cwe: 'CWE-78',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
131
141
|
for (const pattern of VULNERABILITY_PATTERNS) {
|
|
132
142
|
// Check if pattern applies to this file type
|
|
133
143
|
if (!pattern.languages.includes('*') && !pattern.languages.includes(ext)) {
|
|
@@ -145,10 +155,6 @@ export class SecurityPatternsGate extends Gate {
|
|
|
145
155
|
if (pattern.type === 'xss' && this.isSanitizedAssignment(match[0])) {
|
|
146
156
|
continue;
|
|
147
157
|
}
|
|
148
|
-
// For command_injection: check if spawn/exec uses { shell: false } (safe)
|
|
149
|
-
if (pattern.type === 'command_injection' && this.isSafeShellCall(match[0], content, match.index)) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
158
|
// Find line number
|
|
153
159
|
const beforeMatch = content.slice(0, match.index);
|
|
154
160
|
const lineNumber = beforeMatch.split('\n').length;
|
|
@@ -244,20 +250,6 @@ export class SecurityPatternsGate extends Gate {
|
|
|
244
250
|
const rhs = matchText.includes('=') ? matchText.split('=').slice(1).join('=') : matchText;
|
|
245
251
|
return sanitizers.some(s => rhs.toLowerCase().includes(s.toLowerCase()));
|
|
246
252
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Check if a shell execution call is using { shell: false } option (safe pattern).
|
|
249
|
-
* Also, spawn() without { shell: true } is safe by default — don't flag it.
|
|
250
|
-
*/
|
|
251
|
-
isSafeShellCall(matchText, fullContent, matchIndex) {
|
|
252
|
-
// Check surrounding context (100 chars after match) for shell: false
|
|
253
|
-
const context = fullContent.slice(matchIndex, matchIndex + matchText.length + 100);
|
|
254
|
-
if (/shell\s*:\s*false/.test(context))
|
|
255
|
-
return true;
|
|
256
|
-
// spawn() without shell: true is safe by default
|
|
257
|
-
if (/\bspawn(?:Sync)?\s*\(/.test(matchText) && !/shell\s*:\s*true/.test(context))
|
|
258
|
-
return true;
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
253
|
}
|
|
262
254
|
/**
|
|
263
255
|
* Quick helper to check a single file for security issues
|
|
@@ -105,14 +105,49 @@ describe('SecurityPatternsGate', () => {
|
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
107
|
describe('command injection detection', () => {
|
|
108
|
-
it('
|
|
108
|
+
it('detects imported shell execution with user input', async () => {
|
|
109
109
|
const filePath = path.join(testDir, 'shell.ts');
|
|
110
110
|
fs.writeFileSync(filePath, `
|
|
111
|
-
exec
|
|
111
|
+
import { exec as runShell } from 'node:child_process';
|
|
112
|
+
runShell(\`ls \${req.query.path}\`);
|
|
112
113
|
`);
|
|
113
114
|
const vulns = await checkSecurityPatterns(filePath);
|
|
114
115
|
expect(vulns.some(v => v.type === 'command_injection')).toBe(true);
|
|
115
116
|
});
|
|
117
|
+
it('does not treat RegExp.exec or unrelated methods as shell execution', async () => {
|
|
118
|
+
const filePath = path.join(testDir, 'template.ts');
|
|
119
|
+
fs.writeFileSync(filePath, `
|
|
120
|
+
const separator = HB_ELSE.exec(body);
|
|
121
|
+
const other = LIQUID_BRANCH.exec(user);
|
|
122
|
+
const local = { exec(value: string) { return value; } };
|
|
123
|
+
local.exec(request.body);
|
|
124
|
+
`);
|
|
125
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
126
|
+
expect(vulns.filter(v => v.type === 'command_injection')).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
it('detects namespace, require, and shell-enabled spawn calls', async () => {
|
|
129
|
+
const filePath = path.join(testDir, 'shell.js');
|
|
130
|
+
fs.writeFileSync(filePath, `
|
|
131
|
+
const cp = require('child_process');
|
|
132
|
+
const { exec: runShell } = require('node:child_process');
|
|
133
|
+
cp.exec(req.body.command);
|
|
134
|
+
runShell(user.command);
|
|
135
|
+
cp.spawn(process.env.SHELL, [input], { shell: true });
|
|
136
|
+
cp.spawn(process.env.SHELL, [input]);
|
|
137
|
+
`);
|
|
138
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
139
|
+
expect(vulns.filter(v => v.type === 'command_injection')).toHaveLength(3);
|
|
140
|
+
});
|
|
141
|
+
it('does not use an imported shell binding shadowed by a parameter', async () => {
|
|
142
|
+
const filePath = path.join(testDir, 'shadow.ts');
|
|
143
|
+
fs.writeFileSync(filePath, `
|
|
144
|
+
import { exec } from 'child_process';
|
|
145
|
+
function parse(exec: RegExp) { return exec.exec(body); }
|
|
146
|
+
function run(exec: (value: string) => void) { exec(user); }
|
|
147
|
+
`);
|
|
148
|
+
const vulns = await checkSecurityPatterns(filePath);
|
|
149
|
+
expect(vulns.filter(v => v.type === 'command_injection')).toEqual([]);
|
|
150
|
+
});
|
|
116
151
|
});
|
|
117
152
|
describe('severity filtering', () => {
|
|
118
153
|
it('should block on high severity by default', async () => {
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* style which may differ from the project norm.
|
|
7
7
|
*
|
|
8
8
|
* What it checks:
|
|
9
|
-
* 1. Naming conventions —
|
|
9
|
+
* 1. Naming conventions — compare names with the same language conventions
|
|
10
10
|
* 2. Error handling patterns — try-catch vs .catch()/except/rescue consistency
|
|
11
|
-
* 3. Import style — named vs default vs wildcard import consistency
|
|
12
|
-
* 4. Quote style — single vs double quote consistency
|
|
13
11
|
*
|
|
14
12
|
* How it works:
|
|
15
13
|
* 1. First scan: sample source files → compute per-language style fingerprints → store baseline
|
|
@@ -17,7 +15,9 @@
|
|
|
17
15
|
* 3. If a file deviates >25% on any dimension → flag as style drift
|
|
18
16
|
*
|
|
19
17
|
* Baselines are per-language to avoid cross-language contamination
|
|
20
|
-
* (e.g., Python snake_case
|
|
18
|
+
* (e.g., Python snake_case should not flag JS camelCase).
|
|
19
|
+
* JS names mandated by frameworks are excluded from style comparison, and
|
|
20
|
+
* imports are never compared by a language-wide named/default ratio.
|
|
21
21
|
*/
|
|
22
22
|
import { Gate, GateContext } from './base.js';
|
|
23
23
|
import { Failure, Provenance } from '../types/index.js';
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* style which may differ from the project norm.
|
|
7
7
|
*
|
|
8
8
|
* What it checks:
|
|
9
|
-
* 1. Naming conventions —
|
|
9
|
+
* 1. Naming conventions — compare names with the same language conventions
|
|
10
10
|
* 2. Error handling patterns — try-catch vs .catch()/except/rescue consistency
|
|
11
|
-
* 3. Import style — named vs default vs wildcard import consistency
|
|
12
|
-
* 4. Quote style — single vs double quote consistency
|
|
13
11
|
*
|
|
14
12
|
* How it works:
|
|
15
13
|
* 1. First scan: sample source files → compute per-language style fingerprints → store baseline
|
|
@@ -17,12 +15,16 @@
|
|
|
17
15
|
* 3. If a file deviates >25% on any dimension → flag as style drift
|
|
18
16
|
*
|
|
19
17
|
* Baselines are per-language to avoid cross-language contamination
|
|
20
|
-
* (e.g., Python snake_case
|
|
18
|
+
* (e.g., Python snake_case should not flag JS camelCase).
|
|
19
|
+
* JS names mandated by frameworks are excluded from style comparison, and
|
|
20
|
+
* imports are never compared by a language-wide named/default ratio.
|
|
21
21
|
*/
|
|
22
22
|
import { Gate } from './base.js';
|
|
23
23
|
import { FileScanner } from '../utils/scanner.js';
|
|
24
24
|
import { Logger } from '../utils/logger.js';
|
|
25
25
|
import { languageAdapters } from './language-adapters/index.js';
|
|
26
|
+
import { extractComparableJsNames } from './js-style-context.js';
|
|
27
|
+
import { isScoped } from '../utils/scope.js';
|
|
26
28
|
import { TRY_CATCH_PATTERN, CATCH_PATTERN, RESULT_TYPE_PATTERN, NAMED_IMPORT_PATTERN, WILDCARD_IMPORT_PATTERN, SIDE_EFFECT_IMPORT_PATTERN, DEFAULT_IMPORT_PATTERN, countQuotes, } from './style-drift-rules.js';
|
|
27
29
|
import fs from 'fs-extra';
|
|
28
30
|
import path from 'path';
|
|
@@ -67,7 +69,7 @@ export class StyleDriftGate extends Gate {
|
|
|
67
69
|
try {
|
|
68
70
|
const raw = await fs.readJson(baselinePath);
|
|
69
71
|
// Handle migration from old single-fingerprint format
|
|
70
|
-
if (raw.version ===
|
|
72
|
+
if (raw.version === 3) {
|
|
71
73
|
baseline = raw;
|
|
72
74
|
}
|
|
73
75
|
else {
|
|
@@ -78,6 +80,8 @@ export class StyleDriftGate extends Gate {
|
|
|
78
80
|
Logger.debug('Failed to load style baseline, will create new one');
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
if (!baseline && isScoped(context.patterns))
|
|
84
|
+
return []; // never baseline from a subset of files
|
|
81
85
|
if (!baseline) {
|
|
82
86
|
// First scan: create per-language baseline
|
|
83
87
|
baseline = await this.computePerLanguageBaseline(context, filesByLang);
|
|
@@ -116,7 +120,7 @@ export class StyleDriftGate extends Gate {
|
|
|
116
120
|
const baseline = {
|
|
117
121
|
languages: {},
|
|
118
122
|
createdAt: new Date().toISOString(),
|
|
119
|
-
version:
|
|
123
|
+
version: 3,
|
|
120
124
|
};
|
|
121
125
|
for (const [langId, langFiles] of filesByLang) {
|
|
122
126
|
// Sample up to sample_size files per language
|
|
@@ -157,7 +161,9 @@ export class StyleDriftGate extends Gate {
|
|
|
157
161
|
const adapter = languageAdapters.getAdapter(filePath);
|
|
158
162
|
// ── Naming conventions (via adapter) ──
|
|
159
163
|
if (adapter) {
|
|
160
|
-
const namingPatterns = adapter.
|
|
164
|
+
const namingPatterns = adapter.id === 'js'
|
|
165
|
+
? extractComparableJsNames(content, filePath)
|
|
166
|
+
: adapter.extractNamingPatterns(content);
|
|
161
167
|
for (const pattern of namingPatterns) {
|
|
162
168
|
if (pattern.kind === 'function' || pattern.kind === 'method') {
|
|
163
169
|
fp.naming.functions[pattern.convention]++;
|
|
@@ -244,15 +250,8 @@ export class StyleDriftGate extends Gate {
|
|
|
244
250
|
suggestion: `Use ${errDev.baselinePredominant} error handling pattern to match project conventions.`,
|
|
245
251
|
});
|
|
246
252
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
deviations.push({
|
|
250
|
-
dimension: 'import style',
|
|
251
|
-
score: impDev.score,
|
|
252
|
-
detail: `file uses ${impDev.filePredominant}, project uses ${impDev.baselinePredominant}`,
|
|
253
|
-
suggestion: `Use ${impDev.baselinePredominant} imports to match project conventions.`,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
253
|
+
// Import form depends on the imported module's exports. A language-wide
|
|
254
|
+
// named/default ratio cannot establish a valid replacement.
|
|
256
255
|
return deviations;
|
|
257
256
|
}
|
|
258
257
|
distributionDeviation(file, baseline) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { StyleDriftGate } from './style-drift.js';
|
|
6
|
+
describe('StyleDriftGate', () => {
|
|
7
|
+
let cwd;
|
|
8
|
+
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'style-drift-')); });
|
|
9
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
10
|
+
it('does not compare default imports against unrelated named imports', async () => {
|
|
11
|
+
const src = path.join(cwd, 'src');
|
|
12
|
+
fs.mkdirSync(src);
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
fs.writeFileSync(path.join(src, `named${i}.ts`), `import { value } from './shared';\nexport function helper${i}() { return value; }\n`);
|
|
15
|
+
}
|
|
16
|
+
fs.writeFileSync(path.join(src, 'mui.ts'), [
|
|
17
|
+
"import Box from '@mui/material/Box';",
|
|
18
|
+
"import TextField from '@mui/material/TextField';",
|
|
19
|
+
"import Button from '@mui/material/Button';",
|
|
20
|
+
].join('\n'));
|
|
21
|
+
const gate = new StyleDriftGate();
|
|
22
|
+
expect(await gate.run({ cwd })).toEqual([]);
|
|
23
|
+
const findings = await gate.run({ cwd });
|
|
24
|
+
expect(findings.some(finding => finding.details.includes('import style'))).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
|
9
9
|
export { SideEffectAnalysisGate } from './gates/side-effect-analysis/index.js';
|
|
10
10
|
export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js';
|
|
11
11
|
export * from './utils/logger.js';
|
|
12
|
+
export { normalizeScopePatterns, isScoped } from './utils/scope.js';
|
|
13
|
+
export { deepAnalysisError } from './utils/deep-status.js';
|
|
14
|
+
export { parseDiff, changedLinesByFile } from './utils/diff.js';
|
|
12
15
|
export { FileScanner } from './utils/scanner.js';
|
|
13
16
|
export * from './services/score-history.js';
|
|
14
17
|
export * from './hooks/index.js';
|
|
@@ -18,7 +21,9 @@ export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
|
18
21
|
export { createProvider } from './inference/index.js';
|
|
19
22
|
export type { InferenceProvider, DeepFinding, DeepAnalysisResult, ModelTier } from './inference/types.js';
|
|
20
23
|
export { MODELS } from './inference/types.js';
|
|
21
|
-
export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
|
|
24
|
+
export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel } from './inference/model-manager.js';
|
|
25
|
+
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
|
|
26
|
+
export { SidecarProvider } from './inference/sidecar-provider.js';
|
|
22
27
|
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
23
28
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
24
29
|
export type { RigourDB, CompactResult } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
|
9
9
|
export { SideEffectAnalysisGate } from './gates/side-effect-analysis/index.js';
|
|
10
10
|
export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js';
|
|
11
11
|
export * from './utils/logger.js';
|
|
12
|
+
export { normalizeScopePatterns, isScoped } from './utils/scope.js';
|
|
13
|
+
export { deepAnalysisError } from './utils/deep-status.js';
|
|
14
|
+
export { parseDiff, changedLinesByFile } from './utils/diff.js';
|
|
12
15
|
export { FileScanner } from './utils/scanner.js';
|
|
13
16
|
export * from './services/score-history.js';
|
|
14
17
|
export * from './hooks/index.js';
|
|
@@ -18,7 +21,9 @@ export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getPro
|
|
|
18
21
|
export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
19
22
|
export { createProvider } from './inference/index.js';
|
|
20
23
|
export { MODELS } from './inference/types.js';
|
|
21
|
-
export { isModelCached, getModelsDir, getModelInfo } from './inference/model-manager.js';
|
|
24
|
+
export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel } from './inference/model-manager.js';
|
|
25
|
+
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
|
|
26
|
+
export { SidecarProvider } from './inference/sidecar-provider.js';
|
|
22
27
|
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
23
28
|
// Storage (SQLite Brain)
|
|
24
29
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Readable } from 'stream';
|
|
2
|
+
export interface HttpResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
headers: Record<string, string | string[] | undefined>;
|
|
5
|
+
body: Readable;
|
|
6
|
+
}
|
|
7
|
+
export type HttpGet = (url: string, headers: Record<string, string>, timeoutMs: number) => Promise<HttpResponse>;
|
|
8
|
+
export interface DownloadOptions {
|
|
9
|
+
onProgress?: (downloadedBytes: number, totalBytes: number | null) => void;
|
|
10
|
+
stallTimeoutMs?: number;
|
|
11
|
+
/** Resume from an existing partial file at `destPath` (default true). */
|
|
12
|
+
resume?: boolean;
|
|
13
|
+
/** Transport override for tests. */
|
|
14
|
+
get?: HttpGet;
|
|
15
|
+
}
|
|
16
|
+
export interface DownloadResult {
|
|
17
|
+
sha256: string;
|
|
18
|
+
bytes: number;
|
|
19
|
+
/** ETag seen on any hop; Hugging Face exposes the LFS SHA-256 as x-linked-etag. */
|
|
20
|
+
etag: string | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function downloadToFile(url: string, destPath: string, options?: DownloadOptions): Promise<DownloadResult>;
|