@produtype/core 0.62.0 → 0.64.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.
|
@@ -6,6 +6,7 @@ const textSearch_1 = require("../utils/textSearch");
|
|
|
6
6
|
const roleChecks_1 = require("./structural/roleChecks");
|
|
7
7
|
const absenceEvidence_1 = require("./absenceEvidence");
|
|
8
8
|
const fileNames_1 = require("./fileNames");
|
|
9
|
+
const valuesFromPackage_1 = require("./structural/valuesFromPackage");
|
|
9
10
|
/**
|
|
10
11
|
* In a product that talks to a model, `role` usually means who is speaking.
|
|
11
12
|
*
|
|
@@ -71,8 +72,38 @@ async function detectAuth(ctx) {
|
|
|
71
72
|
'passport',
|
|
72
73
|
]);
|
|
73
74
|
const managedAuthPyDeps = (0, detectContext_1.hasAnyPyDep)(ctx, ['django-allauth', 'authlib', 'python-jose', 'fastapi-users', 'flask-login']);
|
|
75
|
+
/**
|
|
76
|
+
* The packages that do the authenticating, as distinct from the words people use
|
|
77
|
+
* around them.
|
|
78
|
+
*
|
|
79
|
+
* An Italian business application hashing with argon2 and holding sessions with
|
|
80
|
+
* iron-session came back `auth.core: missing` — a complete, working sign-in
|
|
81
|
+
* reported as absent, and eight findings wrong behind it, because its routes are
|
|
82
|
+
* `/accedi` and `/registrati` and neither package was on the list.
|
|
83
|
+
*
|
|
84
|
+
* Nothing about that application is unusual. It is what this product's own thesis
|
|
85
|
+
* says out loud: the analyzer was reading a language rather than a program.
|
|
86
|
+
*/
|
|
87
|
+
const AUTH_PACKAGES = [
|
|
88
|
+
'jsonwebtoken',
|
|
89
|
+
'jose',
|
|
90
|
+
'bcrypt',
|
|
91
|
+
'bcryptjs',
|
|
92
|
+
'bcrypt-ts',
|
|
93
|
+
'argon2',
|
|
94
|
+
'@node-rs/argon2',
|
|
95
|
+
'scrypt-kdf',
|
|
96
|
+
'@phc/format',
|
|
97
|
+
'express-session',
|
|
98
|
+
'cookie-session',
|
|
99
|
+
'iron-session',
|
|
100
|
+
'cookie-parser',
|
|
101
|
+
'oslo',
|
|
102
|
+
'@oslojs/crypto',
|
|
103
|
+
'@auth/core',
|
|
104
|
+
];
|
|
74
105
|
const authDeps = [
|
|
75
|
-
...(0, detectContext_1.hasAnyDep)(ctx,
|
|
106
|
+
...(0, detectContext_1.hasAnyDep)(ctx, AUTH_PACKAGES),
|
|
76
107
|
...managedAuthDeps,
|
|
77
108
|
...managedAuthPyDeps,
|
|
78
109
|
];
|
|
@@ -314,6 +345,21 @@ async function detectAuth(ctx) {
|
|
|
314
345
|
/teamId/i,
|
|
315
346
|
/companyId/i,
|
|
316
347
|
], 20);
|
|
348
|
+
/**
|
|
349
|
+
* Where a value from one of those packages is actually used.
|
|
350
|
+
*
|
|
351
|
+
* The dependency says the project installed something that authenticates; this says
|
|
352
|
+
* where it does it, on a line the reader can open. It is also what makes the route
|
|
353
|
+
* names irrelevant: `argon2.verify(...)` inside `/accedi` is the same evidence as
|
|
354
|
+
* inside `/login`, and the binding is what finds it either way.
|
|
355
|
+
*/
|
|
356
|
+
const boundAuthUses = await (0, valuesFromPackage_1.readPackageValueUses)(ctx.root, sourceFiles, AUTH_PACKAGES);
|
|
357
|
+
const authUseEvidence = (boundAuthUses ?? []).slice(0, 6).map((use) => ({
|
|
358
|
+
type: 'file',
|
|
359
|
+
value: 'an authentication package is used here',
|
|
360
|
+
file: use.file,
|
|
361
|
+
line: use.line,
|
|
362
|
+
}));
|
|
317
363
|
const hasAuth = authDeps.length > 0 || routeSignals.length > 0;
|
|
318
364
|
const hasAuthz = permissionSignals.length > 0 || roleSignals.length > 0;
|
|
319
365
|
const b2bHint = b2bSignals.length > 0;
|
|
@@ -323,7 +369,7 @@ async function detectAuth(ctx) {
|
|
|
323
369
|
key: 'auth.core',
|
|
324
370
|
present: hasAuth,
|
|
325
371
|
complete: hasAuth && hasAuthz,
|
|
326
|
-
evidence: (0, absenceEvidence_1.evidenceOrSearch)([...depEvidence(authDeps), ...snippetEvidence(routeSignals)], 'a way for somebody to sign in', ['next-auth', 'passport', 'lucia', '@clerk/', '@supabase/auth', 'django.contrib.auth', 'devise', 'jsonwebtoken', 'a /login or /signin route', 'signIn(', 'authenticate(']),
|
|
372
|
+
evidence: (0, absenceEvidence_1.evidenceOrSearch)([...depEvidence(authDeps), ...authUseEvidence, ...snippetEvidence(routeSignals)], 'a way for somebody to sign in', ['next-auth', 'passport', 'lucia', '@clerk/', '@supabase/auth', 'django.contrib.auth', 'devise', 'jsonwebtoken', 'a /login or /signin route', 'signIn(', 'authenticate(']),
|
|
327
373
|
details: {
|
|
328
374
|
hasAuth,
|
|
329
375
|
hasAuthz,
|
|
@@ -44,19 +44,32 @@ function withoutStringLiterals(line) {
|
|
|
44
44
|
}
|
|
45
45
|
/** The Express middleware, brought into the file that configures it. */
|
|
46
46
|
const IMPORTS_CORS = /(require\(['"]cors['"]\))|(from\s+['"]cors['"])|(import\s+['"]cors['"])/;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The middleware, under whatever name it was given.
|
|
49
|
+
*
|
|
50
|
+
* `cors(` is the name of the export, and an author is free not to use it:
|
|
51
|
+
* `const apriTutto = require('cors'); app.use(apriTutto());` is a wide-open policy
|
|
52
|
+
* that came back as "CORS configuration not detected" — a real hole reported as the
|
|
53
|
+
* absence of a question. The package is the anchor; the names come from the binding
|
|
54
|
+
* and are recognised here rather than guessed.
|
|
55
|
+
*/
|
|
56
|
+
function detectCorsConfig(text, file, boundNames) {
|
|
48
57
|
const loose = [];
|
|
49
58
|
const strict = [];
|
|
50
59
|
const lines = text.split(/\r?\n/);
|
|
51
|
-
const
|
|
60
|
+
const names = [...new Set(['cors', ...boundNames])].map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
61
|
+
const anyName = names.join('|');
|
|
62
|
+
const callRegex = new RegExp(`\\b(?:${anyName})\\s*\\(`);
|
|
63
|
+
const bareCallRegex = new RegExp(`\\b(?:${anyName})\\(\\s*\\)`);
|
|
64
|
+
const corsVarRegex = new RegExp(`\\b(?:${anyName})\\(\\s*([A-Za-z_$][\\w$]*)\\s*\\)`);
|
|
52
65
|
for (let i = 0; i < lines.length; i++) {
|
|
53
66
|
const line = lines[i];
|
|
54
67
|
if (!(0, textSearch_1.isCitableLine)(line))
|
|
55
68
|
continue;
|
|
56
|
-
if (
|
|
69
|
+
if (!callRegex.test(withoutStringLiterals(line)))
|
|
57
70
|
continue;
|
|
58
71
|
const snippet = line.trim().slice(0, 200);
|
|
59
|
-
if (
|
|
72
|
+
if (bareCallRegex.test(line)) {
|
|
60
73
|
loose.push({ file, line: i + 1, snippet });
|
|
61
74
|
continue;
|
|
62
75
|
}
|
|
@@ -202,6 +215,14 @@ async function detectSecurity(ctx) {
|
|
|
202
215
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'headers' });
|
|
203
216
|
for (const m of rateLimitSignals)
|
|
204
217
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'rate-limit' });
|
|
218
|
+
/**
|
|
219
|
+
* Every identifier the `cors` package reaches, per file.
|
|
220
|
+
*
|
|
221
|
+
* `const apriTutto = require('cors')` binds the middleware to a name no word list
|
|
222
|
+
* will ever hold, and `app.use(apriTutto())` is a wide-open policy that read as no
|
|
223
|
+
* policy at all. The import is the fact; the name is whatever this author typed.
|
|
224
|
+
*/
|
|
225
|
+
const corsBindings = await (0, valuesFromPackage_1.readPackageValueUses)(ctx.root, source, ['cors']);
|
|
205
226
|
const corsLoose = [];
|
|
206
227
|
const corsStrict = [];
|
|
207
228
|
for (const file of source) {
|
|
@@ -243,9 +264,11 @@ async function detectSecurity(ctx) {
|
|
|
243
264
|
}
|
|
244
265
|
if (!IMPORTS_CORS.test(text))
|
|
245
266
|
continue;
|
|
246
|
-
|
|
267
|
+
const boundNames = new Set((corsBindings ?? []).filter((use) => use.file === file).map((use) => use.name));
|
|
268
|
+
// `cors(` under its own name, or under the one the binding gave it.
|
|
269
|
+
if (boundNames.size === 0 && !/\bcors\s*\(/.test(text))
|
|
247
270
|
continue;
|
|
248
|
-
const detected = detectCorsConfig(text, file);
|
|
271
|
+
const detected = detectCorsConfig(text, file, boundNames);
|
|
249
272
|
corsLoose.push(...detected.loose);
|
|
250
273
|
corsStrict.push(...detected.strict);
|
|
251
274
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export interface PackageValueUse {
|
|
2
2
|
file: string;
|
|
3
3
|
line: number;
|
|
4
|
+
/**
|
|
5
|
+
* The identifier the value is reached through.
|
|
6
|
+
*
|
|
7
|
+
* A caller that needs to reason about the call — is this `cors()` bare or
|
|
8
|
+
* `cors({ origin })`? — needs the name the author gave it, not to guess the name
|
|
9
|
+
* but to recognise it once the import has already proved what it is.
|
|
10
|
+
*/
|
|
11
|
+
name: string;
|
|
4
12
|
/**
|
|
5
13
|
* The path it was mounted on, where the use is an argument to `use(path, value)`.
|
|
6
14
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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": {
|