candor-ts 0.7.1 → 0.7.5
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/package.json +1 -1
- package/query-core.mjs +6 -1
- package/query.mjs +6 -1
- package/scan-core.mjs +7 -0
- package/scan.mjs +31 -2
package/package.json
CHANGED
package/query-core.mjs
CHANGED
|
@@ -247,8 +247,13 @@ function commonPrefixLen(fns) {
|
|
|
247
247
|
return (best ?? []).length;
|
|
248
248
|
}
|
|
249
249
|
function layerOf(fn, prefixLen) {
|
|
250
|
+
// The layer = the first segment after the common prefix, the leaf excluded. candor-ts names functions
|
|
251
|
+
// with a FILE.fn (free fns) or FILE.Class.method tail — a SHALLOW 1-segment-minimum tail — so the rule is
|
|
252
|
+
// `prefixLen + 1 < length` (matching candor-rust's layer_of). candor-java uses `+2` because its names carry
|
|
253
|
+
// an extra Package.Class.method segment; copying that here collapsed every 2-segment free function to
|
|
254
|
+
// "(root)", killing the dispersion signal on real TS reports.
|
|
250
255
|
const segs = fn.split(".");
|
|
251
|
-
return prefixLen +
|
|
256
|
+
return prefixLen + 1 < segs.length ? segs[prefixLen] : "(root)";
|
|
252
257
|
}
|
|
253
258
|
export function containment(fns, baseFns) {
|
|
254
259
|
const pl = commonPrefixLen(fns);
|
package/query.mjs
CHANGED
|
@@ -89,7 +89,12 @@ switch (cmd) {
|
|
|
89
89
|
// new leak), matching candor-java / candor-query. JSON-only, like every other candor-ts query command.
|
|
90
90
|
const [prefix, basePrefix] = args;
|
|
91
91
|
if (basePrefix) {
|
|
92
|
-
const
|
|
92
|
+
const baseFns = loadReport(basePrefix);
|
|
93
|
+
if (baseFns.length === 0) { // fail CLOSED (exit 2), not a wall of bogus "everything leaked" (exit 1)
|
|
94
|
+
console.error(`candor-ts: no report at baseline prefix '${basePrefix}' — check the path`);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
const r = coreContainment(loadReport(prefix), baseFns);
|
|
93
98
|
emit(r);
|
|
94
99
|
process.exit(r.leaks.length ? 1 : 0);
|
|
95
100
|
}
|
package/scan-core.mjs
CHANGED
|
@@ -63,6 +63,13 @@ export const KAPPA_RULES = [
|
|
|
63
63
|
[/^(node:)?worker_threads$/, /^(postMessage|receiveMessageOnPort)$/, "Ipc"],
|
|
64
64
|
// node:cluster — `fork()` spawns a worker PROCESS and wires its IPC channel.
|
|
65
65
|
[/^(node:)?cluster$/, /^fork$/, "Ipc"],
|
|
66
|
+
// node:vm executes a runtime-supplied code STRING in-process — `runInThisContext`/`runInContext`/
|
|
67
|
+
// `runInNewContext`/`compileFunction`, and the same verbs on a `new vm.Script(code)`. Like `eval`,
|
|
68
|
+
// the effects are whatever the code does (opaque) → genuinely Unknown (NOT Exec: no subprocess).
|
|
69
|
+
// Was unmodeled inside the κ-covered @types/node, so `vm.runInThisContext(code)` read SILENT-PURE
|
|
70
|
+
// (a code-execution sink reported pure — found by real-world corpus testing). The why is attached at
|
|
71
|
+
// the classify site (the only κ rule that resolves to the Unknown trust-marker, SPEC §4).
|
|
72
|
+
[/^(node:)?vm$/, /^(runInThisContext|runInContext|runInNewContext|compileFunction)$/, "Unknown"],
|
|
66
73
|
[/^(node:)?sqlite$/, null, "Db"],
|
|
67
74
|
// the curated npm tier
|
|
68
75
|
[/^(axios|got|node-fetch|undici|ws|socket\.io(-client)?|nodemailer)$/, null, "Net"],
|
package/scan.mjs
CHANGED
|
@@ -251,7 +251,17 @@ function declModule(decl) {
|
|
|
251
251
|
if (m) return m[1];
|
|
252
252
|
if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
|
|
253
253
|
m = f.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
|
|
254
|
-
if (m)
|
|
254
|
+
if (m) {
|
|
255
|
+
// `@types/X` (DefinitelyTyped) provides types for the RUNTIME package X — map it to X so the curated κ
|
|
256
|
+
// tier (keyed by the runtime name: pg/ws/…) fires. Without this a package typed via @types resolved to
|
|
257
|
+
// "@types/pg", the `pg`→Db rule never matched, and the resolved-but-unmodeled external decl read
|
|
258
|
+
// SILENT-PURE — `pool.query()` in a real TS Postgres app (which MUST have @types/pg installed to use
|
|
259
|
+
// pg) reported pure (found by a node_modules corpus run). Scoped runtime pkgs use the `__` convention:
|
|
260
|
+
// `@types/babel__core` → `@babel/core`.
|
|
261
|
+
const tm = m[1].match(/^@types\/(.+)$/);
|
|
262
|
+
if (tm) return tm[1].includes("__") ? "@" + tm[1].replace("__", "/") : tm[1];
|
|
263
|
+
return m[1];
|
|
264
|
+
}
|
|
255
265
|
return f;
|
|
256
266
|
}
|
|
257
267
|
|
|
@@ -1489,7 +1499,13 @@ function visitCalls(node) {
|
|
|
1489
1499
|
if (eff && (ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
|
|
1490
1500
|
&& rootsAtStdStream(node.expression.expression))
|
|
1491
1501
|
eff = null;
|
|
1492
|
-
if (eff)
|
|
1502
|
+
if (eff) {
|
|
1503
|
+
rec.direct.add(eff);
|
|
1504
|
+
// a κ rule that resolves to the Unknown trust-marker (node:vm code execution) is a direct
|
|
1505
|
+
// Unknown SOURCE — SPEC §4 requires a why on it, like eval's `reflect:eval`. (The rest of
|
|
1506
|
+
// the κ table is concrete effects, which carry no why.)
|
|
1507
|
+
if (eff === "Unknown") rec.why.add(`reflect:${mod.replace(/^node:/, "")}.${member}`);
|
|
1508
|
+
}
|
|
1493
1509
|
// the literal surfaces, read only at a CLASSIFIED call (SPEC §2)
|
|
1494
1510
|
if (eff === "Net") {
|
|
1495
1511
|
const lit = firstStringLiteral(node);
|
|
@@ -1658,6 +1674,19 @@ function visitCalls(node) {
|
|
|
1658
1674
|
const owner = enclosing(node);
|
|
1659
1675
|
if (owner) fns.get(owner).direct.add(geff);
|
|
1660
1676
|
}
|
|
1677
|
+
// dynamic `require(<non-literal>)` — the CJS twin of `import(m)` (which already discloses Unknown):
|
|
1678
|
+
// it loads an arbitrary module and runs its top-level code, so the effects are opaque → Unknown. A
|
|
1679
|
+
// LITERAL `require('fs')` is a static, resolvable load (pure until a member call), so ONLY a
|
|
1680
|
+
// non-literal arg is the escape. Gated like `fetch`: a bare `require` whose symbol is NOT a project
|
|
1681
|
+
// declaration (a project's own `function require()` shadow never fabricates). Under-disclose Unknown,
|
|
1682
|
+
// never a concrete effect. (Found by real-world corpus testing; sibling of the node:vm fix.)
|
|
1683
|
+
if (ts.isIdentifier(callee) && callee.text === "require"
|
|
1684
|
+
&& node.arguments?.length === 1 && !ts.isStringLiteralLike(node.arguments[0])
|
|
1685
|
+
&& !(checker.getSymbolAtLocation(callee)?.declarations ?? [])
|
|
1686
|
+
.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
|
|
1687
|
+
const owner = enclosing(node);
|
|
1688
|
+
if (owner) { fns.get(owner).direct.add("Unknown"); fns.get(owner).why.add("reflect:require"); }
|
|
1689
|
+
}
|
|
1661
1690
|
// Object.assign(target, ...sources) copies each SOURCE's own enumerable props → invokes their
|
|
1662
1691
|
// getters (the object-spread twin). Enumerate the sources' local getters.
|
|
1663
1692
|
if (callee.getText().replace(/\s+/g, "") === "Object.assign") {
|