@rigour-labs/core 4.3.0 → 4.3.2
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/frontend-secret-exposure.d.ts +27 -0
- package/dist/gates/frontend-secret-exposure.js +174 -0
- package/dist/gates/frontend-secret-exposure.test.d.ts +1 -0
- package/dist/gates/frontend-secret-exposure.test.js +95 -0
- package/dist/gates/runner.js +9 -0
- package/dist/gates/side-effect-analysis.d.ts +67 -0
- package/dist/gates/side-effect-analysis.js +559 -0
- package/dist/gates/side-effect-helpers.d.ts +260 -0
- package/dist/gates/side-effect-helpers.js +1096 -0
- package/dist/gates/side-effect-rules.d.ts +39 -0
- package/dist/gates/side-effect-rules.js +302 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/templates/universal-config.js +42 -0
- package/dist/types/index.d.ts +268 -0
- package/dist/types/index.js +50 -0
- package/package.json +6 -6
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Gate, GateContext } from './base.js';
|
|
2
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
3
|
+
export interface FrontendSecretExposureConfig {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
block_on_severity?: 'critical' | 'high' | 'medium' | 'low';
|
|
6
|
+
check_process_env?: boolean;
|
|
7
|
+
check_import_meta_env?: boolean;
|
|
8
|
+
secret_env_name_patterns?: string[];
|
|
9
|
+
safe_public_prefixes?: string[];
|
|
10
|
+
frontend_path_patterns?: string[];
|
|
11
|
+
server_path_patterns?: string[];
|
|
12
|
+
allowlist_env_names?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare class FrontendSecretExposureGate extends Gate {
|
|
15
|
+
private config;
|
|
16
|
+
private severityOrder;
|
|
17
|
+
constructor(config?: FrontendSecretExposureConfig);
|
|
18
|
+
protected get provenance(): Provenance;
|
|
19
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
20
|
+
private shouldSkipFile;
|
|
21
|
+
private isClientBundled;
|
|
22
|
+
private isServerOnlyContent;
|
|
23
|
+
private findEnvExposures;
|
|
24
|
+
private collectMatches;
|
|
25
|
+
private isSecretLikeEnvName;
|
|
26
|
+
private matchesAnyPattern;
|
|
27
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Gate } from './base.js';
|
|
2
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
const DEFAULT_SECRET_ENV_NAME_PATTERNS = [
|
|
7
|
+
'(?:^|_)(?:secret|private)(?:_|$)',
|
|
8
|
+
'(?:^|_)(?:token|api[_-]?key|access[_-]?key|client[_-]?secret|signing|webhook)(?:_|$)',
|
|
9
|
+
'(?:^|_)(?:db[_-]?url|database[_-]?url|connection[_-]?string)(?:_|$)',
|
|
10
|
+
];
|
|
11
|
+
const DEFAULT_SAFE_PUBLIC_PREFIXES = [
|
|
12
|
+
'NEXT_PUBLIC_',
|
|
13
|
+
'VITE_',
|
|
14
|
+
'PUBLIC_',
|
|
15
|
+
'NUXT_PUBLIC_',
|
|
16
|
+
'REACT_APP_',
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_FRONTEND_PATH_PATTERNS = [
|
|
19
|
+
'(^|/)pages/(?!api/)',
|
|
20
|
+
'(^|/)components/',
|
|
21
|
+
'(^|/)src/components/',
|
|
22
|
+
'(^|/)src/views/',
|
|
23
|
+
'(^|/)src/app/',
|
|
24
|
+
'(^|/)app/(?!api/)',
|
|
25
|
+
'(^|/)views/',
|
|
26
|
+
'(^|/)public/',
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_SERVER_PATH_PATTERNS = [
|
|
29
|
+
'(^|/)pages/api/',
|
|
30
|
+
'(^|/)src/pages/api/',
|
|
31
|
+
'(^|/)app/api/',
|
|
32
|
+
'(^|/)src/app/api/',
|
|
33
|
+
'\\.server\\.(?:ts|tsx|js|jsx|mjs|cjs)$',
|
|
34
|
+
];
|
|
35
|
+
export class FrontendSecretExposureGate extends Gate {
|
|
36
|
+
config;
|
|
37
|
+
severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
super('frontend-secret-exposure', 'Frontend Secret Exposure Detection');
|
|
40
|
+
this.config = {
|
|
41
|
+
enabled: config.enabled ?? true,
|
|
42
|
+
block_on_severity: config.block_on_severity ?? 'high',
|
|
43
|
+
check_process_env: config.check_process_env ?? true,
|
|
44
|
+
check_import_meta_env: config.check_import_meta_env ?? true,
|
|
45
|
+
secret_env_name_patterns: config.secret_env_name_patterns ?? DEFAULT_SECRET_ENV_NAME_PATTERNS,
|
|
46
|
+
safe_public_prefixes: config.safe_public_prefixes ?? DEFAULT_SAFE_PUBLIC_PREFIXES,
|
|
47
|
+
frontend_path_patterns: config.frontend_path_patterns ?? DEFAULT_FRONTEND_PATH_PATTERNS,
|
|
48
|
+
server_path_patterns: config.server_path_patterns ?? DEFAULT_SERVER_PATH_PATTERNS,
|
|
49
|
+
allowlist_env_names: config.allowlist_env_names ?? [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
get provenance() { return 'security'; }
|
|
53
|
+
async run(context) {
|
|
54
|
+
if (!this.config.enabled)
|
|
55
|
+
return [];
|
|
56
|
+
const files = await FileScanner.findFiles({
|
|
57
|
+
cwd: context.cwd,
|
|
58
|
+
patterns: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
|
|
59
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'],
|
|
60
|
+
});
|
|
61
|
+
const scanFiles = files.filter(file => !this.shouldSkipFile(file));
|
|
62
|
+
const findings = [];
|
|
63
|
+
Logger.info(`Frontend Secret Exposure Gate: Scanning ${scanFiles.length} files`);
|
|
64
|
+
for (const file of scanFiles) {
|
|
65
|
+
const fullPath = path.join(context.cwd, file);
|
|
66
|
+
let content = '';
|
|
67
|
+
try {
|
|
68
|
+
content = await fs.readFile(fullPath, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!this.isClientBundled(file, content))
|
|
74
|
+
continue;
|
|
75
|
+
findings.push(...this.findEnvExposures(file, content));
|
|
76
|
+
}
|
|
77
|
+
findings.sort((a, b) => this.severityOrder[a.severity] - this.severityOrder[b.severity]);
|
|
78
|
+
const threshold = this.severityOrder[this.config.block_on_severity];
|
|
79
|
+
return findings
|
|
80
|
+
.filter(f => this.severityOrder[f.severity] <= threshold)
|
|
81
|
+
.map(f => this.createFailure(`Potential frontend secret exposure: ${f.source}.${f.envVar} is referenced in client-bundled code.`, [f.file], 'Move secret usage to server-only code (API route/server action) and expose only public-safe values.', 'Security: Frontend Secret Exposure', f.line, f.line, f.severity));
|
|
82
|
+
}
|
|
83
|
+
shouldSkipFile(file) {
|
|
84
|
+
const normalized = file.replace(/\\/g, '/');
|
|
85
|
+
if (/\.(test|spec)\.(?:ts|tsx|js|jsx|mjs|cjs)$/i.test(normalized))
|
|
86
|
+
return true;
|
|
87
|
+
if (/\/(?:__tests__|tests|test|__test__|e2e|fixtures|mocks)\//.test(`/${normalized}`))
|
|
88
|
+
return true;
|
|
89
|
+
if (/\/(?:examples|studio-dist)\//.test(`/${normalized}`))
|
|
90
|
+
return true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
isClientBundled(file, content) {
|
|
94
|
+
const normalized = file.replace(/\\/g, '/');
|
|
95
|
+
if (this.matchesAnyPattern(normalized, this.config.server_path_patterns))
|
|
96
|
+
return false;
|
|
97
|
+
if (this.isServerOnlyContent(content))
|
|
98
|
+
return false;
|
|
99
|
+
if (/^\s*['"]use client['"]\s*;?/m.test(content))
|
|
100
|
+
return true;
|
|
101
|
+
if (this.matchesAnyPattern(normalized, this.config.frontend_path_patterns))
|
|
102
|
+
return true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
isServerOnlyContent(content) {
|
|
106
|
+
if (/from\s+['"]server-only['"]/.test(content))
|
|
107
|
+
return true;
|
|
108
|
+
if (/import\s+['"]server-only['"]/.test(content))
|
|
109
|
+
return true;
|
|
110
|
+
if (/export\s+async\s+function\s+getServerSideProps\s*\(/.test(content))
|
|
111
|
+
return true;
|
|
112
|
+
if (/export\s+async\s+function\s+getStaticProps\s*\(/.test(content))
|
|
113
|
+
return true;
|
|
114
|
+
if (/['"]use server['"]/.test(content))
|
|
115
|
+
return true;
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
findEnvExposures(file, content) {
|
|
119
|
+
const findings = [];
|
|
120
|
+
if (this.config.check_process_env) {
|
|
121
|
+
const processEnvRegex = /process\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
122
|
+
findings.push(...this.collectMatches(file, content, processEnvRegex, 'process.env'));
|
|
123
|
+
}
|
|
124
|
+
if (this.config.check_import_meta_env) {
|
|
125
|
+
const importMetaRegex = /import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
126
|
+
findings.push(...this.collectMatches(file, content, importMetaRegex, 'import.meta.env'));
|
|
127
|
+
}
|
|
128
|
+
return findings;
|
|
129
|
+
}
|
|
130
|
+
collectMatches(file, content, regex, source) {
|
|
131
|
+
const matches = [];
|
|
132
|
+
const scanRegex = new RegExp(regex.source, 'g');
|
|
133
|
+
for (const match of content.matchAll(scanRegex)) {
|
|
134
|
+
const envVar = match[1];
|
|
135
|
+
if (!this.isSecretLikeEnvName(envVar))
|
|
136
|
+
continue;
|
|
137
|
+
const startIndex = match.index ?? 0;
|
|
138
|
+
const beforeMatch = content.slice(0, startIndex);
|
|
139
|
+
const line = beforeMatch.split('\n').length;
|
|
140
|
+
matches.push({
|
|
141
|
+
file,
|
|
142
|
+
line,
|
|
143
|
+
envVar,
|
|
144
|
+
source,
|
|
145
|
+
severity: 'high',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return matches;
|
|
149
|
+
}
|
|
150
|
+
isSecretLikeEnvName(envVar) {
|
|
151
|
+
if (this.config.allowlist_env_names.includes(envVar))
|
|
152
|
+
return false;
|
|
153
|
+
if (this.config.safe_public_prefixes.some(prefix => envVar.startsWith(prefix)))
|
|
154
|
+
return false;
|
|
155
|
+
return this.config.secret_env_name_patterns.some(pattern => {
|
|
156
|
+
try {
|
|
157
|
+
return new RegExp(pattern, 'i').test(envVar);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
matchesAnyPattern(value, patterns) {
|
|
165
|
+
return patterns.some(pattern => {
|
|
166
|
+
try {
|
|
167
|
+
return new RegExp(pattern, 'i').test(value);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { FrontendSecretExposureGate } from './frontend-secret-exposure.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
describe('FrontendSecretExposureGate', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontend-secret-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('detects process.env secret usage in client-bundled file', async () => {
|
|
15
|
+
const filePath = path.join(testDir, 'src/components/Checkout.tsx');
|
|
16
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(filePath, `
|
|
18
|
+
export function Checkout() {
|
|
19
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
20
|
+
return <div>{key}</div>;
|
|
21
|
+
}
|
|
22
|
+
`);
|
|
23
|
+
const gate = new FrontendSecretExposureGate();
|
|
24
|
+
const failures = await gate.run({ cwd: testDir });
|
|
25
|
+
expect(failures.length).toBeGreaterThan(0);
|
|
26
|
+
expect(failures[0].id).toBe('frontend-secret-exposure');
|
|
27
|
+
expect(failures[0].files).toContain('src/components/Checkout.tsx');
|
|
28
|
+
});
|
|
29
|
+
it('detects import.meta.env secret usage in frontend app path', async () => {
|
|
30
|
+
const filePath = path.join(testDir, 'src/app/page.tsx');
|
|
31
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
32
|
+
fs.writeFileSync(filePath, `
|
|
33
|
+
export default function Page() {
|
|
34
|
+
return <span>{import.meta.env.OPENAI_API_KEY}</span>;
|
|
35
|
+
}
|
|
36
|
+
`);
|
|
37
|
+
const gate = new FrontendSecretExposureGate();
|
|
38
|
+
const failures = await gate.run({ cwd: testDir });
|
|
39
|
+
expect(failures.length).toBeGreaterThan(0);
|
|
40
|
+
});
|
|
41
|
+
it('does not flag public env prefixes in client files', async () => {
|
|
42
|
+
const filePath = path.join(testDir, 'components/Header.tsx');
|
|
43
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
44
|
+
fs.writeFileSync(filePath, `
|
|
45
|
+
export const key = process.env.NEXT_PUBLIC_STRIPE_KEY;
|
|
46
|
+
`);
|
|
47
|
+
const gate = new FrontendSecretExposureGate();
|
|
48
|
+
const failures = await gate.run({ cwd: testDir });
|
|
49
|
+
expect(failures).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
it('does not flag server-only API route', async () => {
|
|
52
|
+
const filePath = path.join(testDir, 'pages/api/charge.ts');
|
|
53
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
54
|
+
fs.writeFileSync(filePath, `
|
|
55
|
+
export default function handler() {
|
|
56
|
+
return process.env.STRIPE_SECRET_KEY;
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
const gate = new FrontendSecretExposureGate();
|
|
60
|
+
const failures = await gate.run({ cwd: testDir });
|
|
61
|
+
expect(failures).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
it('does not flag .server files', async () => {
|
|
64
|
+
const filePath = path.join(testDir, 'src/lib/payments.server.ts');
|
|
65
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(filePath, `
|
|
67
|
+
export const stripeSecret = process.env.STRIPE_SECRET_KEY;
|
|
68
|
+
`);
|
|
69
|
+
const gate = new FrontendSecretExposureGate();
|
|
70
|
+
const failures = await gate.run({ cwd: testDir });
|
|
71
|
+
expect(failures).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
it('respects explicit allowlist env names', async () => {
|
|
74
|
+
const filePath = path.join(testDir, 'src/views/App.tsx');
|
|
75
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
76
|
+
fs.writeFileSync(filePath, `
|
|
77
|
+
export const x = process.env.INTERNAL_TOKEN_FOR_DOCS;
|
|
78
|
+
`);
|
|
79
|
+
const gate = new FrontendSecretExposureGate({
|
|
80
|
+
allowlist_env_names: ['INTERNAL_TOKEN_FOR_DOCS'],
|
|
81
|
+
});
|
|
82
|
+
const failures = await gate.run({ cwd: testDir });
|
|
83
|
+
expect(failures).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
it('skips when disabled', async () => {
|
|
86
|
+
const filePath = path.join(testDir, 'src/components/Client.tsx');
|
|
87
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
88
|
+
fs.writeFileSync(filePath, `
|
|
89
|
+
export const x = process.env.OPENAI_API_KEY;
|
|
90
|
+
`);
|
|
91
|
+
const gate = new FrontendSecretExposureGate({ enabled: false });
|
|
92
|
+
const failures = await gate.run({ cwd: testDir });
|
|
93
|
+
expect(failures).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
package/dist/gates/runner.js
CHANGED
|
@@ -15,6 +15,7 @@ import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
|
|
|
15
15
|
import { AgentTeamGate } from './agent-team.js';
|
|
16
16
|
import { CheckpointGate } from './checkpoint.js';
|
|
17
17
|
import { SecurityPatternsGate } from './security-patterns.js';
|
|
18
|
+
import { FrontendSecretExposureGate } from './frontend-secret-exposure.js';
|
|
18
19
|
import { DuplicationDriftGate } from './duplication-drift.js';
|
|
19
20
|
import { HallucinatedImportsGate } from './hallucinated-imports.js';
|
|
20
21
|
import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
|
|
@@ -23,6 +24,7 @@ import { PromiseSafetyGate } from './promise-safety.js';
|
|
|
23
24
|
import { PhantomApisGate } from './phantom-apis.js';
|
|
24
25
|
import { DeprecatedApisGate } from './deprecated-apis.js';
|
|
25
26
|
import { TestQualityGate } from './test-quality.js';
|
|
27
|
+
import { SideEffectAnalysisGate } from './side-effect-analysis.js';
|
|
26
28
|
import { execa } from 'execa';
|
|
27
29
|
import { Logger } from '../utils/logger.js';
|
|
28
30
|
export class GateRunner {
|
|
@@ -66,6 +68,9 @@ export class GateRunner {
|
|
|
66
68
|
if (this.config.gates.security?.enabled !== false) {
|
|
67
69
|
this.gates.push(new SecurityPatternsGate(this.config.gates.security));
|
|
68
70
|
}
|
|
71
|
+
if (this.config.gates.frontend_secret_exposure?.enabled !== false) {
|
|
72
|
+
this.gates.push(new FrontendSecretExposureGate(this.config.gates.frontend_secret_exposure));
|
|
73
|
+
}
|
|
69
74
|
// v2.16+ AI-Native Drift Detection Gates (enabled by default)
|
|
70
75
|
if (this.config.gates.duplication_drift?.enabled !== false) {
|
|
71
76
|
this.gates.push(new DuplicationDriftGate(this.config.gates.duplication_drift));
|
|
@@ -93,6 +98,10 @@ export class GateRunner {
|
|
|
93
98
|
if (this.config.gates.test_quality?.enabled !== false) {
|
|
94
99
|
this.gates.push(new TestQualityGate(this.config.gates.test_quality));
|
|
95
100
|
}
|
|
101
|
+
// v4.3+ Side-Effect Safety Analysis (enabled by default)
|
|
102
|
+
if (this.config.gates.side_effect_analysis?.enabled !== false) {
|
|
103
|
+
this.gates.push(new SideEffectAnalysisGate(this.config.gates.side_effect_analysis));
|
|
104
|
+
}
|
|
96
105
|
// Environment Alignment Gate (Should be prioritized)
|
|
97
106
|
if (this.config.gates.environment?.enabled) {
|
|
98
107
|
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-Effect Analysis Gate
|
|
3
|
+
*
|
|
4
|
+
* Context-aware detection of code patterns with real-world consequences:
|
|
5
|
+
* unbounded process spawns, runaway timers, missing circuit breakers,
|
|
6
|
+
* circular file watchers, resource leaks, and auto-restart bombs.
|
|
7
|
+
*
|
|
8
|
+
* SMART DETECTION APPROACH (not hardcoded regex):
|
|
9
|
+
* ───────────────────────────────────────────────
|
|
10
|
+
* 1. VARIABLE BINDING TRACKING: Tracks `const id = setInterval(...)` and
|
|
11
|
+
* verifies `clearInterval(id)` exists — not just "does clearInterval
|
|
12
|
+
* appear somewhere in the file?"
|
|
13
|
+
*
|
|
14
|
+
* 2. SCOPE-AWARE ANALYSIS: Checks cleanup is in the same function scope
|
|
15
|
+
* as creation. A clearInterval in a completely different function doesn't
|
|
16
|
+
* help the one that created the timer.
|
|
17
|
+
*
|
|
18
|
+
* 3. FRAMEWORK AWARENESS: Understands React useEffect cleanup returns,
|
|
19
|
+
* Go `defer f.Close()` idiom, Python `with open()` context managers,
|
|
20
|
+
* Java try-with-resources, C# using statements, Ruby block form.
|
|
21
|
+
*
|
|
22
|
+
* 4. PATH OVERLAP ANALYSIS: For circular triggers, extracts actual paths
|
|
23
|
+
* from watch() and writeFile() calls and checks if they overlap.
|
|
24
|
+
*
|
|
25
|
+
* 5. BASE CASE ORDERING: For recursion, checks that the base case
|
|
26
|
+
* (return/break) comes BEFORE the recursive call, not just that
|
|
27
|
+
* both exist somewhere in the function.
|
|
28
|
+
*
|
|
29
|
+
* Follows the architectural patterns of:
|
|
30
|
+
* - hallucinated-imports (build context → scan → resolve → report)
|
|
31
|
+
* - promise-safety (scope-aware helpers, extractBraceBody, isInsideTryBlock)
|
|
32
|
+
*
|
|
33
|
+
* This is a CORE gate (enabled by default, provenance: ai-drift).
|
|
34
|
+
* Supports: JS/TS, Python, Go, Rust, C#, Java, Ruby
|
|
35
|
+
*
|
|
36
|
+
* @since v4.3.0
|
|
37
|
+
*/
|
|
38
|
+
import { Gate, GateContext } from './base.js';
|
|
39
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
40
|
+
export interface SideEffectAnalysisConfig {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
check_unbounded_timers?: boolean;
|
|
43
|
+
check_unbounded_loops?: boolean;
|
|
44
|
+
check_process_lifecycle?: boolean;
|
|
45
|
+
check_recursive_depth?: boolean;
|
|
46
|
+
check_resource_lifecycle?: boolean;
|
|
47
|
+
check_retry_without_limit?: boolean;
|
|
48
|
+
check_circular_triggers?: boolean;
|
|
49
|
+
check_auto_restart?: boolean;
|
|
50
|
+
ignore_patterns?: string[];
|
|
51
|
+
}
|
|
52
|
+
export declare class SideEffectAnalysisGate extends Gate {
|
|
53
|
+
private cfg;
|
|
54
|
+
constructor(config?: SideEffectAnalysisConfig);
|
|
55
|
+
protected get provenance(): Provenance;
|
|
56
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
57
|
+
private scanFile;
|
|
58
|
+
private checkUnboundedTimers;
|
|
59
|
+
private checkProcessLifecycle;
|
|
60
|
+
private checkUnboundedLoops;
|
|
61
|
+
private checkRetryWithoutLimit;
|
|
62
|
+
private checkCircularTriggers;
|
|
63
|
+
private checkResourceLifecycle;
|
|
64
|
+
private checkRecursiveDepth;
|
|
65
|
+
private checkAutoRestart;
|
|
66
|
+
private buildFailures;
|
|
67
|
+
}
|