@vantageos/mcp-doctor 0.3.2 → 0.4.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/CHANGELOG.md +50 -0
- package/README.md +22 -3
- package/dist/doctor.d.ts +37 -2
- package/dist/doctor.js +1236 -483
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -2,12 +2,997 @@
|
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
4
|
import { readFileSync as readFileSync12, realpathSync } from "fs";
|
|
5
|
-
import { dirname as dirname11, join as
|
|
5
|
+
import { dirname as dirname11, join as join19, resolve as resolve2 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
|
+
// src/scan.ts
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
|
+
import { basename, dirname, join, relative, sep } from "path";
|
|
11
|
+
var SKIP_DIR_INFO = {
|
|
12
|
+
node_modules: { reason: "vendored dependencies", authored: false },
|
|
13
|
+
dist: { reason: "build output", authored: false },
|
|
14
|
+
build: { reason: "build output", authored: false },
|
|
15
|
+
".git": { reason: "version-control metadata", authored: false },
|
|
16
|
+
coverage: { reason: "generated coverage output", authored: false },
|
|
17
|
+
// `authored: true` marks content the target's own authors wrote, as opposed
|
|
18
|
+
// to vendored or generated content. A walk that looks for a site (C-26) may
|
|
19
|
+
// probe an authored directory it skips: a deployable there would otherwise
|
|
20
|
+
// be reported as if it did not exist.
|
|
21
|
+
tests: { reason: "test material", authored: true },
|
|
22
|
+
__tests__: { reason: "test material", authored: true },
|
|
23
|
+
// Test material, not a target's served surface: deliberately-malformed
|
|
24
|
+
// servers and foreign copies live here. Scanning them makes the instrument
|
|
25
|
+
// trip its own fail-closed detectors when pointed at its own tree, and lints
|
|
26
|
+
// inputs that are meant to be broken. A real target's fixtures are equally
|
|
27
|
+
// test inputs, not its product surface.
|
|
28
|
+
fixtures: { reason: "test material (fixtures)", authored: true }
|
|
29
|
+
};
|
|
30
|
+
var SKIP_DIRS = new Set(Object.keys(SKIP_DIR_INFO));
|
|
31
|
+
function recordSkip(tree, dir, root, reason) {
|
|
32
|
+
const path = relative(root, dir).split(sep).join("/");
|
|
33
|
+
const existing = tree.skippedDirs.find((d) => d.path === path);
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
const entry = {
|
|
36
|
+
path,
|
|
37
|
+
reason: reason ?? SKIP_DIR_INFO[basename(dir)]?.reason ?? "skipped"
|
|
38
|
+
};
|
|
39
|
+
tree.skippedDirs.push(entry);
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
function isCommentLine(raw) {
|
|
43
|
+
const t = raw.trim();
|
|
44
|
+
if (t === "") return false;
|
|
45
|
+
return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
|
|
46
|
+
}
|
|
47
|
+
function walk(dir, out, onSkip) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir);
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const name of entries) {
|
|
55
|
+
const full = join(dir, name);
|
|
56
|
+
let st;
|
|
57
|
+
try {
|
|
58
|
+
st = statSync(full);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (st.isDirectory()) {
|
|
63
|
+
if (SKIP_DIRS.has(name)) {
|
|
64
|
+
onSkip(full);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
walk(full, out, onSkip);
|
|
68
|
+
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
69
|
+
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
70
|
+
out.push(full);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function loadFile(path, root) {
|
|
75
|
+
const raw = readFileSync(path, "utf8");
|
|
76
|
+
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
77
|
+
const file = { path, rel: relative(root, path) || path, lines };
|
|
78
|
+
const mask = maskOf(file);
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.comment) continue;
|
|
81
|
+
const row = mask[line.n - 1];
|
|
82
|
+
let any = false;
|
|
83
|
+
let allComment = true;
|
|
84
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
85
|
+
if (line.raw[c] === " " || line.raw[c] === " " || line.raw[c] === "\r") continue;
|
|
86
|
+
any = true;
|
|
87
|
+
if (row[c] !== COM) {
|
|
88
|
+
allComment = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (any && allComment) line.comment = true;
|
|
93
|
+
}
|
|
94
|
+
return file;
|
|
95
|
+
}
|
|
96
|
+
function findPackageJson(startDir) {
|
|
97
|
+
let dir = startDir;
|
|
98
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
99
|
+
const candidate = join(dir, "package.json");
|
|
100
|
+
if (existsSync(candidate)) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
103
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
104
|
+
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
105
|
+
}
|
|
106
|
+
return { path: candidate, json: parsed };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { path: candidate, json: {}, unreadable: err.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parent = dirname(dir);
|
|
112
|
+
if (parent === dir) break;
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function loadTree(root) {
|
|
118
|
+
const st = statSync(root);
|
|
119
|
+
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
120
|
+
const filePaths = [];
|
|
121
|
+
const tree = { root, files: [], packageJson: null, skippedDirs: [] };
|
|
122
|
+
if (st.isDirectory()) walk(root, filePaths, (d) => recordSkip(tree, d, root));
|
|
123
|
+
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
124
|
+
tree.files = filePaths.map((p) => loadFile(p, baseDir));
|
|
125
|
+
tree.packageJson = findPackageJson(baseDir);
|
|
126
|
+
return tree;
|
|
127
|
+
}
|
|
128
|
+
var COM = 1;
|
|
129
|
+
var CODE = 2;
|
|
130
|
+
var maskCache = /* @__PURE__ */ new WeakMap();
|
|
131
|
+
function buildCodeMask(file) {
|
|
132
|
+
const mask = [];
|
|
133
|
+
let mode = "code";
|
|
134
|
+
const tplStack = [];
|
|
135
|
+
let braceDepth = 0;
|
|
136
|
+
let prevSig = "";
|
|
137
|
+
let prevWord = "";
|
|
138
|
+
const regexAfterKeyword = /* @__PURE__ */ new Set([
|
|
139
|
+
"case",
|
|
140
|
+
"default",
|
|
141
|
+
"delete",
|
|
142
|
+
"do",
|
|
143
|
+
"else",
|
|
144
|
+
"extends",
|
|
145
|
+
"in",
|
|
146
|
+
"instanceof",
|
|
147
|
+
"new",
|
|
148
|
+
"of",
|
|
149
|
+
"return",
|
|
150
|
+
"throw",
|
|
151
|
+
"typeof",
|
|
152
|
+
"void",
|
|
153
|
+
"yield",
|
|
154
|
+
"await"
|
|
155
|
+
]);
|
|
156
|
+
const regexAfter = /* @__PURE__ */ new Set([
|
|
157
|
+
"",
|
|
158
|
+
"(",
|
|
159
|
+
",",
|
|
160
|
+
"=",
|
|
161
|
+
":",
|
|
162
|
+
"[",
|
|
163
|
+
"!",
|
|
164
|
+
"&",
|
|
165
|
+
"|",
|
|
166
|
+
"?",
|
|
167
|
+
"{",
|
|
168
|
+
"}",
|
|
169
|
+
";",
|
|
170
|
+
"+",
|
|
171
|
+
"-",
|
|
172
|
+
"*",
|
|
173
|
+
"%",
|
|
174
|
+
"<",
|
|
175
|
+
">",
|
|
176
|
+
"~",
|
|
177
|
+
"^"
|
|
178
|
+
]);
|
|
179
|
+
for (const line of file.lines) {
|
|
180
|
+
const s = line.raw;
|
|
181
|
+
const row = new Uint8Array(s.length);
|
|
182
|
+
let i = 0;
|
|
183
|
+
while (i < s.length) {
|
|
184
|
+
const c = s[i];
|
|
185
|
+
const n = s[i + 1];
|
|
186
|
+
if (mode === "block") {
|
|
187
|
+
row[i] = COM;
|
|
188
|
+
if (c === "*" && n === "/") {
|
|
189
|
+
row[i + 1] = COM;
|
|
190
|
+
i += 2;
|
|
191
|
+
mode = "code";
|
|
192
|
+
} else i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (mode === "sq" || mode === "dq") {
|
|
196
|
+
if (c === "\\") i += 2;
|
|
197
|
+
else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
|
|
198
|
+
row[i] = CODE;
|
|
199
|
+
i++;
|
|
200
|
+
mode = "code";
|
|
201
|
+
prevSig = c;
|
|
202
|
+
} else i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (mode === "tpl") {
|
|
206
|
+
if (c === "\\") i += 2;
|
|
207
|
+
else if (c === "`") {
|
|
208
|
+
row[i] = CODE;
|
|
209
|
+
i++;
|
|
210
|
+
mode = "code";
|
|
211
|
+
prevSig = c;
|
|
212
|
+
} else if (c === "$" && n === "{") {
|
|
213
|
+
row[i] = CODE;
|
|
214
|
+
row[i + 1] = CODE;
|
|
215
|
+
tplStack.push(braceDepth);
|
|
216
|
+
braceDepth++;
|
|
217
|
+
i += 2;
|
|
218
|
+
mode = "code";
|
|
219
|
+
prevSig = "{";
|
|
220
|
+
} else i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (mode === "re" || mode === "recls") {
|
|
224
|
+
if (c === "\\") i += 2;
|
|
225
|
+
else if (mode === "re" && c === "[") {
|
|
226
|
+
mode = "recls";
|
|
227
|
+
i++;
|
|
228
|
+
} else if (mode === "recls" && c === "]") {
|
|
229
|
+
mode = "re";
|
|
230
|
+
i++;
|
|
231
|
+
} else if (mode === "re" && c === "/") {
|
|
232
|
+
row[i] = CODE;
|
|
233
|
+
i++;
|
|
234
|
+
mode = "code";
|
|
235
|
+
prevSig = "/";
|
|
236
|
+
} else i++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (c === "/" && n === "/") {
|
|
240
|
+
row.fill(COM, i);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (c === "/" && n === "*") {
|
|
244
|
+
mode = "block";
|
|
245
|
+
row[i] = COM;
|
|
246
|
+
row[i + 1] = COM;
|
|
247
|
+
i += 2;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
row[i] = CODE;
|
|
251
|
+
const wordBefore = prevWord;
|
|
252
|
+
if (c !== " " && c !== " ") prevWord = "";
|
|
253
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
254
|
+
let j = i + 1;
|
|
255
|
+
while (j < s.length && /[\w$]/.test(s[j])) j++;
|
|
256
|
+
for (let k = i; k < j; k++) row[k] = CODE;
|
|
257
|
+
prevWord = prevSig === "." ? "" : s.slice(i, j);
|
|
258
|
+
prevSig = s[j - 1];
|
|
259
|
+
i = j;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
263
|
+
mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (c === "/" && (regexAfter.has(prevSig) || regexAfterKeyword.has(wordBefore))) {
|
|
268
|
+
mode = "re";
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (c === "{") braceDepth++;
|
|
273
|
+
else if (c === "}") {
|
|
274
|
+
braceDepth--;
|
|
275
|
+
if (tplStack.length > 0 && tplStack[tplStack.length - 1] === braceDepth) {
|
|
276
|
+
tplStack.pop();
|
|
277
|
+
mode = "tpl";
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (c !== " " && c !== " ") prevSig = c;
|
|
283
|
+
i++;
|
|
284
|
+
}
|
|
285
|
+
if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
|
|
286
|
+
mask.push(row);
|
|
287
|
+
}
|
|
288
|
+
return mask;
|
|
289
|
+
}
|
|
290
|
+
function maskOf(file) {
|
|
291
|
+
let mask = maskCache.get(file);
|
|
292
|
+
if (!mask) {
|
|
293
|
+
mask = buildCodeMask(file);
|
|
294
|
+
maskCache.set(file, mask);
|
|
295
|
+
}
|
|
296
|
+
return mask;
|
|
297
|
+
}
|
|
298
|
+
function isCodeAt(file, n, col) {
|
|
299
|
+
const mask = maskOf(file);
|
|
300
|
+
return mask[n - 1]?.[col] === CODE;
|
|
301
|
+
}
|
|
302
|
+
function matchInCode(file, line, re) {
|
|
303
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
304
|
+
for (const m of line.raw.matchAll(g)) {
|
|
305
|
+
if (isCodeAt(file, line.n, m.index ?? 0)) return m;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
function testInCode(file, line, re) {
|
|
310
|
+
return matchInCode(file, line, re) !== null;
|
|
311
|
+
}
|
|
312
|
+
function countInCode(file, line, ch) {
|
|
313
|
+
let n = 0;
|
|
314
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
315
|
+
if (line.raw[c] === ch && isCodeAt(file, line.n, c)) n++;
|
|
316
|
+
}
|
|
317
|
+
return n;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/detectors/couldNotJudge.ts
|
|
321
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
322
|
+
function couldNotJudge(id, anchor, reason) {
|
|
323
|
+
return {
|
|
324
|
+
id,
|
|
325
|
+
anchor,
|
|
326
|
+
state: "could-not-judge",
|
|
327
|
+
reason,
|
|
328
|
+
verdict: `could not judge: ${reason}`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function settle(id, decided, unreadable) {
|
|
332
|
+
if (unreadable.length === 0) return decided;
|
|
333
|
+
const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
|
|
334
|
+
if (decided.state === "non-conforming") {
|
|
335
|
+
return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
|
|
336
|
+
}
|
|
337
|
+
const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
|
|
338
|
+
return couldNotJudge(
|
|
339
|
+
id,
|
|
340
|
+
unreadable[0].anchor,
|
|
341
|
+
`${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
function readJsonObject(path) {
|
|
345
|
+
let value;
|
|
346
|
+
try {
|
|
347
|
+
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function guardUnreadablePackageJson(inner, opts) {
|
|
355
|
+
return {
|
|
356
|
+
...inner,
|
|
357
|
+
run(tree) {
|
|
358
|
+
const decided = inner.run(tree);
|
|
359
|
+
const pkg = tree.packageJson;
|
|
360
|
+
if (!pkg?.unreadable) return decided;
|
|
361
|
+
const u = {
|
|
362
|
+
anchor: pkg.path,
|
|
363
|
+
reason: `package.json is present but could not be parsed (${pkg.unreadable})`
|
|
364
|
+
};
|
|
365
|
+
if (opts.authoritative && decided.state !== "could-not-judge") {
|
|
366
|
+
return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
|
|
367
|
+
}
|
|
368
|
+
return settle(inner.id, decided, [u]);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/detectors/dLocalIdentity.ts
|
|
374
|
+
var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
|
|
375
|
+
var EXEMPT_MARKER = "allow-local-identity:";
|
|
376
|
+
var MIN_JUSTIFICATION = 8;
|
|
377
|
+
var TENANT_NAMES = "orgId|org_id|orgSlug|org_slug|tenantId|tenant_id";
|
|
378
|
+
var TENANT_PROP = new RegExp(`\\.(${TENANT_NAMES})$`, "i");
|
|
379
|
+
var unbracket = (x) => x.replace(/\??\.?\[\s*["'`]([\w$]+)["'`]\s*\]/g, ".$1").replace(/\?/g, "");
|
|
380
|
+
var FOLD_NAMES = "toLowerCase|toUpperCase|toLocaleLowerCase|toLocaleUpperCase|trim|normalize";
|
|
381
|
+
var FOLD_CALL = String.raw`(?:\s*\.\s*(?:${FOLD_NAMES})\s*\([^()]*\))*`;
|
|
382
|
+
var PATH = String.raw`[\w$.?]+(?![\w$])(?<!\.(?:${FOLD_NAMES}))`;
|
|
383
|
+
var OPERAND = String.raw`${PATH}(?:\[\s*["\`][\w$]+["\`]\s*\]\??[\w$.?]*)*${FOLD_CALL}`;
|
|
384
|
+
var NON_OPERAND = /* @__PURE__ */ new Set(["undefined", "null", "true", "false", "NaN"]);
|
|
385
|
+
var isLiteral = (x) => NON_OPERAND.has(x) || /^-?\d/.test(x);
|
|
386
|
+
var unfold = (x) => unbracket(x.replace(new RegExp(`${FOLD_CALL}$`), ""));
|
|
387
|
+
var lastSegment = (x) => x.split(".").pop() ?? "";
|
|
388
|
+
var ACTING_FIELD = /^(createdBy|assignedTo|assignee|from|fromOrchestrator|toOrchestrator|recipient|sender|callerOrchestrator|orchestrator|orchestratorName|actor|actorName|actingAs|claimedBy|callerName|agentName|fromName)$/;
|
|
389
|
+
var ACTING_BARE = /^(createdBy|assignedTo|assignee|fromOrchestrator|toOrchestrator|callerOrchestrator|actingAs|claimedBy)$/;
|
|
390
|
+
var CALLERISH = /^(caller\w*|actor\w*|acting\w*|claimed\w*|\w*Orchestrator|\w*AgentName)$/i;
|
|
391
|
+
var NAME_FIELD = /^(name|agentName|orchestratorName|displayName)$/;
|
|
392
|
+
var isActing = (x) => ACTING_FIELD.test(lastSegment(x)) && (x.includes(".") || ACTING_BARE.test(x)) || x.includes(".") && CALLERISH.test(lastSegment(x));
|
|
393
|
+
var isNameField = (x) => x.includes(".") && NAME_FIELD.test(lastSegment(x));
|
|
394
|
+
var isBareCallerish = (x) => !x.includes(".") && CALLERISH.test(x);
|
|
395
|
+
var comparesTenants = (a, b) => TENANT_PROP.test(a) || TENANT_PROP.test(b);
|
|
396
|
+
var comparesNames = (a, b) => isActing(a) && (isActing(b) || CALLERISH.test(lastSegment(b)) || isNameField(b)) || isActing(b) && (CALLERISH.test(lastSegment(a)) || isNameField(a)) || isBareCallerish(a) && isNameField(b) || isBareCallerish(b) && isNameField(a);
|
|
397
|
+
var comparesIdentity = (a, b) => comparesTenants(a, b) || comparesNames(a, b);
|
|
398
|
+
function operands(x, y) {
|
|
399
|
+
const [a, b] = [unfold(x), unfold(y)];
|
|
400
|
+
return isLiteral(a) || isLiteral(b) ? null : [a, b];
|
|
401
|
+
}
|
|
402
|
+
var CMP = String.raw`(${OPERAND})\s*[!=]==?\s*(${OPERAND})`;
|
|
403
|
+
var OBJECT_IS = String.raw`\bObject\s*\.\s*is\s*\(\s*(${OPERAND})\s*,\s*(${OPERAND})\s*\)`;
|
|
404
|
+
var acceptWith = (pred) => (m) => {
|
|
405
|
+
const ops = operands(m[1], m[2]);
|
|
406
|
+
return ops !== null && pred(ops[0], ops[1]);
|
|
407
|
+
};
|
|
408
|
+
var STRING_LITERAL = String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\`[^\`$\\]*\`)`;
|
|
409
|
+
var LITERAL_CONCAT = new RegExp(String.raw`^${STRING_LITERAL}(?:\s*\+\s*${STRING_LITERAL})*$`);
|
|
410
|
+
function staticConstants(file) {
|
|
411
|
+
const out = /* @__PURE__ */ new Set();
|
|
412
|
+
const decl = /^(?:export\s+)?const\s+([\w$]+)\s*(?::[^=]+)?=\s*(.*)$/;
|
|
413
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
414
|
+
const m = file.lines[i].raw.match(decl);
|
|
415
|
+
if (!m) continue;
|
|
416
|
+
let init = m[2];
|
|
417
|
+
for (let j = i + 1; !/;\s*$/.test(init) && j < Math.min(file.lines.length, i + 40); j++) {
|
|
418
|
+
init += `
|
|
419
|
+
${file.lines[j].raw}`;
|
|
420
|
+
}
|
|
421
|
+
const body = init.replace(/;\s*$/, "").trim();
|
|
422
|
+
if (LITERAL_CONCAT.test(body)) out.add(m[1]);
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function callArgs(text, from) {
|
|
427
|
+
const args = [];
|
|
428
|
+
let depth = 0;
|
|
429
|
+
let start = from;
|
|
430
|
+
for (let i = from; i < text.length; i++) {
|
|
431
|
+
const c = text[i];
|
|
432
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
433
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
437
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
438
|
+
if (depth === 0) {
|
|
439
|
+
args.push(text.slice(start, i).trim());
|
|
440
|
+
return { args, end: i + 1 };
|
|
441
|
+
}
|
|
442
|
+
depth--;
|
|
443
|
+
} else if (c === "," && depth === 0) {
|
|
444
|
+
args.push(text.slice(start, i).trim());
|
|
445
|
+
start = i + 1;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function isStaticInput(expr, statics) {
|
|
451
|
+
const e = expr.trim();
|
|
452
|
+
if (LITERAL_CONCAT.test(e) || statics.has(e)) return true;
|
|
453
|
+
const wrapped = e.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
|
|
454
|
+
if (!wrapped) return false;
|
|
455
|
+
const inner = callArgs(e, wrapped[0].length);
|
|
456
|
+
return inner !== null && inner.end === e.length && isStaticInput(inner.args[0], statics);
|
|
457
|
+
}
|
|
458
|
+
function textFrom(ctx, extra = 12) {
|
|
459
|
+
const { file, line, idx } = ctx;
|
|
460
|
+
const tail = file.lines.slice(line.n, line.n + extra).map((l) => l.raw);
|
|
461
|
+
return [line.raw.slice(idx), ...tail].join("\n");
|
|
462
|
+
}
|
|
463
|
+
function blockFrom(text, from) {
|
|
464
|
+
const open = text.indexOf("{", from);
|
|
465
|
+
if (open < 0) return null;
|
|
466
|
+
let depth = 0;
|
|
467
|
+
for (let i = open; i < text.length; i++) {
|
|
468
|
+
const c = text[i];
|
|
469
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
470
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (c === "{") depth++;
|
|
474
|
+
else if (c === "}" && --depth === 0) return text.slice(open, i + 1);
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function switchComparesIdentity(m, ctx) {
|
|
479
|
+
if (!ctx) return false;
|
|
480
|
+
const subject = unfold(m[1]);
|
|
481
|
+
const body = blockFrom(textFrom(ctx, 60), m[0].length);
|
|
482
|
+
if (body === null) return false;
|
|
483
|
+
const label = new RegExp(String.raw`\bcase\s+(${OPERAND})\s*:`, "g");
|
|
484
|
+
for (const c of body.matchAll(label)) {
|
|
485
|
+
const ops = operands(subject, c[1]);
|
|
486
|
+
if (ops !== null && comparesIdentity(ops[0], ops[1])) return true;
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
function literalHoldsIdentity(m) {
|
|
491
|
+
const arg = unfold(m[2]);
|
|
492
|
+
return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
|
|
493
|
+
}
|
|
494
|
+
function digestsStaticInput(m, ctx) {
|
|
495
|
+
if (!ctx) return false;
|
|
496
|
+
const text = textFrom(ctx);
|
|
497
|
+
const open = text.indexOf("(");
|
|
498
|
+
if (open < 0) return false;
|
|
499
|
+
const call = callArgs(text, open + 1);
|
|
500
|
+
if (!call) return false;
|
|
501
|
+
if (/^subtle/.test(m[0])) {
|
|
502
|
+
return m[1] === "digest" && call.args.length >= 2 && isStaticInput(call.args[1], ctx.statics);
|
|
503
|
+
}
|
|
504
|
+
if (!/^createHash/.test(m[0])) return false;
|
|
505
|
+
const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
|
|
506
|
+
if (!update) return false;
|
|
507
|
+
const data = callArgs(text, call.end + update[0].length);
|
|
508
|
+
return data !== null && isStaticInput(data.args[0], ctx.statics);
|
|
509
|
+
}
|
|
510
|
+
var RENAMEABLE = [
|
|
511
|
+
"createHash",
|
|
512
|
+
"createHmac",
|
|
513
|
+
"timingSafeEqual",
|
|
514
|
+
"jwtVerify",
|
|
515
|
+
"jwtDecrypt",
|
|
516
|
+
"createRemoteJWKSet",
|
|
517
|
+
"createLocalJWKSet",
|
|
518
|
+
"decodeJwt",
|
|
519
|
+
"compactVerify",
|
|
520
|
+
"verifyToken"
|
|
521
|
+
];
|
|
522
|
+
function renamedPrimitives(file) {
|
|
523
|
+
const text = file.lines.map((l) => l.raw).join("\n");
|
|
524
|
+
const out = [];
|
|
525
|
+
const named = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
|
|
526
|
+
for (const m of text.matchAll(named)) {
|
|
527
|
+
if (m[1] || m[3].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
|
|
528
|
+
for (const part of m[2].split(",")) {
|
|
529
|
+
const as = part.trim().match(/^([\w$]+)\s+as\s+([\w$]+)$/);
|
|
530
|
+
if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const destructured = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?(?:require|import)\s*\(\s*["']([^"']+)["']/g;
|
|
534
|
+
for (const m of text.matchAll(destructured)) {
|
|
535
|
+
if (m[2].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
|
|
536
|
+
for (const part of m[1].split(",")) {
|
|
537
|
+
const as = part.trim().match(/^([\w$]+)\s*:\s*([\w$]+)$/);
|
|
538
|
+
if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
var escapeRe = (x) => x.replace(/[$]/g, "\\$&");
|
|
544
|
+
function aliasMatchers(file) {
|
|
545
|
+
const out = [];
|
|
546
|
+
for (const [orig, alias] of renamedPrimitives(file)) {
|
|
547
|
+
for (const mt of MATCHERS) {
|
|
548
|
+
if (!mt.re.source.includes(orig)) continue;
|
|
549
|
+
const re = new RegExp(mt.re.source.split(orig).join(escapeRe(alias)), mt.re.flags);
|
|
550
|
+
const { accept } = mt;
|
|
551
|
+
out.push({
|
|
552
|
+
...mt,
|
|
553
|
+
re,
|
|
554
|
+
// The predicate reads the matched text; give it the original callee name.
|
|
555
|
+
accept: accept ? (m, ctx) => {
|
|
556
|
+
const fake = Object.assign(
|
|
557
|
+
[...m].map((x) => x?.replace(alias, orig)),
|
|
558
|
+
{
|
|
559
|
+
index: m.index,
|
|
560
|
+
input: m.input
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
return accept(fake, ctx);
|
|
564
|
+
} : void 0
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
var MATCHERS = [
|
|
571
|
+
{
|
|
572
|
+
id: "local-jwt-verify",
|
|
573
|
+
category: "resolves-caller",
|
|
574
|
+
re: /\b(jwtVerify|jwtDecrypt|createRemoteJWKSet|createLocalJWKSet|decodeJwt|compactVerify|verifyToken)\s*\(/,
|
|
575
|
+
callee: 1,
|
|
576
|
+
sample: "const { payload } = await jwtVerify(token, jwks);"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: "local-jwt-verify",
|
|
580
|
+
category: "resolves-caller",
|
|
581
|
+
re: /\b(jwt|jsonwebtoken)\.(verify|decode)\s*\(/,
|
|
582
|
+
sample: "const claims = jwt.verify(token, key);"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: "raw-claim-read",
|
|
586
|
+
category: "resolves-caller",
|
|
587
|
+
re: /\b(payload|claims|decoded)\??\.(org_id|org_slug|org_role|orgId|orgSlug|orgRole)\b/,
|
|
588
|
+
sample: "const orgId = payload.org_id as string;"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
// A populated grant attached locally: a non-empty literal, or a value read
|
|
592
|
+
// off another object (a mapping row). A pass-through copy of the same
|
|
593
|
+
// field (`fromAllowList: ctx.fromAllowList`) grants nothing new.
|
|
594
|
+
id: "local-grant",
|
|
595
|
+
category: "resolves-caller",
|
|
596
|
+
re: /\bfromAllowList\s*:\s*(\[\s*[^\]\s][^\]]*\]|[\w$]+(?:\??\.[\w$]+)+)/,
|
|
597
|
+
accept: (m) => !/\.fromAllowList$/.test(m[1]),
|
|
598
|
+
sample: 'fromAllowList: ["*"],'
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
// A role compared to a value. A presence test (`orgRole !== undefined`,
|
|
602
|
+
// forwarding the claim on) asserts nothing and is not a site.
|
|
603
|
+
id: "org-role-assert",
|
|
604
|
+
category: "asserts-membership",
|
|
605
|
+
re: /\b(orgRole|org_role)\s*[!=]==\s*([^\s);&|?:]+)/,
|
|
606
|
+
accept: (m) => !NON_OPERAND.has(m[2]),
|
|
607
|
+
sample: 'if (orgRole === "org:admin") return true;'
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: "org-role-assert",
|
|
611
|
+
category: "asserts-membership",
|
|
612
|
+
re: /[!=]==\s*["'`]org:|\.has\(\s*\{\s*(role|permission)\s*:/,
|
|
613
|
+
sample: 'if (auth.has({ role: "org:admin" })) return true;'
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
// Identifying an agent by NAME: a typed name compared to a roster or an
|
|
617
|
+
// allow-list, by method or by iteration.
|
|
618
|
+
id: "name-allowlist",
|
|
619
|
+
category: "filters-by-scope",
|
|
620
|
+
re: /\b[\w$]*(roster|allow_?list|allowed_?orchestrators)\s*\??\.\s*(includes|indexOf|some|find|findIndex|every|has)\s*\(/i,
|
|
621
|
+
sample: "if (ctx.fromAllowList.includes(from)) return null;"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "name-allowlist",
|
|
625
|
+
category: "filters-by-scope",
|
|
626
|
+
re: /\b(isIn\w*Allow_?list)\s*\(/i,
|
|
627
|
+
callee: 1,
|
|
628
|
+
sample: "if (!isInAllowList(list, name)) return false;"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: "name-allowlist",
|
|
632
|
+
category: "filters-by-scope",
|
|
633
|
+
re: /\bof\s+[\w$.?]*(roster|allow_?list|allowed_?orchestrators)\s*\)/i,
|
|
634
|
+
sample: "for (const entry of ctx.fromAllowList) {"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
// A row kept or dropped by comparing its tenant to the caller's: ===, !==, == or !=.
|
|
638
|
+
id: "tenant-row-filter",
|
|
639
|
+
category: "filters-by-scope",
|
|
640
|
+
re: new RegExp(CMP),
|
|
641
|
+
accept: acceptWith(comparesTenants),
|
|
642
|
+
sample: "return row.orgId == ctx.actor.orgSlug;"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "tenant-row-filter",
|
|
646
|
+
category: "filters-by-scope",
|
|
647
|
+
re: new RegExp(OBJECT_IS),
|
|
648
|
+
accept: acceptWith(comparesTenants),
|
|
649
|
+
sample: "return Object.is(row.orgId, ctx.actor.orgSlug);"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
// A tenant passed to a membership test: `callerOrgs.includes(row.orgId)`.
|
|
653
|
+
id: "tenant-row-filter",
|
|
654
|
+
category: "filters-by-scope",
|
|
655
|
+
re: new RegExp(
|
|
656
|
+
String.raw`\.(includes|has|indexOf|some|find|findIndex)\s*\(\s*(${OPERAND})\s*[,)]`
|
|
657
|
+
),
|
|
658
|
+
accept: (m) => TENANT_PROP.test(unfold(m[2])),
|
|
659
|
+
sample: "return callerOrgs.includes(row.orgId);"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
// Identifying an agent by NAME through a normaliser: either side of a strict
|
|
663
|
+
// comparison is a call to `normalize...Id/Name/Orchestrator/Agent/...`.
|
|
664
|
+
id: "name-compare",
|
|
665
|
+
category: "filters-by-scope",
|
|
666
|
+
re: /\bnormali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*[!=]==|[!=]==\s*normali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\(/i,
|
|
667
|
+
sample: "return normalizeOrchestratorId(claimedName) === only;"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
// A typed or argument name compared straight to an allow-list / roster entry.
|
|
671
|
+
id: "name-compare",
|
|
672
|
+
category: "filters-by-scope",
|
|
673
|
+
re: /(?:\b[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[[^\]]*\]\s*[!=]==|[!=]==\s*[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[)/i,
|
|
674
|
+
sample: "return claimedName === ctx.fromAllowList[0];"
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
// Identifying an agent by NAME through its acting-name fields: who made a row
|
|
678
|
+
// against who is asking. ===, !==, == and !=, case-folded or not.
|
|
679
|
+
id: "name-compare",
|
|
680
|
+
category: "filters-by-scope",
|
|
681
|
+
re: new RegExp(CMP),
|
|
682
|
+
accept: acceptWith(comparesNames),
|
|
683
|
+
sample: "return row.assignedTo.toLowerCase() !== a.callerOrchestrator.toLowerCase();"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "name-compare",
|
|
687
|
+
category: "filters-by-scope",
|
|
688
|
+
re: new RegExp(OBJECT_IS),
|
|
689
|
+
accept: acceptWith(comparesNames),
|
|
690
|
+
sample: "return Object.is(row.createdBy, m.from);"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
// `switch (row.createdBy) { case caller: }` is the same compare, spelled as a jump table.
|
|
694
|
+
id: "name-compare",
|
|
695
|
+
category: "filters-by-scope",
|
|
696
|
+
re: new RegExp(String.raw`\bswitch\s*\(\s*(${OPERAND})\s*\)`),
|
|
697
|
+
accept: (m, ctx) => switchComparesIdentity(m, ctx),
|
|
698
|
+
sample: "switch (row.createdBy) { case caller: return true; }"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
// `[caller].includes(row.createdBy)`: a membership test against a literal that holds the caller.
|
|
702
|
+
id: "name-compare",
|
|
703
|
+
category: "filters-by-scope",
|
|
704
|
+
re: new RegExp(
|
|
705
|
+
String.raw`(?<![\w$\])])\[([^\[\]]*)\]\s*\.\s*(?:includes|indexOf)\s*\(\s*(${OPERAND})\s*[,)]`
|
|
706
|
+
),
|
|
707
|
+
accept: (m) => literalHoldsIdentity(m),
|
|
708
|
+
sample: "return [caller].includes(row.createdBy);"
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: "name-compare",
|
|
712
|
+
category: "filters-by-scope",
|
|
713
|
+
re: new RegExp(
|
|
714
|
+
String.raw`\bnew\s+Set\s*\(\s*\[([^\[\]]*)\]\s*\)\s*\.\s*has\s*\(\s*(${OPERAND})\s*[,)]`
|
|
715
|
+
),
|
|
716
|
+
accept: (m) => literalHoldsIdentity(m),
|
|
717
|
+
sample: "return new Set([caller]).has(row.assignedTo);"
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
// `a.localeCompare(b) === 0` is an equality test, whichever way round it is written.
|
|
721
|
+
id: "name-compare",
|
|
722
|
+
category: "filters-by-scope",
|
|
723
|
+
re: new RegExp(
|
|
724
|
+
String.raw`(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*(?:,[^()]*(?:\([^()]*\)[^()]*)*)?\)\s*[!=]==?\s*0(?![\w$.])`
|
|
725
|
+
),
|
|
726
|
+
accept: acceptWith(comparesIdentity),
|
|
727
|
+
sample: "return row.assignedTo.localeCompare(a.callerOrchestrator) === 0;"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
id: "name-compare",
|
|
731
|
+
category: "filters-by-scope",
|
|
732
|
+
re: new RegExp(
|
|
733
|
+
String.raw`(?<![\w$.])0\s*[!=]==?\s*(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*[,)]`
|
|
734
|
+
),
|
|
735
|
+
accept: acceptWith(comparesIdentity),
|
|
736
|
+
sample: "return 0 === row.assignedTo.localeCompare(a.callerOrchestrator);"
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "local-secret-compare",
|
|
740
|
+
category: "validates-credential",
|
|
741
|
+
re: /\b(timingSafeEqual)\s*\(/,
|
|
742
|
+
callee: 1,
|
|
743
|
+
sample: "return timingSafeEqual(a, b);"
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
// Any digest of a presented credential, whatever the algorithm.
|
|
747
|
+
id: "local-digest",
|
|
748
|
+
category: "validates-credential",
|
|
749
|
+
// A digest over static content (a literal, or a constant made of literals) is
|
|
750
|
+
// not a digest of a credential. HMAC and sign/verify always take a key: a site.
|
|
751
|
+
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
752
|
+
accept: (m, ctx) => !digestsStaticInput(m, ctx),
|
|
753
|
+
sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
|
|
754
|
+
}
|
|
755
|
+
];
|
|
756
|
+
function biteSample(mt, text) {
|
|
757
|
+
const m = text.match(mt.re);
|
|
758
|
+
if (m === null) return false;
|
|
759
|
+
if (!mt.accept) return true;
|
|
760
|
+
const line = { n: 1, raw: text, comment: false };
|
|
761
|
+
const file = { path: "sample", rel: "sample", lines: [line] };
|
|
762
|
+
return mt.accept(m, { file, line, idx: m.index ?? 0, statics: /* @__PURE__ */ new Set() });
|
|
763
|
+
}
|
|
764
|
+
function localIdentityPositiveControls() {
|
|
765
|
+
const out = {};
|
|
766
|
+
MATCHERS.forEach((mt, i) => {
|
|
767
|
+
out[`${mt.id}#${i}`] = biteSample(mt, mt.sample);
|
|
768
|
+
});
|
|
769
|
+
const bites = (line) => MATCHERS.some((mt) => biteSample(mt, line));
|
|
770
|
+
out["negative:pass-through-grant"] = !bites("fromAllowList: ctx.fromAllowList,");
|
|
771
|
+
out["negative:delegated-bearer"] = !MATCHERS.some(
|
|
772
|
+
(mt) => mt.re.test("const r = await validateMasterBearer(authHeader, secret);")
|
|
773
|
+
);
|
|
774
|
+
out["negative:role-presence-test"] = !bites(
|
|
775
|
+
"...(ctx.orgRole !== undefined ? { role: ctx.orgRole } : {}),"
|
|
776
|
+
);
|
|
777
|
+
out["negative:undefined-compare"] = !bites("if (row.orgId === undefined) return;");
|
|
778
|
+
out["negative:acting-field-vs-nullish"] = !bites("if (row.assignedTo !== undefined) return;");
|
|
779
|
+
out["negative:acting-field-vs-literal"] = !bites('return t.createdBy === "system";');
|
|
780
|
+
out["negative:unrelated-field-compare"] = !bites("return page.from === page.to;");
|
|
781
|
+
out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
|
|
782
|
+
out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
|
|
783
|
+
out["negative:localecompare-unrelated"] = !bites("return a.name.localeCompare(b.name) === 0;");
|
|
784
|
+
out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
|
|
785
|
+
return out;
|
|
786
|
+
}
|
|
787
|
+
function cloudIdentityBindings(file) {
|
|
788
|
+
const text = file.lines.map((l) => l.raw).join("\n");
|
|
789
|
+
const names = /* @__PURE__ */ new Set();
|
|
790
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
791
|
+
const pkg = `@vantageos\\/cloud-identity(?:\\/[^"']*)?`;
|
|
792
|
+
const named = new RegExp(`import\\s+(type\\s+)?\\{([^}]*)\\}\\s*from\\s*["']${pkg}["']`, "g");
|
|
793
|
+
for (const m of text.matchAll(named)) {
|
|
794
|
+
if (m[1]) continue;
|
|
795
|
+
for (const part of m[2].split(",")) {
|
|
796
|
+
const p = part.trim();
|
|
797
|
+
if (!p || p.startsWith("type ")) continue;
|
|
798
|
+
const as = p.match(/^[\w$]+\s+as\s+([\w$]+)$/);
|
|
799
|
+
names.add(as ? as[1] : p);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const ns = new RegExp(`import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s*["']${pkg}["']`, "g");
|
|
803
|
+
for (const m of text.matchAll(ns)) namespaces.add(m[1]);
|
|
804
|
+
return { names, namespaces };
|
|
805
|
+
}
|
|
806
|
+
function isDelegated(raw, start, callee, b) {
|
|
807
|
+
const before = raw.slice(0, start);
|
|
808
|
+
const qualified = before.match(/([\w$]+)\s*\.\s*$/);
|
|
809
|
+
if (qualified) return b.namespaces.has(qualified[1]);
|
|
810
|
+
return b.names.has(callee);
|
|
811
|
+
}
|
|
812
|
+
function countDelegatedCalls(file, b) {
|
|
813
|
+
if (b.names.size === 0 && b.namespaces.size === 0) return 0;
|
|
814
|
+
let n = 0;
|
|
815
|
+
for (const line of file.lines) {
|
|
816
|
+
if (line.comment) continue;
|
|
817
|
+
for (const m of line.raw.matchAll(/([\w$]+)\s*\(/g)) {
|
|
818
|
+
const idx = m.index ?? 0;
|
|
819
|
+
if (isCodeAt(file, line.n, idx) && isDelegated(line.raw, idx, m[1], b)) n++;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return n;
|
|
823
|
+
}
|
|
824
|
+
var DECL = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let|var|class)\s+([\w$]+))/;
|
|
825
|
+
function enclosingDecl(file, n) {
|
|
826
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
827
|
+
const l = file.lines[i];
|
|
828
|
+
if (l.comment) continue;
|
|
829
|
+
const m = l.raw.match(DECL);
|
|
830
|
+
if (m) return { name: m[1] ?? m[2], line: l.n };
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
function justificationIn(line, file) {
|
|
835
|
+
const at = line.raw.indexOf(EXEMPT_MARKER);
|
|
836
|
+
if (at < 0) return null;
|
|
837
|
+
if (!line.comment && isCodeAt(file, line.n, at)) return null;
|
|
838
|
+
if (!line.comment && !/\/\/|\/\*/.test(line.raw.slice(0, at))) return null;
|
|
839
|
+
return line.raw.slice(at + EXEMPT_MARKER.length).replace(/\*\/\s*$/, "").trim();
|
|
840
|
+
}
|
|
841
|
+
function commentBlockAbove(file, n) {
|
|
842
|
+
const out = [];
|
|
843
|
+
for (let i = n - 2; i >= 0; i--) {
|
|
844
|
+
const l = file.lines[i];
|
|
845
|
+
if (!l.comment) break;
|
|
846
|
+
out.push(l);
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
function findJustification(file, line, decl) {
|
|
851
|
+
const candidates = [line, ...commentBlockAbove(file, line.n)];
|
|
852
|
+
if (decl) candidates.push(...commentBlockAbove(file, decl.line));
|
|
853
|
+
for (const c of candidates) {
|
|
854
|
+
const j = justificationIn(c, file);
|
|
855
|
+
if (j !== null) return { text: j, valid: j.length >= MIN_JUSTIFICATION };
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
function unreadableReason(file) {
|
|
860
|
+
if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
|
|
861
|
+
return "file is not valid UTF-8 text";
|
|
862
|
+
}
|
|
863
|
+
const pairs = [
|
|
864
|
+
["{", "}"],
|
|
865
|
+
["(", ")"],
|
|
866
|
+
["[", "]"]
|
|
867
|
+
];
|
|
868
|
+
const off = [];
|
|
869
|
+
for (const [open, close] of pairs) {
|
|
870
|
+
let net = 0;
|
|
871
|
+
for (const l of file.lines) net += countInCode(file, l, open) - countInCode(file, l, close);
|
|
872
|
+
if (net !== 0) off.push(`${open}${close} ${net > 0 ? "+" : ""}${net}`);
|
|
873
|
+
}
|
|
874
|
+
return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
|
|
875
|
+
}
|
|
876
|
+
function findLocalIdentitySites(tree) {
|
|
877
|
+
const pkg = tree.packageJson;
|
|
878
|
+
const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
|
|
879
|
+
const scan = {
|
|
880
|
+
selfExempt,
|
|
881
|
+
violations: [],
|
|
882
|
+
exempt: [],
|
|
883
|
+
delegatedCalls: 0,
|
|
884
|
+
unreadable: []
|
|
885
|
+
};
|
|
886
|
+
for (const file of tree.files) {
|
|
887
|
+
const bad = unreadableReason(file);
|
|
888
|
+
if (bad) scan.unreadable.push({ anchor: file.rel, reason: bad });
|
|
889
|
+
const bindings = cloudIdentityBindings(file);
|
|
890
|
+
const statics = staticConstants(file);
|
|
891
|
+
const catalog = [...MATCHERS, ...aliasMatchers(file)];
|
|
892
|
+
scan.delegatedCalls += countDelegatedCalls(file, bindings);
|
|
893
|
+
for (const line of file.lines) {
|
|
894
|
+
if (line.comment) continue;
|
|
895
|
+
let hit = null;
|
|
896
|
+
for (const mt of catalog) {
|
|
897
|
+
const g = new RegExp(mt.re.source, `${mt.re.flags.replace("g", "")}g`);
|
|
898
|
+
for (const m of line.raw.matchAll(g)) {
|
|
899
|
+
const idx = m.index ?? 0;
|
|
900
|
+
if (!isCodeAt(file, line.n, idx)) continue;
|
|
901
|
+
if (mt.accept && !mt.accept(m, { file, line, idx, statics })) continue;
|
|
902
|
+
const callee = mt.callee !== void 0 ? m[mt.callee] : void 0;
|
|
903
|
+
if (callee && isDelegated(line.raw, idx, callee, bindings)) continue;
|
|
904
|
+
hit = mt;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
if (hit) break;
|
|
908
|
+
}
|
|
909
|
+
if (!hit) continue;
|
|
910
|
+
const decl = enclosingDecl(file, line.n);
|
|
911
|
+
const site = {
|
|
912
|
+
file: file.rel,
|
|
913
|
+
line: line.n,
|
|
914
|
+
primitive: hit.id,
|
|
915
|
+
category: hit.category,
|
|
916
|
+
enclosing: decl
|
|
917
|
+
};
|
|
918
|
+
const j = findJustification(file, line, decl);
|
|
919
|
+
if (j?.valid) scan.exempt.push({ ...site, justification: j.text });
|
|
920
|
+
else scan.violations.push(site);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return scan;
|
|
924
|
+
}
|
|
925
|
+
function describeSite(s) {
|
|
926
|
+
const where = s.enclosing ? ` in ${s.enclosing.name} (declared :${s.enclosing.line})` : "";
|
|
927
|
+
return `${s.file}:${s.line} [${s.category}/${s.primitive}]${where}`;
|
|
928
|
+
}
|
|
929
|
+
function exemptSuffix(exempt) {
|
|
930
|
+
if (exempt.length === 0) return "";
|
|
931
|
+
const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
|
|
932
|
+
return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
|
|
933
|
+
}
|
|
934
|
+
function makeLocalIdentity(opts = {}) {
|
|
935
|
+
const ceiling = opts.ceiling ?? 0;
|
|
936
|
+
if (!Number.isInteger(ceiling) || ceiling < 0) {
|
|
937
|
+
throw new Error(`local-identity ceiling must be a non-negative integer, got ${ceiling}`);
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
id: "DLocalIdentity no-local-identity",
|
|
941
|
+
run(tree) {
|
|
942
|
+
const controls = localIdentityPositiveControls();
|
|
943
|
+
const failed = Object.entries(controls).filter(([, ok]) => !ok);
|
|
944
|
+
if (failed.length > 0) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`DLocalIdentity positive control FAILED: ${failed.map(([k]) => k).join(", ")} \u2014 matcher broken, verdict untrustworthy`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
const id = this.id;
|
|
950
|
+
const s = findLocalIdentitySites(tree);
|
|
951
|
+
if (s.selfExempt) {
|
|
952
|
+
const n = s.violations.length + s.exempt.length;
|
|
953
|
+
return {
|
|
954
|
+
id,
|
|
955
|
+
anchor: tree.packageJson?.path ?? "-",
|
|
956
|
+
state: "present",
|
|
957
|
+
verdict: `EXEMPT: the target is ${CLOUD_IDENTITY_PACKAGE} itself, the one place identity primitives live (${n} primitive site(s) not counted)`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const listed = s.violations.map(describeSite).join("; ");
|
|
961
|
+
const delegated = `${s.delegatedCalls} call(s) to ${CLOUD_IDENTITY_PACKAGE}`;
|
|
962
|
+
let decided;
|
|
963
|
+
if (s.violations.length > ceiling) {
|
|
964
|
+
const v = s.violations[0];
|
|
965
|
+
decided = {
|
|
966
|
+
id,
|
|
967
|
+
anchor: `${v.file}:${v.line}`,
|
|
968
|
+
state: "non-conforming",
|
|
969
|
+
verdict: `${s.violations.length} local identity primitive(s), ceiling ${ceiling} (positive-control OK): ${listed}${exemptSuffix(s.exempt)} -> delegate to ${CLOUD_IDENTITY_PACKAGE} (validateMasterBearer, verifyClerkSessionToken, isMasterScope, ...) or justify the site with "// ${EXEMPT_MARKER} <reason>"`
|
|
970
|
+
};
|
|
971
|
+
} else if (s.violations.length > 0) {
|
|
972
|
+
const v = s.violations[0];
|
|
973
|
+
decided = {
|
|
974
|
+
id,
|
|
975
|
+
anchor: `${v.file}:${v.line}`,
|
|
976
|
+
state: "present",
|
|
977
|
+
verdict: `${s.violations.length} local identity primitive(s) at or under the ceiling ${ceiling} \u2014 a ratchet, not a clean bill: ${listed}${exemptSuffix(s.exempt)}`
|
|
978
|
+
};
|
|
979
|
+
} else {
|
|
980
|
+
decided = {
|
|
981
|
+
id,
|
|
982
|
+
anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
|
|
983
|
+
state: "present",
|
|
984
|
+
verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return settle(id, decided, s.unreadable);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
var localIdentity = makeLocalIdentity();
|
|
992
|
+
|
|
8
993
|
// src/detectors/c7-artifact-verifiable.ts
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
994
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
995
|
+
import { join as join2 } from "path";
|
|
11
996
|
var PIN = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
12
997
|
var DOC_FILES = ["README.md", "README.fr.md"];
|
|
13
998
|
var INSTALL_RE = /\b(?:npm\s+(?:install|i)|npx|pnpm\s+add|yarn\s+add)\s+(@?[\w./-]+)/i;
|
|
@@ -46,10 +1031,10 @@ function c7PositiveControls() {
|
|
|
46
1031
|
}
|
|
47
1032
|
function readDocText(root) {
|
|
48
1033
|
for (const name of DOC_FILES) {
|
|
49
|
-
const path =
|
|
50
|
-
if (!
|
|
1034
|
+
const path = join2(root, name);
|
|
1035
|
+
if (!existsSync2(path)) continue;
|
|
51
1036
|
try {
|
|
52
|
-
return { text:
|
|
1037
|
+
return { text: readFileSync3(path, "utf8"), file: name };
|
|
53
1038
|
} catch {
|
|
54
1039
|
}
|
|
55
1040
|
}
|
|
@@ -1177,59 +2162,6 @@ var c13ContextCeiling = {
|
|
|
1177
2162
|
}
|
|
1178
2163
|
};
|
|
1179
2164
|
|
|
1180
|
-
// src/detectors/couldNotJudge.ts
|
|
1181
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
1182
|
-
function couldNotJudge(id, anchor, reason) {
|
|
1183
|
-
return {
|
|
1184
|
-
id,
|
|
1185
|
-
anchor,
|
|
1186
|
-
state: "could-not-judge",
|
|
1187
|
-
reason,
|
|
1188
|
-
verdict: `could not judge: ${reason}`
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
function settle(id, decided, unreadable) {
|
|
1192
|
-
if (unreadable.length === 0) return decided;
|
|
1193
|
-
const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
|
|
1194
|
-
if (decided.state === "non-conforming") {
|
|
1195
|
-
return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
|
|
1196
|
-
}
|
|
1197
|
-
const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
|
|
1198
|
-
return couldNotJudge(
|
|
1199
|
-
id,
|
|
1200
|
-
unreadable[0].anchor,
|
|
1201
|
-
`${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
function readJsonObject(path) {
|
|
1205
|
-
let value;
|
|
1206
|
-
try {
|
|
1207
|
-
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
1208
|
-
} catch {
|
|
1209
|
-
return null;
|
|
1210
|
-
}
|
|
1211
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1212
|
-
return value;
|
|
1213
|
-
}
|
|
1214
|
-
function guardUnreadablePackageJson(inner, opts) {
|
|
1215
|
-
return {
|
|
1216
|
-
...inner,
|
|
1217
|
-
run(tree) {
|
|
1218
|
-
const decided = inner.run(tree);
|
|
1219
|
-
const pkg = tree.packageJson;
|
|
1220
|
-
if (!pkg?.unreadable) return decided;
|
|
1221
|
-
const u = {
|
|
1222
|
-
anchor: pkg.path,
|
|
1223
|
-
reason: `package.json is present but could not be parsed (${pkg.unreadable})`
|
|
1224
|
-
};
|
|
1225
|
-
if (opts.authoritative && decided.state !== "could-not-judge") {
|
|
1226
|
-
return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
|
|
1227
|
-
}
|
|
1228
|
-
return settle(inner.id, decided, [u]);
|
|
1229
|
-
}
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
2165
|
// src/detectors/c14-catalog-registration.ts
|
|
1234
2166
|
var PIN3 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1235
2167
|
var REQUIRED_FIELDS = [
|
|
@@ -1514,11 +2446,11 @@ var c16UntrustedOutput = {
|
|
|
1514
2446
|
};
|
|
1515
2447
|
|
|
1516
2448
|
// src/detectors/c17-supply-chain.ts
|
|
1517
|
-
import { existsSync as
|
|
1518
|
-
import { join as
|
|
2449
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2450
|
+
import { join as join3 } from "path";
|
|
1519
2451
|
var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1520
2452
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
1521
|
-
var WORKFLOW_DIR =
|
|
2453
|
+
var WORKFLOW_DIR = join3(".github", "workflows");
|
|
1522
2454
|
var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
|
|
1523
2455
|
function c17PositiveControls() {
|
|
1524
2456
|
const sample = "npm publish --provenance --access public";
|
|
@@ -1534,7 +2466,7 @@ function c17PositiveControls() {
|
|
|
1534
2466
|
}
|
|
1535
2467
|
function findLockfile(root) {
|
|
1536
2468
|
for (const name of LOCKFILES) {
|
|
1537
|
-
if (
|
|
2469
|
+
if (existsSync3(join3(root, name))) return name;
|
|
1538
2470
|
}
|
|
1539
2471
|
return null;
|
|
1540
2472
|
}
|
|
@@ -1546,20 +2478,20 @@ function findProvenanceClaim(tree, root) {
|
|
|
1546
2478
|
return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
|
|
1547
2479
|
}
|
|
1548
2480
|
}
|
|
1549
|
-
const workflowsDir =
|
|
1550
|
-
if (!
|
|
2481
|
+
const workflowsDir = join3(root, WORKFLOW_DIR);
|
|
2482
|
+
if (!existsSync3(workflowsDir)) return null;
|
|
1551
2483
|
let entries = [];
|
|
1552
2484
|
try {
|
|
1553
|
-
entries =
|
|
2485
|
+
entries = readdirSync2(workflowsDir);
|
|
1554
2486
|
} catch {
|
|
1555
2487
|
return null;
|
|
1556
2488
|
}
|
|
1557
2489
|
for (const name of entries) {
|
|
1558
2490
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
1559
|
-
const path =
|
|
2491
|
+
const path = join3(workflowsDir, name);
|
|
1560
2492
|
let text;
|
|
1561
2493
|
try {
|
|
1562
|
-
text =
|
|
2494
|
+
text = readFileSync4(path, "utf8");
|
|
1563
2495
|
} catch {
|
|
1564
2496
|
continue;
|
|
1565
2497
|
}
|
|
@@ -1612,17 +2544,17 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
|
|
|
1612
2544
|
});
|
|
1613
2545
|
|
|
1614
2546
|
// src/detectors/c18-structure-manifest.ts
|
|
1615
|
-
import { existsSync as
|
|
1616
|
-
import { join as
|
|
2547
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2548
|
+
import { join as join4 } from "path";
|
|
1617
2549
|
var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1618
2550
|
function manifestPath(root) {
|
|
1619
|
-
const extPath =
|
|
1620
|
-
if (
|
|
1621
|
-
return { kind: "server", path:
|
|
2551
|
+
const extPath = join4(root, "extension.json");
|
|
2552
|
+
if (existsSync4(extPath)) return { kind: "extension", path: extPath };
|
|
2553
|
+
return { kind: "server", path: join4(root, "mcp.json") };
|
|
1622
2554
|
}
|
|
1623
2555
|
function readJson(path) {
|
|
1624
2556
|
try {
|
|
1625
|
-
return JSON.parse(
|
|
2557
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1626
2558
|
} catch {
|
|
1627
2559
|
return null;
|
|
1628
2560
|
}
|
|
@@ -1670,7 +2602,7 @@ var c18StructureManifestInner = {
|
|
|
1670
2602
|
}
|
|
1671
2603
|
const { kind, path } = manifestPath(tree.root);
|
|
1672
2604
|
const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
1673
|
-
if (!
|
|
2605
|
+
if (!existsSync4(path)) {
|
|
1674
2606
|
return {
|
|
1675
2607
|
id: this.id,
|
|
1676
2608
|
anchor: tree.root,
|
|
@@ -1753,89 +2685,6 @@ var c18StructureManifest = guardUnreadablePackageJson(
|
|
|
1753
2685
|
{ authoritative: false }
|
|
1754
2686
|
);
|
|
1755
2687
|
|
|
1756
|
-
// src/scan.ts
|
|
1757
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync } from "fs";
|
|
1758
|
-
import { dirname, join as join4, relative } from "path";
|
|
1759
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1760
|
-
"node_modules",
|
|
1761
|
-
"dist",
|
|
1762
|
-
"build",
|
|
1763
|
-
".git",
|
|
1764
|
-
"tests",
|
|
1765
|
-
"__tests__",
|
|
1766
|
-
"coverage",
|
|
1767
|
-
// Test material, not a target's served surface: deliberately-malformed
|
|
1768
|
-
// servers and foreign copies live here. Scanning them makes the instrument
|
|
1769
|
-
// trip its own fail-closed detectors when pointed at its own tree, and lints
|
|
1770
|
-
// inputs that are meant to be broken. A real target's fixtures are equally
|
|
1771
|
-
// test inputs, not its product surface.
|
|
1772
|
-
"fixtures"
|
|
1773
|
-
]);
|
|
1774
|
-
function isCommentLine(raw) {
|
|
1775
|
-
const t = raw.trim();
|
|
1776
|
-
if (t === "") return false;
|
|
1777
|
-
return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
|
|
1778
|
-
}
|
|
1779
|
-
function walk(dir, out) {
|
|
1780
|
-
let entries;
|
|
1781
|
-
try {
|
|
1782
|
-
entries = readdirSync2(dir);
|
|
1783
|
-
} catch {
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
for (const name of entries) {
|
|
1787
|
-
const full = join4(dir, name);
|
|
1788
|
-
let st;
|
|
1789
|
-
try {
|
|
1790
|
-
st = statSync(full);
|
|
1791
|
-
} catch {
|
|
1792
|
-
continue;
|
|
1793
|
-
}
|
|
1794
|
-
if (st.isDirectory()) {
|
|
1795
|
-
if (SKIP_DIRS.has(name)) continue;
|
|
1796
|
-
walk(full, out);
|
|
1797
|
-
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
1798
|
-
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
1799
|
-
out.push(full);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
function loadFile(path, root) {
|
|
1804
|
-
const raw = readFileSync5(path, "utf8");
|
|
1805
|
-
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
1806
|
-
return { path, rel: relative(root, path) || path, lines };
|
|
1807
|
-
}
|
|
1808
|
-
function findPackageJson(startDir) {
|
|
1809
|
-
let dir = startDir;
|
|
1810
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
1811
|
-
const candidate = join4(dir, "package.json");
|
|
1812
|
-
if (existsSync4(candidate)) {
|
|
1813
|
-
try {
|
|
1814
|
-
const parsed = JSON.parse(readFileSync5(candidate, "utf8"));
|
|
1815
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1816
|
-
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
1817
|
-
}
|
|
1818
|
-
return { path: candidate, json: parsed };
|
|
1819
|
-
} catch (err) {
|
|
1820
|
-
return { path: candidate, json: {}, unreadable: err.message };
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
const parent = dirname(dir);
|
|
1824
|
-
if (parent === dir) break;
|
|
1825
|
-
dir = parent;
|
|
1826
|
-
}
|
|
1827
|
-
return null;
|
|
1828
|
-
}
|
|
1829
|
-
function loadTree(root) {
|
|
1830
|
-
const st = statSync(root);
|
|
1831
|
-
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
1832
|
-
const filePaths = [];
|
|
1833
|
-
if (st.isDirectory()) walk(root, filePaths);
|
|
1834
|
-
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
1835
|
-
const files = filePaths.map((p) => loadFile(p, baseDir));
|
|
1836
|
-
return { root, files, packageJson: findPackageJson(baseDir) };
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
2688
|
// src/detectors/c19-served-descriptions.ts
|
|
1840
2689
|
var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1841
2690
|
var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
|
|
@@ -2509,7 +3358,7 @@ import {
|
|
|
2509
3358
|
writeFileSync
|
|
2510
3359
|
} from "fs";
|
|
2511
3360
|
import { tmpdir } from "os";
|
|
2512
|
-
import { join as join6, relative as relative2 } from "path";
|
|
3361
|
+
import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
2513
3362
|
var PIN9 = "20136b50";
|
|
2514
3363
|
var DEPLOY_CONFIG_FILES = [
|
|
2515
3364
|
"railway.json",
|
|
@@ -2526,15 +3375,44 @@ var LOCKFILES2 = [
|
|
|
2526
3375
|
{ name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
|
|
2527
3376
|
{ name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
|
|
2528
3377
|
];
|
|
2529
|
-
|
|
2530
|
-
function walkForConfigs(dir, depth, seen) {
|
|
3378
|
+
function walkForConfigs(dir, depth, seen, onSkip) {
|
|
2531
3379
|
if (depth > 8) return;
|
|
2532
3380
|
let entries;
|
|
2533
3381
|
try {
|
|
2534
|
-
entries = readdirSync3(dir);
|
|
3382
|
+
entries = readdirSync3(dir);
|
|
3383
|
+
} catch {
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
for (const name of entries) {
|
|
3387
|
+
const full = join6(dir, name);
|
|
3388
|
+
let st;
|
|
3389
|
+
try {
|
|
3390
|
+
st = statSync2(full);
|
|
3391
|
+
} catch {
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
if (st.isDirectory()) {
|
|
3395
|
+
if (SKIP_DIRS.has(name)) {
|
|
3396
|
+
onSkip(full, depth + 1);
|
|
3397
|
+
continue;
|
|
3398
|
+
}
|
|
3399
|
+
walkForConfigs(full, depth + 1, seen, onSkip);
|
|
3400
|
+
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
3401
|
+
if (!seen.has(dir)) {
|
|
3402
|
+
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
function probeForCandidate(dir, depth) {
|
|
3408
|
+
if (depth > 8) return null;
|
|
3409
|
+
let entries;
|
|
3410
|
+
try {
|
|
3411
|
+
entries = readdirSync3(dir).sort();
|
|
2535
3412
|
} catch {
|
|
2536
|
-
return;
|
|
3413
|
+
return null;
|
|
2537
3414
|
}
|
|
3415
|
+
const subdirs = [];
|
|
2538
3416
|
for (const name of entries) {
|
|
2539
3417
|
const full = join6(dir, name);
|
|
2540
3418
|
let st;
|
|
@@ -2544,14 +3422,18 @@ function walkForConfigs(dir, depth, seen) {
|
|
|
2544
3422
|
continue;
|
|
2545
3423
|
}
|
|
2546
3424
|
if (st.isDirectory()) {
|
|
2547
|
-
|
|
2548
|
-
|
|
3425
|
+
const info = SKIP_DIR_INFO[name];
|
|
3426
|
+
if (info && !info.authored) continue;
|
|
3427
|
+
subdirs.push(full);
|
|
2549
3428
|
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
2550
|
-
|
|
2551
|
-
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
2552
|
-
}
|
|
3429
|
+
return full;
|
|
2553
3430
|
}
|
|
2554
3431
|
}
|
|
3432
|
+
for (const sub of subdirs) {
|
|
3433
|
+
const hit = probeForCandidate(sub, depth + 1);
|
|
3434
|
+
if (hit) return hit;
|
|
3435
|
+
}
|
|
3436
|
+
return null;
|
|
2555
3437
|
}
|
|
2556
3438
|
function resolveRailwayRootDirectory(configPath) {
|
|
2557
3439
|
let text;
|
|
@@ -2574,15 +3456,31 @@ function resolveRailwayRootDirectory(configPath) {
|
|
|
2574
3456
|
const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
|
|
2575
3457
|
return rootDirectory;
|
|
2576
3458
|
}
|
|
2577
|
-
function c26DeriveScope(root) {
|
|
3459
|
+
function c26DeriveScope(root, tree) {
|
|
2578
3460
|
const seen = /* @__PURE__ */ new Map();
|
|
2579
|
-
|
|
3461
|
+
const skipped = [];
|
|
3462
|
+
const onSkip = (skippedDir, depth) => {
|
|
3463
|
+
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
3464
|
+
path: relative2(root, skippedDir).split(sep2).join("/"),
|
|
3465
|
+
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
3466
|
+
};
|
|
3467
|
+
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
3468
|
+
const hit = probeForCandidate(skippedDir, depth);
|
|
3469
|
+
if (hit) entry.candidate = relative2(root, hit).split(sep2).join("/");
|
|
3470
|
+
}
|
|
3471
|
+
if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
|
|
3472
|
+
};
|
|
3473
|
+
walkForConfigs(root, 0, seen, onSkip);
|
|
2580
3474
|
const sites = [];
|
|
2581
3475
|
for (const site of seen.values()) {
|
|
2582
3476
|
if (site.configFile === "railway.json") {
|
|
2583
3477
|
const resolved = resolveRailwayRootDirectory(site.configPath);
|
|
2584
3478
|
if (resolved && typeof resolved === "object" && "error" in resolved) {
|
|
2585
|
-
return {
|
|
3479
|
+
return {
|
|
3480
|
+
sites: [],
|
|
3481
|
+
skipped,
|
|
3482
|
+
unparsable: { path: site.configPath, reason: resolved.error }
|
|
3483
|
+
};
|
|
2586
3484
|
}
|
|
2587
3485
|
if (typeof resolved === "string" && resolved.trim()) {
|
|
2588
3486
|
sites.push({ ...site, dir: join6(site.dir, resolved) });
|
|
@@ -2595,6 +3493,7 @@ function c26DeriveScope(root) {
|
|
|
2595
3493
|
} catch (err) {
|
|
2596
3494
|
return {
|
|
2597
3495
|
sites: [],
|
|
3496
|
+
skipped,
|
|
2598
3497
|
unparsable: {
|
|
2599
3498
|
path: site.configPath,
|
|
2600
3499
|
reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2604,7 +3503,7 @@ function c26DeriveScope(root) {
|
|
|
2604
3503
|
}
|
|
2605
3504
|
sites.push(site);
|
|
2606
3505
|
}
|
|
2607
|
-
return { sites };
|
|
3506
|
+
return { sites, skipped };
|
|
2608
3507
|
}
|
|
2609
3508
|
function findLockfile2(dir) {
|
|
2610
3509
|
for (const l of LOCKFILES2) {
|
|
@@ -2761,7 +3660,18 @@ var c26SubpackageLockfile = {
|
|
|
2761
3660
|
rule: "C-26",
|
|
2762
3661
|
pin: PIN9,
|
|
2763
3662
|
run(tree) {
|
|
2764
|
-
const scope = c26DeriveScope(tree.root);
|
|
3663
|
+
const scope = c26DeriveScope(tree.root, tree);
|
|
3664
|
+
const hidden = scope.skipped.filter((d) => d.candidate !== void 0);
|
|
3665
|
+
const unjudgedHidden = (prefix) => {
|
|
3666
|
+
const reason = `${prefix}deployment configuration found only under skipped director${hidden.length === 1 ? "y" : "ies"}: ${hidden.map((d) => `${d.path} (${d.reason}) -> ${d.candidate}`).join("; ")} \u2014 C-26 does not descend into skipped directories, so these site(s) were not judged`;
|
|
3667
|
+
return {
|
|
3668
|
+
id: this.id,
|
|
3669
|
+
anchor: join6(tree.root, hidden[0].candidate),
|
|
3670
|
+
state: "could-not-judge",
|
|
3671
|
+
reason,
|
|
3672
|
+
verdict: `could not judge: ${reason}`
|
|
3673
|
+
};
|
|
3674
|
+
};
|
|
2765
3675
|
if (scope.unparsable) {
|
|
2766
3676
|
const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
|
|
2767
3677
|
return {
|
|
@@ -2773,6 +3683,7 @@ var c26SubpackageLockfile = {
|
|
|
2773
3683
|
};
|
|
2774
3684
|
}
|
|
2775
3685
|
if (scope.sites.length === 0) {
|
|
3686
|
+
if (hidden.length > 0) return unjudgedHidden("");
|
|
2776
3687
|
return {
|
|
2777
3688
|
id: this.id,
|
|
2778
3689
|
anchor: tree.root,
|
|
@@ -2784,6 +3695,7 @@ var c26SubpackageLockfile = {
|
|
|
2784
3695
|
const inventory = renderCoverage(scope.sites, verdicts);
|
|
2785
3696
|
const analysed = verdicts.filter((v) => v.status === "analysed");
|
|
2786
3697
|
if (analysed.length === 0) {
|
|
3698
|
+
if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
|
|
2787
3699
|
return {
|
|
2788
3700
|
id: this.id,
|
|
2789
3701
|
anchor: tree.root,
|
|
@@ -2800,6 +3712,11 @@ var c26SubpackageLockfile = {
|
|
|
2800
3712
|
verdict: reds.some((r) => r.detail?.includes("no lockfile")) ? `${reds[0].detail} (C-26). Coverage: ${inventory}` : `${reds.map((r) => r.detail).join(" ; ")} (C-26). Coverage: ${inventory}`
|
|
2801
3713
|
};
|
|
2802
3714
|
}
|
|
3715
|
+
if (hidden.length > 0) {
|
|
3716
|
+
return unjudgedHidden(
|
|
3717
|
+
`${analysed.length} analysed site(s) are consistent, but coverage is partial; `
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
2803
3720
|
return {
|
|
2804
3721
|
id: this.id,
|
|
2805
3722
|
anchor: analysed[0].site.dir,
|
|
@@ -4369,24 +5286,12 @@ var dDebugOptIn = {
|
|
|
4369
5286
|
}
|
|
4370
5287
|
};
|
|
4371
5288
|
|
|
4372
|
-
// src/detectors/
|
|
5289
|
+
// src/detectors/extManifests.ts
|
|
4373
5290
|
import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4374
5291
|
import { join as join12 } from "path";
|
|
4375
|
-
|
|
4376
|
-
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
4377
|
-
var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
|
|
4378
|
-
function dExt1PositiveControls() {
|
|
4379
|
-
return {
|
|
4380
|
-
identifier: {
|
|
4381
|
-
pattern: IDENTIFIER_RE2.source,
|
|
4382
|
-
positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
|
|
4383
|
-
negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
|
|
4384
|
-
}
|
|
4385
|
-
};
|
|
4386
|
-
}
|
|
4387
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4388
|
-
function findExtensionManifests(root) {
|
|
5292
|
+
function findExtensionManifests(tree) {
|
|
4389
5293
|
const out = [];
|
|
5294
|
+
const root = tree.root;
|
|
4390
5295
|
let base;
|
|
4391
5296
|
try {
|
|
4392
5297
|
base = statSync5(root).isDirectory() ? root : join12(root, "..");
|
|
@@ -4409,7 +5314,10 @@ function findExtensionManifests(root) {
|
|
|
4409
5314
|
continue;
|
|
4410
5315
|
}
|
|
4411
5316
|
if (st.isDirectory()) {
|
|
4412
|
-
if (
|
|
5317
|
+
if (SKIP_DIRS.has(name)) {
|
|
5318
|
+
recordSkip(tree, full, base);
|
|
5319
|
+
continue;
|
|
5320
|
+
}
|
|
4413
5321
|
walk2(full);
|
|
4414
5322
|
} else if (name === "extension.json") {
|
|
4415
5323
|
out.push(full);
|
|
@@ -4419,20 +5327,34 @@ function findExtensionManifests(root) {
|
|
|
4419
5327
|
walk2(base);
|
|
4420
5328
|
return out;
|
|
4421
5329
|
}
|
|
5330
|
+
|
|
5331
|
+
// src/detectors/dExt1.ts
|
|
5332
|
+
var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
|
|
5333
|
+
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
5334
|
+
var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
|
|
5335
|
+
function dExt1PositiveControls() {
|
|
5336
|
+
return {
|
|
5337
|
+
identifier: {
|
|
5338
|
+
pattern: IDENTIFIER_RE2.source,
|
|
5339
|
+
positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
|
|
5340
|
+
negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
4422
5344
|
var TS_IDENT_LITERAL_RE = /\bidentifier:\s*["'`]([^"'`]*)["'`]/;
|
|
4423
|
-
var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`]/;
|
|
5345
|
+
var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`{]/;
|
|
4424
5346
|
function findTsIdentifiers(tree) {
|
|
4425
5347
|
const sites = [];
|
|
4426
5348
|
const unresolved = [];
|
|
4427
5349
|
for (const file of tree.files) {
|
|
4428
5350
|
for (const line of file.lines) {
|
|
4429
5351
|
if (line.comment) continue;
|
|
4430
|
-
const lit = line
|
|
5352
|
+
const lit = matchInCode(file, line, TS_IDENT_LITERAL_RE);
|
|
4431
5353
|
if (lit) {
|
|
4432
5354
|
sites.push({ value: lit[1], anchor: `${file.rel}:${line.n}` });
|
|
4433
5355
|
continue;
|
|
4434
5356
|
}
|
|
4435
|
-
if (
|
|
5357
|
+
if (testInCode(file, line, TS_IDENT_KEY_RE)) {
|
|
4436
5358
|
unresolved.push(`${file.rel}:${line.n}`);
|
|
4437
5359
|
}
|
|
4438
5360
|
}
|
|
@@ -4450,7 +5372,7 @@ var dExt1 = {
|
|
|
4450
5372
|
}
|
|
4451
5373
|
const unreadable = [];
|
|
4452
5374
|
const decided = (() => {
|
|
4453
|
-
const manifests = findExtensionManifests(tree
|
|
5375
|
+
const manifests = findExtensionManifests(tree);
|
|
4454
5376
|
for (const manifestPath2 of manifests) {
|
|
4455
5377
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4456
5378
|
const parsed = readJsonObject(manifestPath2);
|
|
@@ -4517,8 +5439,6 @@ var dExt1 = {
|
|
|
4517
5439
|
};
|
|
4518
5440
|
|
|
4519
5441
|
// src/detectors/dExt2.ts
|
|
4520
|
-
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
4521
|
-
import { join as join13 } from "path";
|
|
4522
5442
|
var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
|
|
4523
5443
|
var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
|
|
4524
5444
|
var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
|
|
@@ -4533,41 +5453,6 @@ function dExt2PositiveControls() {
|
|
|
4533
5453
|
}
|
|
4534
5454
|
};
|
|
4535
5455
|
}
|
|
4536
|
-
var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4537
|
-
function findExtensionManifests2(root) {
|
|
4538
|
-
const out = [];
|
|
4539
|
-
let base;
|
|
4540
|
-
try {
|
|
4541
|
-
base = statSync6(root).isDirectory() ? root : join13(root, "..");
|
|
4542
|
-
} catch {
|
|
4543
|
-
return out;
|
|
4544
|
-
}
|
|
4545
|
-
const walk2 = (dir) => {
|
|
4546
|
-
let entries;
|
|
4547
|
-
try {
|
|
4548
|
-
entries = readdirSync5(dir);
|
|
4549
|
-
} catch {
|
|
4550
|
-
return;
|
|
4551
|
-
}
|
|
4552
|
-
for (const name of entries) {
|
|
4553
|
-
const full = join13(dir, name);
|
|
4554
|
-
let st;
|
|
4555
|
-
try {
|
|
4556
|
-
st = statSync6(full);
|
|
4557
|
-
} catch {
|
|
4558
|
-
continue;
|
|
4559
|
-
}
|
|
4560
|
-
if (st.isDirectory()) {
|
|
4561
|
-
if (SKIP_DIRS3.has(name)) continue;
|
|
4562
|
-
walk2(full);
|
|
4563
|
-
} else if (name === "extension.json") {
|
|
4564
|
-
out.push(full);
|
|
4565
|
-
}
|
|
4566
|
-
}
|
|
4567
|
-
};
|
|
4568
|
-
walk2(base);
|
|
4569
|
-
return out;
|
|
4570
|
-
}
|
|
4571
5456
|
function findTsFactorySites(tree) {
|
|
4572
5457
|
const sites = [];
|
|
4573
5458
|
for (const file of tree.files) {
|
|
@@ -4575,7 +5460,7 @@ function findTsFactorySites(tree) {
|
|
|
4575
5460
|
for (let i = 0; i < lines.length; i++) {
|
|
4576
5461
|
const line = lines[i];
|
|
4577
5462
|
if (line.comment) continue;
|
|
4578
|
-
if (!
|
|
5463
|
+
if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE)) continue;
|
|
4579
5464
|
const anchor = `${file.rel}:${line.n}`;
|
|
4580
5465
|
let depth = 0;
|
|
4581
5466
|
let started = false;
|
|
@@ -4585,20 +5470,20 @@ function findTsFactorySites(tree) {
|
|
|
4585
5470
|
const bl = lines[j];
|
|
4586
5471
|
if (!bl.comment) {
|
|
4587
5472
|
if (state === "absent") {
|
|
4588
|
-
if (
|
|
5473
|
+
if (testInCode(file, bl, TS_GD_TRUE_RE)) {
|
|
4589
5474
|
state = "true";
|
|
4590
|
-
} else if (
|
|
5475
|
+
} else if (testInCode(file, bl, TS_GD_FALSE_RE)) {
|
|
4591
5476
|
state = "false";
|
|
4592
5477
|
} else {
|
|
4593
|
-
const key = bl
|
|
5478
|
+
const key = matchInCode(file, bl, TS_GD_KEY_RE);
|
|
4594
5479
|
if (key) {
|
|
4595
5480
|
state = "unresolved";
|
|
4596
5481
|
value = key[1];
|
|
4597
5482
|
}
|
|
4598
5483
|
}
|
|
4599
5484
|
}
|
|
4600
|
-
depth += (bl
|
|
4601
|
-
depth -= (bl
|
|
5485
|
+
depth += countInCode(file, bl, "{");
|
|
5486
|
+
depth -= countInCode(file, bl, "}");
|
|
4602
5487
|
if (depth > 0) started = true;
|
|
4603
5488
|
}
|
|
4604
5489
|
if (started && depth <= 0) break;
|
|
@@ -4619,7 +5504,7 @@ var dExt2 = {
|
|
|
4619
5504
|
}
|
|
4620
5505
|
const unreadable = [];
|
|
4621
5506
|
const decided = (() => {
|
|
4622
|
-
const manifests =
|
|
5507
|
+
const manifests = findExtensionManifests(tree);
|
|
4623
5508
|
for (const manifestPath2 of manifests) {
|
|
4624
5509
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4625
5510
|
const parsed = readJsonObject(manifestPath2);
|
|
@@ -4695,8 +5580,6 @@ var dExt2 = {
|
|
|
4695
5580
|
};
|
|
4696
5581
|
|
|
4697
5582
|
// src/detectors/dExt3.ts
|
|
4698
|
-
import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
4699
|
-
import { join as join14 } from "path";
|
|
4700
5583
|
var TYPE_MATCHERS = [
|
|
4701
5584
|
{ type: "middleware", re: /\bmiddleware\b/ },
|
|
4702
5585
|
{ type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
|
|
@@ -4719,41 +5602,6 @@ function dExt3PositiveControls() {
|
|
|
4719
5602
|
}
|
|
4720
5603
|
};
|
|
4721
5604
|
}
|
|
4722
|
-
var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4723
|
-
function findExtensionManifests3(root) {
|
|
4724
|
-
const out = [];
|
|
4725
|
-
let base;
|
|
4726
|
-
try {
|
|
4727
|
-
base = statSync7(root).isDirectory() ? root : join14(root, "..");
|
|
4728
|
-
} catch {
|
|
4729
|
-
return out;
|
|
4730
|
-
}
|
|
4731
|
-
const walk2 = (dir) => {
|
|
4732
|
-
let entries;
|
|
4733
|
-
try {
|
|
4734
|
-
entries = readdirSync6(dir);
|
|
4735
|
-
} catch {
|
|
4736
|
-
return;
|
|
4737
|
-
}
|
|
4738
|
-
for (const name of entries) {
|
|
4739
|
-
const full = join14(dir, name);
|
|
4740
|
-
let st;
|
|
4741
|
-
try {
|
|
4742
|
-
st = statSync7(full);
|
|
4743
|
-
} catch {
|
|
4744
|
-
continue;
|
|
4745
|
-
}
|
|
4746
|
-
if (st.isDirectory()) {
|
|
4747
|
-
if (SKIP_DIRS4.has(name)) continue;
|
|
4748
|
-
walk2(full);
|
|
4749
|
-
} else if (name === "extension.json") {
|
|
4750
|
-
out.push(full);
|
|
4751
|
-
}
|
|
4752
|
-
}
|
|
4753
|
-
};
|
|
4754
|
-
walk2(base);
|
|
4755
|
-
return out;
|
|
4756
|
-
}
|
|
4757
5605
|
var DEFINE_EXTENSION_OPEN_RE2 = /\bdefineExtension\s*\(\s*\{/;
|
|
4758
5606
|
var TS_TYPE_LITERAL_RE = /\btype:\s*["'`]([^"'`]*)["'`]/;
|
|
4759
5607
|
var TS_TYPE_KEY_RE = /\btype:\s*[^\s"'`]/;
|
|
@@ -4764,7 +5612,7 @@ function findTsTypeSites(tree) {
|
|
|
4764
5612
|
for (let i = 0; i < lines.length; i++) {
|
|
4765
5613
|
const line = lines[i];
|
|
4766
5614
|
if (line.comment) continue;
|
|
4767
|
-
if (!
|
|
5615
|
+
if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE2)) continue;
|
|
4768
5616
|
const anchor = `${file.rel}:${line.n}`;
|
|
4769
5617
|
let depth = 0;
|
|
4770
5618
|
let started = false;
|
|
@@ -4775,17 +5623,17 @@ function findTsTypeSites(tree) {
|
|
|
4775
5623
|
const bl = lines[j];
|
|
4776
5624
|
if (!bl.comment) {
|
|
4777
5625
|
if (!found) {
|
|
4778
|
-
const lit = bl
|
|
5626
|
+
const lit = matchInCode(file, bl, TS_TYPE_LITERAL_RE);
|
|
4779
5627
|
if (lit) {
|
|
4780
5628
|
value = lit[1];
|
|
4781
5629
|
found = true;
|
|
4782
|
-
} else if (
|
|
5630
|
+
} else if (testInCode(file, bl, TS_TYPE_KEY_RE)) {
|
|
4783
5631
|
unresolved = true;
|
|
4784
5632
|
found = true;
|
|
4785
5633
|
}
|
|
4786
5634
|
}
|
|
4787
|
-
depth += (bl
|
|
4788
|
-
depth -= (bl
|
|
5635
|
+
depth += countInCode(file, bl, "{");
|
|
5636
|
+
depth -= countInCode(file, bl, "}");
|
|
4789
5637
|
if (depth > 0) started = true;
|
|
4790
5638
|
}
|
|
4791
5639
|
if (started && depth <= 0) break;
|
|
@@ -4813,7 +5661,7 @@ var dExt3 = {
|
|
|
4813
5661
|
const decided = (() => {
|
|
4814
5662
|
const declared = /* @__PURE__ */ new Set();
|
|
4815
5663
|
let firstAnchor2 = null;
|
|
4816
|
-
const manifests =
|
|
5664
|
+
const manifests = findExtensionManifests(tree);
|
|
4817
5665
|
for (const manifestPath2 of manifests) {
|
|
4818
5666
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4819
5667
|
const parsed = readJsonObject(manifestPath2);
|
|
@@ -4920,8 +5768,8 @@ var dExt3 = {
|
|
|
4920
5768
|
};
|
|
4921
5769
|
|
|
4922
5770
|
// src/detectors/dExt4.ts
|
|
4923
|
-
import { readdirSync as
|
|
4924
|
-
import { join as
|
|
5771
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
5772
|
+
import { join as join13 } from "path";
|
|
4925
5773
|
function canonical(name) {
|
|
4926
5774
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
4927
5775
|
}
|
|
@@ -4940,71 +5788,40 @@ function dExt4PositiveControls() {
|
|
|
4940
5788
|
}
|
|
4941
5789
|
};
|
|
4942
5790
|
}
|
|
4943
|
-
|
|
4944
|
-
|
|
5791
|
+
function findHooksDirs(tree) {
|
|
5792
|
+
const root = tree.root;
|
|
4945
5793
|
const out = [];
|
|
4946
5794
|
let base;
|
|
4947
5795
|
try {
|
|
4948
|
-
base =
|
|
5796
|
+
base = statSync6(root).isDirectory() ? root : join13(root, "..");
|
|
4949
5797
|
} catch {
|
|
4950
5798
|
return out;
|
|
4951
5799
|
}
|
|
4952
5800
|
const walk2 = (dir) => {
|
|
4953
5801
|
let entries;
|
|
4954
5802
|
try {
|
|
4955
|
-
entries =
|
|
5803
|
+
entries = readdirSync5(dir);
|
|
4956
5804
|
} catch {
|
|
4957
5805
|
return;
|
|
4958
5806
|
}
|
|
4959
5807
|
for (const name of entries) {
|
|
4960
|
-
const full =
|
|
5808
|
+
const full = join13(dir, name);
|
|
4961
5809
|
let st;
|
|
4962
5810
|
try {
|
|
4963
|
-
st =
|
|
5811
|
+
st = statSync6(full);
|
|
4964
5812
|
} catch {
|
|
4965
5813
|
continue;
|
|
4966
5814
|
}
|
|
4967
|
-
if (st.isDirectory())
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
} else if (name === "extension.json") {
|
|
4971
|
-
out.push(full);
|
|
4972
|
-
}
|
|
4973
|
-
}
|
|
4974
|
-
};
|
|
4975
|
-
walk2(base);
|
|
4976
|
-
return out;
|
|
4977
|
-
}
|
|
4978
|
-
function findHooksDirs(root) {
|
|
4979
|
-
const out = [];
|
|
4980
|
-
let base;
|
|
4981
|
-
try {
|
|
4982
|
-
base = statSync8(root).isDirectory() ? root : join15(root, "..");
|
|
4983
|
-
} catch {
|
|
4984
|
-
return out;
|
|
4985
|
-
}
|
|
4986
|
-
const walk2 = (dir) => {
|
|
4987
|
-
let entries;
|
|
4988
|
-
try {
|
|
4989
|
-
entries = readdirSync7(dir);
|
|
4990
|
-
} catch {
|
|
4991
|
-
return;
|
|
4992
|
-
}
|
|
4993
|
-
for (const name of entries) {
|
|
4994
|
-
const full = join15(dir, name);
|
|
4995
|
-
let st;
|
|
4996
|
-
try {
|
|
4997
|
-
st = statSync8(full);
|
|
4998
|
-
} catch {
|
|
5815
|
+
if (!st.isDirectory()) continue;
|
|
5816
|
+
if (SKIP_DIRS.has(name)) {
|
|
5817
|
+
recordSkip(tree, full, base);
|
|
4999
5818
|
continue;
|
|
5000
5819
|
}
|
|
5001
|
-
if (!st.isDirectory()) continue;
|
|
5002
|
-
if (SKIP_DIRS5.has(name)) continue;
|
|
5003
5820
|
if (name === "hooks") {
|
|
5004
5821
|
const tokens = /* @__PURE__ */ new Set();
|
|
5005
5822
|
let files;
|
|
5006
5823
|
try {
|
|
5007
|
-
files =
|
|
5824
|
+
files = readdirSync5(full);
|
|
5008
5825
|
} catch {
|
|
5009
5826
|
files = [];
|
|
5010
5827
|
}
|
|
@@ -5033,7 +5850,7 @@ function findFactorySites(tree) {
|
|
|
5033
5850
|
for (let i = 0; i < lines.length; i++) {
|
|
5034
5851
|
const line = lines[i];
|
|
5035
5852
|
if (line.comment) continue;
|
|
5036
|
-
if (
|
|
5853
|
+
if (testInCode(file, line, LIFECYCLE_OBJ_RE)) {
|
|
5037
5854
|
const anchor = `${file.rel}:${line.n}`;
|
|
5038
5855
|
const tokens = /* @__PURE__ */ new Set();
|
|
5039
5856
|
let depth = 0;
|
|
@@ -5041,20 +5858,19 @@ function findFactorySites(tree) {
|
|
|
5041
5858
|
for (let j = i; j < lines.length; j++) {
|
|
5042
5859
|
const bl = lines[j];
|
|
5043
5860
|
if (bl.comment) continue;
|
|
5044
|
-
const raw = bl.raw;
|
|
5045
5861
|
if (started && depth === 1) {
|
|
5046
|
-
const k =
|
|
5862
|
+
const k = matchInCode(file, bl, OBJECT_KEY_RE);
|
|
5047
5863
|
if (k) tokens.add(canonical(k[1]));
|
|
5048
5864
|
}
|
|
5049
|
-
depth += (
|
|
5050
|
-
depth -= (
|
|
5865
|
+
depth += countInCode(file, bl, "{");
|
|
5866
|
+
depth -= countInCode(file, bl, "}");
|
|
5051
5867
|
if (depth > 0) started = true;
|
|
5052
5868
|
if (started && depth <= 0) break;
|
|
5053
5869
|
}
|
|
5054
5870
|
sites.push({ anchor, tokens });
|
|
5055
5871
|
continue;
|
|
5056
5872
|
}
|
|
5057
|
-
if (
|
|
5873
|
+
if (testInCode(file, line, LIFECYCLE_KEY_RE)) {
|
|
5058
5874
|
unresolved.push(`${file.rel}:${line.n}`);
|
|
5059
5875
|
}
|
|
5060
5876
|
}
|
|
@@ -5082,7 +5898,7 @@ var dExt4 = {
|
|
|
5082
5898
|
}
|
|
5083
5899
|
const unreadable = [];
|
|
5084
5900
|
const decided = (() => {
|
|
5085
|
-
const hooksDirs = findHooksDirs(tree
|
|
5901
|
+
const hooksDirs = findHooksDirs(tree);
|
|
5086
5902
|
const { sites, unresolved } = findFactorySites(tree);
|
|
5087
5903
|
for (const anchor of unresolved) {
|
|
5088
5904
|
unreadable.push({
|
|
@@ -5094,7 +5910,7 @@ var dExt4 = {
|
|
|
5094
5910
|
for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
|
|
5095
5911
|
for (const s of sites) for (const t of s.tokens) implemented.add(t);
|
|
5096
5912
|
const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
|
|
5097
|
-
const manifests =
|
|
5913
|
+
const manifests = findExtensionManifests(tree);
|
|
5098
5914
|
let declaredAnyLifecycle = false;
|
|
5099
5915
|
for (const manifestPath2 of manifests) {
|
|
5100
5916
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
@@ -5163,8 +5979,8 @@ var dExt4 = {
|
|
|
5163
5979
|
};
|
|
5164
5980
|
|
|
5165
5981
|
// src/detectors/dExt5.ts
|
|
5166
|
-
import { existsSync as existsSync12
|
|
5167
|
-
import { dirname as dirname7, join as
|
|
5982
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5983
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
5168
5984
|
function nonEmptyString(v) {
|
|
5169
5985
|
return typeof v === "string" && v.trim().length > 0;
|
|
5170
5986
|
}
|
|
@@ -5178,41 +5994,6 @@ function dExt5PositiveControls() {
|
|
|
5178
5994
|
}
|
|
5179
5995
|
};
|
|
5180
5996
|
}
|
|
5181
|
-
var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5182
|
-
function findExtensionManifests5(root) {
|
|
5183
|
-
const out = [];
|
|
5184
|
-
let base;
|
|
5185
|
-
try {
|
|
5186
|
-
base = statSync9(root).isDirectory() ? root : join16(root, "..");
|
|
5187
|
-
} catch {
|
|
5188
|
-
return out;
|
|
5189
|
-
}
|
|
5190
|
-
const walk2 = (dir) => {
|
|
5191
|
-
let entries;
|
|
5192
|
-
try {
|
|
5193
|
-
entries = readdirSync8(dir);
|
|
5194
|
-
} catch {
|
|
5195
|
-
return;
|
|
5196
|
-
}
|
|
5197
|
-
for (const name of entries) {
|
|
5198
|
-
const full = join16(dir, name);
|
|
5199
|
-
let st;
|
|
5200
|
-
try {
|
|
5201
|
-
st = statSync9(full);
|
|
5202
|
-
} catch {
|
|
5203
|
-
continue;
|
|
5204
|
-
}
|
|
5205
|
-
if (st.isDirectory()) {
|
|
5206
|
-
if (SKIP_DIRS6.has(name)) continue;
|
|
5207
|
-
walk2(full);
|
|
5208
|
-
} else if (name === "extension.json") {
|
|
5209
|
-
out.push(full);
|
|
5210
|
-
}
|
|
5211
|
-
}
|
|
5212
|
-
};
|
|
5213
|
-
walk2(base);
|
|
5214
|
-
return out;
|
|
5215
|
-
}
|
|
5216
5997
|
function isToolExtension(type) {
|
|
5217
5998
|
return typeof type === "string" && type.includes("tool-extension");
|
|
5218
5999
|
}
|
|
@@ -5225,7 +6006,7 @@ var dExt5 = {
|
|
|
5225
6006
|
`DExt5 bilingual control FAILED: accept(\`${FR_POSITIVE}\`)=${controls.descriptionFr.positive} reject(empty)=${!controls.descriptionFr.negative} \u2014 FR-string matcher broken, absence verdict untrustworthy`
|
|
5226
6007
|
);
|
|
5227
6008
|
}
|
|
5228
|
-
const manifests =
|
|
6009
|
+
const manifests = findExtensionManifests(tree);
|
|
5229
6010
|
if (manifests.length === 0) {
|
|
5230
6011
|
return {
|
|
5231
6012
|
id: this.id,
|
|
@@ -5259,7 +6040,7 @@ var dExt5 = {
|
|
|
5259
6040
|
if (!nonEmptyString(parsed.description_fr)) {
|
|
5260
6041
|
missing.push("description_fr (extension.json)");
|
|
5261
6042
|
}
|
|
5262
|
-
if (!existsSync12(
|
|
6043
|
+
if (!existsSync12(join14(root, "README.fr.md"))) {
|
|
5263
6044
|
missing.push("README.fr.md");
|
|
5264
6045
|
}
|
|
5265
6046
|
if (isToolExtension(parsed.type) && "tools" in parsed) {
|
|
@@ -5302,8 +6083,8 @@ var dExt5 = {
|
|
|
5302
6083
|
};
|
|
5303
6084
|
|
|
5304
6085
|
// src/detectors/dExt7.ts
|
|
5305
|
-
import { existsSync as existsSync13
|
|
5306
|
-
import { dirname as dirname8, join as
|
|
6086
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6087
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
5307
6088
|
var ABSENCE_FLAG_FIELDS = [
|
|
5308
6089
|
"extensionAbsent",
|
|
5309
6090
|
"extension_absent",
|
|
@@ -5346,41 +6127,6 @@ function dExt7PositiveControls() {
|
|
|
5346
6127
|
negative: isAbsenceCase(ABSENCE_NEGATIVE)
|
|
5347
6128
|
};
|
|
5348
6129
|
}
|
|
5349
|
-
var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5350
|
-
function findExtensionManifests6(root) {
|
|
5351
|
-
const out = [];
|
|
5352
|
-
let base;
|
|
5353
|
-
try {
|
|
5354
|
-
base = statSync10(root).isDirectory() ? root : join17(root, "..");
|
|
5355
|
-
} catch {
|
|
5356
|
-
return out;
|
|
5357
|
-
}
|
|
5358
|
-
const walk2 = (dir) => {
|
|
5359
|
-
let entries;
|
|
5360
|
-
try {
|
|
5361
|
-
entries = readdirSync9(dir);
|
|
5362
|
-
} catch {
|
|
5363
|
-
return;
|
|
5364
|
-
}
|
|
5365
|
-
for (const name of entries) {
|
|
5366
|
-
const full = join17(dir, name);
|
|
5367
|
-
let st;
|
|
5368
|
-
try {
|
|
5369
|
-
st = statSync10(full);
|
|
5370
|
-
} catch {
|
|
5371
|
-
continue;
|
|
5372
|
-
}
|
|
5373
|
-
if (st.isDirectory()) {
|
|
5374
|
-
if (SKIP_DIRS7.has(name)) continue;
|
|
5375
|
-
walk2(full);
|
|
5376
|
-
} else if (name === "extension.json") {
|
|
5377
|
-
out.push(full);
|
|
5378
|
-
}
|
|
5379
|
-
}
|
|
5380
|
-
};
|
|
5381
|
-
walk2(base);
|
|
5382
|
-
return out;
|
|
5383
|
-
}
|
|
5384
6130
|
var dExt7 = {
|
|
5385
6131
|
id: "DExt7 eval-coverage",
|
|
5386
6132
|
run(tree) {
|
|
@@ -5390,7 +6136,7 @@ var dExt7 = {
|
|
|
5390
6136
|
`DExt7 absence-matcher control FAILED: flag=${controls.flag} token=${controls.token} label=${controls.label} reject(normal)=${!controls.negative} \u2014 absence matcher broken, eval-coverage verdict untrustworthy`
|
|
5391
6137
|
);
|
|
5392
6138
|
}
|
|
5393
|
-
const manifests =
|
|
6139
|
+
const manifests = findExtensionManifests(tree);
|
|
5394
6140
|
if (manifests.length === 0) {
|
|
5395
6141
|
return {
|
|
5396
6142
|
id: this.id,
|
|
@@ -5404,7 +6150,7 @@ var dExt7 = {
|
|
|
5404
6150
|
for (const manifestPath2 of manifests) {
|
|
5405
6151
|
const extDir = dirname8(manifestPath2);
|
|
5406
6152
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5407
|
-
const evalsPath =
|
|
6153
|
+
const evalsPath = join15(extDir, "evals", "evals.json");
|
|
5408
6154
|
const shortPath = `${extLabel}/evals/evals.json`;
|
|
5409
6155
|
if (!existsSync13(evalsPath)) {
|
|
5410
6156
|
return {
|
|
@@ -5470,8 +6216,8 @@ var dExt7 = {
|
|
|
5470
6216
|
};
|
|
5471
6217
|
|
|
5472
6218
|
// src/detectors/dExt8.ts
|
|
5473
|
-
import { existsSync as existsSync14
|
|
5474
|
-
import { dirname as dirname9, join as
|
|
6219
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6220
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
5475
6221
|
var SEMVER_RE2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
5476
6222
|
var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
|
|
5477
6223
|
function dExt8PositiveControl() {
|
|
@@ -5481,41 +6227,6 @@ function dExt8PositiveControl() {
|
|
|
5481
6227
|
matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
|
|
5482
6228
|
};
|
|
5483
6229
|
}
|
|
5484
|
-
var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5485
|
-
function findExtensionManifests7(root) {
|
|
5486
|
-
const out = [];
|
|
5487
|
-
let base;
|
|
5488
|
-
try {
|
|
5489
|
-
base = statSync11(root).isDirectory() ? root : join18(root, "..");
|
|
5490
|
-
} catch {
|
|
5491
|
-
return out;
|
|
5492
|
-
}
|
|
5493
|
-
const walk2 = (dir) => {
|
|
5494
|
-
let entries;
|
|
5495
|
-
try {
|
|
5496
|
-
entries = readdirSync10(dir);
|
|
5497
|
-
} catch {
|
|
5498
|
-
return;
|
|
5499
|
-
}
|
|
5500
|
-
for (const name of entries) {
|
|
5501
|
-
const full = join18(dir, name);
|
|
5502
|
-
let st;
|
|
5503
|
-
try {
|
|
5504
|
-
st = statSync11(full);
|
|
5505
|
-
} catch {
|
|
5506
|
-
continue;
|
|
5507
|
-
}
|
|
5508
|
-
if (st.isDirectory()) {
|
|
5509
|
-
if (SKIP_DIRS8.has(name)) continue;
|
|
5510
|
-
walk2(full);
|
|
5511
|
-
} else if (name === "extension.json") {
|
|
5512
|
-
out.push(full);
|
|
5513
|
-
}
|
|
5514
|
-
}
|
|
5515
|
-
};
|
|
5516
|
-
walk2(base);
|
|
5517
|
-
return out;
|
|
5518
|
-
}
|
|
5519
6230
|
var dExt8 = {
|
|
5520
6231
|
id: "DExt8 semver+changelog",
|
|
5521
6232
|
run(tree) {
|
|
@@ -5525,7 +6236,7 @@ var dExt8 = {
|
|
|
5525
6236
|
`DExt8 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
5526
6237
|
);
|
|
5527
6238
|
}
|
|
5528
|
-
const manifests =
|
|
6239
|
+
const manifests = findExtensionManifests(tree);
|
|
5529
6240
|
if (manifests.length === 0) {
|
|
5530
6241
|
return {
|
|
5531
6242
|
id: this.id,
|
|
@@ -5540,7 +6251,7 @@ var dExt8 = {
|
|
|
5540
6251
|
for (const manifestPath2 of manifests) {
|
|
5541
6252
|
const extDir = dirname9(manifestPath2);
|
|
5542
6253
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5543
|
-
const pkgPath =
|
|
6254
|
+
const pkgPath = join16(extDir, "package.json");
|
|
5544
6255
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
5545
6256
|
if (!existsSync14(pkgPath)) {
|
|
5546
6257
|
unreadable.push({
|
|
@@ -5577,7 +6288,7 @@ var dExt8 = {
|
|
|
5577
6288
|
continue;
|
|
5578
6289
|
}
|
|
5579
6290
|
const validSemver = SEMVER_RE2.test(version);
|
|
5580
|
-
const changelogPath =
|
|
6291
|
+
const changelogPath = join16(extDir, "CHANGELOG.md");
|
|
5581
6292
|
const hasChangelog = existsSync14(changelogPath);
|
|
5582
6293
|
if (!validSemver) {
|
|
5583
6294
|
finding ??= {
|
|
@@ -5612,38 +6323,41 @@ var dExt8 = {
|
|
|
5612
6323
|
};
|
|
5613
6324
|
|
|
5614
6325
|
// src/detectors/dExt9.ts
|
|
5615
|
-
import { readFileSync as readFileSync10, readdirSync as
|
|
5616
|
-
import { join as
|
|
5617
|
-
|
|
5618
|
-
|
|
6326
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
6327
|
+
import { join as join17 } from "path";
|
|
6328
|
+
function collectHooksDirSources(tree, unreadable) {
|
|
6329
|
+
const root = tree.root;
|
|
5619
6330
|
const out = [];
|
|
5620
6331
|
let base;
|
|
5621
6332
|
try {
|
|
5622
|
-
base =
|
|
6333
|
+
base = statSync7(root).isDirectory() ? root : join17(root, "..");
|
|
5623
6334
|
} catch {
|
|
5624
6335
|
return out;
|
|
5625
6336
|
}
|
|
5626
6337
|
const walk2 = (dir) => {
|
|
5627
6338
|
let entries;
|
|
5628
6339
|
try {
|
|
5629
|
-
entries =
|
|
6340
|
+
entries = readdirSync6(dir);
|
|
5630
6341
|
} catch {
|
|
5631
6342
|
return;
|
|
5632
6343
|
}
|
|
5633
6344
|
for (const name of entries) {
|
|
5634
|
-
const full =
|
|
6345
|
+
const full = join17(dir, name);
|
|
5635
6346
|
let st;
|
|
5636
6347
|
try {
|
|
5637
|
-
st =
|
|
6348
|
+
st = statSync7(full);
|
|
5638
6349
|
} catch {
|
|
5639
6350
|
continue;
|
|
5640
6351
|
}
|
|
5641
6352
|
if (!st.isDirectory()) continue;
|
|
5642
|
-
if (
|
|
6353
|
+
if (SKIP_DIRS.has(name)) {
|
|
6354
|
+
recordSkip(tree, full, base);
|
|
6355
|
+
continue;
|
|
6356
|
+
}
|
|
5643
6357
|
if (name === "hooks") {
|
|
5644
6358
|
let files;
|
|
5645
6359
|
try {
|
|
5646
|
-
files =
|
|
6360
|
+
files = readdirSync6(full);
|
|
5647
6361
|
} catch {
|
|
5648
6362
|
files = [];
|
|
5649
6363
|
}
|
|
@@ -5651,7 +6365,7 @@ function collectHooksDirSources(root, unreadable) {
|
|
|
5651
6365
|
if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
|
|
5652
6366
|
const stem = f.replace(/\.ts$/, "");
|
|
5653
6367
|
if (stem === "index") continue;
|
|
5654
|
-
const filePath =
|
|
6368
|
+
const filePath = join17(full, f);
|
|
5655
6369
|
let body;
|
|
5656
6370
|
try {
|
|
5657
6371
|
body = readFileSync10(filePath, "utf8");
|
|
@@ -5685,7 +6399,7 @@ function collectFactorySources(tree) {
|
|
|
5685
6399
|
for (let i = 0; i < lines.length; i++) {
|
|
5686
6400
|
const line = lines[i];
|
|
5687
6401
|
if (line.comment) continue;
|
|
5688
|
-
if (!
|
|
6402
|
+
if (!testInCode(file, line, LIFECYCLE_OBJ_RE2)) continue;
|
|
5689
6403
|
let depth = 0;
|
|
5690
6404
|
let started = false;
|
|
5691
6405
|
let curKey = null;
|
|
@@ -5695,7 +6409,7 @@ function collectFactorySources(tree) {
|
|
|
5695
6409
|
if (bl.comment) continue;
|
|
5696
6410
|
const raw = bl.raw;
|
|
5697
6411
|
if (started && depth === 1) {
|
|
5698
|
-
const k =
|
|
6412
|
+
const k = matchInCode(file, bl, OBJECT_KEY_RE2);
|
|
5699
6413
|
if (k) {
|
|
5700
6414
|
curKey = canonical(k[1]);
|
|
5701
6415
|
keyBodies.set(curKey, []);
|
|
@@ -5704,8 +6418,8 @@ function collectFactorySources(tree) {
|
|
|
5704
6418
|
if (curKey && depth >= 1) {
|
|
5705
6419
|
keyBodies.get(curKey)?.push(raw);
|
|
5706
6420
|
}
|
|
5707
|
-
depth += (
|
|
5708
|
-
depth -= (
|
|
6421
|
+
depth += countInCode(file, bl, "{");
|
|
6422
|
+
depth -= countInCode(file, bl, "}");
|
|
5709
6423
|
if (depth > 0) started = true;
|
|
5710
6424
|
if (started && depth <= 0) break;
|
|
5711
6425
|
}
|
|
@@ -5756,7 +6470,7 @@ var dExt9 = {
|
|
|
5756
6470
|
const unreadable = [];
|
|
5757
6471
|
const decided = (() => {
|
|
5758
6472
|
const hookSources = [
|
|
5759
|
-
...collectHooksDirSources(tree
|
|
6473
|
+
...collectHooksDirSources(tree, unreadable),
|
|
5760
6474
|
...collectFactorySources(tree)
|
|
5761
6475
|
];
|
|
5762
6476
|
const implByToken = /* @__PURE__ */ new Map();
|
|
@@ -5765,7 +6479,7 @@ var dExt9 = {
|
|
|
5765
6479
|
list.push(s);
|
|
5766
6480
|
implByToken.set(s.token, list);
|
|
5767
6481
|
}
|
|
5768
|
-
const manifests =
|
|
6482
|
+
const manifests = findExtensionManifests(tree);
|
|
5769
6483
|
let declaredAnyLifecycle = false;
|
|
5770
6484
|
for (const manifestPath2 of manifests) {
|
|
5771
6485
|
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
@@ -6547,22 +7261,23 @@ var detectors = [
|
|
|
6547
7261
|
dExt5,
|
|
6548
7262
|
dExt7,
|
|
6549
7263
|
dExt8,
|
|
6550
|
-
dExt9
|
|
7264
|
+
dExt9,
|
|
7265
|
+
localIdentity
|
|
6551
7266
|
];
|
|
6552
7267
|
|
|
6553
7268
|
// src/runtime/launch.ts
|
|
6554
7269
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6555
|
-
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as
|
|
6556
|
-
import { dirname as dirname10, isAbsolute, join as
|
|
7270
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
7271
|
+
import { dirname as dirname10, isAbsolute, join as join18, resolve } from "path";
|
|
6557
7272
|
function findPackageDir(target) {
|
|
6558
7273
|
let dir;
|
|
6559
7274
|
try {
|
|
6560
|
-
dir =
|
|
7275
|
+
dir = statSync8(target).isDirectory() ? target : dirname10(target);
|
|
6561
7276
|
} catch {
|
|
6562
7277
|
return null;
|
|
6563
7278
|
}
|
|
6564
7279
|
for (let depth = 0; depth < 8; depth++) {
|
|
6565
|
-
if (existsSync15(
|
|
7280
|
+
if (existsSync15(join18(dir, "package.json"))) return dir;
|
|
6566
7281
|
const parent = dirname10(dir);
|
|
6567
7282
|
if (parent === dir) break;
|
|
6568
7283
|
dir = parent;
|
|
@@ -6587,10 +7302,10 @@ function deriveLaunchPlan(target) {
|
|
|
6587
7302
|
}
|
|
6588
7303
|
let pkg;
|
|
6589
7304
|
try {
|
|
6590
|
-
pkg = JSON.parse(readFileSync11(
|
|
7305
|
+
pkg = JSON.parse(readFileSync11(join18(pkgDir, "package.json"), "utf8"));
|
|
6591
7306
|
} catch (err) {
|
|
6592
7307
|
return {
|
|
6593
|
-
error: `could not parse ${
|
|
7308
|
+
error: `could not parse ${join18(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
6594
7309
|
};
|
|
6595
7310
|
}
|
|
6596
7311
|
const bin = firstBinEntry(pkg.bin);
|
|
@@ -6668,7 +7383,7 @@ function readEvalCounts(target) {
|
|
|
6668
7383
|
if (!pkgDir) return counts;
|
|
6669
7384
|
let dir = pkgDir;
|
|
6670
7385
|
for (let depth = 0; depth < 8; depth++) {
|
|
6671
|
-
const candidate =
|
|
7386
|
+
const candidate = join18(dir, "evals", "evals.json");
|
|
6672
7387
|
if (existsSync15(candidate)) {
|
|
6673
7388
|
try {
|
|
6674
7389
|
const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
|
|
@@ -6687,7 +7402,7 @@ function readEvalCounts(target) {
|
|
|
6687
7402
|
}
|
|
6688
7403
|
function statSyncSafe(p) {
|
|
6689
7404
|
try {
|
|
6690
|
-
return
|
|
7405
|
+
return statSync8(p).isDirectory();
|
|
6691
7406
|
} catch {
|
|
6692
7407
|
return false;
|
|
6693
7408
|
}
|
|
@@ -6806,6 +7521,9 @@ function reconcile(id, staticR, dynamicR) {
|
|
|
6806
7521
|
};
|
|
6807
7522
|
}
|
|
6808
7523
|
function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
7524
|
+
return runDoctorReport(target, detectorList, opts).results;
|
|
7525
|
+
}
|
|
7526
|
+
function runDoctorReport(target, detectorList = detectors, opts = {}) {
|
|
6809
7527
|
const mode = opts.mode ?? "static";
|
|
6810
7528
|
const tree = loadTree(target);
|
|
6811
7529
|
const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
|
|
@@ -6840,7 +7558,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
|
6840
7558
|
results.push(reconcile(d.id, staticR, dynamicR));
|
|
6841
7559
|
}
|
|
6842
7560
|
}
|
|
6843
|
-
return results;
|
|
7561
|
+
return { results, skipped: tree.skippedDirs };
|
|
6844
7562
|
}
|
|
6845
7563
|
async function invokeLiveChecked(d, session) {
|
|
6846
7564
|
if (!session) {
|
|
@@ -6895,14 +7613,32 @@ async function runDoctorAsync(target, opts = {}) {
|
|
|
6895
7613
|
}
|
|
6896
7614
|
return results;
|
|
6897
7615
|
}
|
|
6898
|
-
function
|
|
7616
|
+
function renderSkipped(skipped) {
|
|
7617
|
+
if (skipped.length === 0) {
|
|
7618
|
+
return "skipped directories (0) \u2014 none: no directory was skipped on this run.";
|
|
7619
|
+
}
|
|
7620
|
+
const sorted = [...skipped].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
7621
|
+
const head = `skipped directories (${sorted.length}) \u2014 NOT read on this run; a verdict above says nothing about what lies inside:`;
|
|
7622
|
+
const rows = sorted.map(
|
|
7623
|
+
(d) => `${d.path} | ${d.reason}${d.candidate ? ` | holds a deployment configuration: ${d.candidate}` : ""}`
|
|
7624
|
+
);
|
|
7625
|
+
return [head, "-".repeat(head.length), ...rows].join("\n");
|
|
7626
|
+
}
|
|
7627
|
+
function renderTable(surface, results, skipped) {
|
|
7628
|
+
const body = renderResults(surface, results);
|
|
7629
|
+
const section = skipped === void 0 ? "skipped directories: NOT REPORTED \u2014 the caller did not pass this run's skipped list (use runDoctorReport), so the scope above may be narrower than it reads." : renderSkipped(skipped);
|
|
7630
|
+
return `${body}
|
|
7631
|
+
|
|
7632
|
+
${section}`;
|
|
7633
|
+
}
|
|
7634
|
+
function renderResults(surface, results) {
|
|
6899
7635
|
const cols = { item: 24, anchor: 26, state: 15 };
|
|
6900
7636
|
const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
|
|
6901
|
-
const
|
|
7637
|
+
const sep3 = "-".repeat(header.length);
|
|
6902
7638
|
const rows = results.map(
|
|
6903
7639
|
(r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
|
|
6904
7640
|
);
|
|
6905
|
-
const table = [`surface: ${surface}`, header,
|
|
7641
|
+
const table = [`surface: ${surface}`, header, sep3, ...rows].join("\n");
|
|
6906
7642
|
const unjudged = results.filter((r) => r.state === "could-not-judge");
|
|
6907
7643
|
if (unjudged.length === 0) return table;
|
|
6908
7644
|
const cnjHeader = `could-not-judge (${unjudged.length}) \u2014 the doctor could NOT read these; this is NOT a clean bill:`;
|
|
@@ -6923,6 +7659,9 @@ var USAGE = [
|
|
|
6923
7659
|
" --mode=static read the tree only (default)",
|
|
6924
7660
|
" --mode=dynamic launch the server and judge what it serves",
|
|
6925
7661
|
" --mode=both both, dynamic findings taking precedence",
|
|
7662
|
+
" --local-identity-ceiling=N",
|
|
7663
|
+
" local identity primitives tolerated before DLocalIdentity",
|
|
7664
|
+
" fails (default 0)",
|
|
6926
7665
|
" --version print the version and exit",
|
|
6927
7666
|
" --help print this and exit",
|
|
6928
7667
|
"",
|
|
@@ -6931,7 +7670,7 @@ var USAGE = [
|
|
|
6931
7670
|
function readVersion() {
|
|
6932
7671
|
try {
|
|
6933
7672
|
const here = dirname11(fileURLToPath(import.meta.url));
|
|
6934
|
-
const manifest = JSON.parse(readFileSync12(
|
|
7673
|
+
const manifest = JSON.parse(readFileSync12(join19(here, "..", "package.json"), "utf8"));
|
|
6935
7674
|
return manifest.version ?? null;
|
|
6936
7675
|
} catch {
|
|
6937
7676
|
return null;
|
|
@@ -6963,8 +7702,20 @@ function main() {
|
|
|
6963
7702
|
console.error(`unknown --mode "${mode}" \u2014 expected static|dynamic|both`);
|
|
6964
7703
|
process.exit(1);
|
|
6965
7704
|
}
|
|
6966
|
-
const
|
|
6967
|
-
|
|
7705
|
+
const ceilingArg = args.find((a) => a.startsWith("--local-identity-ceiling="));
|
|
7706
|
+
let detectorList = detectors;
|
|
7707
|
+
if (ceilingArg) {
|
|
7708
|
+
const raw = ceilingArg.slice("--local-identity-ceiling=".length);
|
|
7709
|
+
const ceiling = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
|
|
7710
|
+
if (!Number.isInteger(ceiling)) {
|
|
7711
|
+
console.error(`invalid --local-identity-ceiling "${raw}" \u2014 expected a non-negative integer`);
|
|
7712
|
+
process.exit(1);
|
|
7713
|
+
}
|
|
7714
|
+
const ratchet = makeLocalIdentity({ ceiling });
|
|
7715
|
+
detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
|
|
7716
|
+
}
|
|
7717
|
+
const { results, skipped } = runDoctorReport(target, detectorList, { mode });
|
|
7718
|
+
console.log(renderTable(target, results, skipped));
|
|
6968
7719
|
process.exitCode = computeExitCode(results);
|
|
6969
7720
|
}
|
|
6970
7721
|
function isDirectInvocation() {
|
|
@@ -6986,7 +7737,9 @@ if (isDirectInvocation()) main();
|
|
|
6986
7737
|
export {
|
|
6987
7738
|
computeExitCode,
|
|
6988
7739
|
isLiveDetector,
|
|
7740
|
+
renderSkipped,
|
|
6989
7741
|
renderTable,
|
|
6990
7742
|
runDoctor,
|
|
6991
|
-
runDoctorAsync
|
|
7743
|
+
runDoctorAsync,
|
|
7744
|
+
runDoctorReport
|
|
6992
7745
|
};
|