@vantageos/backend-doctor 0.2.0 → 0.3.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/README.md +48 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +19 -1
- package/dist/build-stamp.json +2 -2
- package/dist/caller-resolution.js +11 -8
- package/dist/cli.js +164 -15
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.js +5 -4
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +32 -16
- package/dist/detectors/predicates.js +12 -3
- package/dist/detectors/shared.js +3 -2
- package/dist/detectors/source/_ast.d.ts +92 -0
- package/dist/detectors/source/_ast.js +353 -0
- package/dist/detectors/source/_scan.js +4 -3
- package/dist/detectors/source/backfill.js +62 -17
- package/dist/detectors/source/contract.js +21 -13
- package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
- package/dist/detectors/source/cross-tenant-write.js +1408 -0
- package/dist/detectors/source/guard-helper-flow.d.ts +22 -0
- package/dist/detectors/source/guard-helper-flow.js +49 -6
- package/dist/detectors/source/identity-by-id.d.ts +80 -0
- package/dist/detectors/source/identity-by-id.js +1783 -0
- package/dist/detectors/source/identity.d.ts +8 -1
- package/dist/detectors/source/identity.js +364 -35
- package/dist/detectors/source/jobs.js +13 -2
- package/dist/detectors/source/lifecycle.js +14 -2
- package/dist/detectors/source/local-identity.d.ts +26 -0
- package/dist/detectors/source/local-identity.js +661 -0
- package/dist/detectors/source/package-principal.d.ts +9 -0
- package/dist/detectors/source/package-principal.js +177 -0
- package/dist/detectors/source/r6-shapes.d.ts +20 -0
- package/dist/detectors/source/r6-shapes.js +285 -0
- package/dist/detectors/source/readbudget.js +291 -0
- package/dist/detectors/source/recipient-by-id.d.ts +40 -0
- package/dist/detectors/source/recipient-by-id.js +1108 -0
- package/dist/detectors/source/refusal.d.ts +14 -0
- package/dist/detectors/source/refusal.js +75 -2
- package/dist/detectors/source/security.js +455 -26
- package/dist/detectors/source/writesafety.js +27 -14
- package/dist/detectors/standard/_doc.d.ts +1 -1
- package/dist/detectors/standard/_doc.js +5 -4
- package/dist/detectors/standard/r41-frontier.js +3 -2
- package/dist/detectors/standard/r46-returns-validator.js +3 -2
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +75 -43
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +11 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +27 -3
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +14 -6
- package/dist/violation-count.d.ts +48 -0
- package/dist/violation-count.js +94 -0
- package/fixtures/backend-standard-2825122.md +722 -0
- package/fixtures/backend-standard-4dcd3e0.md +722 -0
- package/fixtures/rule-classes-2825122.json +228 -0
- package/package.json +1 -1
|
@@ -9,6 +9,24 @@ export type HelperVerdict = {
|
|
|
9
9
|
verdict: "could-not-judge";
|
|
10
10
|
why: string;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* SCOPE REFINERS, by name: a call `await R(ctx, <scope>, …)` whose result is
|
|
14
|
+
* scope-derived exactly when the scope it is handed (at `argIndex`) is. The
|
|
15
|
+
* refiner returns that scope unchanged, or rebuilds it from a SERVER-SIDE row
|
|
16
|
+
* after refusing every transport but the fleet service account — vantage-peers
|
|
17
|
+
* `convex/lib/personPrincipal.ts::resolveVerifiedPerson` (the `verifiedPerson`
|
|
18
|
+
* proof is believed from the service account only and the person's scope is
|
|
19
|
+
* re-read from `oauth_access_tokens`). The other arguments it is handed (a
|
|
20
|
+
* proof, a door name, an asserted name) never become its return.
|
|
21
|
+
*
|
|
22
|
+
* Trusted by NAME, on the same footing as `withOrgScope` in isIdentityRead: the
|
|
23
|
+
* flow walk cannot model "a scope rebuilt from a stored row" as the identity.
|
|
24
|
+
* The trust is narrow — the result is pure only when the scope argument itself
|
|
25
|
+
* is pure; a refiner handed anything else confers nothing.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SCOPE_REFINERS: Readonly<Record<string, {
|
|
28
|
+
argIndex: number;
|
|
29
|
+
}>>;
|
|
12
30
|
/** A callee name, as seen from the function being judged, resolved to its
|
|
13
31
|
* definition in the tree (and to a resolver for names seen from THAT file). */
|
|
14
32
|
export type CallResolver = (name: string) => {
|
|
@@ -96,6 +114,10 @@ export interface HandlerGuards {
|
|
|
96
114
|
line: number;
|
|
97
115
|
}[];
|
|
98
116
|
}
|
|
117
|
+
/** The block body of the `handler` of `const <handlerName> = <builder>({ … })`
|
|
118
|
+
* at the top level of `sf` (arrow, function expression or method); undefined
|
|
119
|
+
* when it cannot be located. */
|
|
120
|
+
export declare function handlerBodyOf(sf: ts.SourceFile, handlerName: string): ts.Block | undefined;
|
|
99
121
|
/** Read the direct statements of handler `name` before `beforeLine`. `null`
|
|
100
122
|
* when the handler's body cannot be located. */
|
|
101
123
|
export declare function readHandlerGuards(fileName: string, source: string, handlerName: string, beforeLine: number): HandlerGuards | null;
|
|
@@ -56,6 +56,37 @@ function isIdentityRead(n) {
|
|
|
56
56
|
return callee.name.text === "getUserIdentity";
|
|
57
57
|
return ts.isIdentifier(callee) && callee.text === "withOrgScope";
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* SCOPE REFINERS, by name: a call `await R(ctx, <scope>, …)` whose result is
|
|
61
|
+
* scope-derived exactly when the scope it is handed (at `argIndex`) is. The
|
|
62
|
+
* refiner returns that scope unchanged, or rebuilds it from a SERVER-SIDE row
|
|
63
|
+
* after refusing every transport but the fleet service account — vantage-peers
|
|
64
|
+
* `convex/lib/personPrincipal.ts::resolveVerifiedPerson` (the `verifiedPerson`
|
|
65
|
+
* proof is believed from the service account only and the person's scope is
|
|
66
|
+
* re-read from `oauth_access_tokens`). The other arguments it is handed (a
|
|
67
|
+
* proof, a door name, an asserted name) never become its return.
|
|
68
|
+
*
|
|
69
|
+
* Trusted by NAME, on the same footing as `withOrgScope` in isIdentityRead: the
|
|
70
|
+
* flow walk cannot model "a scope rebuilt from a stored row" as the identity.
|
|
71
|
+
* The trust is narrow — the result is pure only when the scope argument itself
|
|
72
|
+
* is pure; a refiner handed anything else confers nothing.
|
|
73
|
+
*/
|
|
74
|
+
export const SCOPE_REFINERS = {
|
|
75
|
+
resolveVerifiedPerson: { argIndex: 1 },
|
|
76
|
+
};
|
|
77
|
+
/** `R(…, <pure>, …)` for a SCOPE_REFINERS name, the scope argument pure. */
|
|
78
|
+
function isRefinedPure(n, pure) {
|
|
79
|
+
if (!ts.isCallExpression(n) || !ts.isIdentifier(n.expression))
|
|
80
|
+
return false;
|
|
81
|
+
const spec = SCOPE_REFINERS[n.expression.text];
|
|
82
|
+
if (!spec)
|
|
83
|
+
return false;
|
|
84
|
+
const a = n.arguments[spec.argIndex];
|
|
85
|
+
if (!a)
|
|
86
|
+
return false;
|
|
87
|
+
const u = unwrap(a);
|
|
88
|
+
return isIdentityRead(u) || (ts.isIdentifier(u) && pure.has(u.text));
|
|
89
|
+
}
|
|
59
90
|
/** Does `expr` depend on the verified identity (a tainted local, or a read)? */
|
|
60
91
|
function dependsOnIdentity(expr, tainted) {
|
|
61
92
|
let hit = false;
|
|
@@ -723,7 +754,9 @@ function pureIdentityLocals(fn, seed = new Set()) {
|
|
|
723
754
|
if (pure.has(n) || reassigned.has(n))
|
|
724
755
|
continue;
|
|
725
756
|
const u = unwrap(init);
|
|
726
|
-
if (isIdentityRead(u) ||
|
|
757
|
+
if (isIdentityRead(u) ||
|
|
758
|
+
(ts.isIdentifier(u) && pure.has(u.text)) ||
|
|
759
|
+
isRefinedPure(u, pure)) {
|
|
727
760
|
pure.add(n);
|
|
728
761
|
grew = true;
|
|
729
762
|
}
|
|
@@ -769,7 +802,10 @@ export function judgeResolverFunction(fn) {
|
|
|
769
802
|
const where = `the \`return\` at line ${lineOf(r.node)}`;
|
|
770
803
|
const e = ts.isReturnStatement(r.node) ? r.node.expression : undefined;
|
|
771
804
|
const u = e ? unwrap(e) : undefined;
|
|
772
|
-
if (!u ||
|
|
805
|
+
if (!u ||
|
|
806
|
+
!(isIdentityRead(u) ||
|
|
807
|
+
(ts.isIdentifier(u) && pure.has(u.text)) ||
|
|
808
|
+
isRefinedPure(u, pure)))
|
|
773
809
|
return {
|
|
774
810
|
verdict: "refused",
|
|
775
811
|
why: `${where} hands back a value that is not itself the verified identity/scope`,
|
|
@@ -1055,10 +1091,10 @@ function alwaysThrowsStmt(st) {
|
|
|
1055
1091
|
});
|
|
1056
1092
|
return !ret && ts.isThrowStatement(st.statements[st.statements.length - 1]);
|
|
1057
1093
|
}
|
|
1058
|
-
/**
|
|
1059
|
-
*
|
|
1060
|
-
|
|
1061
|
-
|
|
1094
|
+
/** The block body of the `handler` of `const <handlerName> = <builder>({ … })`
|
|
1095
|
+
* at the top level of `sf` (arrow, function expression or method); undefined
|
|
1096
|
+
* when it cannot be located. */
|
|
1097
|
+
export function handlerBodyOf(sf, handlerName) {
|
|
1062
1098
|
let body;
|
|
1063
1099
|
for (const st of sf.statements) {
|
|
1064
1100
|
if (!ts.isVariableStatement(st))
|
|
@@ -1085,6 +1121,13 @@ export function readHandlerGuards(fileName, source, handlerName, beforeLine) {
|
|
|
1085
1121
|
}
|
|
1086
1122
|
}
|
|
1087
1123
|
}
|
|
1124
|
+
return body;
|
|
1125
|
+
}
|
|
1126
|
+
/** Read the direct statements of handler `name` before `beforeLine`. `null`
|
|
1127
|
+
* when the handler's body cannot be located. */
|
|
1128
|
+
export function readHandlerGuards(fileName, source, handlerName, beforeLine) {
|
|
1129
|
+
const sf = parse(fileName, source);
|
|
1130
|
+
const body = handlerBodyOf(sf, handlerName);
|
|
1088
1131
|
if (!body)
|
|
1089
1132
|
return null;
|
|
1090
1133
|
const out = { consts: new Map(), refusals: [] };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { Detector } from "../../engine-types.js";
|
|
3
|
+
import { type FnDef, type Parsed, type SrcFile } from "./_ast.js";
|
|
4
|
+
import { type Scan, type SourceDetectorEntry } from "./_scan.js";
|
|
5
|
+
export declare const IDENTITY_PACKAGE = "@vantageos/cloud-identity";
|
|
6
|
+
export declare const R53_ID = "R-53 an actor and a target are identified by ID through @vantageos/cloud-identity; a name never selects or authorises a row";
|
|
7
|
+
/** Name fields: a display label that must never be an admission operand. */
|
|
8
|
+
export declare const NAME_TOKENS: ReadonlySet<string>;
|
|
9
|
+
/** The package's principal resolvers: the ONLY sinks a host credential read
|
|
10
|
+
* may feed (first argument). The package verifies nothing about the credential
|
|
11
|
+
* (cloud-identity 0.11.0 `principal-by-id.d.ts:37`, `person-principal.d.ts:28`),
|
|
12
|
+
* so the host MUST read `ctx.auth` and hand the claims over; the read is a local
|
|
13
|
+
* identity decision only when its value goes anywhere else. */
|
|
14
|
+
export declare const PACKAGE_RESOLVERS: ReadonlySet<string>;
|
|
15
|
+
/** Name comparisons in a node's OWN body: `a === b` with a name on one side
|
|
16
|
+
* and no literal on either, or `names.includes(x)` over a name list. */
|
|
17
|
+
export declare function nameComparisons(n: ts.Node): ts.Node[];
|
|
18
|
+
/** The package's own target check (cloud-identity `principal-by-id`):
|
|
19
|
+
* `assertTargetBelongsTo(principal, { orgId, ownerId? }, opts?)` at a6e5780,
|
|
20
|
+
* `await assertTargetBelongsTo(principal, target, lookups, opts?)` at PR #21. */
|
|
21
|
+
export declare const TARGET_CHECK = "assertTargetBelongsTo";
|
|
22
|
+
/** A file's bindings of the package: local name -> exported name for a named
|
|
23
|
+
* import (aliases resolved), and the locals bound to a namespace import. */
|
|
24
|
+
interface PkgImports {
|
|
25
|
+
named: Map<string, string>;
|
|
26
|
+
namespaces: Set<string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function importsOf(p: Parsed): {
|
|
29
|
+
pkg: Set<string>;
|
|
30
|
+
imports: PkgImports;
|
|
31
|
+
unresolved: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** The package export a callee names, through the file's own import of it
|
|
34
|
+
* (`assertTargetBelongsTo`, `x as y`, `identity.assertTargetBelongsTo`). */
|
|
35
|
+
export declare function pkgExportOf(callee: ts.Expression, imp: PkgImports): string | undefined;
|
|
36
|
+
/** `!v.ok`, `v.ok === false`, `v.ok !== true` (either operand order). */
|
|
37
|
+
export declare function refusesOnNotOk(c0: ts.Expression, v: string): boolean;
|
|
38
|
+
export declare class HostReadFlow {
|
|
39
|
+
private readonly parsed;
|
|
40
|
+
private readonly fns;
|
|
41
|
+
private readonly pkgImports;
|
|
42
|
+
private readonly defOf;
|
|
43
|
+
private readonly memo;
|
|
44
|
+
constructor(parsed: Parsed[], fns: Map<string, FnDef[]>, pkgImports: Map<string, PkgImports>);
|
|
45
|
+
/** Every `getUserIdentity()` call in `n` whose value does NOT end only in a
|
|
46
|
+
* package resolver. */
|
|
47
|
+
unsanctioned(n: ts.Node): ts.CallExpression[];
|
|
48
|
+
sanctioned(read: ts.CallExpression): boolean;
|
|
49
|
+
/** Does the value of expression `e` reach ONLY package resolvers? */
|
|
50
|
+
private flows;
|
|
51
|
+
private bindingFlows;
|
|
52
|
+
/** One read of a bound value: an absence test forwarded to the package, or
|
|
53
|
+
* a value that flows on. */
|
|
54
|
+
private refFlows;
|
|
55
|
+
/** A function's return reaches only package resolvers, at EVERY call site. */
|
|
56
|
+
private returnFlows;
|
|
57
|
+
}
|
|
58
|
+
export declare const ORG_KIND_ADAPTER = "orgKindOf";
|
|
59
|
+
export declare const ORG_BY_ID_ADAPTER = "organisationById";
|
|
60
|
+
/** The host adapters that may hold the ID-keyed mapping read. */
|
|
61
|
+
export declare const ORG_ID_ADAPTERS: ReadonlySet<string>;
|
|
62
|
+
export declare const ORG_ID_INDEX = "by_clerk_org_id";
|
|
63
|
+
export declare const ORG_ID_FIELD = "clerkOrgId";
|
|
64
|
+
/**
|
|
65
|
+
* The `client_org_mapping` reads the org-kind ruling admits (see OrgKindReads),
|
|
66
|
+
* computed over `parsed` so a caller holding the same AST nodes can test
|
|
67
|
+
* membership. Exported for CR-2 (local-identity.ts), which must not re-derive
|
|
68
|
+
* R-53's ruling.
|
|
69
|
+
*/
|
|
70
|
+
export declare function conformantMappingReads(parsed: Parsed[]): Set<ts.CallExpression>;
|
|
71
|
+
export interface R53Detail {
|
|
72
|
+
scan: Scan;
|
|
73
|
+
doors: number;
|
|
74
|
+
gates: number;
|
|
75
|
+
}
|
|
76
|
+
export declare function scanR53(files: SrcFile[]): Scan;
|
|
77
|
+
export declare function scanR53Detailed(files: SrcFile[]): R53Detail;
|
|
78
|
+
export declare const R53: Detector;
|
|
79
|
+
export declare const SOURCE_IDENTITY_BY_ID_DETECTORS: SourceDetectorEntry[];
|
|
80
|
+
export {};
|