@produtype/core 0.87.0 → 0.89.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
- present: managedAuthDeps.length > 0 &&
403
- (0, detectContext_1.hasAnyDep)(ctx, ['bcrypt', 'bcryptjs', 'argon2', 'scrypt-kdf', 'passport-local']).length === 0 &&
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(managedAuthDeps),
407
- details: { managedProviders: managedAuthDeps.length },
435
+ evidence: depEvidence(externalIdentityProviders),
436
+ details: { managedProviders: externalIdentityProviders.length },
408
437
  },
409
438
  {
410
439
  key: 'auth.2fa',
@@ -38,7 +38,7 @@ async function detectObservability(ctx) {
38
38
  evidence.push({ type: 'dependency', value: d, claim: 'logging' });
39
39
  for (const d of sentryDeps)
40
40
  evidence.push({ type: 'dependency', value: d, claim: 'logging' });
41
- const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/\/(health|healthz|readyz)\b/i, /x-request-id/i, /correlation-id/i, /error\s*handler/i, /RotatingFileHandler/i], 25);
41
+ const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/\/(health|healthz|readyz|livez|alive)\b/i, /x-request-id/i, /correlation-id/i, /error\s*handler/i, /RotatingFileHandler/i], 25);
42
42
  /**
43
43
  * One search, three answers, so the claim is decided per hit rather than per search.
44
44
  *
@@ -47,7 +47,7 @@ async function detectObservability(ctx) {
47
47
  * is what the line is evidence of.
48
48
  */
49
49
  for (const m of hits) {
50
- const claim = /\/(health|healthz|readyz)\b/i.test(m.snippet)
50
+ const claim = /\/(health|healthz|readyz|livez|alive)\b/i.test(m.snippet)
51
51
  ? 'health'
52
52
  : /x-request-id|correlation-id/i.test(m.snippet)
53
53
  ? 'request-id'
@@ -60,7 +60,7 @@ async function detectObservability(ctx) {
60
60
  const healthFiles = ctx.files.all.filter((file) => /(^|\/)(health|healthz|readyz|liveness|readiness)(\/route|\.[a-z]+)?$|(^|\/)(health|healthz|readyz)\//i.test(file));
61
61
  for (const file of healthFiles)
62
62
  evidence.push({ type: 'file', value: file, claim: 'health' });
63
- const hasHealth = healthFiles.length > 0 || hits.some((h) => /\/(health|healthz|readyz)\b/i.test(h.snippet));
63
+ const hasHealth = healthFiles.length > 0 || hits.some((h) => /\/(health|healthz|readyz|livez|alive)\b/i.test(h.snippet));
64
64
  const hasReqId = hits.some((h) => /x-request-id|correlation-id/i.test(h.snippet));
65
65
  // Structured logging without a logging library is still structured logging. What
66
66
  // matters is that entries are machine-readable and correlated, not which package
@@ -199,12 +199,24 @@ async function detectSecurity(ctx) {
199
199
  * The packages the ecosystem names, as distinct from the variables authors do.
200
200
  * Shared between the dependency check below and the binding walk further down.
201
201
  */
202
+ /**
203
+ * Two of these were checked against the wrong manifest for as long as they existed.
204
+ *
205
+ * `hasDep` reads `package.json` and nothing else, so `django-ratelimit` and
206
+ * `slowapi` — a Python package and a Python package — were being looked for among a
207
+ * project's npm dependencies, where they can never be. Written as though they
208
+ * worked, never able to match.
209
+ *
210
+ * Rust joins them, measured: vaultwarden declares `governor` and calls
211
+ * `check_limit_login(&ip.ip)` on its login route, and was told at `high` that it
212
+ * does not throttle authentication.
213
+ */
202
214
  const rateLimitDep = (0, detectContext_1.hasDep)(ctx, 'express-rate-limit') ||
203
215
  (0, detectContext_1.hasDep)(ctx, '@upstash/ratelimit') ||
204
216
  (0, detectContext_1.hasDep)(ctx, 'rate-limiter-flexible') ||
205
217
  (0, detectContext_1.hasDep)(ctx, 'next-rate-limit') ||
206
- (0, detectContext_1.hasDep)(ctx, 'django-ratelimit') ||
207
- (0, detectContext_1.hasDep)(ctx, 'slowapi');
218
+ (0, detectContext_1.hasAnyPyDep)(ctx, ['django-ratelimit', 'slowapi', 'flask-limiter']).length > 0 ||
219
+ (0, detectContext_1.hasAnyRustDep)(ctx, ['governor', 'tower_governor', 'tower-governor', 'actix-governor', 'ratelimit']).length > 0;
208
220
  /**
209
221
  * Throttling by what the protocol says, not by what the variable is called.
210
222
  *
@@ -36,8 +36,14 @@ const PATTERN_DECLARATION = /^(?:const\s+\w+(?:\s*:[^=]+)?\s*=\s*)?\/(?:[^/\\]|\
36
36
  * A signal that appears only in a comment was never evidence of behaviour, which is the
37
37
  * whole argument: the report cites a line and says "this is what your code does", and a
38
38
  * sentence about the code is not that.
39
+ *
40
+ * `#` is where this went wrong for a whole language. It opens a comment in Python, YAML
41
+ * and shell, and in Rust `#[get("/alive")]` is the route itself — so every Rust
42
+ * attribute was invisible to every search this analyzer makes. vaultwarden declares its
43
+ * liveness endpoint that way and was told it has none; the same blindness covered
44
+ * `#[post(...)]`, every derive and every `#[cfg]`.
39
45
  */
40
- const COMMENT_LINE = /^(?:\/\/|\/\*|\*\/?|#(?!!)|<!--|--\s)/;
46
+ const COMMENT_LINE = /^(?:\/\/|\/\*|\*\/?|#(?![![])(?!!)|<!--|--\s)/;
41
47
  /**
42
48
  * A table of strings is not a third rule, and that was a mistake worth recording.
43
49
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.87.0",
3
+ "version": "0.89.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": {