@produtype/core 0.58.0 → 0.60.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.
@@ -204,9 +204,20 @@ async function detectAuth(ctx) {
204
204
  const roleSignals = [...unambiguousRoles, ...comparedRoles].slice(0, 20);
205
205
  const permissionSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/requirePermission/i, /permission_classes/i, /permissions\.py/i, /authorize\(/i, /\bcan\(/i], 20);
206
206
  const resourceLevelSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
207
- /requirePermission/i,
208
- /permission_classes/i,
209
- /authorize\(/i,
207
+ /**
208
+ * Per-record, which is what this capability claims.
209
+ *
210
+ * `requirePermission` and `permission_classes` were in this list, and they are
211
+ * checks on a route: the capability's own description is "per-record checks that
212
+ * a caller may act on the specific resource, **not just the route**". Three
213
+ * projects were credited with protection against reading another user's rows —
214
+ * one of them on the strength of `permission_classes = [AllowAny]`, a line that
215
+ * says the opposite. They belong to `authz.permissions`, where they already are.
216
+ *
217
+ * `authorize(` keeps its place but needs an argument. `google_calendar.authorize()`
218
+ * is an OAuth handshake, and it was standing in for an ownership check.
219
+ */
220
+ /\bauthorize\(\s*[^)\s]/i,
210
221
  /\bcanAccess\(/i,
211
222
  /\bhasAccessTo\(/i,
212
223
  /ownerId/i,
@@ -375,7 +386,9 @@ async function detectAuth(ctx) {
375
386
  },
376
387
  {
377
388
  key: 'authz.resourceLevel',
378
- present: resourceLevelSignals.length > 0 || permissionSignals.length > 0,
389
+ // Route-level permission checks no longer stand in for per-record ones: with the
390
+ // needles above narrowed, this clause could only reintroduce what they removed.
391
+ present: resourceLevelSignals.length > 0,
379
392
  evidence: (0, absenceEvidence_1.evidenceOrSearch)(snippetEvidence(resourceLevelSignals), 'a check that the row belongs to the caller', ['requirePermission', 'permission_classes', 'authorize(', 'canAccess(', 'hasAccessTo(', 'ownerId', 'createdBy', 'req.user.id', 'userId ===']),
380
393
  },
381
394
  {
@@ -135,6 +135,29 @@ async function detectSecurity(ctx) {
135
135
  (0, detectContext_1.hasDep)(ctx, 'slowapi');
136
136
  const rateLimitSignals = await (0, textSearch_1.searchInFiles)(ctx.root, source, [/rateLimit\s*\(/, /rate_?limit/i, /Retry-After/i, /\b429\b/, /TooManyRequests/i], 20);
137
137
  const rateLimit = rateLimitDep || rateLimitSignals.length > 0;
138
+ /**
139
+ * Rate limiting where the brute force happens.
140
+ *
141
+ * The rule is titled "Rate limit on auth surfaces" and its own passing sentence says
142
+ * "detected on the authentication surface", and the flag behind both was rate
143
+ * limiting *anywhere*: a limiter on a public feed cleared the check for a sign-in
144
+ * page that has none. Sign-in is the endpoint the limit exists for.
145
+ *
146
+ * The same file, which is as far as this reaches honestly. Where a project splits
147
+ * the limiter from the login the answer becomes "found, not shown to cover sign-in",
148
+ * which is true and which the reader can dismiss in two seconds if they know better.
149
+ *
150
+ * A prefix rule was written for this and removed. TranscribeAI protects its login
151
+ * with `app.use('/api/', limiter)` above `app.use('/api/auth', authRoutes)`, and
152
+ * matching the limiter's mount path against the auth router's looked like the right
153
+ * generalisation — but the mount line says `limiter`, not `rateLimit`, so no rate
154
+ * limit signal is ever on it and the rule never fired on the one case it was written
155
+ * for. Following the variable would work and is a third layer of guessing on top of
156
+ * two; TranscribeAI stays `partial`, which is what this can show.
157
+ */
158
+ const authSurfaceFiles = new Set((await (0, textSearch_1.searchInFiles)(ctx.root, source, [/['"`]\/(login|signin|sign-in|auth|session)/i, /passport\./, /signIn\s*\(/, /authenticate\s*\(/], 40))
159
+ .map((match) => match.file));
160
+ const rateLimitNearAuth = rateLimitSignals.some((match) => authSurfaceFiles.has(match.file));
138
161
  if (helmetDep)
139
162
  evidence.push({ type: 'dependency', value: 'helmet', claim: 'headers' });
140
163
  if (rateLimitDep)
@@ -253,6 +276,7 @@ async function detectSecurity(ctx) {
253
276
  details: {
254
277
  helmet,
255
278
  rateLimit,
279
+ rateLimitNearAuth,
256
280
  corsLoose: corsLoose.length > 0,
257
281
  corsStrict: corsStrict.length > 0,
258
282
  webhookSignature: webhookSig.length > 0,
@@ -358,7 +358,18 @@ exports.rules = [
358
358
  const sec = analysis.detectors['security.core'];
359
359
  const hasAuth = Boolean(auth?.present);
360
360
  const hasRate = Boolean(sec?.details?.rateLimit);
361
- const status = !isExpress || !hasAuth ? 'unknown' : hasRate ? 'passed' : 'missing';
361
+ /**
362
+ * Near the login, not merely somewhere.
363
+ *
364
+ * This passed on `rateLimit` alone, which is throttling anywhere in the
365
+ * repository — a limiter on a public feed cleared the check for a sign-in page
366
+ * that has none. The rule's own passing sentence says "on the authentication
367
+ * surface", and now it only says that when something shows it.
368
+ */
369
+ const nearAuth = Boolean(sec?.details?.rateLimitNearAuth);
370
+ const status = !isExpress || !hasAuth
371
+ ? 'unknown'
372
+ : nearAuth ? 'passed' : hasRate ? 'partial' : 'missing';
362
373
  return mkFinding({
363
374
  id: 'security.rate-limit-auth',
364
375
  title: 'Authentication rate limiting',
@@ -373,11 +384,13 @@ exports.rules = [
373
384
  */
374
385
  description: status === 'passed'
375
386
  ? 'Rate limiting signals detected on the authentication surface.'
376
- : status === 'missing'
377
- ? 'No auth-focused rate limiting detected.'
378
- : !hasAuth
379
- ? 'Nothing here authenticates anybody, so there is no login surface to throttle.'
380
- : 'This check reads Express middleware, and this project does not use it — any throttling it has is somewhere this cannot see.',
387
+ : status === 'partial'
388
+ ? 'Rate limiting is in place somewhere, but nothing here shows it covering sign-in.'
389
+ : status === 'missing'
390
+ ? 'No auth-focused rate limiting detected.'
391
+ : !hasAuth
392
+ ? 'Nothing here authenticates anybody, so there is no login surface to throttle.'
393
+ : 'This check reads Express middleware, and this project does not use it — any throttling it has is somewhere this cannot see.',
381
394
  recommendation: 'Apply express-rate-limit (or equivalent) to login/register/password reset endpoints.',
382
395
  /**
383
396
  * The claim is about rate limiting, so the evidence is about rate limiting.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.58.0",
3
+ "version": "0.60.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": {