@rigour-labs/core 2.18.0 → 2.18.1
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/context.test.js +15 -1
- package/dist/discovery.js +13 -1
- package/dist/gates/agent-team.d.ts +50 -0
- package/dist/gates/agent-team.js +159 -0
- package/dist/gates/agent-team.test.d.ts +1 -0
- package/dist/gates/agent-team.test.js +113 -0
- package/dist/gates/checkpoint.d.ts +72 -0
- package/dist/gates/checkpoint.js +231 -0
- package/dist/gates/checkpoint.test.d.ts +1 -0
- package/dist/gates/checkpoint.test.js +102 -0
- package/dist/gates/context.d.ts +35 -0
- package/dist/gates/context.js +151 -2
- package/dist/gates/runner.js +15 -0
- package/dist/gates/security-patterns.d.ts +48 -0
- package/dist/gates/security-patterns.js +236 -0
- package/dist/gates/security-patterns.test.d.ts +1 -0
- package/dist/gates/security-patterns.test.js +133 -0
- package/dist/services/adaptive-thresholds.d.ts +63 -0
- package/dist/services/adaptive-thresholds.js +204 -0
- package/dist/services/adaptive-thresholds.test.d.ts +1 -0
- package/dist/services/adaptive-thresholds.test.js +129 -0
- package/dist/templates/index.js +34 -0
- package/dist/types/fix-packet.d.ts +4 -4
- package/dist/types/index.d.ts +404 -0
- package/dist/types/index.js +36 -0
- package/package.json +1 -1
- package/src/context.test.ts +15 -1
- package/src/discovery.ts +14 -2
- package/src/gates/agent-team.test.ts +134 -0
- package/src/gates/agent-team.ts +210 -0
- package/src/gates/checkpoint.test.ts +135 -0
- package/src/gates/checkpoint.ts +311 -0
- package/src/gates/context.ts +200 -2
- package/src/gates/runner.ts +18 -0
- package/src/gates/security-patterns.test.ts +162 -0
- package/src/gates/security-patterns.ts +303 -0
- package/src/services/adaptive-thresholds.test.ts +189 -0
- package/src/services/adaptive-thresholds.ts +275 -0
- package/src/templates/index.ts +34 -0
- package/src/types/index.ts +36 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CheckpointGate, recordCheckpoint, getCheckpointSession, clearCheckpointSession, getOrCreateCheckpointSession, completeCheckpointSession, abortCheckpointSession } from './checkpoint.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('CheckpointGate', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'checkpoint-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
clearCheckpointSession(testDir);
|
|
13
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
describe('gate initialization', () => {
|
|
16
|
+
it('should create gate with default config', () => {
|
|
17
|
+
const gate = new CheckpointGate();
|
|
18
|
+
expect(gate.id).toBe('checkpoint');
|
|
19
|
+
expect(gate.title).toBe('Checkpoint Supervision');
|
|
20
|
+
});
|
|
21
|
+
it('should skip when not enabled', async () => {
|
|
22
|
+
const gate = new CheckpointGate({ enabled: false });
|
|
23
|
+
const failures = await gate.run({ cwd: testDir });
|
|
24
|
+
expect(failures).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('session management', () => {
|
|
28
|
+
it('should create a new session', () => {
|
|
29
|
+
const session = getOrCreateCheckpointSession(testDir);
|
|
30
|
+
expect(session.sessionId).toMatch(/^chk-session-/);
|
|
31
|
+
expect(session.status).toBe('active');
|
|
32
|
+
expect(session.checkpoints).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
it('should record a checkpoint', () => {
|
|
35
|
+
const result = recordCheckpoint(testDir, 25, // progressPct
|
|
36
|
+
['src/api/users.ts'], 'Implemented user API', 85 // qualityScore
|
|
37
|
+
);
|
|
38
|
+
expect(result.continue).toBe(true);
|
|
39
|
+
expect(result.checkpoint.progressPct).toBe(25);
|
|
40
|
+
expect(result.checkpoint.qualityScore).toBe(85);
|
|
41
|
+
});
|
|
42
|
+
it('should persist session to disk', () => {
|
|
43
|
+
recordCheckpoint(testDir, 50, [], 'Test', 90);
|
|
44
|
+
const sessionPath = path.join(testDir, '.rigour', 'checkpoint-session.json');
|
|
45
|
+
expect(fs.existsSync(sessionPath)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('should complete session', () => {
|
|
48
|
+
getOrCreateCheckpointSession(testDir);
|
|
49
|
+
completeCheckpointSession(testDir);
|
|
50
|
+
const session = getCheckpointSession(testDir);
|
|
51
|
+
expect(session?.status).toBe('completed');
|
|
52
|
+
});
|
|
53
|
+
it('should abort session with reason', () => {
|
|
54
|
+
getOrCreateCheckpointSession(testDir);
|
|
55
|
+
abortCheckpointSession(testDir, 'Quality too low');
|
|
56
|
+
const session = getCheckpointSession(testDir);
|
|
57
|
+
expect(session?.status).toBe('aborted');
|
|
58
|
+
expect(session?.checkpoints[0].summary).toContain('Quality too low');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('quality threshold', () => {
|
|
62
|
+
it('should continue when quality above threshold', () => {
|
|
63
|
+
const result = recordCheckpoint(testDir, 50, [], 'Good work', 85);
|
|
64
|
+
expect(result.continue).toBe(true);
|
|
65
|
+
expect(result.warnings).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
it('should stop when quality below threshold', () => {
|
|
68
|
+
const result = recordCheckpoint(testDir, 50, [], 'Poor work', 70);
|
|
69
|
+
expect(result.continue).toBe(false);
|
|
70
|
+
expect(result.warnings).toContain('Quality score 70% is below threshold 80%');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('drift detection', () => {
|
|
74
|
+
it('should detect quality degradation', () => {
|
|
75
|
+
// Record several checkpoints with declining quality
|
|
76
|
+
recordCheckpoint(testDir, 20, [], 'Start', 95);
|
|
77
|
+
recordCheckpoint(testDir, 40, [], 'Middle', 90);
|
|
78
|
+
const result = recordCheckpoint(testDir, 60, [], 'Decline', 75);
|
|
79
|
+
expect(result.warnings.some(w => w.includes('Drift detected'))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('should not flag stable quality', () => {
|
|
82
|
+
recordCheckpoint(testDir, 20, [], 'Start', 85);
|
|
83
|
+
recordCheckpoint(testDir, 40, [], 'Middle', 85);
|
|
84
|
+
const result = recordCheckpoint(testDir, 60, [], 'Stable', 85);
|
|
85
|
+
expect(result.warnings.filter(w => w.includes('Drift'))).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('gate run', () => {
|
|
89
|
+
it('should pass with healthy checkpoints', async () => {
|
|
90
|
+
const gate = new CheckpointGate({ enabled: true, quality_threshold: 80 });
|
|
91
|
+
recordCheckpoint(testDir, 50, [], 'Good work', 90);
|
|
92
|
+
const failures = await gate.run({ cwd: testDir });
|
|
93
|
+
expect(failures).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
it('should fail when quality below threshold', async () => {
|
|
96
|
+
const gate = new CheckpointGate({ enabled: true, quality_threshold: 80 });
|
|
97
|
+
recordCheckpoint(testDir, 50, [], 'Poor work', 70);
|
|
98
|
+
const failures = await gate.run({ cwd: testDir });
|
|
99
|
+
expect(failures.some(f => f.title === 'Quality Below Threshold')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
package/dist/gates/context.d.ts
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import { Gate, GateContext } from './base.js';
|
|
2
2
|
import { Failure, Gates } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extended Context Configuration (v2.14+)
|
|
5
|
+
* For 1M token frontier models like Opus 4.6
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtendedContextConfig {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
sensitivity?: number;
|
|
10
|
+
mining_depth?: number;
|
|
11
|
+
cross_file_patterns?: boolean;
|
|
12
|
+
naming_consistency?: boolean;
|
|
13
|
+
import_relationships?: boolean;
|
|
14
|
+
max_cross_file_depth?: number;
|
|
15
|
+
}
|
|
3
16
|
export declare class ContextGate extends Gate {
|
|
4
17
|
private config;
|
|
18
|
+
private extendedConfig;
|
|
5
19
|
constructor(config: Gates);
|
|
6
20
|
run(context: GateContext): Promise<Failure[]>;
|
|
7
21
|
private checkEnvDrift;
|
|
22
|
+
/**
|
|
23
|
+
* Collect naming patterns (function names, class names, variable names)
|
|
24
|
+
*/
|
|
25
|
+
private collectNamingPatterns;
|
|
26
|
+
/**
|
|
27
|
+
* Collect import patterns
|
|
28
|
+
*/
|
|
29
|
+
private collectImportPatterns;
|
|
30
|
+
/**
|
|
31
|
+
* Analyze naming consistency across files
|
|
32
|
+
*/
|
|
33
|
+
private analyzeNamingConsistency;
|
|
34
|
+
/**
|
|
35
|
+
* Analyze import patterns for consistency
|
|
36
|
+
*/
|
|
37
|
+
private analyzeImportPatterns;
|
|
38
|
+
/**
|
|
39
|
+
* Detect casing convention of an identifier
|
|
40
|
+
*/
|
|
41
|
+
private detectCasing;
|
|
42
|
+
private addPattern;
|
|
8
43
|
}
|
package/dist/gates/context.js
CHANGED
|
@@ -4,25 +4,51 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
export class ContextGate extends Gate {
|
|
6
6
|
config;
|
|
7
|
+
extendedConfig;
|
|
7
8
|
constructor(config) {
|
|
8
9
|
super('context-drift', 'Context Awareness & Drift Detection');
|
|
9
10
|
this.config = config;
|
|
11
|
+
this.extendedConfig = {
|
|
12
|
+
enabled: config.context?.enabled ?? false,
|
|
13
|
+
sensitivity: config.context?.sensitivity ?? 0.8,
|
|
14
|
+
mining_depth: config.context?.mining_depth ?? 100,
|
|
15
|
+
cross_file_patterns: true, // Default ON for frontier model support
|
|
16
|
+
naming_consistency: true,
|
|
17
|
+
import_relationships: true,
|
|
18
|
+
max_cross_file_depth: 50,
|
|
19
|
+
};
|
|
10
20
|
}
|
|
11
21
|
async run(context) {
|
|
12
22
|
const failures = [];
|
|
13
23
|
const record = context.record;
|
|
14
|
-
if (!record || !this.
|
|
24
|
+
if (!record || !this.extendedConfig.enabled)
|
|
15
25
|
return [];
|
|
16
26
|
const files = await FileScanner.findFiles({ cwd: context.cwd });
|
|
17
27
|
const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
|
|
28
|
+
// Collect all patterns across files for cross-file analysis
|
|
29
|
+
const namingPatterns = new Map();
|
|
30
|
+
const importPatterns = new Map();
|
|
18
31
|
for (const file of files) {
|
|
19
32
|
try {
|
|
20
33
|
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
21
|
-
// 1. Detect Redundant Suffixes (The Golden Example)
|
|
34
|
+
// 1. Original: Detect Redundant Suffixes (The Golden Example)
|
|
22
35
|
this.checkEnvDrift(content, file, envAnchors, failures);
|
|
36
|
+
// 2. NEW: Cross-file pattern collection
|
|
37
|
+
if (this.extendedConfig.cross_file_patterns) {
|
|
38
|
+
this.collectNamingPatterns(content, file, namingPatterns);
|
|
39
|
+
this.collectImportPatterns(content, file, importPatterns);
|
|
40
|
+
}
|
|
23
41
|
}
|
|
24
42
|
catch (e) { }
|
|
25
43
|
}
|
|
44
|
+
// 3. NEW: Analyze naming consistency across files
|
|
45
|
+
if (this.extendedConfig.naming_consistency) {
|
|
46
|
+
this.analyzeNamingConsistency(namingPatterns, failures);
|
|
47
|
+
}
|
|
48
|
+
// 4. NEW: Analyze import relationship patterns
|
|
49
|
+
if (this.extendedConfig.import_relationships) {
|
|
50
|
+
this.analyzeImportPatterns(importPatterns, failures);
|
|
51
|
+
}
|
|
26
52
|
return failures;
|
|
27
53
|
}
|
|
28
54
|
checkEnvDrift(content, file, anchors, failures) {
|
|
@@ -40,4 +66,127 @@ export class ContextGate extends Gate {
|
|
|
40
66
|
}
|
|
41
67
|
}
|
|
42
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Collect naming patterns (function names, class names, variable names)
|
|
71
|
+
*/
|
|
72
|
+
collectNamingPatterns(content, file, patterns) {
|
|
73
|
+
// Function declarations
|
|
74
|
+
const funcMatches = content.matchAll(/(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=(]/g);
|
|
75
|
+
for (const match of funcMatches) {
|
|
76
|
+
const name = match[1];
|
|
77
|
+
const casing = this.detectCasing(name);
|
|
78
|
+
this.addPattern(patterns, 'function', { casing, file, count: 1 });
|
|
79
|
+
}
|
|
80
|
+
// Class declarations
|
|
81
|
+
const classMatches = content.matchAll(/class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
|
|
82
|
+
for (const match of classMatches) {
|
|
83
|
+
const casing = this.detectCasing(match[1]);
|
|
84
|
+
this.addPattern(patterns, 'class', { casing, file, count: 1 });
|
|
85
|
+
}
|
|
86
|
+
// Interface declarations (TypeScript)
|
|
87
|
+
const interfaceMatches = content.matchAll(/interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
|
|
88
|
+
for (const match of interfaceMatches) {
|
|
89
|
+
const casing = this.detectCasing(match[1]);
|
|
90
|
+
this.addPattern(patterns, 'interface', { casing, file, count: 1 });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Collect import patterns
|
|
95
|
+
*/
|
|
96
|
+
collectImportPatterns(content, file, patterns) {
|
|
97
|
+
// ES6 imports
|
|
98
|
+
const importMatches = content.matchAll(/import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g);
|
|
99
|
+
for (const match of importMatches) {
|
|
100
|
+
const importPath = match[1];
|
|
101
|
+
if (!patterns.has(file)) {
|
|
102
|
+
patterns.set(file, []);
|
|
103
|
+
}
|
|
104
|
+
patterns.get(file).push(importPath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Analyze naming consistency across files
|
|
109
|
+
*/
|
|
110
|
+
analyzeNamingConsistency(patterns, failures) {
|
|
111
|
+
for (const [type, entries] of patterns) {
|
|
112
|
+
const casingCounts = new Map();
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
casingCounts.set(entry.casing, (casingCounts.get(entry.casing) || 0) + entry.count);
|
|
115
|
+
}
|
|
116
|
+
// Find dominant casing
|
|
117
|
+
let dominant = '';
|
|
118
|
+
let maxCount = 0;
|
|
119
|
+
for (const [casing, count] of casingCounts) {
|
|
120
|
+
if (count > maxCount) {
|
|
121
|
+
dominant = casing;
|
|
122
|
+
maxCount = count;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Report violations (non-dominant casing with significant usage)
|
|
126
|
+
const total = entries.reduce((sum, e) => sum + e.count, 0);
|
|
127
|
+
const threshold = total * (1 - (this.extendedConfig.sensitivity ?? 0.8));
|
|
128
|
+
for (const [casing, count] of casingCounts) {
|
|
129
|
+
if (casing !== dominant && count > threshold) {
|
|
130
|
+
const violatingFiles = entries.filter(e => e.casing === casing).map(e => e.file);
|
|
131
|
+
const uniqueFiles = [...new Set(violatingFiles)].slice(0, 5);
|
|
132
|
+
failures.push(this.createFailure(`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`, uniqueFiles, `Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`, 'Naming Convention Drift'));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Analyze import patterns for consistency
|
|
139
|
+
*/
|
|
140
|
+
analyzeImportPatterns(patterns, failures) {
|
|
141
|
+
// Check for mixed import styles (relative vs absolute)
|
|
142
|
+
const relativeCount = new Map();
|
|
143
|
+
const absoluteCount = new Map();
|
|
144
|
+
for (const [file, imports] of patterns) {
|
|
145
|
+
for (const imp of imports) {
|
|
146
|
+
if (imp.startsWith('.') || imp.startsWith('..')) {
|
|
147
|
+
relativeCount.set(file, (relativeCount.get(file) || 0) + 1);
|
|
148
|
+
}
|
|
149
|
+
else if (!imp.startsWith('@') && !imp.includes('/')) {
|
|
150
|
+
// Skip external packages
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
absoluteCount.set(file, (absoluteCount.get(file) || 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Detect files with both relative AND absolute local imports
|
|
158
|
+
const mixedFiles = [];
|
|
159
|
+
for (const file of patterns.keys()) {
|
|
160
|
+
const hasRelative = (relativeCount.get(file) || 0) > 0;
|
|
161
|
+
const hasAbsolute = (absoluteCount.get(file) || 0) > 0;
|
|
162
|
+
if (hasRelative && hasAbsolute) {
|
|
163
|
+
mixedFiles.push(file);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (mixedFiles.length > 3) {
|
|
167
|
+
failures.push(this.createFailure(`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`, mixedFiles.slice(0, 5), 'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.', 'Import Pattern Drift'));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Detect casing convention of an identifier
|
|
172
|
+
*/
|
|
173
|
+
detectCasing(name) {
|
|
174
|
+
if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name))
|
|
175
|
+
return 'PascalCase';
|
|
176
|
+
if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name))
|
|
177
|
+
return 'camelCase';
|
|
178
|
+
if (/^[a-z]+(_[a-z]+)+$/.test(name))
|
|
179
|
+
return 'snake_case';
|
|
180
|
+
if (/^[A-Z]+(_[A-Z]+)*$/.test(name))
|
|
181
|
+
return 'SCREAMING_SNAKE';
|
|
182
|
+
if (/^[A-Z][a-zA-Z]*$/.test(name))
|
|
183
|
+
return 'PascalCase';
|
|
184
|
+
return 'unknown';
|
|
185
|
+
}
|
|
186
|
+
addPattern(patterns, type, entry) {
|
|
187
|
+
if (!patterns.has(type)) {
|
|
188
|
+
patterns.set(type, []);
|
|
189
|
+
}
|
|
190
|
+
patterns.get(type).push(entry);
|
|
191
|
+
}
|
|
43
192
|
}
|
package/dist/gates/runner.js
CHANGED
|
@@ -9,6 +9,9 @@ import { ContextGate } from './context.js';
|
|
|
9
9
|
import { ContextEngine } from '../services/context-engine.js';
|
|
10
10
|
import { EnvironmentGate } from './environment.js';
|
|
11
11
|
import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
|
|
12
|
+
import { AgentTeamGate } from './agent-team.js';
|
|
13
|
+
import { CheckpointGate } from './checkpoint.js';
|
|
14
|
+
import { SecurityPatternsGate } from './security-patterns.js';
|
|
12
15
|
import { execa } from 'execa';
|
|
13
16
|
import { Logger } from '../utils/logger.js';
|
|
14
17
|
export class GateRunner {
|
|
@@ -40,6 +43,18 @@ export class GateRunner {
|
|
|
40
43
|
if (this.config.gates.context?.enabled) {
|
|
41
44
|
this.gates.push(new ContextGate(this.config.gates));
|
|
42
45
|
}
|
|
46
|
+
// Agent Team Governance Gate (for Opus 4.6 / GPT-5.3 multi-agent workflows)
|
|
47
|
+
if (this.config.gates.agent_team?.enabled) {
|
|
48
|
+
this.gates.push(new AgentTeamGate(this.config.gates.agent_team));
|
|
49
|
+
}
|
|
50
|
+
// Checkpoint Supervision Gate (for long-running GPT-5.3 coworking mode)
|
|
51
|
+
if (this.config.gates.checkpoint?.enabled) {
|
|
52
|
+
this.gates.push(new CheckpointGate(this.config.gates.checkpoint));
|
|
53
|
+
}
|
|
54
|
+
// Security Patterns Gate (code-level vulnerability detection)
|
|
55
|
+
if (this.config.gates.security?.enabled) {
|
|
56
|
+
this.gates.push(new SecurityPatternsGate(this.config.gates.security));
|
|
57
|
+
}
|
|
43
58
|
// Environment Alignment Gate (Should be prioritized)
|
|
44
59
|
if (this.config.gates.environment?.enabled) {
|
|
45
60
|
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Patterns Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects code-level security vulnerabilities for frontier models
|
|
5
|
+
* that may generate insecure patterns at scale.
|
|
6
|
+
*
|
|
7
|
+
* Patterns covered:
|
|
8
|
+
* - SQL Injection
|
|
9
|
+
* - XSS (Cross-Site Scripting)
|
|
10
|
+
* - Path Traversal
|
|
11
|
+
* - Hardcoded Secrets
|
|
12
|
+
* - Insecure Randomness
|
|
13
|
+
* - Command Injection
|
|
14
|
+
*
|
|
15
|
+
* @since v2.14.0
|
|
16
|
+
*/
|
|
17
|
+
import { Gate, GateContext } from './base.js';
|
|
18
|
+
import { Failure } from '../types/index.js';
|
|
19
|
+
export interface SecurityVulnerability {
|
|
20
|
+
type: string;
|
|
21
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
match: string;
|
|
25
|
+
description: string;
|
|
26
|
+
cwe?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SecurityPatternsConfig {
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
sql_injection?: boolean;
|
|
31
|
+
xss?: boolean;
|
|
32
|
+
path_traversal?: boolean;
|
|
33
|
+
hardcoded_secrets?: boolean;
|
|
34
|
+
insecure_randomness?: boolean;
|
|
35
|
+
command_injection?: boolean;
|
|
36
|
+
block_on_severity?: 'critical' | 'high' | 'medium' | 'low';
|
|
37
|
+
}
|
|
38
|
+
export declare class SecurityPatternsGate extends Gate {
|
|
39
|
+
private config;
|
|
40
|
+
private severityOrder;
|
|
41
|
+
constructor(config?: SecurityPatternsConfig);
|
|
42
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
43
|
+
private scanFileForVulnerabilities;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Quick helper to check a single file for security issues
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkSecurityPatterns(filePath: string, config?: SecurityPatternsConfig): Promise<SecurityVulnerability[]>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Patterns Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects code-level security vulnerabilities for frontier models
|
|
5
|
+
* that may generate insecure patterns at scale.
|
|
6
|
+
*
|
|
7
|
+
* Patterns covered:
|
|
8
|
+
* - SQL Injection
|
|
9
|
+
* - XSS (Cross-Site Scripting)
|
|
10
|
+
* - Path Traversal
|
|
11
|
+
* - Hardcoded Secrets
|
|
12
|
+
* - Insecure Randomness
|
|
13
|
+
* - Command Injection
|
|
14
|
+
*
|
|
15
|
+
* @since v2.14.0
|
|
16
|
+
*/
|
|
17
|
+
import { Gate } from './base.js';
|
|
18
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
19
|
+
import { Logger } from '../utils/logger.js';
|
|
20
|
+
import fs from 'fs-extra';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
// Pattern definitions with regex and metadata
|
|
23
|
+
const VULNERABILITY_PATTERNS = [
|
|
24
|
+
// SQL Injection
|
|
25
|
+
{
|
|
26
|
+
type: 'sql_injection',
|
|
27
|
+
regex: /(?:execute|query|raw|exec)\s*\(\s*[`'"].*\$\{.+\}|`\s*\+\s*\w+|\$\{.+\}.*(?:SELECT|INSERT|UPDATE|DELETE|DROP)/gi,
|
|
28
|
+
severity: 'critical',
|
|
29
|
+
description: 'Potential SQL injection: User input concatenated into SQL query',
|
|
30
|
+
cwe: 'CWE-89',
|
|
31
|
+
languages: ['ts', 'js', 'py']
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'sql_injection',
|
|
35
|
+
regex: /\.query\s*\(\s*['"`].*\+.*\+.*['"`]\s*\)/g,
|
|
36
|
+
severity: 'critical',
|
|
37
|
+
description: 'SQL query built with string concatenation',
|
|
38
|
+
cwe: 'CWE-89',
|
|
39
|
+
languages: ['ts', 'js']
|
|
40
|
+
},
|
|
41
|
+
// XSS
|
|
42
|
+
{
|
|
43
|
+
type: 'xss',
|
|
44
|
+
regex: /innerHTML\s*=\s*(?!\s*['"`]\s*['"`])[^;]+/g,
|
|
45
|
+
severity: 'high',
|
|
46
|
+
description: 'Potential XSS: innerHTML assignment with dynamic content',
|
|
47
|
+
cwe: 'CWE-79',
|
|
48
|
+
languages: ['ts', 'js', 'tsx', 'jsx']
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'xss',
|
|
52
|
+
regex: /dangerouslySetInnerHTML\s*=\s*\{/g,
|
|
53
|
+
severity: 'high',
|
|
54
|
+
description: 'dangerouslySetInnerHTML usage (ensure content is sanitized)',
|
|
55
|
+
cwe: 'CWE-79',
|
|
56
|
+
languages: ['tsx', 'jsx']
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'xss',
|
|
60
|
+
regex: /document\.write\s*\(/g,
|
|
61
|
+
severity: 'high',
|
|
62
|
+
description: 'document.write is dangerous for XSS',
|
|
63
|
+
cwe: 'CWE-79',
|
|
64
|
+
languages: ['ts', 'js']
|
|
65
|
+
},
|
|
66
|
+
// Path Traversal
|
|
67
|
+
{
|
|
68
|
+
type: 'path_traversal',
|
|
69
|
+
regex: /(?:readFile|writeFile|readdir|unlink|rmdir)\s*\([^)]*(?:req\.(?:params|query|body)|\.\.\/)/g,
|
|
70
|
+
severity: 'high',
|
|
71
|
+
description: 'Potential path traversal: File operation with user input',
|
|
72
|
+
cwe: 'CWE-22',
|
|
73
|
+
languages: ['ts', 'js']
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'path_traversal',
|
|
77
|
+
regex: /path\.join\s*\([^)]*req\./g,
|
|
78
|
+
severity: 'medium',
|
|
79
|
+
description: 'path.join with request data (verify input sanitization)',
|
|
80
|
+
cwe: 'CWE-22',
|
|
81
|
+
languages: ['ts', 'js']
|
|
82
|
+
},
|
|
83
|
+
// Hardcoded Secrets
|
|
84
|
+
{
|
|
85
|
+
type: 'hardcoded_secrets',
|
|
86
|
+
regex: /(?:password|secret|api_key|apikey|auth_token|access_token|private_key)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
87
|
+
severity: 'critical',
|
|
88
|
+
description: 'Hardcoded secret detected in code',
|
|
89
|
+
cwe: 'CWE-798',
|
|
90
|
+
languages: ['ts', 'js', 'py', 'java', 'go']
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'hardcoded_secrets',
|
|
94
|
+
regex: /(?:sk-|pk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_)[a-zA-Z0-9]{20,}/g,
|
|
95
|
+
severity: 'critical',
|
|
96
|
+
description: 'API key pattern detected (OpenAI, GitHub, etc.)',
|
|
97
|
+
cwe: 'CWE-798',
|
|
98
|
+
languages: ['*']
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'hardcoded_secrets',
|
|
102
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
103
|
+
severity: 'critical',
|
|
104
|
+
description: 'Private key embedded in source code',
|
|
105
|
+
cwe: 'CWE-798',
|
|
106
|
+
languages: ['*']
|
|
107
|
+
},
|
|
108
|
+
// Insecure Randomness
|
|
109
|
+
{
|
|
110
|
+
type: 'insecure_randomness',
|
|
111
|
+
regex: /Math\.random\s*\(\s*\)/g,
|
|
112
|
+
severity: 'medium',
|
|
113
|
+
description: 'Math.random() is not cryptographically secure',
|
|
114
|
+
cwe: 'CWE-338',
|
|
115
|
+
languages: ['ts', 'js', 'tsx', 'jsx']
|
|
116
|
+
},
|
|
117
|
+
// Command Injection
|
|
118
|
+
{
|
|
119
|
+
type: 'command_injection',
|
|
120
|
+
regex: /(?:exec|spawn|execSync|spawnSync)\s*\([^)]*(?:req\.|`.*\$\{)/g,
|
|
121
|
+
severity: 'critical',
|
|
122
|
+
description: 'Potential command injection: shell execution with user input',
|
|
123
|
+
cwe: 'CWE-78',
|
|
124
|
+
languages: ['ts', 'js']
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'command_injection',
|
|
128
|
+
regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\(/g,
|
|
129
|
+
severity: 'high',
|
|
130
|
+
description: 'child_process usage detected (verify input sanitization)',
|
|
131
|
+
cwe: 'CWE-78',
|
|
132
|
+
languages: ['ts', 'js']
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
export class SecurityPatternsGate extends Gate {
|
|
136
|
+
config;
|
|
137
|
+
severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
138
|
+
constructor(config = {}) {
|
|
139
|
+
super('security-patterns', 'Security Pattern Detection');
|
|
140
|
+
this.config = {
|
|
141
|
+
enabled: config.enabled ?? false,
|
|
142
|
+
sql_injection: config.sql_injection ?? true,
|
|
143
|
+
xss: config.xss ?? true,
|
|
144
|
+
path_traversal: config.path_traversal ?? true,
|
|
145
|
+
hardcoded_secrets: config.hardcoded_secrets ?? true,
|
|
146
|
+
insecure_randomness: config.insecure_randomness ?? true,
|
|
147
|
+
command_injection: config.command_injection ?? true,
|
|
148
|
+
block_on_severity: config.block_on_severity ?? 'high',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async run(context) {
|
|
152
|
+
if (!this.config.enabled) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const failures = [];
|
|
156
|
+
const vulnerabilities = [];
|
|
157
|
+
const files = await FileScanner.findFiles({
|
|
158
|
+
cwd: context.cwd,
|
|
159
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py,java,go}'],
|
|
160
|
+
});
|
|
161
|
+
Logger.info(`Security Patterns Gate: Scanning ${files.length} files`);
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
try {
|
|
164
|
+
const fullPath = path.join(context.cwd, file);
|
|
165
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
166
|
+
const ext = path.extname(file).slice(1);
|
|
167
|
+
this.scanFileForVulnerabilities(content, file, ext, vulnerabilities);
|
|
168
|
+
}
|
|
169
|
+
catch (e) { }
|
|
170
|
+
}
|
|
171
|
+
// Filter by enabled checks
|
|
172
|
+
const filteredVulns = vulnerabilities.filter(v => {
|
|
173
|
+
switch (v.type) {
|
|
174
|
+
case 'sql_injection': return this.config.sql_injection;
|
|
175
|
+
case 'xss': return this.config.xss;
|
|
176
|
+
case 'path_traversal': return this.config.path_traversal;
|
|
177
|
+
case 'hardcoded_secrets': return this.config.hardcoded_secrets;
|
|
178
|
+
case 'insecure_randomness': return this.config.insecure_randomness;
|
|
179
|
+
case 'command_injection': return this.config.command_injection;
|
|
180
|
+
default: return true;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Sort by severity
|
|
184
|
+
filteredVulns.sort((a, b) => this.severityOrder[a.severity] - this.severityOrder[b.severity]);
|
|
185
|
+
// Convert to failures based on block_on_severity threshold
|
|
186
|
+
const blockThreshold = this.severityOrder[this.config.block_on_severity ?? 'high'];
|
|
187
|
+
for (const vuln of filteredVulns) {
|
|
188
|
+
if (this.severityOrder[vuln.severity] <= blockThreshold) {
|
|
189
|
+
failures.push(this.createFailure(`[${vuln.cwe}] ${vuln.description} at line ${vuln.line}`, [vuln.file], `Found: "${vuln.match.slice(0, 60)}..." - Use parameterized queries/sanitization.`, `Security: ${vuln.type.replace('_', ' ').toUpperCase()}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (filteredVulns.length > 0 && failures.length === 0) {
|
|
193
|
+
// Vulnerabilities found but below threshold - log info
|
|
194
|
+
Logger.info(`Security scan found ${filteredVulns.length} issues below ${this.config.block_on_severity} threshold`);
|
|
195
|
+
}
|
|
196
|
+
return failures;
|
|
197
|
+
}
|
|
198
|
+
scanFileForVulnerabilities(content, file, ext, vulnerabilities) {
|
|
199
|
+
const lines = content.split('\n');
|
|
200
|
+
for (const pattern of VULNERABILITY_PATTERNS) {
|
|
201
|
+
// Check if pattern applies to this file type
|
|
202
|
+
if (!pattern.languages.includes('*') && !pattern.languages.includes(ext)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Reset regex state
|
|
206
|
+
pattern.regex.lastIndex = 0;
|
|
207
|
+
let match;
|
|
208
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
209
|
+
// Find line number
|
|
210
|
+
const beforeMatch = content.slice(0, match.index);
|
|
211
|
+
const lineNumber = beforeMatch.split('\n').length;
|
|
212
|
+
vulnerabilities.push({
|
|
213
|
+
type: pattern.type,
|
|
214
|
+
severity: pattern.severity,
|
|
215
|
+
file,
|
|
216
|
+
line: lineNumber,
|
|
217
|
+
match: match[0],
|
|
218
|
+
description: pattern.description,
|
|
219
|
+
cwe: pattern.cwe,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Quick helper to check a single file for security issues
|
|
227
|
+
*/
|
|
228
|
+
export async function checkSecurityPatterns(filePath, config = { enabled: true }) {
|
|
229
|
+
const gate = new SecurityPatternsGate(config);
|
|
230
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
231
|
+
const ext = path.extname(filePath).slice(1);
|
|
232
|
+
const vulnerabilities = [];
|
|
233
|
+
// Use the private method via reflection for testing
|
|
234
|
+
gate.scanFileForVulnerabilities(content, filePath, ext, vulnerabilities);
|
|
235
|
+
return vulnerabilities;
|
|
236
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|