candor-ts 0.5.6 → 0.5.7
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/scan-core.mjs +5 -1
- package/scan.mjs +79 -5
package/package.json
CHANGED
package/scan-core.mjs
CHANGED
|
@@ -60,7 +60,11 @@ export const KAPPA_RULES = [
|
|
|
60
60
|
// entropy: node:crypto's random surface + the password-hashing libs (salted -> Rand). Found by
|
|
61
61
|
// the CTA dogfood on a Nest app: argon2.hash came out SILENTLY PURE (the curated-kappa caveat
|
|
62
62
|
// landing on exactly the call a security review cares about).
|
|
63
|
-
[/^(node:)?crypto$/, /^random/, "Rand"],
|
|
63
|
+
[/^(node:)?crypto$/, /^(random|getRandomValues)/, "Rand"],
|
|
64
|
+
// node:os identity reads — userInfo (the OS user record) and hostname (the machine name) are
|
|
65
|
+
// environment/host reads (Env), like System.getenv's host-identity cousins. The rest of node:os
|
|
66
|
+
// (platform/arch/cpus/totalmem/…) is inert host introspection, left pure.
|
|
67
|
+
[/^(node:)?os$/, /^(userInfo|hostname)$/, "Env"],
|
|
64
68
|
[/^(argon2|bcrypt|bcryptjs)$/, null, "Rand"],
|
|
65
69
|
// The ORM tier — VERB-PRECISE (the CLASSIFIER discipline: tag the execution boundary, not
|
|
66
70
|
// builders; `createQueryBuilder` is pure, its `getMany`/`execute` is the I/O). Found on the
|
package/scan.mjs
CHANGED
|
@@ -242,7 +242,12 @@ const sources = program.getSourceFiles().filter((f) => projectFiles.has(path.res
|
|
|
242
242
|
function declModule(decl) {
|
|
243
243
|
const f = path.resolve(decl.getSourceFile().fileName);
|
|
244
244
|
if (projectFiles.has(f)) return "<local>";
|
|
245
|
-
|
|
245
|
+
// `(.+?)` not `(\w+?)`: a SUBPATH typing (`@types/node/fs/promises.d.ts`, `dns/promises.d.ts`) carries
|
|
246
|
+
// a `/` that `\w` can't cross, so the module collapsed to `@types/node` (via the node_modules branch
|
|
247
|
+
// below) and the `fs(\/promises)?` / `dns(\/promises)?` κ rules — written to cover exactly these — could
|
|
248
|
+
// never fire (`fs/promises` is the dominant modern Node FS API: a silent-pure under-report). Keep the
|
|
249
|
+
// slash so the module reads `fs/promises`, which the rules match.
|
|
250
|
+
let m = f.match(/@types\/node\/(.+?)\.d\.ts$/);
|
|
246
251
|
if (m) return m[1];
|
|
247
252
|
if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
|
|
248
253
|
m = f.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
|
|
@@ -617,9 +622,30 @@ function recordAccessorHit(owner, hit, label) {
|
|
|
617
622
|
}
|
|
618
623
|
}
|
|
619
624
|
|
|
625
|
+
// Object PROPERTY-ENUMERATION (`{...obj}`, `const {...rest} = obj`, `Object.assign(t, obj)`): copying an
|
|
626
|
+
// object's own enumerable props INVOKES each source getter — the whole-object analog of `obj.prop`,
|
|
627
|
+
// invisible to the property-access arm (no PropertyAccess node per key). Edge `owner` to every LOCAL
|
|
628
|
+
// getter on the source type. A rest/spread can't name one key, so ALL getters are enumerated (sound
|
|
629
|
+
// over-approximation); a plain prop resolves to no accessor and adds nothing (no fabrication).
|
|
630
|
+
function enumerateGetters(owner, type) {
|
|
631
|
+
if (!owner || !type || !type.getProperties) return;
|
|
632
|
+
for (const p of type.getProperties()) {
|
|
633
|
+
const hit = accessorFromSym(p, "get");
|
|
634
|
+
if (hit) recordAccessorHit(owner, hit, p.getName());
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
620
638
|
// nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
|
|
621
639
|
function enclosing(node) {
|
|
622
640
|
for (let p = node; p; p = p.parent) {
|
|
641
|
+
// A call/effect lexically inside a DECORATOR (`@factory(arg)`) runs at class-DEFINITION time, NOT in
|
|
642
|
+
// the decorated declaration's body. The parent chain of a decorator's expression is
|
|
643
|
+
// CallExpression → Decorator → MethodDeclaration/ClassDeclaration/Parameter, so `enclosing` otherwise
|
|
644
|
+
// lands on the decorated unit and FABRICATES the factory's effects onto that method/class/param and
|
|
645
|
+
// every transitive caller (a cardinal sin — @Entity/@Injectable factories that touch I/O would
|
|
646
|
+
// poison every decorated handler). Stop at the Decorator: the factory's own effects live in its own
|
|
647
|
+
// function unit; the application site attributes to nothing (load-time, like a no-arg decorator).
|
|
648
|
+
if (ts.isDecorator(p)) return null;
|
|
623
649
|
const n = nodeName.get(p);
|
|
624
650
|
if (n) return n;
|
|
625
651
|
}
|
|
@@ -727,6 +753,22 @@ function visitCalls(node) {
|
|
|
727
753
|
}
|
|
728
754
|
} else {
|
|
729
755
|
const mod = declModule(decl);
|
|
756
|
+
// A LOCAL function/method passed BY REFERENCE to a NON-LOCAL (opaque) callee — `xs.map(loadFree)`,
|
|
757
|
+
// `arr.forEach(this.m)`, `setTimeout(handler)`, `external(cb)` — may be INVOKED by that callee, so
|
|
758
|
+
// its effects are reachable here. The precise callback-flow below only resolves a LOCAL callee's
|
|
759
|
+
// params; a non-local HOF dropped the reference entirely (a silent-pure hole — confirmed for
|
|
760
|
+
// `map`/`forEach`/`reduce`). Edge to the referenced unit: the sound over-approximation, matching
|
|
761
|
+
// the Rust engine's fn-as-value edge. An inline closure is already charged lexically; a non-fn
|
|
762
|
+
// argument resolves to no minted unit (`nodeName` miss) and adds nothing — no fabrication. Gated
|
|
763
|
+
// on a non-local callee so a local callee that merely STORES (never invokes) keeps its precision.
|
|
764
|
+
if (mod !== "<local>") {
|
|
765
|
+
for (const a of node.arguments ?? []) {
|
|
766
|
+
if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) continue;
|
|
767
|
+
const d2 = realDecl(checker.getSymbolAtLocation(a));
|
|
768
|
+
const t = d2 && nodeName.get(d2);
|
|
769
|
+
if (t) rec.edges.add(t);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
730
772
|
if (mod === "<local>") {
|
|
731
773
|
const targetName = nodeName.get(decl);
|
|
732
774
|
if (targetName) {
|
|
@@ -1001,6 +1043,34 @@ function visitCalls(node) {
|
|
|
1001
1043
|
const owner = enclosing(node);
|
|
1002
1044
|
if (owner) fns.get(owner).direct.add("Env");
|
|
1003
1045
|
}
|
|
1046
|
+
// Runtime GLOBALS reached as CALLS with no import for the κ resolver to classify: `process.hrtime()`/
|
|
1047
|
+
// `.hrtime.bigint()` is a monotonic clock read (Clock); `process.send(...)` is the child↔parent IPC
|
|
1048
|
+
// channel (Ipc); the global `fetch(...)` is the standard modern HTTP client (Net). Matched on the
|
|
1049
|
+
// callee — `process.*` by exact text (mirroring the process.env match), `fetch` by identifier whose
|
|
1050
|
+
// symbol is NOT a local declaration (so a project's own `fetch` shadow never fabricates Net).
|
|
1051
|
+
if (ts.isCallExpression(node)) {
|
|
1052
|
+
const callee = node.expression;
|
|
1053
|
+
const ctext = callee.getText().replace(/\s+/g, "");
|
|
1054
|
+
let geff = null;
|
|
1055
|
+
if (ctext === "process.hrtime" || ctext === "process.hrtime.bigint") geff = "Clock";
|
|
1056
|
+
else if (ctext === "process.send") geff = "Ipc";
|
|
1057
|
+
else if (ts.isIdentifier(callee) && callee.text === "fetch"
|
|
1058
|
+
&& !(checker.getSymbolAtLocation(callee)?.declarations ?? [])
|
|
1059
|
+
.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName))))
|
|
1060
|
+
geff = "Net";
|
|
1061
|
+
if (geff) {
|
|
1062
|
+
const owner = enclosing(node);
|
|
1063
|
+
if (owner) fns.get(owner).direct.add(geff);
|
|
1064
|
+
}
|
|
1065
|
+
// Object.assign(target, ...sources) copies each SOURCE's own enumerable props → invokes their
|
|
1066
|
+
// getters (the object-spread twin). Enumerate the sources' local getters.
|
|
1067
|
+
if (callee.getText().replace(/\s+/g, "") === "Object.assign") {
|
|
1068
|
+
const owner = enclosing(node);
|
|
1069
|
+
for (const src of (node.arguments ?? []).slice(1)) {
|
|
1070
|
+
enumerateGetters(owner, checker.getTypeAtLocation(src));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1004
1074
|
// GET/SET ACCESSOR access (the silent-pure-accessor fix): a property read that resolves to a
|
|
1005
1075
|
// getter, or a property assignment whose target resolves to a setter, is effectively a call into
|
|
1006
1076
|
// the accessor body — model it as a call EDGE so the accessor's effects propagate (like a method
|
|
@@ -1034,7 +1104,8 @@ function visitCalls(node) {
|
|
|
1034
1104
|
if (owner) {
|
|
1035
1105
|
const recvType = checker.getTypeAtLocation(node.initializer);
|
|
1036
1106
|
for (const el of node.name.elements) {
|
|
1037
|
-
if (el.dotDotDotToken) continue; // `...rest`
|
|
1107
|
+
if (el.dotDotDotToken) { enumerateGetters(owner, recvType); continue; } // `...rest` copies every
|
|
1108
|
+
// remaining prop → invokes every (remaining) getter; enumerate all (the bound ones double-handle).
|
|
1038
1109
|
const key = el.propertyName ?? el.name; // `{prop}` shorthand, or `{prop: alias}`
|
|
1039
1110
|
const keyName = ts.isIdentifier(key) ? key.text
|
|
1040
1111
|
: ts.isStringLiteralLike(key) ? key.text : null;
|
|
@@ -1052,9 +1123,12 @@ function visitCalls(node) {
|
|
|
1052
1123
|
let iterExpr = null, iterAsync = false;
|
|
1053
1124
|
if (ts.isForOfStatement(node)) { iterExpr = node.expression; iterAsync = !!node.awaitModifier; }
|
|
1054
1125
|
else if (ts.isSpreadElement(node)) iterExpr = node.expression; // [...bag] / f(...bag)
|
|
1055
|
-
else if (ts.isSpreadAssignment(node))
|
|
1056
|
-
|
|
1057
|
-
|
|
1126
|
+
else if (ts.isSpreadAssignment(node)) {
|
|
1127
|
+
iterExpr = node.expression; // {...bag} — object spread is NOT iteration (copies own enumerable
|
|
1128
|
+
// props, no [Symbol.iterator]); wellKnownSymbolMember finds none and edges nothing for iteration.
|
|
1129
|
+
// But the copy DOES invoke each source getter — enumerate them (the silent-pure object-spread hole).
|
|
1130
|
+
enumerateGetters(enclosing(node), checker.getTypeAtLocation(node.expression));
|
|
1131
|
+
}
|
|
1058
1132
|
else if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer)
|
|
1059
1133
|
iterExpr = node.initializer; // const [a] = bag
|
|
1060
1134
|
else if (ts.isCallExpression(node) && node.arguments?.[0]
|