@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.
Files changed (63) hide show
  1. package/README.md +48 -4
  2. package/dist/args.d.ts +1 -1
  3. package/dist/args.js +19 -1
  4. package/dist/build-stamp.json +2 -2
  5. package/dist/caller-resolution.js +11 -8
  6. package/dist/cli.js +164 -15
  7. package/dist/contract.d.ts +25 -7
  8. package/dist/contract.js +52 -7
  9. package/dist/coverage.d.ts +1 -1
  10. package/dist/coverage.js +2 -2
  11. package/dist/detectors/oracle-axis1.js +5 -4
  12. package/dist/detectors/oracle-axis2.d.ts +6 -0
  13. package/dist/detectors/oracle-axis2.js +32 -16
  14. package/dist/detectors/predicates.js +12 -3
  15. package/dist/detectors/shared.js +3 -2
  16. package/dist/detectors/source/_ast.d.ts +92 -0
  17. package/dist/detectors/source/_ast.js +353 -0
  18. package/dist/detectors/source/_scan.js +4 -3
  19. package/dist/detectors/source/backfill.js +62 -17
  20. package/dist/detectors/source/contract.js +21 -13
  21. package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
  22. package/dist/detectors/source/cross-tenant-write.js +1408 -0
  23. package/dist/detectors/source/guard-helper-flow.d.ts +22 -0
  24. package/dist/detectors/source/guard-helper-flow.js +49 -6
  25. package/dist/detectors/source/identity-by-id.d.ts +80 -0
  26. package/dist/detectors/source/identity-by-id.js +1783 -0
  27. package/dist/detectors/source/identity.d.ts +8 -1
  28. package/dist/detectors/source/identity.js +364 -35
  29. package/dist/detectors/source/jobs.js +13 -2
  30. package/dist/detectors/source/lifecycle.js +14 -2
  31. package/dist/detectors/source/local-identity.d.ts +26 -0
  32. package/dist/detectors/source/local-identity.js +661 -0
  33. package/dist/detectors/source/package-principal.d.ts +9 -0
  34. package/dist/detectors/source/package-principal.js +177 -0
  35. package/dist/detectors/source/r6-shapes.d.ts +20 -0
  36. package/dist/detectors/source/r6-shapes.js +285 -0
  37. package/dist/detectors/source/readbudget.js +291 -0
  38. package/dist/detectors/source/recipient-by-id.d.ts +40 -0
  39. package/dist/detectors/source/recipient-by-id.js +1108 -0
  40. package/dist/detectors/source/refusal.d.ts +14 -0
  41. package/dist/detectors/source/refusal.js +75 -2
  42. package/dist/detectors/source/security.js +455 -26
  43. package/dist/detectors/source/writesafety.js +27 -14
  44. package/dist/detectors/standard/_doc.d.ts +1 -1
  45. package/dist/detectors/standard/_doc.js +5 -4
  46. package/dist/detectors/standard/r41-frontier.js +3 -2
  47. package/dist/detectors/standard/r46-returns-validator.js +3 -2
  48. package/dist/detectors/structural.d.ts +6 -7
  49. package/dist/detectors/structural.js +75 -43
  50. package/dist/not-checkable.d.ts +1 -1
  51. package/dist/not-checkable.js +11 -0
  52. package/dist/registry.d.ts +1 -1
  53. package/dist/registry.js +27 -3
  54. package/dist/rule-class.d.ts +85 -0
  55. package/dist/rule-class.js +218 -0
  56. package/dist/tally.d.ts +13 -5
  57. package/dist/tally.js +14 -6
  58. package/dist/violation-count.d.ts +48 -0
  59. package/dist/violation-count.js +94 -0
  60. package/fixtures/backend-standard-2825122.md +722 -0
  61. package/fixtures/backend-standard-4dcd3e0.md +722 -0
  62. package/fixtures/rule-classes-2825122.json +228 -0
  63. 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) || (ts.isIdentifier(u) && pure.has(u.text))) {
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 || !(isIdentityRead(u) || (ts.isIdentifier(u) && pure.has(u.text))))
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
- /** Read the direct statements of handler `name` before `beforeLine`. `null`
1059
- * when the handler's body cannot be located. */
1060
- export function readHandlerGuards(fileName, source, handlerName, beforeLine) {
1061
- const sf = parse(fileName, source);
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 {};