@produtype/core 0.65.0 → 0.66.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.
@@ -7,6 +7,7 @@ const roleChecks_1 = require("./structural/roleChecks");
7
7
  const absenceEvidence_1 = require("./absenceEvidence");
8
8
  const fileNames_1 = require("./fileNames");
9
9
  const valuesFromPackage_1 = require("./structural/valuesFromPackage");
10
+ const ownershipChecks_1 = require("./structural/ownershipChecks");
10
11
  /**
11
12
  * In a product that talks to a model, `role` usually means who is speaking.
12
13
  *
@@ -360,6 +361,21 @@ async function detectAuth(ctx) {
360
361
  file: use.file,
361
362
  line: use.line,
362
363
  }));
364
+ /**
365
+ * The same question asked of the shape rather than of the vocabulary.
366
+ *
367
+ * This capability is the one with no package to anchor on — there is no
368
+ * `npm install authorization` — so every reading of it was a list of the words
369
+ * people happen to use. An Italian application guarding every route with
370
+ * `if (nota.proprietario !== richiesta.utente.id)` was told it had no per-record
371
+ * checks, under a recommendation to add what it already had.
372
+ *
373
+ * Express supplies the anchor it lacks: a router comes from the package, `.get(path,
374
+ * handler)` is the framework saying "this is a request handler", and the handler's
375
+ * first parameter is the request whatever its author called it. The rest is
376
+ * structure and needs no vocabulary at all.
377
+ */
378
+ const structuralOwnership = await (0, ownershipChecks_1.readOwnershipChecks)(ctx.root, sourceFiles);
363
379
  const hasAuth = authDeps.length > 0 || routeSignals.length > 0;
364
380
  const hasAuthz = permissionSignals.length > 0 || roleSignals.length > 0;
365
381
  const b2bHint = b2bSignals.length > 0;
@@ -434,8 +450,16 @@ async function detectAuth(ctx) {
434
450
  key: 'authz.resourceLevel',
435
451
  // Route-level permission checks no longer stand in for per-record ones: with the
436
452
  // needles above narrowed, this clause could only reintroduce what they removed.
437
- present: resourceLevelSignals.length > 0,
438
- evidence: (0, absenceEvidence_1.evidenceOrSearch)(snippetEvidence(resourceLevelSignals), 'a check that the row belongs to the caller', ['requirePermission', 'permission_classes', 'authorize(', 'canAccess(', 'hasAccessTo(', 'ownerId', 'createdBy', 'req.user.id', 'userId ===']),
453
+ present: resourceLevelSignals.length > 0 || (structuralOwnership ?? []).length > 0,
454
+ evidence: (0, absenceEvidence_1.evidenceOrSearch)([
455
+ ...snippetEvidence(resourceLevelSignals),
456
+ ...(structuralOwnership ?? []).slice(0, 6).map((check) => ({
457
+ type: 'snippet',
458
+ value: check.snippet,
459
+ file: check.file,
460
+ line: check.line,
461
+ })),
462
+ ], 'a check that the row belongs to the caller', ['requirePermission', 'permission_classes', 'authorize(', 'canAccess(', 'hasAccessTo(', 'ownerId', 'createdBy', 'req.user.id', 'userId ===']),
439
463
  },
440
464
  {
441
465
  key: 'tenancy.organization',
@@ -0,0 +1,6 @@
1
+ export interface OwnershipCheck {
2
+ file: string;
3
+ line: number;
4
+ snippet: string;
5
+ }
6
+ export declare function readOwnershipChecks(root: string, files: string[]): Promise<OwnershipCheck[] | null>;
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readOwnershipChecks = readOwnershipChecks;
4
+ const readTextFileSafe_1 = require("../../utils/readTextFileSafe");
5
+ const loadTypeScript_1 = require("./loadTypeScript");
6
+ /**
7
+ * A route handler comparing a field of something it fetched against a field of the
8
+ * caller.
9
+ *
10
+ * This is the one capability on the list with no package to anchor on. There is no
11
+ * `npm install authorization`: ownership is written by hand, in whatever words the
12
+ * author has, and every reading of it here was a list of those words — `ownerId`,
13
+ * `createdBy`, `req.user.id`. An Italian application guarding every route with
14
+ *
15
+ * if (nota.proprietario !== richiesta.utente.id) return risposta.sendStatus(404);
16
+ *
17
+ * was told it had no per-record checks, under a recommendation to add the thing it
18
+ * already had.
19
+ *
20
+ * The anchor that survives is the framework's contract. `express()` and
21
+ * `express.Router()` come from a package; a router's `.get(path, handler)` is Express
22
+ * saying "this is a request handler"; and the handler's first parameter is the
23
+ * request, whatever its author called it. From there the question is structural and
24
+ * needs no vocabulary at all: does this handler compare a property reached through
25
+ * that parameter against a property of something else?
26
+ *
27
+ * `nota.proprietario !== richiesta.utente.id` answers yes. `richiesta.method !== 'GET'`
28
+ * does not — the other side is a literal, and comparing the request to a constant is
29
+ * routing, not authorisation. Neither does a comparison with the request on both
30
+ * sides, which is a request talking to itself rather than to a row.
31
+ */
32
+ const READABLE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
33
+ /** Express methods that take a handler whose first parameter is the request. */
34
+ const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all']);
35
+ function expressBindings(ts, source) {
36
+ const fromPackage = new Set();
37
+ const routers = new Set();
38
+ const visit = (node) => {
39
+ if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier) && node.moduleSpecifier.text === 'express') {
40
+ if (node.importClause?.name)
41
+ fromPackage.add(node.importClause.name.text);
42
+ }
43
+ if (ts.isVariableDeclaration(node) && node.initializer && ts.isIdentifier(node.name)) {
44
+ const initializer = node.initializer;
45
+ /** `const express = require('express')` */
46
+ if (ts.isCallExpression(initializer)
47
+ && ts.isIdentifier(initializer.expression)
48
+ && initializer.expression.text === 'require'
49
+ && initializer.arguments.length === 1
50
+ && ts.isStringLiteralLike(initializer.arguments[0])
51
+ && initializer.arguments[0].text === 'express') {
52
+ fromPackage.add(node.name.text);
53
+ }
54
+ /** `const app = express()` and `const rotte = express.Router()` */
55
+ if (ts.isCallExpression(initializer)) {
56
+ const callee = initializer.expression;
57
+ if (ts.isIdentifier(callee) && fromPackage.has(callee.text))
58
+ routers.add(node.name.text);
59
+ if (ts.isPropertyAccessExpression(callee)
60
+ && callee.name.text === 'Router'
61
+ && ts.isIdentifier(callee.expression)
62
+ && fromPackage.has(callee.expression.text)) {
63
+ routers.add(node.name.text);
64
+ }
65
+ }
66
+ }
67
+ ts.forEachChild(node, visit);
68
+ };
69
+ visit(source);
70
+ return routers;
71
+ }
72
+ /** The identifier a property access is reached through: `a.b.c` is rooted at `a`. */
73
+ function rootOf(ts, node) {
74
+ let current = node;
75
+ while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
76
+ current = current.expression;
77
+ }
78
+ return ts.isIdentifier(current) ? current.text : null;
79
+ }
80
+ function isPropertyPath(ts, node) {
81
+ return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node);
82
+ }
83
+ async function readOwnershipChecks(root, files) {
84
+ const ts = await (0, loadTypeScript_1.loadTypeScript)();
85
+ if (!ts)
86
+ return null;
87
+ const checks = [];
88
+ for (const file of files) {
89
+ if (!READABLE.test(file))
90
+ continue;
91
+ const text = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
92
+ if (!text)
93
+ continue;
94
+ if (!text.includes('express'))
95
+ continue;
96
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
97
+ const routers = expressBindings(ts, source);
98
+ if (routers.size === 0)
99
+ continue;
100
+ const lines = text.split(/\r?\n/);
101
+ const visit = (node) => {
102
+ if (ts.isCallExpression(node)) {
103
+ const callee = node.expression;
104
+ const isRoute = ts.isPropertyAccessExpression(callee)
105
+ && ROUTE_METHODS.has(callee.name.text)
106
+ && ts.isIdentifier(callee.expression)
107
+ && routers.has(callee.expression.text);
108
+ if (isRoute) {
109
+ for (const argument of node.arguments) {
110
+ if (!ts.isArrowFunction(argument) && !ts.isFunctionExpression(argument))
111
+ continue;
112
+ /**
113
+ * The request's position, not its name. Express hands an error-first
114
+ * middleware four parameters and puts the request second; every other
115
+ * handler takes it first.
116
+ */
117
+ const parameters = argument.parameters;
118
+ const request = parameters.length === 4 ? parameters[1] : parameters[0];
119
+ if (!request || !ts.isIdentifier(request.name))
120
+ continue;
121
+ const requestName = request.name.text;
122
+ const inspect = (inner) => {
123
+ if (ts.isBinaryExpression(inner)
124
+ && [
125
+ ts.SyntaxKind.EqualsEqualsEqualsToken,
126
+ ts.SyntaxKind.ExclamationEqualsEqualsToken,
127
+ ts.SyntaxKind.EqualsEqualsToken,
128
+ ts.SyntaxKind.ExclamationEqualsToken,
129
+ ].includes(inner.operatorToken.kind)) {
130
+ const left = inner.left;
131
+ const right = inner.right;
132
+ const leftRoot = rootOf(ts, left);
133
+ const rightRoot = rootOf(ts, right);
134
+ const fromRequest = (root, side) => root === requestName && isPropertyPath(ts, side);
135
+ const fromElsewhere = (root, side) => root !== null && root !== requestName && isPropertyPath(ts, side);
136
+ const compares = (fromRequest(leftRoot, left) && fromElsewhere(rightRoot, right))
137
+ || (fromRequest(rightRoot, right) && fromElsewhere(leftRoot, left));
138
+ if (compares) {
139
+ const line = source.getLineAndCharacterOfPosition(inner.getStart(source)).line;
140
+ checks.push({ file, line: line + 1, snippet: (lines[line] ?? '').trim().slice(0, 200) });
141
+ }
142
+ }
143
+ ts.forEachChild(inner, inspect);
144
+ };
145
+ if (argument.body)
146
+ inspect(argument.body);
147
+ }
148
+ }
149
+ }
150
+ ts.forEachChild(node, visit);
151
+ };
152
+ visit(source);
153
+ }
154
+ return checks;
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {