@rigour-labs/core 2.22.0 → 3.0.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.
Files changed (117) hide show
  1. package/README.md +58 -0
  2. package/dist/context.test.js +2 -3
  3. package/dist/environment.test.js +2 -1
  4. package/dist/gates/agent-team.d.ts +2 -1
  5. package/dist/gates/agent-team.js +1 -0
  6. package/dist/gates/base.d.ts +3 -1
  7. package/dist/gates/base.js +3 -0
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/context-window-artifacts.d.ts +2 -1
  11. package/dist/gates/context-window-artifacts.js +6 -3
  12. package/dist/gates/context.d.ts +2 -1
  13. package/dist/gates/context.js +1 -0
  14. package/dist/gates/coverage.js +3 -1
  15. package/dist/gates/dependency.js +5 -5
  16. package/dist/gates/duplication-drift.d.ts +2 -1
  17. package/dist/gates/duplication-drift.js +4 -1
  18. package/dist/gates/environment.js +4 -4
  19. package/dist/gates/hallucinated-imports.d.ts +21 -2
  20. package/dist/gates/hallucinated-imports.js +116 -2
  21. package/dist/gates/inconsistent-error-handling.d.ts +2 -1
  22. package/dist/gates/inconsistent-error-handling.js +21 -7
  23. package/dist/gates/promise-safety.d.ts +68 -0
  24. package/dist/gates/promise-safety.js +509 -0
  25. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  26. package/dist/gates/retry-loop-breaker.js +2 -1
  27. package/dist/gates/runner.js +34 -1
  28. package/dist/gates/safety.d.ts +2 -1
  29. package/dist/gates/safety.js +2 -1
  30. package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
  31. package/dist/gates/security-patterns-owasp.test.js +171 -0
  32. package/dist/gates/security-patterns.d.ts +6 -1
  33. package/dist/gates/security-patterns.js +101 -0
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/hooks/checker.d.ts +23 -0
  36. package/dist/hooks/checker.js +222 -0
  37. package/dist/hooks/checker.test.d.ts +1 -0
  38. package/dist/hooks/checker.test.js +132 -0
  39. package/dist/hooks/index.d.ts +9 -0
  40. package/dist/hooks/index.js +8 -0
  41. package/dist/hooks/standalone-checker.d.ts +15 -0
  42. package/dist/hooks/standalone-checker.js +106 -0
  43. package/dist/hooks/templates.d.ts +22 -0
  44. package/dist/hooks/templates.js +232 -0
  45. package/dist/hooks/types.d.ts +34 -0
  46. package/dist/hooks/types.js +21 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +2 -0
  49. package/dist/services/fix-packet-service.d.ts +0 -1
  50. package/dist/services/fix-packet-service.js +9 -14
  51. package/dist/services/score-history.d.ts +54 -0
  52. package/dist/services/score-history.js +122 -0
  53. package/dist/templates/index.js +176 -0
  54. package/dist/types/fix-packet.d.ts +5 -5
  55. package/dist/types/fix-packet.js +1 -1
  56. package/dist/types/index.d.ts +207 -0
  57. package/dist/types/index.js +32 -0
  58. package/package.json +21 -1
  59. package/src/context.test.ts +0 -256
  60. package/src/discovery.test.ts +0 -88
  61. package/src/discovery.ts +0 -112
  62. package/src/environment.test.ts +0 -115
  63. package/src/gates/agent-team.test.ts +0 -134
  64. package/src/gates/agent-team.ts +0 -210
  65. package/src/gates/ast-handlers/base.ts +0 -13
  66. package/src/gates/ast-handlers/python.ts +0 -145
  67. package/src/gates/ast-handlers/python_parser.py +0 -181
  68. package/src/gates/ast-handlers/typescript.ts +0 -264
  69. package/src/gates/ast-handlers/universal.ts +0 -184
  70. package/src/gates/ast.ts +0 -54
  71. package/src/gates/base.ts +0 -28
  72. package/src/gates/checkpoint.test.ts +0 -135
  73. package/src/gates/checkpoint.ts +0 -311
  74. package/src/gates/content.ts +0 -51
  75. package/src/gates/context-window-artifacts.ts +0 -277
  76. package/src/gates/context.ts +0 -270
  77. package/src/gates/coverage.ts +0 -74
  78. package/src/gates/dependency.ts +0 -108
  79. package/src/gates/duplication-drift.ts +0 -231
  80. package/src/gates/environment.ts +0 -94
  81. package/src/gates/file.ts +0 -46
  82. package/src/gates/hallucinated-imports.ts +0 -361
  83. package/src/gates/inconsistent-error-handling.ts +0 -254
  84. package/src/gates/retry-loop-breaker.ts +0 -151
  85. package/src/gates/runner.ts +0 -188
  86. package/src/gates/safety.ts +0 -56
  87. package/src/gates/security-patterns.test.ts +0 -162
  88. package/src/gates/security-patterns.ts +0 -306
  89. package/src/gates/structure.ts +0 -36
  90. package/src/index.ts +0 -13
  91. package/src/pattern-index/embeddings.ts +0 -84
  92. package/src/pattern-index/index.ts +0 -59
  93. package/src/pattern-index/indexer.test.ts +0 -276
  94. package/src/pattern-index/indexer.ts +0 -1023
  95. package/src/pattern-index/matcher.test.ts +0 -293
  96. package/src/pattern-index/matcher.ts +0 -493
  97. package/src/pattern-index/overrides.ts +0 -235
  98. package/src/pattern-index/security.ts +0 -151
  99. package/src/pattern-index/staleness.test.ts +0 -313
  100. package/src/pattern-index/staleness.ts +0 -568
  101. package/src/pattern-index/types.ts +0 -339
  102. package/src/safety.test.ts +0 -53
  103. package/src/services/adaptive-thresholds.test.ts +0 -189
  104. package/src/services/adaptive-thresholds.ts +0 -275
  105. package/src/services/context-engine.ts +0 -104
  106. package/src/services/fix-packet-service.ts +0 -42
  107. package/src/services/state-service.ts +0 -138
  108. package/src/smoke.test.ts +0 -18
  109. package/src/templates/index.ts +0 -338
  110. package/src/types/fix-packet.ts +0 -32
  111. package/src/types/index.ts +0 -200
  112. package/src/utils/logger.ts +0 -43
  113. package/src/utils/scanner.test.ts +0 -37
  114. package/src/utils/scanner.ts +0 -43
  115. package/tsconfig.json +0 -10
  116. package/vitest.config.ts +0 -7
  117. package/vitest.setup.ts +0 -30
@@ -1,134 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { AgentTeamGate, registerAgent, getSession, clearSession } from './agent-team.js';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
-
7
- describe('AgentTeamGate', () => {
8
- let testDir: string;
9
-
10
- beforeEach(() => {
11
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-team-test-'));
12
- });
13
-
14
- afterEach(() => {
15
- clearSession(testDir);
16
- fs.rmSync(testDir, { recursive: true, force: true });
17
- });
18
-
19
- describe('gate initialization', () => {
20
- it('should create gate with default config', () => {
21
- const gate = new AgentTeamGate();
22
- expect(gate.id).toBe('agent-team');
23
- expect(gate.title).toBe('Agent Team Governance');
24
- });
25
-
26
- it('should skip when not enabled', async () => {
27
- const gate = new AgentTeamGate({ enabled: false });
28
- const failures = await gate.run({ cwd: testDir });
29
- expect(failures).toEqual([]);
30
- });
31
- });
32
-
33
- describe('session management', () => {
34
- it('should register an agent', () => {
35
- const session = registerAgent(testDir, 'agent-a', ['src/api/**']);
36
- expect(session.agents).toHaveLength(1);
37
- expect(session.agents[0].agentId).toBe('agent-a');
38
- expect(session.agents[0].taskScope).toEqual(['src/api/**']);
39
- });
40
-
41
- it('should update existing agent registration', () => {
42
- registerAgent(testDir, 'agent-a', ['src/api/**']);
43
- const session = registerAgent(testDir, 'agent-a', ['src/api/**', 'src/utils/**']);
44
- expect(session.agents).toHaveLength(1);
45
- expect(session.agents[0].taskScope).toEqual(['src/api/**', 'src/utils/**']);
46
- });
47
-
48
- it('should persist session to disk', () => {
49
- registerAgent(testDir, 'agent-a', ['src/**']);
50
- const sessionPath = path.join(testDir, '.rigour', 'agent-session.json');
51
- expect(fs.existsSync(sessionPath)).toBe(true);
52
- });
53
-
54
- it('should load session from disk', () => {
55
- registerAgent(testDir, 'agent-a', ['src/**']);
56
- // Clear in-memory cache
57
- clearSession(testDir);
58
- // Re-register to re-create session file
59
- registerAgent(testDir, 'agent-b', ['tests/**']);
60
-
61
- const session = getSession(testDir);
62
- expect(session).not.toBeNull();
63
- expect(session!.agents).toHaveLength(1); // Only agent-b after clearSession
64
- });
65
-
66
- it('should clear session', () => {
67
- registerAgent(testDir, 'agent-a', ['src/**']);
68
- clearSession(testDir);
69
- const session = getSession(testDir);
70
- expect(session).toBeNull();
71
- });
72
- });
73
-
74
- describe('max concurrent agents check', () => {
75
- it('should pass when under limit', async () => {
76
- const gate = new AgentTeamGate({ enabled: true, max_concurrent_agents: 3 });
77
- registerAgent(testDir, 'agent-a', ['src/a/**']);
78
- registerAgent(testDir, 'agent-b', ['src/b/**']);
79
-
80
- const failures = await gate.run({ cwd: testDir });
81
- expect(failures).toEqual([]);
82
- });
83
-
84
- it('should fail when over limit', async () => {
85
- const gate = new AgentTeamGate({ enabled: true, max_concurrent_agents: 2 });
86
- registerAgent(testDir, 'agent-a', ['src/a/**']);
87
- registerAgent(testDir, 'agent-b', ['src/b/**']);
88
- registerAgent(testDir, 'agent-c', ['src/c/**']);
89
-
90
- const failures = await gate.run({ cwd: testDir });
91
- expect(failures).toHaveLength(1);
92
- expect(failures[0].title).toBe('Too Many Concurrent Agents');
93
- });
94
- });
95
-
96
- describe('task scope conflicts (strict mode)', () => {
97
- it('should pass when scopes are disjoint', async () => {
98
- const gate = new AgentTeamGate({
99
- enabled: true,
100
- task_ownership: 'strict'
101
- });
102
- registerAgent(testDir, 'agent-a', ['src/api/**']);
103
- registerAgent(testDir, 'agent-b', ['src/ui/**']);
104
-
105
- const failures = await gate.run({ cwd: testDir });
106
- expect(failures).toEqual([]);
107
- });
108
-
109
- it('should fail when scopes overlap', async () => {
110
- const gate = new AgentTeamGate({
111
- enabled: true,
112
- task_ownership: 'strict'
113
- });
114
- registerAgent(testDir, 'agent-a', ['src/api/**']);
115
- registerAgent(testDir, 'agent-b', ['src/api/**']); // Same scope!
116
-
117
- const failures = await gate.run({ cwd: testDir });
118
- expect(failures).toHaveLength(1);
119
- expect(failures[0].title).toBe('Task Scope Conflict');
120
- });
121
-
122
- it('should allow overlapping scopes in collaborative mode', async () => {
123
- const gate = new AgentTeamGate({
124
- enabled: true,
125
- task_ownership: 'collaborative'
126
- });
127
- registerAgent(testDir, 'agent-a', ['src/api/**']);
128
- registerAgent(testDir, 'agent-b', ['src/api/**']); // Same scope - OK in collaborative
129
-
130
- const failures = await gate.run({ cwd: testDir });
131
- expect(failures).toEqual([]);
132
- });
133
- });
134
- });
@@ -1,210 +0,0 @@
1
- /**
2
- * Agent Team Governance Gate
3
- *
4
- * Supervises multi-agent coordination for frontier models like
5
- * Opus 4.6 (agent teams) and GPT-5.3-Codex (coworking mode).
6
- *
7
- * Detects:
8
- * - Cross-agent pattern conflicts
9
- * - Task scope violations
10
- * - Handoff context loss
11
- *
12
- * @since v2.14.0
13
- */
14
-
15
- import { Gate, GateContext } from './base.js';
16
- import { Failure } from '../types/index.js';
17
- import { Logger } from '../utils/logger.js';
18
- import * as fs from 'fs';
19
- import * as path from 'path';
20
-
21
- export interface AgentRegistration {
22
- agentId: string;
23
- taskScope: string[]; // Glob patterns for files this agent owns
24
- registeredAt: Date;
25
- lastActivity?: Date;
26
- }
27
-
28
- export interface AgentTeamSession {
29
- sessionId: string;
30
- agents: AgentRegistration[];
31
- startedAt: Date;
32
- }
33
-
34
- export interface AgentTeamConfig {
35
- enabled?: boolean;
36
- max_concurrent_agents?: number;
37
- cross_agent_pattern_check?: boolean;
38
- handoff_verification?: boolean;
39
- task_ownership?: 'strict' | 'collaborative';
40
- }
41
-
42
- // In-memory session store (persisted to .rigour/agent-session.json)
43
- let currentSession: AgentTeamSession | null = null;
44
-
45
- /**
46
- * Register an agent in the current session
47
- */
48
- export function registerAgent(cwd: string, agentId: string, taskScope: string[]): AgentTeamSession {
49
- if (!currentSession) {
50
- currentSession = {
51
- sessionId: `session-${Date.now()}`,
52
- agents: [],
53
- startedAt: new Date(),
54
- };
55
- }
56
-
57
- // Check if agent already registered
58
- const existing = currentSession.agents.find(a => a.agentId === agentId);
59
- if (existing) {
60
- existing.taskScope = taskScope;
61
- existing.lastActivity = new Date();
62
- } else {
63
- currentSession.agents.push({
64
- agentId,
65
- taskScope,
66
- registeredAt: new Date(),
67
- });
68
- }
69
-
70
- // Persist session
71
- persistSession(cwd);
72
- return currentSession;
73
- }
74
-
75
- /**
76
- * Get current session status
77
- */
78
- export function getSession(cwd: string): AgentTeamSession | null {
79
- if (!currentSession) {
80
- loadSession(cwd);
81
- }
82
- return currentSession;
83
- }
84
-
85
- /**
86
- * Clear current session
87
- */
88
- export function clearSession(cwd: string): void {
89
- currentSession = null;
90
- const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
91
- if (fs.existsSync(sessionPath)) {
92
- fs.unlinkSync(sessionPath);
93
- }
94
- }
95
-
96
- function persistSession(cwd: string): void {
97
- const rigourDir = path.join(cwd, '.rigour');
98
- if (!fs.existsSync(rigourDir)) {
99
- fs.mkdirSync(rigourDir, { recursive: true });
100
- }
101
- const sessionPath = path.join(rigourDir, 'agent-session.json');
102
- fs.writeFileSync(sessionPath, JSON.stringify(currentSession, null, 2));
103
- }
104
-
105
- function loadSession(cwd: string): void {
106
- const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
107
- if (fs.existsSync(sessionPath)) {
108
- try {
109
- const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
110
- currentSession = {
111
- ...data,
112
- startedAt: new Date(data.startedAt),
113
- agents: data.agents.map((a: any) => ({
114
- ...a,
115
- registeredAt: new Date(a.registeredAt),
116
- lastActivity: a.lastActivity ? new Date(a.lastActivity) : undefined,
117
- })),
118
- };
119
- } catch (err) {
120
- Logger.warn('Failed to load agent session, starting fresh');
121
- currentSession = null;
122
- }
123
- }
124
- }
125
-
126
- /**
127
- * Check if two glob patterns might overlap
128
- */
129
- function scopesOverlap(scope1: string[], scope2: string[]): string[] {
130
- const overlapping: string[] = [];
131
- for (const s1 of scope1) {
132
- for (const s2 of scope2) {
133
- // Simple overlap detection - same path or one is prefix of other
134
- if (s1 === s2 || s1.startsWith(s2.replace('**', '')) || s2.startsWith(s1.replace('**', ''))) {
135
- overlapping.push(`${s1} ↔ ${s2}`);
136
- }
137
- }
138
- }
139
- return overlapping;
140
- }
141
-
142
- export class AgentTeamGate extends Gate {
143
- private config: AgentTeamConfig;
144
-
145
- constructor(config: AgentTeamConfig = {}) {
146
- super('agent-team', 'Agent Team Governance');
147
- this.config = {
148
- enabled: config.enabled ?? false,
149
- max_concurrent_agents: config.max_concurrent_agents ?? 3,
150
- cross_agent_pattern_check: config.cross_agent_pattern_check ?? true,
151
- handoff_verification: config.handoff_verification ?? true,
152
- task_ownership: config.task_ownership ?? 'strict',
153
- };
154
- }
155
-
156
- async run(context: GateContext): Promise<Failure[]> {
157
- if (!this.config.enabled) {
158
- return [];
159
- }
160
-
161
- const failures: Failure[] = [];
162
- const session = getSession(context.cwd);
163
-
164
- if (!session || session.agents.length === 0) {
165
- // No multi-agent session active, skip
166
- return [];
167
- }
168
-
169
- Logger.info(`Agent Team Gate: ${session.agents.length} agents in session`);
170
-
171
- // Check 1: Max concurrent agents
172
- if (session.agents.length > (this.config.max_concurrent_agents ?? 3)) {
173
- failures.push(this.createFailure(
174
- `Too many concurrent agents: ${session.agents.length} (max: ${this.config.max_concurrent_agents})`,
175
- undefined,
176
- 'Reduce the number of concurrent agents or increase max_concurrent_agents in rigour.yml',
177
- 'Too Many Concurrent Agents'
178
- ));
179
- }
180
-
181
- // Check 2: Task scope conflicts (strict mode only)
182
- if (this.config.task_ownership === 'strict') {
183
- for (let i = 0; i < session.agents.length; i++) {
184
- for (let j = i + 1; j < session.agents.length; j++) {
185
- const agent1 = session.agents[i];
186
- const agent2 = session.agents[j];
187
- const overlaps = scopesOverlap(agent1.taskScope, agent2.taskScope);
188
-
189
- if (overlaps.length > 0) {
190
- failures.push(this.createFailure(
191
- `Task scope conflict between ${agent1.agentId} and ${agent2.agentId}: ${overlaps.join(', ')}`,
192
- undefined,
193
- 'In strict mode, each agent must have exclusive task scope. Either adjust scopes or set task_ownership: collaborative',
194
- 'Task Scope Conflict'
195
- ));
196
- }
197
- }
198
- }
199
- }
200
-
201
- // Check 3: Cross-agent pattern detection (if enabled)
202
- if (this.config.cross_agent_pattern_check && context.record) {
203
- // This would integrate with the Pattern Index to detect conflicting patterns
204
- // For now, we log that we would do this check
205
- Logger.debug('Cross-agent pattern check: would analyze patterns across agent scopes');
206
- }
207
-
208
- return failures;
209
- }
210
- }
@@ -1,13 +0,0 @@
1
- import { Failure, Gates } from '../../types/index.js';
2
-
3
- export interface ASTHandlerContext {
4
- cwd: string;
5
- file: string;
6
- content: string;
7
- }
8
-
9
- export abstract class ASTHandler {
10
- constructor(protected config: Gates) { }
11
- abstract supports(file: string): boolean;
12
- abstract run(context: ASTHandlerContext): Promise<Failure[]>;
13
- }
@@ -1,145 +0,0 @@
1
- import { execa } from 'execa';
2
- import { ASTHandler, ASTHandlerContext } from './base.js';
3
- import { Failure } from '../../types/index.js';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
-
9
- interface SecurityIssue {
10
- type: string;
11
- issue: string;
12
- name: string;
13
- lineno: number;
14
- message: string;
15
- }
16
-
17
- interface MetricItem {
18
- type: string;
19
- name: string;
20
- complexity?: number;
21
- parameters?: number;
22
- methods?: number;
23
- lineno: number;
24
- }
25
-
26
- interface PythonAnalysisResult {
27
- metrics?: MetricItem[];
28
- security?: SecurityIssue[];
29
- error?: string;
30
- }
31
-
32
- export class PythonHandler extends ASTHandler {
33
- supports(file: string): boolean {
34
- return /\.py$/.test(file);
35
- }
36
-
37
- async run(context: ASTHandlerContext): Promise<Failure[]> {
38
- const failures: Failure[] = [];
39
- const scriptPath = path.join(__dirname, 'python_parser.py');
40
-
41
- // Dynamic command detection for cross-platform support (Mac/Linux usually python3, Windows usually python)
42
- let pythonCmd = 'python3';
43
- try {
44
- await execa('python3', ['--version']);
45
- } catch (e) {
46
- try {
47
- await execa('python', ['--version']);
48
- pythonCmd = 'python';
49
- } catch (e2) {
50
- // Both missing - handled by main catch
51
- }
52
- }
53
-
54
- try {
55
- const { stdout } = await execa(pythonCmd, [scriptPath], {
56
- input: context.content,
57
- cwd: context.cwd
58
- });
59
-
60
- const result: PythonAnalysisResult = JSON.parse(stdout);
61
- if (result.error) return [];
62
-
63
- const astConfig = this.config.ast || {};
64
- const safetyConfig = this.config.safety || {};
65
- const maxComplexity = astConfig.complexity || 10;
66
- const maxParams = astConfig.max_params || 5;
67
- const maxMethods = astConfig.max_methods || 10;
68
-
69
- // Process metrics (complexity, params, methods)
70
- const metrics = result.metrics || [];
71
- for (const item of metrics) {
72
- if (item.type === 'function') {
73
- if (item.parameters && item.parameters > maxParams) {
74
- failures.push({
75
- id: 'AST_MAX_PARAMS',
76
- title: `Function '${item.name}' has ${item.parameters} parameters (max: ${maxParams})`,
77
- details: `High parameter count detected in ${context.file} at line ${item.lineno}`,
78
- files: [context.file],
79
- hint: `Reduce number of parameters or use an options object.`
80
- });
81
- }
82
- if (item.complexity && item.complexity > maxComplexity) {
83
- failures.push({
84
- id: 'AST_COMPLEXITY',
85
- title: `Function '${item.name}' has complexity of ${item.complexity} (max: ${maxComplexity})`,
86
- details: `High complexity detected in ${context.file} at line ${item.lineno}`,
87
- files: [context.file],
88
- hint: `Refactor '${item.name}' into smaller, more focused functions.`
89
- });
90
- }
91
- } else if (item.type === 'class') {
92
- if (item.methods && item.methods > maxMethods) {
93
- failures.push({
94
- id: 'AST_MAX_METHODS',
95
- title: `Class '${item.name}' has ${item.methods} methods (max: ${maxMethods})`,
96
- details: `God Object pattern detected in ${context.file} at line ${item.lineno}`,
97
- files: [context.file],
98
- hint: `Split class '${item.name}' into smaller services.`
99
- });
100
- }
101
- }
102
- }
103
-
104
- // Process security issues (CSRF, hardcoded secrets, SQL injection, etc.)
105
- const securityIssues = result.security || [];
106
- for (const issue of securityIssues) {
107
- const issueIdMap: Record<string, string> = {
108
- 'hardcoded_secret': 'SECURITY_HARDCODED_SECRET',
109
- 'csrf_disabled': 'SECURITY_CSRF_DISABLED',
110
- 'code_injection': 'SECURITY_CODE_INJECTION',
111
- 'insecure_deserialization': 'SECURITY_INSECURE_DESERIALIZATION',
112
- 'command_injection': 'SECURITY_COMMAND_INJECTION',
113
- 'sql_injection': 'SECURITY_SQL_INJECTION'
114
- };
115
-
116
- const id = issueIdMap[issue.issue] || 'SECURITY_ISSUE';
117
-
118
- failures.push({
119
- id,
120
- title: issue.message,
121
- details: `Security issue in ${context.file} at line ${issue.lineno}: ${issue.name}`,
122
- files: [context.file],
123
- hint: this.getSecurityHint(issue.issue)
124
- });
125
- }
126
-
127
- } catch (e: any) {
128
- // If python3 is missing, we skip AST but other gates still run
129
- }
130
-
131
- return failures;
132
- }
133
-
134
- private getSecurityHint(issueType: string): string {
135
- const hints: Record<string, string> = {
136
- 'hardcoded_secret': 'Use environment variables: os.environ.get("SECRET_KEY")',
137
- 'csrf_disabled': 'Enable CSRF protection for all forms handling sensitive data',
138
- 'code_injection': 'Avoid eval/exec. Use safer alternatives like ast.literal_eval() for data parsing',
139
- 'insecure_deserialization': 'Use json.loads() instead of pickle for untrusted data',
140
- 'command_injection': 'Use subprocess with shell=False and pass arguments as a list',
141
- 'sql_injection': 'Use parameterized queries: cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))'
142
- };
143
- return hints[issueType] || 'Review and fix the security issue.';
144
- }
145
- }
@@ -1,181 +0,0 @@
1
- import ast
2
- import sys
3
- import json
4
- import re
5
-
6
- class MetricsVisitor(ast.NodeVisitor):
7
- def __init__(self):
8
- self.metrics = []
9
-
10
- def visit_FunctionDef(self, node):
11
- self.analyze_function(node)
12
- self.generic_visit(node)
13
-
14
- def visit_AsyncFunctionDef(self, node):
15
- self.analyze_function(node)
16
- self.generic_visit(node)
17
-
18
- def visit_ClassDef(self, node):
19
- methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
20
- self.metrics.append({
21
- "type": "class",
22
- "name": node.name,
23
- "methods": len(methods),
24
- "lineno": node.lineno
25
- })
26
- self.generic_visit(node)
27
-
28
- def analyze_function(self, node):
29
- complexity = 1
30
- for n in ast.walk(node):
31
- if isinstance(n, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.Try, ast.ExceptHandler, ast.With, ast.AsyncWith)):
32
- complexity += 1
33
- elif isinstance(n, ast.BoolOp):
34
- complexity += len(n.values) - 1
35
- elif isinstance(n, ast.IfExp):
36
- complexity += 1
37
-
38
- params = len(node.args.args) + len(node.args.kwonlyargs)
39
- if node.args.vararg: params += 1
40
- if node.args.kwarg: params += 1
41
-
42
- self.metrics.append({
43
- "type": "function",
44
- "name": node.name,
45
- "complexity": complexity,
46
- "parameters": params,
47
- "lineno": node.lineno
48
- })
49
-
50
-
51
- class SecurityVisitor(ast.NodeVisitor):
52
- """Detects security issues in Python code via AST analysis."""
53
-
54
- def __init__(self, content):
55
- self.issues = []
56
- self.content = content
57
- self.lines = content.split('\n')
58
-
59
- def visit_Assign(self, node):
60
- """Check for hardcoded secrets and CSRF disabled."""
61
- for target in node.targets:
62
- if isinstance(target, ast.Name):
63
- name = target.id
64
- # Check for hardcoded SECRET_KEY
65
- if name == 'SECRET_KEY':
66
- if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
67
- self.issues.append({
68
- "type": "security",
69
- "issue": "hardcoded_secret",
70
- "name": "SECRET_KEY",
71
- "lineno": node.lineno,
72
- "message": "Hardcoded SECRET_KEY detected. Use environment variables instead."
73
- })
74
- # Check for hardcoded passwords/tokens
75
- if name.upper() in ('PASSWORD', 'API_KEY', 'TOKEN', 'AUTH_TOKEN', 'PRIVATE_KEY', 'AWS_SECRET_KEY'):
76
- if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
77
- self.issues.append({
78
- "type": "security",
79
- "issue": "hardcoded_secret",
80
- "name": name,
81
- "lineno": node.lineno,
82
- "message": f"Hardcoded {name} detected. Use environment variables instead."
83
- })
84
- # Check for csrf = False
85
- if name.lower() in ('csrf', 'csrf_enabled', 'wtf_csrf_enabled'):
86
- if isinstance(node.value, ast.Constant) and node.value.value == False:
87
- self.issues.append({
88
- "type": "security",
89
- "issue": "csrf_disabled",
90
- "name": name,
91
- "lineno": node.lineno,
92
- "message": "CSRF protection is disabled. This is a security vulnerability."
93
- })
94
- self.generic_visit(node)
95
-
96
- def visit_Call(self, node):
97
- """Check for dangerous function calls."""
98
- func_name = None
99
- if isinstance(node.func, ast.Name):
100
- func_name = node.func.id
101
- elif isinstance(node.func, ast.Attribute):
102
- func_name = node.func.attr
103
-
104
- # Check for eval/exec usage
105
- if func_name in ('eval', 'exec'):
106
- self.issues.append({
107
- "type": "security",
108
- "issue": "code_injection",
109
- "name": func_name,
110
- "lineno": node.lineno,
111
- "message": f"Use of {func_name}() detected. This can lead to code injection vulnerabilities."
112
- })
113
-
114
- # Check for pickle usage (insecure deserialization)
115
- if func_name in ('loads', 'load') and isinstance(node.func, ast.Attribute):
116
- if isinstance(node.func.value, ast.Name) and node.func.value.id == 'pickle':
117
- self.issues.append({
118
- "type": "security",
119
- "issue": "insecure_deserialization",
120
- "name": "pickle",
121
- "lineno": node.lineno,
122
- "message": "Pickle deserialization is unsafe. Use json instead for untrusted data."
123
- })
124
-
125
- # Check for shell=True in subprocess
126
- if func_name in ('run', 'call', 'Popen', 'check_output', 'check_call'):
127
- for keyword in node.keywords:
128
- if keyword.arg == 'shell' and isinstance(keyword.value, ast.Constant) and keyword.value.value == True:
129
- self.issues.append({
130
- "type": "security",
131
- "issue": "command_injection",
132
- "name": func_name,
133
- "lineno": node.lineno,
134
- "message": "shell=True in subprocess can lead to command injection."
135
- })
136
-
137
- self.generic_visit(node)
138
-
139
- def check_sql_injection(self):
140
- """Check for SQL injection patterns using regex on source."""
141
- sql_patterns = [
142
- (r'execute\s*\(\s*["\'].*%s', 'SQL string formatting with %s'),
143
- (r'execute\s*\(\s*f["\']', 'SQL f-string'),
144
- (r'execute\s*\(\s*["\'].*\+', 'SQL string concatenation'),
145
- (r'cursor\.execute\s*\(\s*["\'].*\.format\s*\(', 'SQL .format()'),
146
- ]
147
- for pattern, desc in sql_patterns:
148
- for i, line in enumerate(self.lines, 1):
149
- if re.search(pattern, line, re.IGNORECASE):
150
- self.issues.append({
151
- "type": "security",
152
- "issue": "sql_injection",
153
- "name": desc,
154
- "lineno": i,
155
- "message": f"Potential SQL injection: {desc}. Use parameterized queries."
156
- })
157
-
158
-
159
- def analyze_code(content):
160
- try:
161
- tree = ast.parse(content)
162
-
163
- # Collect metrics
164
- visitor = MetricsVisitor()
165
- visitor.visit(tree)
166
-
167
- # Collect security issues
168
- security_visitor = SecurityVisitor(content)
169
- security_visitor.visit(tree)
170
- security_visitor.check_sql_injection()
171
-
172
- return {
173
- "metrics": visitor.metrics,
174
- "security": security_visitor.issues
175
- }
176
- except Exception as e:
177
- return {"error": str(e)}
178
-
179
- if __name__ == "__main__":
180
- content = sys.stdin.read()
181
- print(json.dumps(analyze_code(content)))