@rigour-labs/cli 4.2.2 → 4.2.3
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/cli.js +23 -9
- package/dist/commands/demo-helpers.d.ts +1 -0
- package/dist/commands/demo-injections.d.ts +28 -0
- package/dist/commands/demo-injections.js +186 -0
- package/dist/commands/demo-scaffold.d.ts +6 -0
- package/dist/commands/demo-scaffold.js +170 -0
- package/dist/commands/demo-scenarios.d.ts +2 -6
- package/dist/commands/demo-scenarios.js +3 -159
- package/dist/commands/demo.d.ts +3 -3
- package/dist/commands/demo.js +96 -10
- package/dist/commands/scan-deep.d.ts +8 -0
- package/dist/commands/scan-deep.js +164 -0
- package/dist/commands/scan.d.ts +16 -0
- package/dist/commands/scan.js +57 -36
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -91,12 +91,23 @@ program
|
|
|
91
91
|
.option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
|
|
92
92
|
.option('--json', 'Output report in JSON format')
|
|
93
93
|
.option('-c, --config <path>', 'Path to custom rigour.yml configuration (optional)')
|
|
94
|
+
.option('--deep', 'Enable deep LLM-powered analysis (local, 350MB one-time download)')
|
|
95
|
+
.option('--pro', 'Use larger model for deep analysis (900MB, higher quality)')
|
|
96
|
+
.option('-k, --api-key <key>', 'Use cloud API key instead of local model (BYOK)')
|
|
97
|
+
.option('--provider <name>', 'Cloud provider: claude, openai, gemini, groq, mistral, together, deepseek, ollama')
|
|
98
|
+
.option('--api-base-url <url>', 'Custom API base URL')
|
|
99
|
+
.option('--model-name <name>', 'Override cloud model name')
|
|
100
|
+
.option('--agents <count>', 'Number of parallel agents for deep scan (cloud-only)', '1')
|
|
94
101
|
.addHelpText('after', `
|
|
95
102
|
Examples:
|
|
96
|
-
$ rigour scan
|
|
97
|
-
$ rigour scan
|
|
98
|
-
$ rigour scan --
|
|
99
|
-
$ rigour scan --
|
|
103
|
+
$ rigour scan # Zero-config AST scan
|
|
104
|
+
$ rigour scan --deep # Zero-config + local LLM deep analysis
|
|
105
|
+
$ rigour scan --deep --pro # Zero-config + larger local model
|
|
106
|
+
$ rigour scan --deep -k sk-ant-xxx # Zero-config + Claude API
|
|
107
|
+
$ rigour scan --deep --provider groq -k gsk_xxx # Zero-config + Groq
|
|
108
|
+
$ rigour scan ./src --deep # Deep scan specific directory
|
|
109
|
+
$ rigour scan --json # Machine-readable output
|
|
110
|
+
$ rigour scan --ci # CI-friendly output
|
|
100
111
|
`)
|
|
101
112
|
.action(async (files, options) => {
|
|
102
113
|
await scanCommand(process.cwd(), files, options);
|
|
@@ -150,19 +161,22 @@ program
|
|
|
150
161
|
.option('--cinematic', 'Screen-recording mode: typewriter effects, simulated AI agent, before/after scores')
|
|
151
162
|
.option('--hooks', 'Focus on real-time hooks catching issues as AI writes code')
|
|
152
163
|
.option('--speed <speed>', 'Pacing: fast, normal, slow (default: normal)', 'normal')
|
|
164
|
+
.option('--repo <url>', 'Clone a real GitHub repo and inject drift into it (festival mode)')
|
|
153
165
|
.addHelpText('after', `
|
|
154
166
|
Examples:
|
|
155
|
-
$ rigour demo
|
|
156
|
-
$ rigour demo --cinematic
|
|
157
|
-
$ rigour demo --cinematic --speed slow
|
|
158
|
-
$ rigour demo --
|
|
159
|
-
$
|
|
167
|
+
$ rigour demo # Flagship demo (synthetic project)
|
|
168
|
+
$ rigour demo --cinematic # Screen-recording optimized
|
|
169
|
+
$ rigour demo --cinematic --speed slow # Slower pacing for presentations
|
|
170
|
+
$ rigour demo --cinematic --repo https://github.com/fastapi/fastapi # Live demo on real repo
|
|
171
|
+
$ rigour demo --hooks # Focus on real-time hooks
|
|
172
|
+
$ npx @rigour-labs/cli demo # Try without installing
|
|
160
173
|
`)
|
|
161
174
|
.action(async (options) => {
|
|
162
175
|
await demoCommand({
|
|
163
176
|
cinematic: !!options.cinematic,
|
|
164
177
|
hooks: !!options.hooks,
|
|
165
178
|
speed: options.speed || 'normal',
|
|
179
|
+
repo: options.repo,
|
|
166
180
|
});
|
|
167
181
|
});
|
|
168
182
|
program
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift injection patterns for live demo on real repos.
|
|
3
|
+
* Each injection is a file to write with intentional code issues.
|
|
4
|
+
*/
|
|
5
|
+
export interface DriftInjection {
|
|
6
|
+
filename: string;
|
|
7
|
+
description: string;
|
|
8
|
+
gate: string;
|
|
9
|
+
severity: 'critical' | 'high' | 'medium';
|
|
10
|
+
hookMessage: string;
|
|
11
|
+
code: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Universal injections that work in any TypeScript/JavaScript repo.
|
|
15
|
+
*/
|
|
16
|
+
export declare const TS_INJECTIONS: DriftInjection[];
|
|
17
|
+
/**
|
|
18
|
+
* Python/FastAPI injections.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PYTHON_INJECTIONS: DriftInjection[];
|
|
21
|
+
/**
|
|
22
|
+
* Fixed versions of TS injections (for before/after demo).
|
|
23
|
+
*/
|
|
24
|
+
export declare const TS_FIXES: Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Detect which injection set to use based on repo contents.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectInjectionSet(languages: string[]): DriftInjection[];
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift injection patterns for live demo on real repos.
|
|
3
|
+
* Each injection is a file to write with intentional code issues.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Universal injections that work in any TypeScript/JavaScript repo.
|
|
7
|
+
*/
|
|
8
|
+
export const TS_INJECTIONS = [
|
|
9
|
+
{
|
|
10
|
+
filename: 'src/auth-handler.ts',
|
|
11
|
+
description: 'AI writes authentication with hardcoded secret',
|
|
12
|
+
gate: 'security-patterns',
|
|
13
|
+
severity: 'critical',
|
|
14
|
+
hookMessage: 'Possible hardcoded secret or API key detected',
|
|
15
|
+
code: [
|
|
16
|
+
'import express from \'express\';',
|
|
17
|
+
'',
|
|
18
|
+
'const API_SECRET = "sk-live-4f3c2b1a0987654321abcdef";',
|
|
19
|
+
'const DB_PASSWORD = "super_secret_p@ssw0rd!";',
|
|
20
|
+
'',
|
|
21
|
+
'export function authenticate(req: express.Request) {',
|
|
22
|
+
' const token = req.headers.authorization;',
|
|
23
|
+
' if (token === API_SECRET) {',
|
|
24
|
+
' return { authenticated: true, role: \'admin\' };',
|
|
25
|
+
' }',
|
|
26
|
+
' return { authenticated: false };',
|
|
27
|
+
'}',
|
|
28
|
+
].join('\n'),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
filename: 'src/ai-data-loader.ts',
|
|
32
|
+
description: 'AI hallucinates a non-existent package',
|
|
33
|
+
gate: 'hallucinated-imports',
|
|
34
|
+
severity: 'critical',
|
|
35
|
+
hookMessage: "Import 'ai-data-magic' does not resolve to any installed or known package",
|
|
36
|
+
code: [
|
|
37
|
+
'import { z } from \'zod\';',
|
|
38
|
+
'import { magicParser } from \'ai-data-magic\';',
|
|
39
|
+
'import { ultraCache } from \'quantum-cache-pro\';',
|
|
40
|
+
'',
|
|
41
|
+
'const schema = z.object({',
|
|
42
|
+
' name: z.string(),',
|
|
43
|
+
' email: z.string().email(),',
|
|
44
|
+
'});',
|
|
45
|
+
'',
|
|
46
|
+
'export function loadData(raw: unknown) {',
|
|
47
|
+
' return schema.parse(raw);',
|
|
48
|
+
'}',
|
|
49
|
+
].join('\n'),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
filename: 'src/api-handler.ts',
|
|
53
|
+
description: 'AI forgets to await an async function',
|
|
54
|
+
gate: 'promise-safety',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
hookMessage: 'Unhandled promise — fetchUser() called without await or .catch()',
|
|
57
|
+
code: [
|
|
58
|
+
'export async function fetchUser(id: string) {',
|
|
59
|
+
' const res = await fetch(`/api/users/${id}`);',
|
|
60
|
+
' return res.json();',
|
|
61
|
+
'}',
|
|
62
|
+
'',
|
|
63
|
+
'export function handleRequest(req: any, res: any) {',
|
|
64
|
+
' fetchUser(req.params.id); // floating promise!',
|
|
65
|
+
' res.send(\'Processing...\');',
|
|
66
|
+
'}',
|
|
67
|
+
].join('\n'),
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Python/FastAPI injections.
|
|
72
|
+
*/
|
|
73
|
+
export const PYTHON_INJECTIONS = [
|
|
74
|
+
{
|
|
75
|
+
filename: 'src/middleware/cors_config.py',
|
|
76
|
+
description: 'AI sets wildcard CORS with credentials',
|
|
77
|
+
gate: 'security-patterns',
|
|
78
|
+
severity: 'critical',
|
|
79
|
+
hookMessage: 'Wildcard CORS with allow_credentials=True — any origin can steal session tokens',
|
|
80
|
+
code: [
|
|
81
|
+
'from fastapi.middleware.cors import CORSMiddleware',
|
|
82
|
+
'',
|
|
83
|
+
'def setup_cors(app):',
|
|
84
|
+
' app.add_middleware(',
|
|
85
|
+
' CORSMiddleware,',
|
|
86
|
+
' allow_origins=["*"],',
|
|
87
|
+
' allow_credentials=True,',
|
|
88
|
+
' allow_methods=["*"],',
|
|
89
|
+
' allow_headers=["*"],',
|
|
90
|
+
' )',
|
|
91
|
+
].join('\n'),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
filename: 'src/middleware/logging_middleware.py',
|
|
95
|
+
description: 'AI logs full request bodies including passwords',
|
|
96
|
+
gate: 'security-patterns',
|
|
97
|
+
severity: 'high',
|
|
98
|
+
hookMessage: 'Request body logged — passwords, tokens, PII exposed in log output',
|
|
99
|
+
code: [
|
|
100
|
+
'import logging',
|
|
101
|
+
'from starlette.middleware.base import BaseHTTPMiddleware',
|
|
102
|
+
'from starlette.requests import Request',
|
|
103
|
+
'',
|
|
104
|
+
'logger = logging.getLogger(__name__)',
|
|
105
|
+
'',
|
|
106
|
+
'class LoggingMiddleware(BaseHTTPMiddleware):',
|
|
107
|
+
' async def dispatch(self, request: Request, call_next):',
|
|
108
|
+
' body = await request.body()',
|
|
109
|
+
' logger.info("Body: %s Headers: %s", body, dict(request.headers))',
|
|
110
|
+
' response = await call_next(request)',
|
|
111
|
+
' return response',
|
|
112
|
+
].join('\n'),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
filename: 'src/config.py',
|
|
116
|
+
description: 'AI hardcodes secrets in config class',
|
|
117
|
+
gate: 'security-patterns',
|
|
118
|
+
severity: 'critical',
|
|
119
|
+
hookMessage: 'Hardcoded SECRET_KEY and database credentials in source code',
|
|
120
|
+
code: [
|
|
121
|
+
'class Config:',
|
|
122
|
+
' SECRET_KEY = "super-secret-key-12345"',
|
|
123
|
+
' DATABASE_URL = "postgresql://admin:p@ssw0rd@prod-db:5432/app"',
|
|
124
|
+
' DEBUG = True',
|
|
125
|
+
' SESSION_COOKIE_SECURE = False',
|
|
126
|
+
' SESSION_COOKIE_HTTPONLY = False',
|
|
127
|
+
].join('\n'),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
/**
|
|
131
|
+
* Fixed versions of TS injections (for before/after demo).
|
|
132
|
+
*/
|
|
133
|
+
export const TS_FIXES = {
|
|
134
|
+
'src/auth-handler.ts': [
|
|
135
|
+
'import express from \'express\';',
|
|
136
|
+
'',
|
|
137
|
+
'export function authenticate(req: express.Request) {',
|
|
138
|
+
' const token = req.headers.authorization;',
|
|
139
|
+
' if (!token) return { authenticated: false };',
|
|
140
|
+
' return { authenticated: validateToken(token) };',
|
|
141
|
+
'}',
|
|
142
|
+
'',
|
|
143
|
+
'function validateToken(token: string): boolean {',
|
|
144
|
+
' return token.startsWith(\'Bearer \') && token.length > 20;',
|
|
145
|
+
'}',
|
|
146
|
+
].join('\n'),
|
|
147
|
+
'src/ai-data-loader.ts': [
|
|
148
|
+
'import { z } from \'zod\';',
|
|
149
|
+
'',
|
|
150
|
+
'const schema = z.object({',
|
|
151
|
+
' name: z.string(),',
|
|
152
|
+
' email: z.string().email(),',
|
|
153
|
+
'});',
|
|
154
|
+
'',
|
|
155
|
+
'export function loadData(raw: unknown) {',
|
|
156
|
+
' return schema.parse(raw);',
|
|
157
|
+
'}',
|
|
158
|
+
].join('\n'),
|
|
159
|
+
'src/api-handler.ts': [
|
|
160
|
+
'import express from \'express\';',
|
|
161
|
+
'',
|
|
162
|
+
'export async function fetchUser(id: string) {',
|
|
163
|
+
' const res = await fetch(`/api/users/${id}`);',
|
|
164
|
+
' if (!res.ok) throw new Error(`HTTP ${res.status}`);',
|
|
165
|
+
' return res.json();',
|
|
166
|
+
'}',
|
|
167
|
+
'',
|
|
168
|
+
'export async function handleRequest(req: express.Request, res: express.Response) {',
|
|
169
|
+
' try {',
|
|
170
|
+
' const data = await fetchUser(req.params.id);',
|
|
171
|
+
' res.json(data);',
|
|
172
|
+
' } catch (error) {',
|
|
173
|
+
' res.status(500).json({ error: \'Failed to fetch user\' });',
|
|
174
|
+
' }',
|
|
175
|
+
'}',
|
|
176
|
+
].join('\n'),
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Detect which injection set to use based on repo contents.
|
|
180
|
+
*/
|
|
181
|
+
export function detectInjectionSet(languages) {
|
|
182
|
+
if (languages.includes('Python')) {
|
|
183
|
+
return PYTHON_INJECTIONS;
|
|
184
|
+
}
|
|
185
|
+
return TS_INJECTIONS;
|
|
186
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function scaffoldDemoProject(dir: string): Promise<void>;
|
|
2
|
+
export declare function buildDemoConfig(): Record<string, unknown>;
|
|
3
|
+
export declare function buildDemoPackageJson(): Record<string, unknown>;
|
|
4
|
+
export declare function writeIssueFiles(dir: string): Promise<void>;
|
|
5
|
+
export declare function writeGodFile(dir: string): Promise<void>;
|
|
6
|
+
export declare function generateDemoAudit(dir: string, report: any, outputPath: string): Promise<void>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
// ── Scaffold demo project ───────────────────────────────────────────
|
|
5
|
+
export async function scaffoldDemoProject(dir) {
|
|
6
|
+
const config = buildDemoConfig();
|
|
7
|
+
await fs.writeFile(path.join(dir, 'rigour.yml'), yaml.stringify(config));
|
|
8
|
+
await fs.writeJson(path.join(dir, 'package.json'), buildDemoPackageJson(), { spaces: 2 });
|
|
9
|
+
await fs.ensureDir(path.join(dir, 'src'));
|
|
10
|
+
await fs.ensureDir(path.join(dir, 'docs'));
|
|
11
|
+
await writeIssueFiles(dir);
|
|
12
|
+
await writeGodFile(dir);
|
|
13
|
+
await fs.writeFile(path.join(dir, 'README.md'), '# Demo Project\n\nThis is a demo project for Rigour.\n');
|
|
14
|
+
}
|
|
15
|
+
export function buildDemoConfig() {
|
|
16
|
+
return {
|
|
17
|
+
version: 1,
|
|
18
|
+
preset: 'api',
|
|
19
|
+
gates: {
|
|
20
|
+
max_file_lines: 300,
|
|
21
|
+
forbid_todos: true,
|
|
22
|
+
forbid_fixme: true,
|
|
23
|
+
ast: { complexity: 10, max_params: 5 },
|
|
24
|
+
security: { enabled: true, block_on_severity: 'high' },
|
|
25
|
+
hallucinated_imports: { enabled: true, severity: 'critical' },
|
|
26
|
+
promise_safety: { enabled: true, severity: 'high' },
|
|
27
|
+
},
|
|
28
|
+
hooks: { enabled: true, tools: ['claude'] },
|
|
29
|
+
ignore: ['.git/**', 'node_modules/**'],
|
|
30
|
+
output: { report_path: 'rigour-report.json' },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function buildDemoPackageJson() {
|
|
34
|
+
return {
|
|
35
|
+
name: 'rigour-demo',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
dependencies: { express: '^4.18.0', zod: '^3.22.0' },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function writeIssueFiles(dir) {
|
|
41
|
+
await writeAuthFile(dir);
|
|
42
|
+
await writeApiHandlerFile(dir);
|
|
43
|
+
await writeDataLoaderFile(dir);
|
|
44
|
+
await writeUtilsFile(dir);
|
|
45
|
+
}
|
|
46
|
+
async function writeAuthFile(dir) {
|
|
47
|
+
await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
|
|
48
|
+
import express from 'express';
|
|
49
|
+
|
|
50
|
+
const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";
|
|
51
|
+
const DB_PASSWORD = "super_secret_p@ssw0rd!";
|
|
52
|
+
|
|
53
|
+
export function authenticate(req: express.Request) {
|
|
54
|
+
const token = req.headers.authorization;
|
|
55
|
+
if (token === API_KEY) {
|
|
56
|
+
return { authenticated: true };
|
|
57
|
+
}
|
|
58
|
+
return { authenticated: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function connectDatabase() {
|
|
62
|
+
return { host: 'prod-db.internal', password: DB_PASSWORD };
|
|
63
|
+
}
|
|
64
|
+
`.trim());
|
|
65
|
+
}
|
|
66
|
+
async function writeApiHandlerFile(dir) {
|
|
67
|
+
await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
|
|
68
|
+
import express from 'express';
|
|
69
|
+
|
|
70
|
+
export async function fetchUserData(userId: string) {
|
|
71
|
+
const response = await fetch(\`https://api.example.com/users/\${userId}\`);
|
|
72
|
+
return response.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function handleRequest(req: express.Request, res: express.Response) {
|
|
76
|
+
fetchUserData(req.params.id);
|
|
77
|
+
res.send('Processing...');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function batchProcess(ids: string[]) {
|
|
81
|
+
ids.forEach(id => fetchUserData(id));
|
|
82
|
+
}
|
|
83
|
+
`.trim());
|
|
84
|
+
}
|
|
85
|
+
async function writeDataLoaderFile(dir) {
|
|
86
|
+
await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
|
|
87
|
+
import { z } from 'zod';
|
|
88
|
+
import { magicParser } from 'ai-data-magic';
|
|
89
|
+
import { ultraCache } from 'quantum-cache-pro';
|
|
90
|
+
|
|
91
|
+
const schema = z.object({
|
|
92
|
+
name: z.string(),
|
|
93
|
+
email: z.string().email(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export function loadData(raw: unknown) {
|
|
97
|
+
const parsed = schema.parse(raw);
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
`.trim());
|
|
101
|
+
}
|
|
102
|
+
async function writeUtilsFile(dir) {
|
|
103
|
+
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
104
|
+
// NOTE: Claude suggested this but I need to review
|
|
105
|
+
// NOTE: This function has edge cases
|
|
106
|
+
export function formatDate(date: Date): string {
|
|
107
|
+
return date.toISOString().split('T')[0];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sanitizeInput(input: string): string {
|
|
111
|
+
// NOTE: Add proper sanitization
|
|
112
|
+
return input.trim();
|
|
113
|
+
}
|
|
114
|
+
`.trim());
|
|
115
|
+
}
|
|
116
|
+
export async function writeGodFile(dir) {
|
|
117
|
+
const lines = [
|
|
118
|
+
'// Auto-generated data processing module',
|
|
119
|
+
'export class DataProcessor {',
|
|
120
|
+
];
|
|
121
|
+
for (let i = 0; i < 60; i++) {
|
|
122
|
+
lines.push(` process${i}(data: any) {`);
|
|
123
|
+
lines.push(` const result = data.map((x: any) => x * ${i + 1});`);
|
|
124
|
+
lines.push(` if (result.length > ${i * 10}) {`);
|
|
125
|
+
lines.push(` return result.slice(0, ${i * 10});`);
|
|
126
|
+
lines.push(` }`);
|
|
127
|
+
lines.push(` return result;`);
|
|
128
|
+
lines.push(` }`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('}');
|
|
131
|
+
await fs.writeFile(path.join(dir, 'src', 'god-file.ts'), lines.join('\n'));
|
|
132
|
+
}
|
|
133
|
+
// ── Audit report generator ──────────────────────────────────────────
|
|
134
|
+
export async function generateDemoAudit(dir, report, outputPath) {
|
|
135
|
+
const stats = report.stats || {};
|
|
136
|
+
const failures = report.failures || [];
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push('# Rigour Audit Report — Demo');
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
141
|
+
lines.push(`**Status:** ${report.status}`);
|
|
142
|
+
lines.push(`**Score:** ${stats.score ?? 100}/100`);
|
|
143
|
+
if (stats.ai_health_score !== undefined) {
|
|
144
|
+
lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
|
|
145
|
+
}
|
|
146
|
+
if (stats.structural_score !== undefined) {
|
|
147
|
+
lines.push(`**Structural:** ${stats.structural_score}/100`);
|
|
148
|
+
}
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push('## Violations');
|
|
151
|
+
lines.push('');
|
|
152
|
+
for (let i = 0; i < failures.length; i++) {
|
|
153
|
+
const f = failures[i];
|
|
154
|
+
lines.push(`### ${i + 1}. [${(f.severity || 'medium').toUpperCase()}] ${f.title}`);
|
|
155
|
+
lines.push(`- **ID:** \`${f.id}\``);
|
|
156
|
+
lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
|
|
157
|
+
lines.push(`- **Details:** ${f.details}`);
|
|
158
|
+
if (f.files?.length) {
|
|
159
|
+
lines.push(`- **Files:** ${f.files.join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
if (f.hint) {
|
|
162
|
+
lines.push(`- **Hint:** ${f.hint}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push('');
|
|
165
|
+
}
|
|
166
|
+
lines.push('---');
|
|
167
|
+
lines.push('*Generated by Rigour — https://rigour.run*');
|
|
168
|
+
lines.push('*Research: https://zenodo.org/records/18673564*');
|
|
169
|
+
await fs.writeFile(outputPath, lines.join('\n'));
|
|
170
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { DemoOptions } from './demo-helpers.js';
|
|
2
|
+
import { scaffoldDemoProject, generateDemoAudit } from './demo-scaffold.js';
|
|
3
|
+
export { scaffoldDemoProject, generateDemoAudit };
|
|
2
4
|
export declare function runHooksDemo(demoDir: string, options: DemoOptions): Promise<void>;
|
|
3
5
|
export declare function simulateAgentWrite(filename: string, codeLines: string[], gate: string, file: string, message: string, severity: string, options: DemoOptions): Promise<void>;
|
|
4
6
|
export declare function runFullGates(demoDir: string, options: DemoOptions): Promise<void>;
|
|
5
7
|
export declare function runBeforeAfterDemo(demoDir: string, options: DemoOptions): Promise<void>;
|
|
6
|
-
export declare function scaffoldDemoProject(dir: string): Promise<void>;
|
|
7
|
-
export declare function buildDemoConfig(): Record<string, unknown>;
|
|
8
|
-
export declare function buildDemoPackageJson(): Record<string, unknown>;
|
|
9
|
-
export declare function writeIssueFiles(dir: string): Promise<void>;
|
|
10
|
-
export declare function writeGodFile(dir: string): Promise<void>;
|
|
11
|
-
export declare function generateDemoAudit(dir: string, report: any, outputPath: string): Promise<void>;
|
|
@@ -6,6 +6,9 @@ import { GateRunner, ConfigSchema } from '@rigour-labs/core';
|
|
|
6
6
|
import { recordScore } from '@rigour-labs/core';
|
|
7
7
|
import { pause, typewrite } from './demo-helpers.js';
|
|
8
8
|
import { simulateCodeWrite, simulateHookCatch, renderScoreBar, renderTrendChart, displayGateResults, } from './demo-display.js';
|
|
9
|
+
import { scaffoldDemoProject, generateDemoAudit } from './demo-scaffold.js';
|
|
10
|
+
// Re-export scaffolding functions for backward compatibility
|
|
11
|
+
export { scaffoldDemoProject, generateDemoAudit };
|
|
9
12
|
// ── Hooks demo: simulate AI agent → hook catches ────────────────────
|
|
10
13
|
export async function runHooksDemo(demoDir, options) {
|
|
11
14
|
const divider = chalk.cyan('━'.repeat(50));
|
|
@@ -195,162 +198,3 @@ export async function handleRequest(req: express.Request, res: express.Response)
|
|
|
195
198
|
console.error(chalk.red(`Re-check error: ${msg}`));
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
|
-
// ── Scaffold demo project ───────────────────────────────────────────
|
|
199
|
-
export async function scaffoldDemoProject(dir) {
|
|
200
|
-
const config = buildDemoConfig();
|
|
201
|
-
await fs.writeFile(path.join(dir, 'rigour.yml'), yaml.stringify(config));
|
|
202
|
-
await fs.writeJson(path.join(dir, 'package.json'), buildDemoPackageJson(), { spaces: 2 });
|
|
203
|
-
await fs.ensureDir(path.join(dir, 'src'));
|
|
204
|
-
await fs.ensureDir(path.join(dir, 'docs'));
|
|
205
|
-
await writeIssueFiles(dir);
|
|
206
|
-
await writeGodFile(dir);
|
|
207
|
-
await fs.writeFile(path.join(dir, 'README.md'), '# Demo Project\n\nThis is a demo project for Rigour.\n');
|
|
208
|
-
}
|
|
209
|
-
export function buildDemoConfig() {
|
|
210
|
-
return {
|
|
211
|
-
version: 1,
|
|
212
|
-
preset: 'api',
|
|
213
|
-
gates: {
|
|
214
|
-
max_file_lines: 300,
|
|
215
|
-
forbid_todos: true,
|
|
216
|
-
forbid_fixme: true,
|
|
217
|
-
ast: { complexity: 10, max_params: 5 },
|
|
218
|
-
security: { enabled: true, block_on_severity: 'high' },
|
|
219
|
-
hallucinated_imports: { enabled: true, severity: 'critical' },
|
|
220
|
-
promise_safety: { enabled: true, severity: 'high' },
|
|
221
|
-
},
|
|
222
|
-
hooks: { enabled: true, tools: ['claude'] },
|
|
223
|
-
ignore: ['.git/**', 'node_modules/**'],
|
|
224
|
-
output: { report_path: 'rigour-report.json' },
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
export function buildDemoPackageJson() {
|
|
228
|
-
return {
|
|
229
|
-
name: 'rigour-demo',
|
|
230
|
-
version: '1.0.0',
|
|
231
|
-
dependencies: { express: '^4.18.0', zod: '^3.22.0' },
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
export async function writeIssueFiles(dir) {
|
|
235
|
-
// Issue 1: Hardcoded API key
|
|
236
|
-
await fs.writeFile(path.join(dir, 'src', 'auth.ts'), `
|
|
237
|
-
import express from 'express';
|
|
238
|
-
|
|
239
|
-
const API_KEY = "sk-live-4f3c2b1a0987654321abcdef";
|
|
240
|
-
const DB_PASSWORD = "super_secret_p@ssw0rd!";
|
|
241
|
-
|
|
242
|
-
export function authenticate(req: express.Request) {
|
|
243
|
-
const token = req.headers.authorization;
|
|
244
|
-
if (token === API_KEY) {
|
|
245
|
-
return { authenticated: true };
|
|
246
|
-
}
|
|
247
|
-
return { authenticated: false };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function connectDatabase() {
|
|
251
|
-
return { host: 'prod-db.internal', password: DB_PASSWORD };
|
|
252
|
-
}
|
|
253
|
-
`.trim());
|
|
254
|
-
// Issue 2: Unhandled promise
|
|
255
|
-
await fs.writeFile(path.join(dir, 'src', 'api-handler.ts'), `
|
|
256
|
-
import express from 'express';
|
|
257
|
-
|
|
258
|
-
export async function fetchUserData(userId: string) {
|
|
259
|
-
const response = await fetch(\`https://api.example.com/users/\${userId}\`);
|
|
260
|
-
return response.json();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function handleRequest(req: express.Request, res: express.Response) {
|
|
264
|
-
fetchUserData(req.params.id);
|
|
265
|
-
res.send('Processing...');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function batchProcess(ids: string[]) {
|
|
269
|
-
ids.forEach(id => fetchUserData(id));
|
|
270
|
-
}
|
|
271
|
-
`.trim());
|
|
272
|
-
// Issue 3: Hallucinated import
|
|
273
|
-
await fs.writeFile(path.join(dir, 'src', 'data-loader.ts'), `
|
|
274
|
-
import { z } from 'zod';
|
|
275
|
-
import { magicParser } from 'ai-data-magic';
|
|
276
|
-
import { ultraCache } from 'quantum-cache-pro';
|
|
277
|
-
|
|
278
|
-
const schema = z.object({
|
|
279
|
-
name: z.string(),
|
|
280
|
-
email: z.string().email(),
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
export function loadData(raw: unknown) {
|
|
284
|
-
const parsed = schema.parse(raw);
|
|
285
|
-
return parsed;
|
|
286
|
-
}
|
|
287
|
-
`.trim());
|
|
288
|
-
// Issue 4: Placeholder markers
|
|
289
|
-
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
290
|
-
// NOTE: Claude suggested this but I need to review
|
|
291
|
-
// NOTE: This function has edge cases
|
|
292
|
-
export function formatDate(date: Date): string {
|
|
293
|
-
return date.toISOString().split('T')[0];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function sanitizeInput(input: string): string {
|
|
297
|
-
// NOTE: Add proper sanitization
|
|
298
|
-
return input.trim();
|
|
299
|
-
}
|
|
300
|
-
`.trim());
|
|
301
|
-
}
|
|
302
|
-
export async function writeGodFile(dir) {
|
|
303
|
-
const lines = [
|
|
304
|
-
'// Auto-generated data processing module',
|
|
305
|
-
'export class DataProcessor {',
|
|
306
|
-
];
|
|
307
|
-
for (let i = 0; i < 60; i++) {
|
|
308
|
-
lines.push(` process${i}(data: any) {`);
|
|
309
|
-
lines.push(` const result = data.map((x: any) => x * ${i + 1});`);
|
|
310
|
-
lines.push(` if (result.length > ${i * 10}) {`);
|
|
311
|
-
lines.push(` return result.slice(0, ${i * 10});`);
|
|
312
|
-
lines.push(` }`);
|
|
313
|
-
lines.push(` return result;`);
|
|
314
|
-
lines.push(` }`);
|
|
315
|
-
}
|
|
316
|
-
lines.push('}');
|
|
317
|
-
await fs.writeFile(path.join(dir, 'src', 'god-file.ts'), lines.join('\n'));
|
|
318
|
-
}
|
|
319
|
-
// ── Audit report generator ──────────────────────────────────────────
|
|
320
|
-
export async function generateDemoAudit(dir, report, outputPath) {
|
|
321
|
-
const stats = report.stats || {};
|
|
322
|
-
const failures = report.failures || [];
|
|
323
|
-
const lines = [];
|
|
324
|
-
lines.push('# Rigour Audit Report — Demo');
|
|
325
|
-
lines.push('');
|
|
326
|
-
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
327
|
-
lines.push(`**Status:** ${report.status}`);
|
|
328
|
-
lines.push(`**Score:** ${stats.score ?? 100}/100`);
|
|
329
|
-
if (stats.ai_health_score !== undefined) {
|
|
330
|
-
lines.push(`**AI Health:** ${stats.ai_health_score}/100`);
|
|
331
|
-
}
|
|
332
|
-
if (stats.structural_score !== undefined) {
|
|
333
|
-
lines.push(`**Structural:** ${stats.structural_score}/100`);
|
|
334
|
-
}
|
|
335
|
-
lines.push('');
|
|
336
|
-
lines.push('## Violations');
|
|
337
|
-
lines.push('');
|
|
338
|
-
for (let i = 0; i < failures.length; i++) {
|
|
339
|
-
const f = failures[i];
|
|
340
|
-
lines.push(`### ${i + 1}. [${(f.severity || 'medium').toUpperCase()}] ${f.title}`);
|
|
341
|
-
lines.push(`- **ID:** \`${f.id}\``);
|
|
342
|
-
lines.push(`- **Provenance:** ${f.provenance || 'traditional'}`);
|
|
343
|
-
lines.push(`- **Details:** ${f.details}`);
|
|
344
|
-
if (f.files?.length) {
|
|
345
|
-
lines.push(`- **Files:** ${f.files.join(', ')}`);
|
|
346
|
-
}
|
|
347
|
-
if (f.hint) {
|
|
348
|
-
lines.push(`- **Hint:** ${f.hint}`);
|
|
349
|
-
}
|
|
350
|
-
lines.push('');
|
|
351
|
-
}
|
|
352
|
-
lines.push('---');
|
|
353
|
-
lines.push('*Generated by Rigour — https://rigour.run*');
|
|
354
|
-
lines.push('*Research: https://zenodo.org/records/18673564*');
|
|
355
|
-
await fs.writeFile(outputPath, lines.join('\n'));
|
|
356
|
-
}
|
package/dist/commands/demo.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rigour demo
|
|
3
3
|
*
|
|
4
|
-
* Creates a temp project
|
|
5
|
-
* runs Rigour against it
|
|
6
|
-
* @since v2.17.0 (extended v3.0.0)
|
|
4
|
+
* Creates a temp project (or clones a real repo) with intentional
|
|
5
|
+
* AI-generated code issues, runs Rigour against it in real time.
|
|
6
|
+
* @since v2.17.0 (extended v3.0.0, repo mode v3.1.0)
|
|
7
7
|
*/
|
|
8
8
|
import type { DemoOptions } from './demo-helpers.js';
|
|
9
9
|
export type { DemoOptions } from './demo-helpers.js';
|
package/dist/commands/demo.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rigour demo
|
|
3
3
|
*
|
|
4
|
-
* Creates a temp project
|
|
5
|
-
* runs Rigour against it
|
|
6
|
-
* @since v2.17.0 (extended v3.0.0)
|
|
4
|
+
* Creates a temp project (or clones a real repo) with intentional
|
|
5
|
+
* AI-generated code issues, runs Rigour against it in real time.
|
|
6
|
+
* @since v2.17.0 (extended v3.0.0, repo mode v3.1.0)
|
|
7
7
|
*/
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
10
|
import os from 'os';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
12
13
|
import { pause, typewrite } from './demo-helpers.js';
|
|
13
|
-
import { printBanner, printPlantedIssues, printClosing } from './demo-display.js';
|
|
14
|
+
import { printBanner, printPlantedIssues, printClosing, simulateCodeWrite, simulateHookCatch } from './demo-display.js';
|
|
14
15
|
import { runHooksDemo, runFullGates, runBeforeAfterDemo, scaffoldDemoProject } from './demo-scenarios.js';
|
|
15
|
-
|
|
16
|
+
import { TS_INJECTIONS, PYTHON_INJECTIONS, TS_FIXES } from './demo-injections.js';
|
|
16
17
|
export async function demoCommand(options = {}) {
|
|
17
18
|
const isCinematic = !!options.cinematic;
|
|
18
19
|
const showHooks = !!options.hooks || isCinematic;
|
|
19
20
|
printBanner(isCinematic);
|
|
21
|
+
if (options.repo) {
|
|
22
|
+
await runRepoDemo(options);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Original synthetic demo flow
|
|
26
|
+
await runSyntheticDemo(options, isCinematic, showHooks);
|
|
27
|
+
}
|
|
28
|
+
async function runSyntheticDemo(options, isCinematic, showHooks) {
|
|
20
29
|
if (isCinematic) {
|
|
21
30
|
await typewrite(chalk.bold('Rigour Demo — Watch AI code governance in real time.\n'), options);
|
|
22
31
|
await pause(800, options);
|
|
@@ -24,7 +33,6 @@ export async function demoCommand(options = {}) {
|
|
|
24
33
|
else {
|
|
25
34
|
console.log(chalk.bold('Rigour Demo — See AI code governance in action.\n'));
|
|
26
35
|
}
|
|
27
|
-
// 1. Create temp project
|
|
28
36
|
const demoDir = path.join(os.tmpdir(), `rigour-demo-${Date.now()}`);
|
|
29
37
|
await fs.ensureDir(demoDir);
|
|
30
38
|
if (isCinematic) {
|
|
@@ -36,19 +44,97 @@ export async function demoCommand(options = {}) {
|
|
|
36
44
|
}
|
|
37
45
|
await scaffoldDemoProject(demoDir);
|
|
38
46
|
console.log(chalk.green('✓ Demo project scaffolded.\n'));
|
|
39
|
-
// 2. Simulate AI agent writing flawed code (cinematic/hooks mode)
|
|
40
47
|
if (showHooks) {
|
|
41
48
|
await runHooksDemo(demoDir, options);
|
|
42
49
|
}
|
|
43
50
|
else {
|
|
44
51
|
printPlantedIssues();
|
|
45
52
|
}
|
|
46
|
-
// 3. Run full quality gates
|
|
47
53
|
await runFullGates(demoDir, options);
|
|
48
|
-
// 4. Show "after fix" improvement (cinematic only)
|
|
49
54
|
if (isCinematic) {
|
|
50
55
|
await runBeforeAfterDemo(demoDir, options);
|
|
51
56
|
}
|
|
52
|
-
// 5. Closing
|
|
53
57
|
printClosing(isCinematic);
|
|
54
58
|
}
|
|
59
|
+
async function runRepoDemo(options) {
|
|
60
|
+
const repoUrl = options.repo;
|
|
61
|
+
const repoName = extractRepoName(repoUrl);
|
|
62
|
+
await typewrite(chalk.bold(`Rigour Live Demo — Real repo, real issues, real-time.\n`), options);
|
|
63
|
+
await pause(600, options);
|
|
64
|
+
// 1. Clone
|
|
65
|
+
const demoDir = path.join(os.tmpdir(), `rigour-demo-${repoName}-${Date.now()}`);
|
|
66
|
+
await typewrite(chalk.dim(`Cloning ${repoUrl}...`), options);
|
|
67
|
+
if (!/^https?:\/\/[^\s;|&]+$/.test(repoUrl)) {
|
|
68
|
+
console.error(chalk.red('Invalid repo URL. Use https://github.com/owner/repo format.'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
execSync(`git clone --depth 1 ${repoUrl} ${demoDir}`, { stdio: 'pipe' });
|
|
72
|
+
console.log(chalk.green(`✓ Cloned ${repoName}\n`));
|
|
73
|
+
await pause(400, options);
|
|
74
|
+
// 2. Detect language, ensure rigour.yml, pick injections
|
|
75
|
+
const isPython = await detectPythonRepo(demoDir);
|
|
76
|
+
const injections = isPython ? PYTHON_INJECTIONS : TS_INJECTIONS;
|
|
77
|
+
// Ensure a rigour.yml exists (real repos may not have one)
|
|
78
|
+
if (!await fs.pathExists(path.join(demoDir, 'rigour.yml'))) {
|
|
79
|
+
const { buildDemoConfig } = await import('./demo-scaffold.js');
|
|
80
|
+
const yaml = await import('yaml');
|
|
81
|
+
await fs.writeFile(path.join(demoDir, 'rigour.yml'), yaml.default.stringify(buildDemoConfig()));
|
|
82
|
+
}
|
|
83
|
+
// 3. Show "AI agent modifying codebase..."
|
|
84
|
+
const divider = chalk.cyan('━'.repeat(50));
|
|
85
|
+
console.log(divider);
|
|
86
|
+
await typewrite(chalk.bold.magenta(' Simulating AI agent writing code with hooks active...\n'), options);
|
|
87
|
+
await pause(600, options);
|
|
88
|
+
// 4. Inject drift and simulate hook catches
|
|
89
|
+
for (const injection of injections) {
|
|
90
|
+
await injectAndCatch(demoDir, injection, options);
|
|
91
|
+
}
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.magenta.bold(` Hooks caught ${injections.length} issues in real time — before the commit.`));
|
|
94
|
+
console.log(divider);
|
|
95
|
+
console.log('');
|
|
96
|
+
await pause(1000, options);
|
|
97
|
+
// 5. Run full gates
|
|
98
|
+
await runFullGates(demoDir, options);
|
|
99
|
+
// 6. Fix and show improvement (cinematic)
|
|
100
|
+
if (options.cinematic && !isPython) {
|
|
101
|
+
await runRepoBeforeAfter(demoDir, options);
|
|
102
|
+
}
|
|
103
|
+
// 7. Closing
|
|
104
|
+
printClosing(true);
|
|
105
|
+
}
|
|
106
|
+
async function injectAndCatch(demoDir, injection, options) {
|
|
107
|
+
const filePath = path.join(demoDir, injection.filename);
|
|
108
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
109
|
+
console.log(chalk.blue.bold(` Agent: Write → ${injection.filename}`));
|
|
110
|
+
await simulateCodeWrite(injection.filename, injection.code.split('\n'), options);
|
|
111
|
+
await fs.writeFile(filePath, injection.code);
|
|
112
|
+
await simulateHookCatch(injection.gate, injection.filename, injection.hookMessage, injection.severity, options);
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
async function runRepoBeforeAfter(demoDir, options) {
|
|
116
|
+
console.log(chalk.bold.green('Simulating agent fixing issues...\n'));
|
|
117
|
+
await pause(600, options);
|
|
118
|
+
for (const [filename, fixedCode] of Object.entries(TS_FIXES)) {
|
|
119
|
+
await typewrite(chalk.dim(` Agent: Fixing ${filename}...`), options);
|
|
120
|
+
await fs.writeFile(path.join(demoDir, filename), fixedCode);
|
|
121
|
+
console.log(chalk.green(` ✓ Fixed: ${filename}`));
|
|
122
|
+
await pause(300, options);
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
await pause(500, options);
|
|
126
|
+
console.log(chalk.bold.blue('Re-running quality gates after fixes...\n'));
|
|
127
|
+
await runFullGates(demoDir, options);
|
|
128
|
+
}
|
|
129
|
+
function extractRepoName(url) {
|
|
130
|
+
const match = url.match(/([^/]+?)(?:\.git)?$/);
|
|
131
|
+
return match ? match[1] : 'repo';
|
|
132
|
+
}
|
|
133
|
+
async function detectPythonRepo(dir) {
|
|
134
|
+
const pyFiles = ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile'];
|
|
135
|
+
for (const f of pyFiles) {
|
|
136
|
+
if (await fs.pathExists(path.join(dir, f)))
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Report, Config } from '@rigour-labs/core';
|
|
2
|
+
import type { DeepOptions } from '@rigour-labs/core';
|
|
3
|
+
import type { ScanOptions, StackSignals } from './scan.js';
|
|
4
|
+
export declare function buildDeepOpts(options: ScanOptions, isSilent: boolean): DeepOptions & {
|
|
5
|
+
onProgress?: (msg: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function persistDeepResults(cwd: string, report: Report, isDeep: boolean, options: ScanOptions): void;
|
|
8
|
+
export declare function renderDeepScanResults(report: Report, stackSignals: StackSignals, config: Config, cwd: string): void;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getScoreTrend, resolveDeepOptions } from '@rigour-labs/core';
|
|
3
|
+
import { extractHallucinatedImports, renderCoverageWarnings } from './scan.js';
|
|
4
|
+
export function buildDeepOpts(options, isSilent) {
|
|
5
|
+
const resolved = resolveDeepOptions({
|
|
6
|
+
apiKey: options.apiKey,
|
|
7
|
+
provider: options.provider,
|
|
8
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
9
|
+
modelName: options.modelName,
|
|
10
|
+
});
|
|
11
|
+
const hasApiKey = !!resolved.apiKey;
|
|
12
|
+
const agentCount = Math.max(1, parseInt(options.agents || '1', 10) || 1);
|
|
13
|
+
return {
|
|
14
|
+
enabled: true,
|
|
15
|
+
pro: !!options.pro,
|
|
16
|
+
apiKey: resolved.apiKey,
|
|
17
|
+
provider: hasApiKey ? (resolved.provider || 'claude') : 'local',
|
|
18
|
+
apiBaseUrl: resolved.apiBaseUrl,
|
|
19
|
+
modelName: resolved.modelName,
|
|
20
|
+
agents: agentCount > 1 ? agentCount : undefined,
|
|
21
|
+
onProgress: isSilent ? undefined : (msg) => {
|
|
22
|
+
process.stderr.write(msg + '\n');
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function persistDeepResults(cwd, report, isDeep, options) {
|
|
27
|
+
if (!isDeep)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
import('@rigour-labs/core').then(({ openDatabase, insertScan, insertFindings }) => {
|
|
31
|
+
const db = openDatabase();
|
|
32
|
+
if (!db)
|
|
33
|
+
return;
|
|
34
|
+
const repoName = require('path').basename(cwd);
|
|
35
|
+
const scanId = insertScan(db, repoName, report, {
|
|
36
|
+
deepTier: report.stats.deep?.tier || (options.pro ? 'pro' : 'deep'),
|
|
37
|
+
deepModel: report.stats.deep?.model,
|
|
38
|
+
});
|
|
39
|
+
insertFindings(db, scanId, report.failures);
|
|
40
|
+
db.close();
|
|
41
|
+
}).catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
catch { /* silent */ }
|
|
44
|
+
}
|
|
45
|
+
function severityIcon(s) {
|
|
46
|
+
switch (s) {
|
|
47
|
+
case 'critical': return chalk.red.bold('CRIT');
|
|
48
|
+
case 'high': return chalk.red('HIGH');
|
|
49
|
+
case 'medium': return chalk.yellow('MED ');
|
|
50
|
+
case 'low': return chalk.dim('LOW ');
|
|
51
|
+
case 'info': return chalk.dim('INFO');
|
|
52
|
+
default: return chalk.yellow('MED ');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function renderDeepScanResults(report, stackSignals, config, cwd) {
|
|
56
|
+
const stats = report.stats;
|
|
57
|
+
const aiHealth = stats.ai_health_score ?? 100;
|
|
58
|
+
const codeQuality = stats.code_quality_score ?? stats.structural_score ?? 100;
|
|
59
|
+
const overall = stats.score ?? 100;
|
|
60
|
+
const scoreColor = (s) => s >= 80 ? chalk.green : s >= 60 ? chalk.yellow : chalk.red;
|
|
61
|
+
console.log(` ${chalk.bold('AI Health:')} ${scoreColor(aiHealth).bold(aiHealth + '/100')}`);
|
|
62
|
+
console.log(` ${chalk.bold('Code Quality:')} ${scoreColor(codeQuality).bold(codeQuality + '/100')}`);
|
|
63
|
+
console.log(` ${chalk.bold('Overall:')} ${scoreColor(overall).bold(overall + '/100')}`);
|
|
64
|
+
console.log('');
|
|
65
|
+
const isLocal = stats.deep?.tier ? stats.deep.tier !== 'cloud' : true;
|
|
66
|
+
if (isLocal) {
|
|
67
|
+
console.log(chalk.green(' 🔒 Local sidecar execution. Code remains on this machine.'));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(chalk.yellow(` ☁️ Cloud execution. Code context sent to ${stats.deep?.tier || 'cloud'} API.`));
|
|
71
|
+
}
|
|
72
|
+
if (stats.deep) {
|
|
73
|
+
const model = stats.deep.model || 'unknown';
|
|
74
|
+
const ms = stats.deep.total_ms ? ` ${(stats.deep.total_ms / 1000).toFixed(1)}s` : '';
|
|
75
|
+
console.log(chalk.dim(` Model: ${model} (${stats.deep.tier})${ms}`));
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
renderScaryHeadlines(report.failures);
|
|
79
|
+
renderCategorizedFindings(report.failures);
|
|
80
|
+
renderCoverageWarnings(stackSignals);
|
|
81
|
+
const trend = getScoreTrend(cwd);
|
|
82
|
+
if (trend && trend.recentScores.length >= 3) {
|
|
83
|
+
const arrow = trend.direction === 'improving' ? '↑' : trend.direction === 'degrading' ? '↓' : '→';
|
|
84
|
+
const color = trend.direction === 'improving' ? chalk.green : trend.direction === 'degrading' ? chalk.red : chalk.dim;
|
|
85
|
+
console.log(color(`\nTrend: ${trend.recentScores.join(' → ')} ${arrow}`));
|
|
86
|
+
}
|
|
87
|
+
console.log(chalk.yellow(`\nFull report: ${config.output.report_path}`));
|
|
88
|
+
if (report.status === 'FAIL') {
|
|
89
|
+
console.log(chalk.yellow('Fix packet: rigour-fix-packet.json'));
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.dim(`Finished in ${(stats.duration_ms / 1000).toFixed(1)}s | Score: ${overall}/100`));
|
|
92
|
+
console.log('');
|
|
93
|
+
if (report.status === 'FAIL') {
|
|
94
|
+
console.log(chalk.bold('Next steps:'));
|
|
95
|
+
console.log(` ${chalk.cyan('rigour explain')} — plain-English fix suggestions`);
|
|
96
|
+
console.log(` ${chalk.cyan('rigour init')} — add quality gates to your project`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(chalk.green.bold('✓ This repo is clean.'));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function renderScaryHeadlines(failures) {
|
|
103
|
+
const secrets = failures.filter(f => f.id === 'security-patterns' && f.severity === 'critical');
|
|
104
|
+
const fakeImports = extractHallucinatedImports(failures);
|
|
105
|
+
const phantoms = failures.filter(f => f.id === 'phantom-apis');
|
|
106
|
+
let count = 0;
|
|
107
|
+
if (secrets.length > 0) {
|
|
108
|
+
console.log(chalk.red.bold(`🔑 HARDCODED SECRETS: ${secrets.length} credential(s) exposed`));
|
|
109
|
+
count++;
|
|
110
|
+
}
|
|
111
|
+
if (fakeImports.length > 0) {
|
|
112
|
+
const unique = [...new Set(fakeImports)];
|
|
113
|
+
console.log(chalk.red.bold(`📦 HALLUCINATED PACKAGES: ${unique.length} import(s) don't exist`));
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
if (phantoms.length > 0) {
|
|
117
|
+
console.log(chalk.red.bold(`👻 PHANTOM APIs: ${phantoms.length} call(s) to non-existent methods`));
|
|
118
|
+
count++;
|
|
119
|
+
}
|
|
120
|
+
if (count > 0)
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
|
123
|
+
function renderCategorizedFindings(failures) {
|
|
124
|
+
const deep = failures.filter((f) => f.provenance === 'deep-analysis');
|
|
125
|
+
const aiDrift = failures.filter((f) => f.provenance === 'ai-drift');
|
|
126
|
+
const security = failures.filter((f) => f.provenance === 'security');
|
|
127
|
+
const other = failures.filter((f) => f.provenance !== 'deep-analysis' && f.provenance !== 'ai-drift' && f.provenance !== 'security');
|
|
128
|
+
if (deep.length > 0) {
|
|
129
|
+
console.log(chalk.bold(` ── Deep Analysis (${deep.length} verified) ──\n`));
|
|
130
|
+
for (const f of deep.slice(0, 6)) {
|
|
131
|
+
const desc = (f.details || f.title).substring(0, 120);
|
|
132
|
+
console.log(` ${severityIcon(f.severity)} [${f.id}] ${f.files?.[0] || ''}`);
|
|
133
|
+
console.log(` ${desc}`);
|
|
134
|
+
if (f.hint)
|
|
135
|
+
console.log(` → ${f.hint}`);
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (aiDrift.length > 0) {
|
|
140
|
+
console.log(chalk.bold(` ── AI Drift (${aiDrift.length}) ──\n`));
|
|
141
|
+
for (const f of aiDrift.slice(0, 5)) {
|
|
142
|
+
console.log(` ${severityIcon(f.severity)} ${f.title}`);
|
|
143
|
+
if (f.files?.length)
|
|
144
|
+
console.log(chalk.dim(` ${f.files.slice(0, 2).join(', ')}`));
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (security.length > 0) {
|
|
149
|
+
console.log(chalk.bold(` ── Security (${security.length}) ──\n`));
|
|
150
|
+
for (const f of security.slice(0, 5)) {
|
|
151
|
+
console.log(` ${severityIcon(f.severity)} ${f.title}`);
|
|
152
|
+
if (f.hint)
|
|
153
|
+
console.log(chalk.cyan(` ${f.hint}`));
|
|
154
|
+
console.log('');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (other.length > 0) {
|
|
158
|
+
console.log(chalk.bold(` ── Quality (${other.length}) ──\n`));
|
|
159
|
+
for (const f of other.slice(0, 5)) {
|
|
160
|
+
console.log(` ${severityIcon(f.severity)} [${f.id}] ${f.title}`);
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { type Failure } from '@rigour-labs/core';
|
|
1
2
|
export interface ScanOptions {
|
|
2
3
|
ci?: boolean;
|
|
3
4
|
json?: boolean;
|
|
4
5
|
config?: string;
|
|
6
|
+
deep?: boolean;
|
|
7
|
+
pro?: boolean;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
modelName?: string;
|
|
12
|
+
agents?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface StackSignals {
|
|
15
|
+
languages: string[];
|
|
16
|
+
hasDocker: boolean;
|
|
17
|
+
hasTerraform: boolean;
|
|
18
|
+
hasSql: boolean;
|
|
5
19
|
}
|
|
6
20
|
export declare function scanCommand(cwd: string, files?: string[], options?: ScanOptions): Promise<void>;
|
|
21
|
+
export declare function renderCoverageWarnings(stackSignals: StackSignals): void;
|
|
22
|
+
export declare function extractHallucinatedImports(failures: Failure[]): string[];
|
package/dist/commands/scan.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import yaml from 'yaml';
|
|
5
5
|
import { globby } from 'globby';
|
|
6
6
|
import { GateRunner, ConfigSchema, DiscoveryService, FixPacketService, recordScore, getScoreTrend, } from '@rigour-labs/core';
|
|
7
|
+
import { buildDeepOpts, persistDeepResults, renderDeepScanResults } from './scan-deep.js';
|
|
7
8
|
// Exit codes per spec
|
|
8
9
|
const EXIT_PASS = 0;
|
|
9
10
|
const EXIT_FAIL = 1;
|
|
@@ -48,43 +49,30 @@ export async function scanCommand(cwd, files = [], options = {}) {
|
|
|
48
49
|
try {
|
|
49
50
|
const scanCtx = await resolveScanConfig(cwd, options);
|
|
50
51
|
const stackSignals = await detectStackSignals(cwd);
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const isDeep = !!options.deep || !!options.pro || !!options.apiKey;
|
|
53
|
+
const isSilent = !!options.ci || !!options.json;
|
|
54
|
+
if (!isSilent) {
|
|
55
|
+
renderScanHeader(scanCtx, stackSignals, isDeep);
|
|
53
56
|
}
|
|
54
57
|
const runner = new GateRunner(scanCtx.config);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
recordScore(cwd, report);
|
|
60
|
-
// Generate fix packet on failure
|
|
61
|
-
if (report.status === 'FAIL') {
|
|
62
|
-
const fixPacketService = new FixPacketService();
|
|
63
|
-
const fixPacket = fixPacketService.generate(report, scanCtx.config);
|
|
64
|
-
const fixPacketPath = path.join(cwd, 'rigour-fix-packet.json');
|
|
65
|
-
await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
66
|
-
}
|
|
58
|
+
const deepOpts = isDeep ? buildDeepOpts(options, isSilent) : undefined;
|
|
59
|
+
const report = await runner.run(cwd, files.length > 0 ? files : undefined, deepOpts);
|
|
60
|
+
await writeReportArtifacts(cwd, report, scanCtx.config);
|
|
61
|
+
persistDeepResults(cwd, report, isDeep, options);
|
|
67
62
|
if (options.json) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
preset: scanCtx.detectedPreset ?? scanCtx.config.preset,
|
|
71
|
-
paradigm: scanCtx.detectedParadigm ?? scanCtx.config.paradigm,
|
|
72
|
-
stack: stackSignals,
|
|
73
|
-
report,
|
|
74
|
-
}, null, 2) + '\n');
|
|
75
|
-
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
63
|
+
outputJson(scanCtx, stackSignals, report);
|
|
64
|
+
return;
|
|
76
65
|
}
|
|
77
66
|
if (options.ci) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
outputCi(report);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (isDeep) {
|
|
71
|
+
renderDeepScanResults(report, stackSignals, scanCtx.config, cwd);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
renderScanResults(report, stackSignals, scanCtx.config.output.report_path, cwd);
|
|
86
75
|
}
|
|
87
|
-
renderScanResults(report, stackSignals, scanCtx.config.output.report_path, cwd);
|
|
88
76
|
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
89
77
|
}
|
|
90
78
|
catch (error) {
|
|
@@ -99,6 +87,36 @@ export async function scanCommand(cwd, files = [], options = {}) {
|
|
|
99
87
|
process.exit(EXIT_INTERNAL_ERROR);
|
|
100
88
|
}
|
|
101
89
|
}
|
|
90
|
+
async function writeReportArtifacts(cwd, report, config) {
|
|
91
|
+
const reportPath = path.join(cwd, config.output.report_path);
|
|
92
|
+
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
93
|
+
recordScore(cwd, report);
|
|
94
|
+
if (report.status === 'FAIL') {
|
|
95
|
+
const fixPacketService = new FixPacketService();
|
|
96
|
+
const fixPacket = fixPacketService.generate(report, config);
|
|
97
|
+
await fs.writeJson(path.join(cwd, 'rigour-fix-packet.json'), fixPacket, { spaces: 2 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function outputJson(scanCtx, stackSignals, report) {
|
|
101
|
+
process.stdout.write(JSON.stringify({
|
|
102
|
+
mode: scanCtx.mode,
|
|
103
|
+
preset: scanCtx.detectedPreset ?? scanCtx.config.preset,
|
|
104
|
+
paradigm: scanCtx.detectedParadigm ?? scanCtx.config.paradigm,
|
|
105
|
+
stack: stackSignals,
|
|
106
|
+
report,
|
|
107
|
+
}, null, 2) + '\n');
|
|
108
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
109
|
+
}
|
|
110
|
+
function outputCi(report) {
|
|
111
|
+
const score = report.stats.score ?? 0;
|
|
112
|
+
if (report.status === 'PASS') {
|
|
113
|
+
console.log(`PASS (${score}/100)`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(`FAIL: ${report.failures.length} violation(s) | Score: ${score}/100`);
|
|
117
|
+
}
|
|
118
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
119
|
+
}
|
|
102
120
|
async function resolveScanConfig(cwd, options) {
|
|
103
121
|
const explicitConfig = options.config ? path.resolve(cwd, options.config) : undefined;
|
|
104
122
|
const defaultConfig = path.join(cwd, 'rigour.yml');
|
|
@@ -142,9 +160,12 @@ async function detectStackSignals(cwd) {
|
|
|
142
160
|
hasSql: sqlMatches.length > 0,
|
|
143
161
|
};
|
|
144
162
|
}
|
|
145
|
-
function renderScanHeader(scanCtx, stackSignals) {
|
|
146
|
-
console.log(chalk.bold.cyan('\nRigour Scan'));
|
|
147
|
-
|
|
163
|
+
function renderScanHeader(scanCtx, stackSignals, isDeep = false) {
|
|
164
|
+
console.log(chalk.bold.cyan('\nRigour Scan') + (isDeep ? chalk.blue.bold(' + Deep Analysis') : ''));
|
|
165
|
+
const desc = isDeep
|
|
166
|
+
? 'Zero-config sweep with LLM-powered deep analysis.'
|
|
167
|
+
: 'Zero-config security and AI-drift sweep using existing Rigour gates.';
|
|
168
|
+
console.log(chalk.dim(desc + '\n'));
|
|
148
169
|
const modeLabel = scanCtx.mode === 'existing-config'
|
|
149
170
|
? `Using existing config: ${path.basename(scanCtx.configPath || 'rigour.yml')}`
|
|
150
171
|
: 'Auto-discovered config (no rigour.yml required)';
|
|
@@ -244,7 +265,7 @@ function renderScanResults(report, stackSignals, reportPath, cwd) {
|
|
|
244
265
|
console.log(` ${chalk.cyan('rigour init')} — write quality gates to rigour.yml + CI config`);
|
|
245
266
|
}
|
|
246
267
|
}
|
|
247
|
-
function renderCoverageWarnings(stackSignals) {
|
|
268
|
+
export function renderCoverageWarnings(stackSignals) {
|
|
248
269
|
const gaps = [];
|
|
249
270
|
for (const language of stackSignals.languages) {
|
|
250
271
|
const supportedBy = Object.entries(HEADLINE_GATE_SUPPORT)
|
|
@@ -265,7 +286,7 @@ function renderCoverageWarnings(stackSignals) {
|
|
|
265
286
|
gaps.forEach(gap => console.log(chalk.yellow(` - ${gap}`)));
|
|
266
287
|
}
|
|
267
288
|
}
|
|
268
|
-
function extractHallucinatedImports(failures) {
|
|
289
|
+
export function extractHallucinatedImports(failures) {
|
|
269
290
|
const fakeImports = [];
|
|
270
291
|
for (const failure of failures) {
|
|
271
292
|
if (failure.id !== 'hallucinated-imports')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.3",
|
|
4
4
|
"description": "CLI quality gates for AI-generated code. Forces AI agents (Claude, Cursor, Copilot) to meet strict engineering standards with PASS/FAIL enforcement.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"inquirer": "9.2.16",
|
|
45
45
|
"ora": "^8.0.1",
|
|
46
46
|
"yaml": "^2.8.2",
|
|
47
|
-
"@rigour-labs/core": "4.2.
|
|
47
|
+
"@rigour-labs/core": "4.2.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/fs-extra": "^11.0.4",
|