@produtype/core 0.8.1 → 0.8.2

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.
@@ -57,7 +57,24 @@ async function detectAuth(ctx) {
57
57
  ], 30);
58
58
  const twoFaSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/two[_-]?factor/i, /otp/i, /totp/i], 20);
59
59
  const apiKeySignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/x-api-key/i, /apiKey/i, /API_KEY/, /token\s*scope/i], 20);
60
- const passwordResetSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/forgot\s*password/i, /password[_-]?reset/i, /reset\s*token/i], 20);
60
+ const passwordResetSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
61
+ /forgot\s*password/i,
62
+ /password[_-]?reset/i,
63
+ /reset\s*token/i,
64
+ /**
65
+ * Django ships the whole flow — token, expiry, single use — behind one include.
66
+ * A report on a real school platform called password reset missing while
67
+ * `path("accounts/", include("django.contrib.auth.urls"))` sat in its urls.py:
68
+ * the words "password" and "reset" appear nowhere, because the framework
69
+ * supplies them.
70
+ */
71
+ /django\.contrib\.auth\.urls/,
72
+ /PasswordReset(View|ConfirmView|DoneView|CompleteView)/,
73
+ /auth_views\.PasswordReset/,
74
+ // The same shape in other frameworks that hand you the flow rather than the words.
75
+ /Devise|devise_for/,
76
+ /Auth::routes\(/,
77
+ ], 20);
61
78
  const emailVerificationSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/verify\s*email/i, /email[_-]?verification/i, /confirm\s*email/i, /isEmailVerified/i], 20);
62
79
  const sessionSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/session/i, /cookie/i, /jwt/i, /refresh\s*token/i, /httpOnly/i], 25);
63
80
  const roleSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
@@ -83,6 +100,23 @@ async function detectAuth(ctx) {
83
100
  /organizationId/i,
84
101
  /tenantId/i,
85
102
  /workspaceId/i,
103
+ /**
104
+ * Django checks ownership inside the view, not in middleware before it.
105
+ *
106
+ * A report said "no resource-level authorization signals detected" of a codebase
107
+ * whose view reads
108
+ * `if not request.user.is_staff and request.user.pk != user.pk: raise
109
+ * PermissionDenied`, with visibility helpers called in every other view. The
110
+ * patterns above are all Express and DRF shaped, so none of that was visible.
111
+ */
112
+ /raise\s+PermissionDenied/,
113
+ /request\.user\.pk\s*!?==/,
114
+ /request\.user\.is_staff/,
115
+ /UserPassesTestMixin/,
116
+ /PermissionRequiredMixin/,
117
+ /@user_passes_test/,
118
+ /get_queryset\([^)]*\)[\s\S]{0,120}filter\([^)]*user/,
119
+ /\bis_visible_to\b/,
86
120
  ], 20);
87
121
  /**
88
122
  * Words that only mean tenancy, and words that usually mean something else.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.rules = void 0;
4
+ const inferProductProfile_1 = require("../expectations/inferProductProfile");
4
5
  function statusFromFlags(present, complete) {
5
6
  if (!present)
6
7
  return 'missing';
@@ -204,7 +205,17 @@ exports.rules = [
204
205
  category: 'security',
205
206
  status,
206
207
  severity: sevForStatus(status, 'medium'),
207
- description: hasHelmet ? 'Helmet detected.' : 'Helmet/security headers not detected for Express app.',
208
+ /**
209
+ * The description used to branch on `hasHelmet` alone, so a project that is not
210
+ * an Express application at all — status `unknown` — still read "Helmet
211
+ * detected." A Django repository was told that, which is a contradiction inside
212
+ * one finding and exactly the kind of thing that costs a report its credibility.
213
+ */
214
+ description: !isExpress
215
+ ? `This check is about Express middleware; the backend here is ${analysis.stack.backend.join(', ') || 'not an Express application'}.`
216
+ : hasHelmet
217
+ ? 'Helmet detected.'
218
+ : 'Helmet/security headers not detected for Express app.',
208
219
  recommendation: 'Enable helmet() and review CSP/HSTS settings for your deployment model.',
209
220
  evidence: det?.evidence ?? [],
210
221
  });
@@ -409,7 +420,22 @@ exports.rules = [
409
420
  // Tenant boundaries are a backend data-access concern: without a detected
410
421
  // backend the B2B keyword hint alone (e.g. in a frontend client) is noise.
411
422
  const backendDetected = analysis.stack.backend.length > 0;
412
- const status = !b2bHint || !backendDetected ? 'unknown' : missingTenantRisk ? 'missing' : hasMembership ? 'passed' : 'partial';
423
+ /**
424
+ * And not when the product is not a business one.
425
+ *
426
+ * This fired on a school platform that the same report classified as a B2C app,
427
+ * so the document said "consumer application" in one place and "B2B/SaaS signals
428
+ * detected" in another. A reader cannot act on a report that contradicts itself,
429
+ * and of the two statements the profile is the one built from weighted evidence.
430
+ */
431
+ const consumerProduct = ['b2c-app', 'client-app', 'game', 'mobile-app', 'static-site'].includes(String((0, inferProductProfile_1.inferProductProfile)(analysis).inferredProfile));
432
+ const status = !b2bHint || !backendDetected || consumerProduct
433
+ ? 'unknown'
434
+ : missingTenantRisk
435
+ ? 'missing'
436
+ : hasMembership
437
+ ? 'passed'
438
+ : 'partial';
413
439
  return mkFinding({
414
440
  id: 'tenancy.b2b',
415
441
  title: 'Tenant and organization boundaries',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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": {