@timidan/rite 0.1.0
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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/assets/favicon.ico +0 -0
- package/dist/assets/index-CUOHpkRl.js +18 -0
- package/dist/assets/index-kPEQ2Fln.css +1 -0
- package/dist/assets/rite-180.png +0 -0
- package/dist/assets/rite-logo-primary-1200.png +0 -0
- package/dist/assets/rite-og-1200x630.png +0 -0
- package/dist/evidence/proof.json +3473 -0
- package/dist/index.html +30 -0
- package/package.json +70 -0
- package/src/cli/rite.js +510 -0
- package/src/core/engine.js +263 -0
- package/src/core/loader.js +72 -0
- package/src/core/sarif.js +126 -0
- package/src/github/app.js +134 -0
- package/src/github/server.js +411 -0
- package/src/graph/walker.js +220 -0
- package/src/instrument/auto-adapter.js +237 -0
- package/src/instrument/effect-schema.js +168 -0
- package/src/mcp/server.js +185 -0
- package/src/watsonx/draft.js +252 -0
- package/templates/rite.yml +43 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Rite — Authorization checks for the paths tests miss</title>
|
|
7
|
+
<meta name="color-scheme" content="light" />
|
|
8
|
+
<meta name="theme-color" content="#faf8f3" />
|
|
9
|
+
<meta name="description" content="Turn explicit authorization rules and mapped entry paths into black-box behavior checks." />
|
|
10
|
+
|
|
11
|
+
<!-- Favicon / touch icon -->
|
|
12
|
+
<link rel="icon" href="./assets/favicon.ico" sizes="any" />
|
|
13
|
+
<link rel="apple-touch-icon" href="./assets/rite-180.png" />
|
|
14
|
+
|
|
15
|
+
<!-- Open Graph / Twitter -->
|
|
16
|
+
<meta property="og:type" content="website" />
|
|
17
|
+
<meta property="og:title" content="Rite — Authorization checks for the paths tests miss" />
|
|
18
|
+
<meta property="og:description" content="Authorization checks for every sensitive entry path you map." />
|
|
19
|
+
<meta property="og:image" content="https://rite.timidan.xyz/assets/rite-og-1200x630.png" />
|
|
20
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
21
|
+
<meta name="twitter:title" content="Rite — Authorization checks for the paths tests miss" />
|
|
22
|
+
<meta name="twitter:description" content="Authorization checks for every sensitive entry path you map." />
|
|
23
|
+
<meta name="twitter:image" content="https://rite.timidan.xyz/assets/rite-og-1200x630.png" />
|
|
24
|
+
<script type="module" crossorigin src="./assets/index-CUOHpkRl.js"></script>
|
|
25
|
+
<link rel="stylesheet" crossorigin href="./assets/index-kPEQ2Fln.css">
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="root"></div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timidan/rite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Authorization checks for the paths tests miss.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rite": "src/cli/rite.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/cli/",
|
|
11
|
+
"src/core/",
|
|
12
|
+
"src/github/",
|
|
13
|
+
"src/graph/",
|
|
14
|
+
"src/instrument/",
|
|
15
|
+
"src/mcp/",
|
|
16
|
+
"src/watsonx/",
|
|
17
|
+
"dist/",
|
|
18
|
+
"templates/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "node src/cli/rite.js server",
|
|
27
|
+
"dev": "vite",
|
|
28
|
+
"build": "vite build",
|
|
29
|
+
"preview": "vite preview",
|
|
30
|
+
"test": "node --test test/refunds.test.js test/engine.test.js test/schema.test.js test/graph.test.js test/github.test.js test/cli.test.js",
|
|
31
|
+
"prove": "node scripts/prove.mjs",
|
|
32
|
+
"cli:verify": "node src/cli/rite.js verify --config examples/refund/rite.config.json --sarif /tmp/rite-latest.sarif",
|
|
33
|
+
"cli:report": "node src/cli/rite.js report",
|
|
34
|
+
"cli:graph": "node src/cli/rite.js graph --file src/refunds.js --entries customerRefund,supportRefund --sink issueRefund",
|
|
35
|
+
"mcp": "node src/cli/rite.js mcp"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
39
|
+
"@octokit/auth-app": "^8.3.1",
|
|
40
|
+
"adm-zip": "^0.6.1",
|
|
41
|
+
"express": "^5.2.1",
|
|
42
|
+
"express-session": "^1.19.0",
|
|
43
|
+
"zod": "^4.6.5"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
47
|
+
"react": "^18.3.1",
|
|
48
|
+
"react-dom": "^18.3.1",
|
|
49
|
+
"vite": "^8.3.1"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/Timidan/rite.git"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://rite.timidan.xyz",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/Timidan/rite/issues"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"keywords": [
|
|
64
|
+
"authorization",
|
|
65
|
+
"security-testing",
|
|
66
|
+
"github-actions",
|
|
67
|
+
"mcp"
|
|
68
|
+
],
|
|
69
|
+
"author": "Timidan"
|
|
70
|
+
}
|
package/src/cli/rite.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* src/cli/rite.js — Rite CLI.
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* rite init [--dir <path>] Create starter config and adapter
|
|
7
|
+
* rite verify --config <file> Run verification, write report JSON
|
|
8
|
+
* [--out <file>] [--sarif <file>]
|
|
9
|
+
* rite report <file> Render an existing report to stdout
|
|
10
|
+
* rite graph --file <file> Walk entry→sink paths in a JS file
|
|
11
|
+
* --entries a,b --sink fn
|
|
12
|
+
* rite instrument --module <file> Generate a rite.adapter.mjs
|
|
13
|
+
* --entries a,b --sink fn [--factory fn] [--snapshot fn] [--out <file>]
|
|
14
|
+
* rite draft --rule <file> Ask watsonx.ai for candidate config
|
|
15
|
+
* [--snippet <file>]
|
|
16
|
+
* rite server [--port 3001] Start GitHub App Express server
|
|
17
|
+
* rite mcp Start local MCP stdio server
|
|
18
|
+
*
|
|
19
|
+
* Exit codes:
|
|
20
|
+
* 0 PASS
|
|
21
|
+
* 1 FAIL
|
|
22
|
+
* 2 ERROR (config/adapter/runtime problem)
|
|
23
|
+
* 3 Usage error
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
27
|
+
import { resolve, dirname, join } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
import { validateConfig, verify, ENGINE_VERSION } from '../core/engine.js';
|
|
31
|
+
import { toSarif } from '../core/sarif.js';
|
|
32
|
+
import { loadAdapterFromConfig } from '../core/loader.js';
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
|
|
36
|
+
// ------------------------------------------------------------------ //
|
|
37
|
+
// Arg parser with declarative flag spec //
|
|
38
|
+
// ------------------------------------------------------------------ //
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse argv and validate against a flag spec for the given command.
|
|
42
|
+
*
|
|
43
|
+
* @param {string[]} argv
|
|
44
|
+
* @param {Record<string, { required?: string[], optional?: string[], numbers?: string[], positional?: number }>} specs
|
|
45
|
+
* — map of command → spec; positional = expected positional arg count
|
|
46
|
+
* @returns {{ cmd: string, flags: Record<string,string|true|number>, positional: string[], errors: string[] }}
|
|
47
|
+
*/
|
|
48
|
+
function parseArgs(argv, specs = {}) {
|
|
49
|
+
const args = argv.slice(2);
|
|
50
|
+
const cmd = args[0] ?? '';
|
|
51
|
+
const rawFlags = {};
|
|
52
|
+
const positional = [];
|
|
53
|
+
|
|
54
|
+
for (let i = 1; i < args.length; i++) {
|
|
55
|
+
if (args[i].startsWith('--')) {
|
|
56
|
+
const key = args[i].slice(2);
|
|
57
|
+
const next = args[i + 1];
|
|
58
|
+
rawFlags[key] = (next && !next.startsWith('--')) ? (i++, next) : true;
|
|
59
|
+
} else {
|
|
60
|
+
positional.push(args[i]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const spec = specs[cmd] ?? {};
|
|
65
|
+
const errors = [];
|
|
66
|
+
const flags = { ...rawFlags };
|
|
67
|
+
|
|
68
|
+
// Coerce number flags
|
|
69
|
+
for (const name of (spec.numbers ?? [])) {
|
|
70
|
+
if (flags[name] !== undefined && flags[name] !== true) {
|
|
71
|
+
const n = Number(flags[name]);
|
|
72
|
+
if (Number.isNaN(n)) errors.push(`--${name} must be a number (got "${flags[name]}")`);
|
|
73
|
+
else flags[name] = n;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check required flags
|
|
78
|
+
for (const name of (spec.required ?? [])) {
|
|
79
|
+
if (!flags[name]) errors.push(`--${name} is required`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { cmd, flags, positional, errors };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Exit with a usage error if parsed.errors is non-empty. */
|
|
86
|
+
function checkArgs(parsed) {
|
|
87
|
+
if (parsed.errors.length) {
|
|
88
|
+
for (const e of parsed.errors) stderr(` ${e}`);
|
|
89
|
+
process.exit(3);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ------------------------------------------------------------------ //
|
|
94
|
+
// Output helpers //
|
|
95
|
+
// ------------------------------------------------------------------ //
|
|
96
|
+
|
|
97
|
+
function stderr(msg) { process.stderr.write(msg + '\n'); }
|
|
98
|
+
function stdout(msg) { process.stdout.write(msg + '\n'); }
|
|
99
|
+
|
|
100
|
+
function renderReport(report) {
|
|
101
|
+
const width = 68;
|
|
102
|
+
const bar = '─'.repeat(width);
|
|
103
|
+
|
|
104
|
+
stdout('');
|
|
105
|
+
stdout(` Rite v${ENGINE_VERSION} · ${report.ruleId}`);
|
|
106
|
+
stdout(` ${bar}`);
|
|
107
|
+
stdout(` Rule: ${report.rule}`);
|
|
108
|
+
stdout(` Generated: ${report.generatedAt}`);
|
|
109
|
+
stdout(` ${bar}`);
|
|
110
|
+
|
|
111
|
+
for (const r of report.results) {
|
|
112
|
+
const icon = r.status === 'PASS' ? '✓' : r.status === 'FAIL' ? '✗' : '!';
|
|
113
|
+
stdout(` ${icon} ${r.status.padEnd(5)} [${r.path}] ${r.id}`);
|
|
114
|
+
if (r.failures?.length) {
|
|
115
|
+
for (const f of r.failures) stdout(` ${f}`);
|
|
116
|
+
}
|
|
117
|
+
if (r.error) stdout(` ERROR: ${r.error.split('\n')[0]}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
stdout(` ${bar}`);
|
|
121
|
+
const total = report.results.length;
|
|
122
|
+
const passed = report.results.filter(r => r.status === 'PASS').length;
|
|
123
|
+
const failed = report.results.filter(r => r.status === 'FAIL').length;
|
|
124
|
+
const errors = report.results.filter(r => r.status === 'ERROR').length;
|
|
125
|
+
stdout(` ${report.status.padEnd(5)} ${passed}/${total} passed` +
|
|
126
|
+
(failed ? ` ${failed} failed` : '') +
|
|
127
|
+
(errors ? ` ${errors} errors` : ''));
|
|
128
|
+
stdout('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ------------------------------------------------------------------ //
|
|
132
|
+
// init //
|
|
133
|
+
// ------------------------------------------------------------------ //
|
|
134
|
+
|
|
135
|
+
const STARTER_CONFIG = JSON.stringify({
|
|
136
|
+
version: 1,
|
|
137
|
+
ruleId: 'my-rule',
|
|
138
|
+
rule: 'Describe the authorization rule here.',
|
|
139
|
+
adapter: './rite.adapter.mjs',
|
|
140
|
+
paths: [
|
|
141
|
+
{ entry: 'myEntry', sink: 'mySink', source: 'src/service.js' },
|
|
142
|
+
],
|
|
143
|
+
cases: [
|
|
144
|
+
{
|
|
145
|
+
id: 'allow-example',
|
|
146
|
+
path: 'myEntry',
|
|
147
|
+
actor: 'actor-id',
|
|
148
|
+
input: { resourceId: 'resource-1' },
|
|
149
|
+
expected: { decision: 'allow', effects: [], stateChanged: false },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'deny-example',
|
|
153
|
+
path: 'myEntry',
|
|
154
|
+
actor: 'other-actor',
|
|
155
|
+
input: { resourceId: 'resource-1' },
|
|
156
|
+
expected: { decision: 'deny', effects: [], stateChanged: false },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
}, null, 2);
|
|
160
|
+
|
|
161
|
+
const STARTER_ADAPTER = `/**
|
|
162
|
+
* rite.adapter.mjs — starter adapter.
|
|
163
|
+
* Implement runCase to call your service and observe effects.
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
export async function runCase(caseSpec) {
|
|
167
|
+
const { path: entryName, actor, input } = caseSpec;
|
|
168
|
+
|
|
169
|
+
// TODO: import your service and create a fresh fixture here.
|
|
170
|
+
// const svc = createMyService({ fixtures: ... });
|
|
171
|
+
|
|
172
|
+
// Call the entry function:
|
|
173
|
+
// const result = svc[entryName](input, { actorId: actor });
|
|
174
|
+
|
|
175
|
+
// Observe sink events and state:
|
|
176
|
+
// const effects = svc.capturedEffects();
|
|
177
|
+
// const stateChanged = ...;
|
|
178
|
+
|
|
179
|
+
throw new Error('Adapter not implemented yet — edit rite.adapter.mjs');
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
decision: 'deny', // 'allow' | 'deny'
|
|
183
|
+
effects: [], // array of observed sink events
|
|
184
|
+
stateBefore: null, // snapshot before call
|
|
185
|
+
stateAfter: null, // snapshot after call
|
|
186
|
+
stateChanged: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
async function cmdInit({ flags }) {
|
|
192
|
+
const targetDir = resolve(flags.dir ?? '.');
|
|
193
|
+
const configPath = join(targetDir, 'rite.config.json');
|
|
194
|
+
const adapterPath = join(targetDir, 'rite.adapter.mjs');
|
|
195
|
+
const workflowPath = join(targetDir, '.github', 'workflows', 'rite.yml');
|
|
196
|
+
const workflowTemplate = join(__dirname, '..', '..', 'templates', 'rite.yml');
|
|
197
|
+
|
|
198
|
+
if (existsSync(configPath) || existsSync(adapterPath) || existsSync(workflowPath)) {
|
|
199
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
200
|
+
const answer = await new Promise(res =>
|
|
201
|
+
rl.question('Rite config, adapter, or workflow already exists. Overwrite? [y/N] ', res)
|
|
202
|
+
);
|
|
203
|
+
rl.close();
|
|
204
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
205
|
+
stderr('Aborted.'); process.exit(3);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
mkdirSync(targetDir, { recursive: true });
|
|
210
|
+
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
211
|
+
writeFileSync(configPath, STARTER_CONFIG, 'utf-8');
|
|
212
|
+
writeFileSync(adapterPath, STARTER_ADAPTER, 'utf-8');
|
|
213
|
+
writeFileSync(workflowPath, readFileSync(workflowTemplate, 'utf-8'), 'utf-8');
|
|
214
|
+
stderr(`Created ${configPath}`);
|
|
215
|
+
stderr(`Created ${adapterPath}`);
|
|
216
|
+
stderr(`Created ${workflowPath}`);
|
|
217
|
+
stderr('Edit rite.config.json and rite.adapter.mjs, then run: rite verify --config rite.config.json');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ------------------------------------------------------------------ //
|
|
221
|
+
// verify //
|
|
222
|
+
// ------------------------------------------------------------------ //
|
|
223
|
+
|
|
224
|
+
async function cmdVerify({ flags }) {
|
|
225
|
+
const configArg = flags.config;
|
|
226
|
+
const configPath = resolve(configArg);
|
|
227
|
+
if (!existsSync(configPath)) {
|
|
228
|
+
stderr(`Config not found: ${configPath}`);
|
|
229
|
+
process.exit(2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Parse config
|
|
233
|
+
let config;
|
|
234
|
+
try {
|
|
235
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
236
|
+
} catch (e) {
|
|
237
|
+
stderr(`Failed to parse config: ${e.message}`);
|
|
238
|
+
process.exit(2);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Validate
|
|
242
|
+
const errs = validateConfig(config);
|
|
243
|
+
if (errs.length) {
|
|
244
|
+
stderr('Config validation errors:');
|
|
245
|
+
for (const e of errs) stderr(` ${e}`);
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Load adapter via shared loader (bounds-checked, no path traversal)
|
|
250
|
+
const loaded = await loadAdapterFromConfig(config, configPath);
|
|
251
|
+
if (!loaded.ok) { stderr(loaded.error); process.exit(2); }
|
|
252
|
+
const { adapter } = loaded;
|
|
253
|
+
|
|
254
|
+
// Run
|
|
255
|
+
let report;
|
|
256
|
+
try {
|
|
257
|
+
report = await verify(config, adapter, { configPath });
|
|
258
|
+
} catch (e) {
|
|
259
|
+
stderr(`Unexpected engine error: ${e.message}`);
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// GitHub Actions provides the triggering commit. Embed it so the server can
|
|
264
|
+
// reject artifacts copied from a different workflow run.
|
|
265
|
+
if (/^[0-9a-f]{40}$/i.test(process.env.GITHUB_SHA ?? '')) {
|
|
266
|
+
report.commitSha = process.env.GITHUB_SHA;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Write JSON report
|
|
270
|
+
const outArg = flags.out;
|
|
271
|
+
if (outArg) {
|
|
272
|
+
const outPath = resolve(outArg);
|
|
273
|
+
writeFileSync(outPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
274
|
+
stderr(`Report written to: ${outPath}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Write SARIF report
|
|
278
|
+
const sarifArg = flags.sarif;
|
|
279
|
+
if (sarifArg) {
|
|
280
|
+
const sarifPath = resolve(sarifArg);
|
|
281
|
+
const sarif = toSarif(report);
|
|
282
|
+
writeFileSync(sarifPath, JSON.stringify(sarif, null, 2), 'utf-8');
|
|
283
|
+
stderr(`SARIF written to: ${sarifPath}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
renderReport(report);
|
|
287
|
+
|
|
288
|
+
// Exit code by status
|
|
289
|
+
if (report.status === 'PASS') process.exit(0);
|
|
290
|
+
if (report.status === 'FAIL') process.exit(1);
|
|
291
|
+
process.exit(2);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ------------------------------------------------------------------ //
|
|
295
|
+
// report //
|
|
296
|
+
// ------------------------------------------------------------------ //
|
|
297
|
+
|
|
298
|
+
async function cmdReport({ positional }) {
|
|
299
|
+
const filePath = positional[0];
|
|
300
|
+
if (!filePath) {
|
|
301
|
+
stderr('Usage: rite report <file>');
|
|
302
|
+
process.exit(3);
|
|
303
|
+
}
|
|
304
|
+
const resolved = resolve(filePath);
|
|
305
|
+
if (!existsSync(resolved)) {
|
|
306
|
+
stderr(`Report not found: ${resolved}`);
|
|
307
|
+
process.exit(2);
|
|
308
|
+
}
|
|
309
|
+
let report;
|
|
310
|
+
try {
|
|
311
|
+
report = JSON.parse(readFileSync(resolved, 'utf-8'));
|
|
312
|
+
} catch (e) {
|
|
313
|
+
stderr(`Failed to parse report: ${e.message}`);
|
|
314
|
+
process.exit(2);
|
|
315
|
+
}
|
|
316
|
+
if (!report.results || !report.status) {
|
|
317
|
+
stderr('File does not look like a Rite report (missing results or status).');
|
|
318
|
+
process.exit(2);
|
|
319
|
+
}
|
|
320
|
+
renderReport(report);
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ------------------------------------------------------------------ //
|
|
325
|
+
// graph //
|
|
326
|
+
// ------------------------------------------------------------------ //
|
|
327
|
+
|
|
328
|
+
async function cmdGraph({ flags }) {
|
|
329
|
+
const fileArg = flags.file;
|
|
330
|
+
const entriesArg = flags.entries;
|
|
331
|
+
const sinkArg = flags.sink;
|
|
332
|
+
const filePath = resolve(fileArg);
|
|
333
|
+
if (!existsSync(filePath)) { stderr(`File not found: ${filePath}`); process.exit(2); }
|
|
334
|
+
|
|
335
|
+
const { walkFile, formatWalkResult } = await import('../graph/walker.js');
|
|
336
|
+
const entries = entriesArg.split(',').map(s => s.trim()).filter(Boolean);
|
|
337
|
+
const result = walkFile({ file: filePath, entries, sink: sinkArg });
|
|
338
|
+
|
|
339
|
+
stdout(formatWalkResult(result));
|
|
340
|
+
stdout('');
|
|
341
|
+
for (const t of result.traces) {
|
|
342
|
+
const status = t.found ? 'REACHES SINK' : 'NO PATH FOUND';
|
|
343
|
+
stderr(` ${t.entry} → ${sinkArg}: ${status}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ------------------------------------------------------------------ //
|
|
348
|
+
// instrument //
|
|
349
|
+
// ------------------------------------------------------------------ //
|
|
350
|
+
|
|
351
|
+
async function cmdInstrument({ flags }) {
|
|
352
|
+
const moduleArg = flags.module;
|
|
353
|
+
const entriesArg = flags.entries;
|
|
354
|
+
const sinkArg = flags.sink;
|
|
355
|
+
const factoryArg = flags.factory;
|
|
356
|
+
const snapshotArg = flags.snapshot;
|
|
357
|
+
const outArg = flags.out ?? 'rite.adapter.mjs';
|
|
358
|
+
const modulePath = resolve(moduleArg);
|
|
359
|
+
if (!existsSync(modulePath)) { stderr(`Module not found: ${modulePath}`); process.exit(2); }
|
|
360
|
+
|
|
361
|
+
const { writeAdapterFile } = await import('../instrument/auto-adapter.js');
|
|
362
|
+
const entries = entriesArg.split(',').map(s => s.trim()).filter(Boolean);
|
|
363
|
+
const outPath = resolve(outArg);
|
|
364
|
+
|
|
365
|
+
// Relative path from the output file back to the module
|
|
366
|
+
const { relative: rel, dirname: dn } = await import('node:path');
|
|
367
|
+
const relModPath = './' + rel(dn(outPath), modulePath).replace(/\\/g, '/');
|
|
368
|
+
|
|
369
|
+
writeAdapterFile({
|
|
370
|
+
modulePath, outPath, relativeModulePath: relModPath,
|
|
371
|
+
entries, sink: sinkArg,
|
|
372
|
+
factoryFn: factoryArg, snapshotFn: snapshotArg,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
stderr(`Generated adapter: ${outPath}`);
|
|
376
|
+
stderr('Edit the SEEDS map and seed loader, then run: rite verify --config rite.config.json');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ------------------------------------------------------------------ //
|
|
380
|
+
// draft //
|
|
381
|
+
// ------------------------------------------------------------------ //
|
|
382
|
+
|
|
383
|
+
async function cmdDraft({ flags }) {
|
|
384
|
+
const ruleArg = flags.rule;
|
|
385
|
+
const snippetArg = flags.snippet;
|
|
386
|
+
const rulePath = resolve(ruleArg);
|
|
387
|
+
if (!existsSync(rulePath)) { stderr(`Rule file not found: ${rulePath}`); process.exit(2); }
|
|
388
|
+
const rule = readFileSync(rulePath, 'utf-8').trim();
|
|
389
|
+
|
|
390
|
+
let snippet = '';
|
|
391
|
+
if (snippetArg) {
|
|
392
|
+
const snippetPath = resolve(snippetArg);
|
|
393
|
+
if (!existsSync(snippetPath)) { stderr(`Snippet file not found: ${snippetPath}`); process.exit(2); }
|
|
394
|
+
snippet = readFileSync(snippetPath, 'utf-8');
|
|
395
|
+
stderr(`Sending ${snippet.length} chars of snippet to watsonx.ai (review before continuing).`);
|
|
396
|
+
// Give developer a chance to cancel
|
|
397
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
398
|
+
const answer = await new Promise(res =>
|
|
399
|
+
rl.question('Confirm sending this snippet to IBM watsonx.ai? [y/N] ', res)
|
|
400
|
+
);
|
|
401
|
+
rl.close();
|
|
402
|
+
if (!answer.toLowerCase().startsWith('y')) { stderr('Aborted.'); process.exit(3); }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const { draftCases } = await import('../watsonx/draft.js');
|
|
406
|
+
stderr('Calling watsonx.ai…');
|
|
407
|
+
const result = await draftCases({ rule, snippet });
|
|
408
|
+
|
|
409
|
+
if (!result.available) {
|
|
410
|
+
stderr(`watsonx.ai not available: ${result.reason}`);
|
|
411
|
+
stderr('Set IBMCLOUD_API_KEY and WATSONX_PROJECT_ID to enable AI drafting.');
|
|
412
|
+
stderr('Verification works fully without this step.');
|
|
413
|
+
process.exit(2);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
stdout('');
|
|
417
|
+
stdout('=== watsonx.ai draft (hypothesis — review before use) ===');
|
|
418
|
+
stdout(`Model: ${result.modelId}`);
|
|
419
|
+
stdout('');
|
|
420
|
+
stdout('Suggested paths:');
|
|
421
|
+
for (const p of result.paths)
|
|
422
|
+
stdout(` ${p.entry} → ${p.sink} (${p.source})${p.hypothesis ? ' [hypothesis]' : ''}`);
|
|
423
|
+
stdout('');
|
|
424
|
+
stdout('Suggested cases:');
|
|
425
|
+
for (const c of result.cases)
|
|
426
|
+
stdout(` ${c.id} [${c.path}] ${c.expected?.decision} — ${c.reason}`);
|
|
427
|
+
stdout('');
|
|
428
|
+
stdout('Full JSON diff (copy into rite.config.json after review):');
|
|
429
|
+
stdout(JSON.stringify({ paths: result.paths, cases: result.cases }, null, 2));
|
|
430
|
+
stdout('');
|
|
431
|
+
stderr('Label: hypothesis. Review all citations. Do not merge into config without approval.');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ------------------------------------------------------------------ //
|
|
435
|
+
// server //
|
|
436
|
+
// ------------------------------------------------------------------ //
|
|
437
|
+
|
|
438
|
+
async function cmdServer({ flags }) {
|
|
439
|
+
const port = Number(flags.port ?? process.env.PORT ?? 3001);
|
|
440
|
+
try {
|
|
441
|
+
const { startGitHubServer } = await import('../github/server.js');
|
|
442
|
+
await startGitHubServer({ port });
|
|
443
|
+
// Keep process alive
|
|
444
|
+
await new Promise(() => {});
|
|
445
|
+
} catch (e) {
|
|
446
|
+
stderr(`rite server: ${e.message}`);
|
|
447
|
+
process.exit(2);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ------------------------------------------------------------------ //
|
|
452
|
+
// mcp //
|
|
453
|
+
// ------------------------------------------------------------------ //
|
|
454
|
+
|
|
455
|
+
async function cmdMcp() {
|
|
456
|
+
const { startMcpServer } = await import('../mcp/server.js');
|
|
457
|
+
await startMcpServer();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ------------------------------------------------------------------ //
|
|
461
|
+
// Entry //
|
|
462
|
+
// ------------------------------------------------------------------ //
|
|
463
|
+
|
|
464
|
+
const USAGE = `
|
|
465
|
+
Rite v${ENGINE_VERSION} — Authorization checks for the paths tests miss.
|
|
466
|
+
|
|
467
|
+
Usage:
|
|
468
|
+
rite init [--dir <path>]
|
|
469
|
+
rite verify --config <file> [--out <file>] [--sarif <file>]
|
|
470
|
+
rite report <file>
|
|
471
|
+
rite graph --file <js-file> --entries fn1,fn2 --sink sinkFn
|
|
472
|
+
rite instrument --module <file> --entries fn1,fn2 --sink sinkFn [--factory fn] [--snapshot fn] [--out adapter.mjs]
|
|
473
|
+
rite draft --rule <file> [--snippet <file>]
|
|
474
|
+
rite server [--port 3001]
|
|
475
|
+
rite mcp
|
|
476
|
+
|
|
477
|
+
Commands:
|
|
478
|
+
init Create a starter rite.config.json and rite.adapter.mjs
|
|
479
|
+
verify Run verification (exit 0=PASS, 1=FAIL, 2=ERROR); optionally write --sarif
|
|
480
|
+
report Render an existing JSON report file
|
|
481
|
+
graph Walk a JS file and trace entry→sink call paths (static, single-file)
|
|
482
|
+
instrument Generate a rite.adapter.mjs for a JS module
|
|
483
|
+
draft Ask watsonx.ai for candidate paths and cases (requires IBM Cloud creds)
|
|
484
|
+
server Start GitHub App Express server (requires env vars — see src/github/app.js)
|
|
485
|
+
mcp Start a local MCP stdio server
|
|
486
|
+
`.trim();
|
|
487
|
+
|
|
488
|
+
const FLAG_SPECS = {
|
|
489
|
+
verify: { required: ['config'], optional: ['out', 'sarif'] },
|
|
490
|
+
graph: { required: ['file', 'entries', 'sink'] },
|
|
491
|
+
instrument: { required: ['module', 'entries', 'sink'], optional: ['factory', 'snapshot', 'out'] },
|
|
492
|
+
draft: { required: ['rule'], optional: ['snippet'] },
|
|
493
|
+
server: { numbers: ['port'], optional: ['port'] },
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const parsed = parseArgs(process.argv, FLAG_SPECS);
|
|
497
|
+
|
|
498
|
+
switch (parsed.cmd) {
|
|
499
|
+
case 'init': await cmdInit(parsed); break;
|
|
500
|
+
case 'verify': checkArgs(parsed); await cmdVerify(parsed); break;
|
|
501
|
+
case 'report': await cmdReport(parsed); break;
|
|
502
|
+
case 'graph': checkArgs(parsed); await cmdGraph(parsed); break;
|
|
503
|
+
case 'instrument': checkArgs(parsed); await cmdInstrument(parsed); break;
|
|
504
|
+
case 'draft': checkArgs(parsed); await cmdDraft(parsed); break;
|
|
505
|
+
case 'server': checkArgs(parsed); await cmdServer(parsed); break;
|
|
506
|
+
case 'mcp': await cmdMcp(); break;
|
|
507
|
+
default:
|
|
508
|
+
stderr(USAGE);
|
|
509
|
+
process.exit(parsed.cmd ? 3 : 0);
|
|
510
|
+
}
|