@produtype/core 0.88.0 → 0.90.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.
|
@@ -74,6 +74,22 @@ async function detectAuth(ctx) {
|
|
|
74
74
|
'passport',
|
|
75
75
|
]);
|
|
76
76
|
const managedAuthPyDeps = (0, detectContext_1.hasAnyPyDep)(ctx, ['django-allauth', 'authlib', 'python-jose', 'fastapi-users', 'flask-login']);
|
|
77
|
+
/** Identity handed to somebody else, in every ecosystem this analyzer reads. */
|
|
78
|
+
const externalIdentityProviders = [
|
|
79
|
+
...managedAuthDeps,
|
|
80
|
+
...(0, detectContext_1.hasAnyPyDep)(ctx, ['django-allauth', 'authlib', 'social-auth-app-django']),
|
|
81
|
+
...(0, detectContext_1.hasAnyRustDep)(ctx, ['oauth2', 'openidconnect']),
|
|
82
|
+
...(0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-boot-starter-oauth2-client', 'com.okta.spring']),
|
|
83
|
+
...(0, detectContext_1.hasAnyDotnetDep)(ctx, ['Microsoft.AspNetCore.Authentication.OpenIdConnect', 'Microsoft.Identity.Web']),
|
|
84
|
+
];
|
|
85
|
+
/** And a password of its own, which is what makes a reset flow something to have. */
|
|
86
|
+
const storesAPasswordItself = [
|
|
87
|
+
...(0, detectContext_1.hasAnyDep)(ctx, ['bcrypt', 'bcryptjs', 'argon2', 'scrypt-kdf', 'passport-local']),
|
|
88
|
+
...(0, detectContext_1.hasAnyPyDep)(ctx, ['django', 'passlib', 'bcrypt', 'argon2-cffi', 'werkzeug']),
|
|
89
|
+
...(0, detectContext_1.hasAnyRustDep)(ctx, ['argon2', 'rust-argon2', 'bcrypt', 'scrypt', 'password-hash', 'pbkdf2']),
|
|
90
|
+
...(0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-security-crypto', 'org.mindrot:jbcrypt']),
|
|
91
|
+
...(0, detectContext_1.hasAnyDotnetDep)(ctx, ['Microsoft.AspNetCore.Identity']),
|
|
92
|
+
];
|
|
77
93
|
/**
|
|
78
94
|
* The packages that do the authenticating, as distinct from the words people use
|
|
79
95
|
* around them.
|
|
@@ -399,12 +415,25 @@ async function detectAuth(ctx) {
|
|
|
399
415
|
// makes password reset not applicable rather than missing — there is no
|
|
400
416
|
// password to reset.
|
|
401
417
|
key: 'auth.externalIdentityOnly',
|
|
402
|
-
|
|
403
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Read from npm alone, so only a JavaScript project could ever reach it.
|
|
420
|
+
*
|
|
421
|
+
* crates.io authenticates through GitHub and nothing else: `oauth2` in its
|
|
422
|
+
* Cargo.toml, no password-hashing crate anywhere, no reset route. It was told
|
|
423
|
+
* at `high` to build a password reset flow for passwords it does not have.
|
|
424
|
+
*
|
|
425
|
+
* The disqualifying half matters more than the qualifying one, so it is the
|
|
426
|
+
* wider of the two: anything that hashes a password, in any of these
|
|
427
|
+
* ecosystems, means there is a password to reset. Django is on that list as
|
|
428
|
+
* itself — `django-allauth` sits on top of Django's own user model, and those
|
|
429
|
+
* projects can almost always reset a password.
|
|
430
|
+
*/
|
|
431
|
+
present: externalIdentityProviders.length > 0 &&
|
|
432
|
+
storesAPasswordItself.length === 0 &&
|
|
404
433
|
passwordResetSignals.length === 0 &&
|
|
405
434
|
passwordResetFiles.length === 0,
|
|
406
|
-
evidence: depEvidence(
|
|
407
|
-
details: { managedProviders:
|
|
435
|
+
evidence: depEvidence(externalIdentityProviders),
|
|
436
|
+
details: { managedProviders: externalIdentityProviders.length },
|
|
408
437
|
},
|
|
409
438
|
{
|
|
410
439
|
key: 'auth.2fa',
|
|
@@ -18,6 +18,8 @@ const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /all
|
|
|
18
18
|
* `ALLOWED_ORIGINS = ["*"]` are the same decision written in two frameworks, and only
|
|
19
19
|
* the first was being caught.
|
|
20
20
|
*/
|
|
21
|
+
/** `== StatusCode::TOO_MANY_REQUESTS` and its spellings: a status being read. */
|
|
22
|
+
const COMPARES_A_STATUS = /[=!]==?\s*(?:StatusCode::TOO_MANY_REQUESTS|http\.StatusTooManyRequests|HttpStatus\.TOO_MANY_REQUESTS|HttpStatusCode\.TooManyRequests|Status429TooManyRequests)|(?:StatusCode::TOO_MANY_REQUESTS|http\.StatusTooManyRequests|HttpStatus\.TOO_MANY_REQUESTS|HttpStatusCode\.TooManyRequests)\s*[=!]==?/;
|
|
21
23
|
const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i;
|
|
22
24
|
/**
|
|
23
25
|
* A line that names an origin somebody chose.
|
|
@@ -252,8 +254,36 @@ async function detectSecurity(ctx) {
|
|
|
252
254
|
/HTTPException\(\s*429/,
|
|
253
255
|
/['"`]Retry-After['"`]\s*[,:]/i,
|
|
254
256
|
/setHeader\(\s*['"`]Retry-After/i,
|
|
257
|
+
/**
|
|
258
|
+
* The same refusal, spelled the way each platform spells it.
|
|
259
|
+
*
|
|
260
|
+
* Every shape above is JavaScript or Python, so a service that throttles in any
|
|
261
|
+
* other language had to declare a package to be seen at all. crates.io writes
|
|
262
|
+
* its own limiter — `src/rate_limiter.rs`, no crate — and refuses with
|
|
263
|
+
* `StatusCode::TOO_MANY_REQUESTS`; it was told at `high` that it does not
|
|
264
|
+
* throttle.
|
|
265
|
+
*
|
|
266
|
+
* These are constants the standard library or the framework defines for one
|
|
267
|
+
* number in RFC 6585. Nobody picks the name, and each of them is the server
|
|
268
|
+
* issuing the refusal rather than reading one: a constant is what you construct
|
|
269
|
+
* a response from, where receiving is a comparison against `.status`.
|
|
270
|
+
*/
|
|
271
|
+
/StatusCode::TOO_MANY_REQUESTS/,
|
|
272
|
+
/http\.StatusTooManyRequests/,
|
|
273
|
+
/HttpStatus\.TOO_MANY_REQUESTS/,
|
|
274
|
+
/HttpStatusCode\.TooManyRequests/,
|
|
275
|
+
/Status429TooManyRequests/,
|
|
255
276
|
], 20);
|
|
256
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Still issuing, not receiving — the constants need the same test the numbers got.
|
|
279
|
+
*
|
|
280
|
+
* `res.status(429)` can only be a server refusing, but
|
|
281
|
+
* `if resp.status() == StatusCode::TOO_MANY_REQUESTS` is this project being
|
|
282
|
+
* refused by somebody else's, which is what nocodb's webhook invoker does. A
|
|
283
|
+
* comparison is the reading direction; an argument is the writing one.
|
|
284
|
+
*/
|
|
285
|
+
const issuedRateLimits = rateLimitSignals.filter((hit) => !COMPARES_A_STATUS.test(hit.snippet));
|
|
286
|
+
const rateLimit = rateLimitDep || issuedRateLimits.length > 0;
|
|
257
287
|
/**
|
|
258
288
|
* Rate limiting where the brute force happens.
|
|
259
289
|
*
|
|
@@ -297,14 +327,14 @@ async function detectSecurity(ctx) {
|
|
|
297
327
|
const coversAnAuthMount = mountedPaths.some((prefix) => authMountPaths.some((mount) => mount === prefix || mount.startsWith(`${prefix}/`)));
|
|
298
328
|
const rateLimitNearAuth = coversAnAuthMount
|
|
299
329
|
|| (boundLimiterUses ?? []).some((use) => authSurfaceFiles.has(use.file))
|
|
300
|
-
||
|
|
330
|
+
|| issuedRateLimits.some((match) => authSurfaceFiles.has(match.file));
|
|
301
331
|
if (helmetDep)
|
|
302
332
|
evidence.push({ type: 'dependency', value: 'helmet', claim: 'headers' });
|
|
303
333
|
if (rateLimitDep)
|
|
304
334
|
evidence.push({ type: 'dependency', value: 'rate limiting package', claim: 'rate-limit' });
|
|
305
335
|
for (const m of headerSignals)
|
|
306
336
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'headers' });
|
|
307
|
-
for (const m of
|
|
337
|
+
for (const m of issuedRateLimits)
|
|
308
338
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'rate-limit' });
|
|
309
339
|
/**
|
|
310
340
|
* Every identifier the `cors` package reaches, per file.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.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": {
|