@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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/watsonx/draft.js — watsonx.ai case and path drafting.
|
|
3
|
+
*
|
|
4
|
+
* Sends a rule + limited code snippet to IBM watsonx.ai (via the chat API)
|
|
5
|
+
* and returns a candidate config diff for developer review.
|
|
6
|
+
*
|
|
7
|
+
* Authentication: IBM Cloud IAM API key → bearer token exchange.
|
|
8
|
+
* The token is never written to disk or logged.
|
|
9
|
+
*
|
|
10
|
+
* Environment variables (never passed to verify; server-side only):
|
|
11
|
+
* IBMCLOUD_API_KEY — IBM Cloud IAM API key
|
|
12
|
+
* WATSONX_PROJECT_ID — watsonx.ai project ID
|
|
13
|
+
* WATSONX_REGION — e.g. us-south (default)
|
|
14
|
+
* WATSONX_MODEL_ID — e.g. ibm/granite-3-3-8b-instruct (checked at runtime)
|
|
15
|
+
*
|
|
16
|
+
* If any credential is absent, draftCases() returns { available: false, reason }.
|
|
17
|
+
* The caller must display this state and never show a mocked response as live.
|
|
18
|
+
*
|
|
19
|
+
* API reference: https://cloud.ibm.com/docs/apis/watsonx-ai
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const IAM_TOKEN_URL = 'https://iam.cloud.ibm.com/identity/token';
|
|
23
|
+
const DEFAULT_REGION = 'us-south';
|
|
24
|
+
|
|
25
|
+
// ------------------------------------------------------------------ //
|
|
26
|
+
// IAM token exchange //
|
|
27
|
+
// ------------------------------------------------------------------ //
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Exchange an IBM Cloud API key for a short-lived bearer token.
|
|
31
|
+
* @param {string} apiKey
|
|
32
|
+
* @returns {Promise<string>} access token
|
|
33
|
+
*/
|
|
34
|
+
async function getIamToken(apiKey) {
|
|
35
|
+
const body = new URLSearchParams({
|
|
36
|
+
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
|
37
|
+
apikey: apiKey,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const resp = await fetch(IAM_TOKEN_URL, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
43
|
+
body: body.toString(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!resp.ok) {
|
|
47
|
+
const text = await resp.text().catch(() => '');
|
|
48
|
+
throw new Error(`IAM token exchange failed (${resp.status}): ${text.slice(0, 200)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = await resp.json();
|
|
52
|
+
if (!data.access_token) throw new Error('IAM response missing access_token');
|
|
53
|
+
return data.access_token;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ------------------------------------------------------------------ //
|
|
57
|
+
// Model listing — verify entitlement at runtime //
|
|
58
|
+
// ------------------------------------------------------------------ //
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List available foundation models in the project.
|
|
62
|
+
* Returns the first Granite text model found, or null.
|
|
63
|
+
* @param {string} token
|
|
64
|
+
* @param {string} projectId
|
|
65
|
+
* @param {string} region
|
|
66
|
+
* @returns {Promise<string|null>}
|
|
67
|
+
*/
|
|
68
|
+
async function resolveModelId(token, projectId, region) {
|
|
69
|
+
const url = `https://${region}.ml.cloud.ibm.com/ml/v1/foundation_model_specs?version=2024-09-16&limit=50`;
|
|
70
|
+
try {
|
|
71
|
+
const resp = await fetch(url, {
|
|
72
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
73
|
+
});
|
|
74
|
+
if (!resp.ok) return null;
|
|
75
|
+
const data = await resp.json();
|
|
76
|
+
const models = data.resources ?? [];
|
|
77
|
+
// Prefer a granite instruct model
|
|
78
|
+
const granite = models.find(m =>
|
|
79
|
+
m.model_id?.includes('granite') && m.model_id?.includes('instruct')
|
|
80
|
+
);
|
|
81
|
+
return granite?.model_id ?? models[0]?.model_id ?? null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ------------------------------------------------------------------ //
|
|
88
|
+
// Prompt construction //
|
|
89
|
+
// ------------------------------------------------------------------ //
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the prompt asking watsonx for candidate config entries.
|
|
93
|
+
* Sends ONLY the rule and explicitly selected code snippet (size-limited).
|
|
94
|
+
* @param {string} rule
|
|
95
|
+
* @param {string} snippet — public/synthetic only, reviewed before sending
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
function buildPrompt(rule, snippet) {
|
|
99
|
+
return `You are helping a developer configure an authorization verification tool called Rite.
|
|
100
|
+
|
|
101
|
+
The developer has written this authorization rule:
|
|
102
|
+
"${rule}"
|
|
103
|
+
|
|
104
|
+
Here is a limited code snippet from their service (synthetic/public data only):
|
|
105
|
+
\`\`\`js
|
|
106
|
+
${snippet.slice(0, 2000)}
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
Please identify:
|
|
110
|
+
1. Candidate entry point function names that could reach a sensitive operation.
|
|
111
|
+
2. The likely sink function name (the sensitive operation).
|
|
112
|
+
3. Three to five test cases: at least one that should be ALLOWED and at least two that should be DENIED (with different denial reasons).
|
|
113
|
+
4. For each case: a unique ID, which entry path, a test actor ID, a minimal input object, expected decision (allow/deny), and a brief reason.
|
|
114
|
+
|
|
115
|
+
IMPORTANT:
|
|
116
|
+
- Only cite functions that appear in the snippet above. Label anything inferred as a hypothesis.
|
|
117
|
+
- Do not invent actors, amounts, or resource IDs beyond what the snippet implies.
|
|
118
|
+
- Return ONLY valid JSON matching this schema (no prose, no markdown fences):
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
"hypothesis": true,
|
|
122
|
+
"paths": [
|
|
123
|
+
{ "entry": "functionName", "sink": "sinkName", "source": "filename.js" }
|
|
124
|
+
],
|
|
125
|
+
"cases": [
|
|
126
|
+
{
|
|
127
|
+
"id": "case-id",
|
|
128
|
+
"path": "entryFunctionName",
|
|
129
|
+
"actor": "actor-id",
|
|
130
|
+
"input": { "resourceId": "resource-1" },
|
|
131
|
+
"expected": { "decision": "allow|deny", "effects": [], "stateChanged": false },
|
|
132
|
+
"reason": "One sentence."
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ------------------------------------------------------------------ //
|
|
139
|
+
// Main draft function //
|
|
140
|
+
// ------------------------------------------------------------------ //
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef {{
|
|
144
|
+
* available: true,
|
|
145
|
+
* modelId: string,
|
|
146
|
+
* hypothesis: true,
|
|
147
|
+
* paths: object[],
|
|
148
|
+
* cases: object[],
|
|
149
|
+
* rawResponse: string,
|
|
150
|
+
* } | {
|
|
151
|
+
* available: false,
|
|
152
|
+
* reason: string,
|
|
153
|
+
* }} DraftResult
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Call watsonx.ai to draft candidate paths and cases from a rule + snippet.
|
|
158
|
+
* Returns available:false when credentials are missing or the call fails.
|
|
159
|
+
* Never throws — errors are returned as available:false with a reason.
|
|
160
|
+
*
|
|
161
|
+
* @param {{ rule: string, snippet: string }} opts
|
|
162
|
+
* @returns {Promise<DraftResult>}
|
|
163
|
+
*/
|
|
164
|
+
export async function draftCases({ rule, snippet }) {
|
|
165
|
+
const apiKey = process.env.IBMCLOUD_API_KEY;
|
|
166
|
+
const projectId = process.env.WATSONX_PROJECT_ID;
|
|
167
|
+
const region = process.env.WATSONX_REGION ?? DEFAULT_REGION;
|
|
168
|
+
const modelIdEnv = process.env.WATSONX_MODEL_ID;
|
|
169
|
+
|
|
170
|
+
if (!apiKey) return { available: false, reason: 'IBMCLOUD_API_KEY not set' };
|
|
171
|
+
if (!projectId) return { available: false, reason: 'WATSONX_PROJECT_ID not set' };
|
|
172
|
+
|
|
173
|
+
let token;
|
|
174
|
+
try {
|
|
175
|
+
token = await getIamToken(apiKey);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return { available: false, reason: `IAM auth failed: ${e.message}` };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve model at runtime — do not guess a model ID
|
|
181
|
+
const modelId = modelIdEnv ?? await resolveModelId(token, projectId, region);
|
|
182
|
+
if (!modelId) {
|
|
183
|
+
return { available: false, reason: 'No foundation model available in this project. Set WATSONX_MODEL_ID or check project entitlements.' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const prompt = buildPrompt(rule, snippet);
|
|
187
|
+
const url = `https://${region}.ml.cloud.ibm.com/ml/v1/text/chat?version=2024-09-16`;
|
|
188
|
+
|
|
189
|
+
let rawResponse;
|
|
190
|
+
try {
|
|
191
|
+
const resp = await fetch(url, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${token}`,
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
model_id: modelId,
|
|
199
|
+
project_id: projectId,
|
|
200
|
+
messages: [{ role: 'user', content: prompt }],
|
|
201
|
+
parameters: {
|
|
202
|
+
max_new_tokens: 1200,
|
|
203
|
+
temperature: 0.2,
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!resp.ok) {
|
|
209
|
+
const text = await resp.text().catch(() => '');
|
|
210
|
+
return { available: false, reason: `watsonx API error (${resp.status}): ${text.slice(0, 300)}` };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const data = await resp.json();
|
|
214
|
+
rawResponse = data?.results?.[0]?.generated_text
|
|
215
|
+
?? data?.choices?.[0]?.message?.content
|
|
216
|
+
?? JSON.stringify(data);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { available: false, reason: `Network error calling watsonx: ${e.message}` };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Parse response — treat as untrusted
|
|
222
|
+
let parsed;
|
|
223
|
+
try {
|
|
224
|
+
// Strip markdown fences if present
|
|
225
|
+
const cleaned = rawResponse.replace(/^```(?:json)?\s*/m, '').replace(/\s*```\s*$/m, '').trim();
|
|
226
|
+
parsed = JSON.parse(cleaned);
|
|
227
|
+
} catch {
|
|
228
|
+
return { available: false, reason: `watsonx returned non-JSON response. Raw: ${rawResponse.slice(0, 300)}` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate required fields
|
|
232
|
+
if (!Array.isArray(parsed.paths) || !Array.isArray(parsed.cases)) {
|
|
233
|
+
return { available: false, reason: 'watsonx response missing paths or cases arrays. Check prompt and model.' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Verify all path/case citations are in the snippet
|
|
237
|
+
for (const p of parsed.paths) {
|
|
238
|
+
if (typeof p.entry === 'string' && !snippet.includes(p.entry)) {
|
|
239
|
+
// Label as unverified hypothesis rather than reject
|
|
240
|
+
p.hypothesis = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
available: true,
|
|
246
|
+
modelId,
|
|
247
|
+
hypothesis: true, // Always label model output as hypothesis
|
|
248
|
+
paths: parsed.paths,
|
|
249
|
+
cases: parsed.cases,
|
|
250
|
+
rawResponse,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Rite Authorization Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
actions: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
rite-verify:
|
|
15
|
+
name: Rite — verify authorization rule
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Node.js
|
|
23
|
+
uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: "22"
|
|
26
|
+
|
|
27
|
+
- name: Run Rite verification
|
|
28
|
+
run: >-
|
|
29
|
+
npx --yes @timidan/rite@0.1.0 verify
|
|
30
|
+
--config rite.config.json
|
|
31
|
+
--out rite-report.json
|
|
32
|
+
|
|
33
|
+
- name: Upload Rite report
|
|
34
|
+
if: always() && hashFiles('rite-report.json') != ''
|
|
35
|
+
uses: actions/upload-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: rite-report
|
|
38
|
+
path: rite-report.json
|
|
39
|
+
retention-days: 90
|
|
40
|
+
|
|
41
|
+
- name: Write job summary
|
|
42
|
+
if: always() && hashFiles('rite-report.json') != ''
|
|
43
|
+
run: npx --yes @timidan/rite@0.1.0 report rite-report.json >> "$GITHUB_STEP_SUMMARY"
|