@produtype/core 0.65.0 → 0.67.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)(
|
|
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',
|
|
@@ -161,7 +161,19 @@ async function detectSecurity(ctx) {
|
|
|
161
161
|
(0, detectContext_1.hasDep)(ctx, 'next-rate-limit') ||
|
|
162
162
|
(0, detectContext_1.hasDep)(ctx, 'django-ratelimit') ||
|
|
163
163
|
(0, detectContext_1.hasDep)(ctx, 'slowapi');
|
|
164
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Throttling by what the protocol says, not by what the variable is called.
|
|
166
|
+
*
|
|
167
|
+
* `/rate_?limit/i` matched `rateLimitEnabled: z.boolean().optional()` — a field in
|
|
168
|
+
* a zod schema for a form where dokploy's *users* configure limits on their own API
|
|
169
|
+
* keys. The application has no limiter and no 429 anywhere in its source, and the
|
|
170
|
+
* report said it was throttled. The word was there; the thing was not.
|
|
171
|
+
*
|
|
172
|
+
* `429`, `Retry-After` and `TooManyRequests` stay because nobody chose them: they
|
|
173
|
+
* are what HTTP calls this, and a hand-rolled limiter that writes one of them is
|
|
174
|
+
* really limiting something. The package and its binding cover the rest.
|
|
175
|
+
*/
|
|
176
|
+
const rateLimitSignals = await (0, textSearch_1.searchInFiles)(ctx.root, source, [/Retry-After/i, /\b429\b/, /TooManyRequests/i], 20);
|
|
165
177
|
const rateLimit = rateLimitDep || rateLimitSignals.length > 0;
|
|
166
178
|
/**
|
|
167
179
|
* Rate limiting where the brute force happens.
|
|
@@ -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.
|
|
3
|
+
"version": "0.67.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": {
|