@produtype/core 1.10.0 → 1.11.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.
@@ -180,6 +180,16 @@ const RATE_LIMIT_PACKAGES = [
180
180
  'koa-ratelimit',
181
181
  'fastify-rate-limit',
182
182
  '@fastify/rate-limit',
183
+ /**
184
+ * Nest's own, which is a module and a guard rather than a middleware function.
185
+ *
186
+ * ghostfolio registers `ThrottlerModule.forRootAsync` and puts a
187
+ * `CustomThrottlerGuard` on its sign-in route, and was told at `medium` that it has
188
+ * no rate limiting at all. The package name is Nest's; what the guard is called is
189
+ * ghostfolio's business.
190
+ */
191
+ '@nestjs/throttler',
192
+ '@nest-lab/throttler-storage-redis',
183
193
  ];
184
194
  async function detectSecurity(ctx) {
185
195
  const evidence = [];
@@ -310,6 +320,7 @@ async function detectSecurity(ctx) {
310
320
  (0, detectContext_1.hasDep)(ctx, '@upstash/ratelimit') ||
311
321
  (0, detectContext_1.hasDep)(ctx, 'rate-limiter-flexible') ||
312
322
  (0, detectContext_1.hasDep)(ctx, 'next-rate-limit') ||
323
+ (0, detectContext_1.hasDep)(ctx, '@nestjs/throttler') ||
313
324
  (0, detectContext_1.hasAnyPyDep)(ctx, ['django-ratelimit', 'slowapi', 'flask-limiter']).length > 0 ||
314
325
  (0, detectContext_1.hasAnyRustDep)(ctx, ['governor', 'tower_governor', 'tower-governor', 'actix-governor', 'ratelimit']).length > 0 ||
315
326
  /**
@@ -437,7 +448,22 @@ async function detectSecurity(ctx) {
437
448
  * project's authentication surface. Per pattern, the decisive search keeps its own
438
449
  * budget however much noise the others find.
439
450
  */
440
- const AUTH_SURFACE_PATTERNS = [/['"`]\/(login|signin|sign-in|auth|session)/i, /passport\./, /signIn\s*\(/, /authenticate\s*\(/];
451
+ /**
452
+ * A route decorator writes the path without a leading slash.
453
+ *
454
+ * Every pattern here wanted `'/login'`, and NestJS writes `@Controller('auth')` and
455
+ * `@Post('login')` — the slash belongs to the router, not to the string. ghostfolio
456
+ * throttles its sign-in with a `CustomThrottlerGuard` two lines under
457
+ * `@Controller('auth')`, and `auth.controller.ts` was not in this set at all: four
458
+ * files were, none of them the controller, and the report said the product has no
459
+ * rate limiting.
460
+ *
461
+ * The decorator name is the anchor. Dropping the slash from the plain pattern
462
+ * instead would have let every `from './auth'` into the authentication surface, and
463
+ * that surface decides what counts as protecting a sign-in.
464
+ */
465
+ const ROUTE_DECORATOR = /@(?:Controller|Post|Get|Put|Patch|All|RequestMapping)\(\s*(?:value\s*=\s*)?['"`]\/?(?:login|signin|sign-in|auth|session)/i;
466
+ const AUTH_SURFACE_PATTERNS = [/['"`]\/(login|signin|sign-in|auth|session)/i, ROUTE_DECORATOR, /passport\./, /signIn\s*\(/, /authenticate\s*\(/];
441
467
  const authSurfaceFiles = new Set();
442
468
  for (const pattern of AUTH_SURFACE_PATTERNS) {
443
469
  for (const match of await (0, textSearch_1.searchInFiles)(ctx.root, source, [pattern], 40))
@@ -477,6 +503,7 @@ async function detectSecurity(ctx) {
477
503
  * limiter package at all. A schema field is neither.
478
504
  */
479
505
  const AUTH_ROUTE_DECLARATION = /['"`]\/(?:login|signin|sign-in|sessions?|auth\/login)['"`]/i;
506
+ const DECLARES_AN_AUTH_ROUTE = (line) => AUTH_ROUTE_DECLARATION.test(line) || ROUTE_DECORATOR.test(line);
480
507
  const LIMIT_IN_ROUTE_OPTIONS = /rate[_-]?limit|throttle/i;
481
508
  const ROUTE_OPTIONS_WINDOW = 8;
482
509
  const limitedAuthRoutes = [];
@@ -487,7 +514,7 @@ async function detectSecurity(ctx) {
487
514
  continue;
488
515
  const lines = text.split(/\r?\n/);
489
516
  for (let i = 0; i < lines.length; i++) {
490
- if (!AUTH_ROUTE_DECLARATION.test(lines[i]))
517
+ if (!DECLARES_AN_AUTH_ROUTE(lines[i]))
491
518
  continue;
492
519
  const window = lines.slice(i + 1, i + 1 + ROUTE_OPTIONS_WINDOW);
493
520
  const offset = window.findIndex((line) => LIMIT_IN_ROUTE_OPTIONS.test(line));
@@ -501,8 +528,33 @@ async function detectSecurity(ctx) {
501
528
  for (const hit of limitedAuthRoutes.slice(0, 3)) {
502
529
  evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line, claim: 'rate-limit' });
503
530
  }
531
+ /**
532
+ * A guard is applied to a class, not to a line.
533
+ *
534
+ * ghostfolio puts `@UseGuards(CustomThrottlerGuard)` on three routes of
535
+ * `auth.controller.ts`, nine lines below `@Controller('auth')` — one past the window
536
+ * that reads a route's own options, and that window is the wrong question anyway. A
537
+ * Nest guard protects the handler it decorates, wherever in the class it sits.
538
+ *
539
+ * `Throttler` is the word `@nestjs/throttler` exports; what the guard wrapping it is
540
+ * called is the author's business. So the claim is narrow: the package is declared,
541
+ * and its name appears in a file this analyzer already established as the
542
+ * authentication surface.
543
+ *
544
+ * Matched without word boundaries and without case, because the author's name for it
545
+ * wraps the package's: `CustomThrottlerGuard` has no boundary before `Throttler`,
546
+ * and `custom-throttler.guard` spells it in lower case. The first version of this
547
+ * rule asked for `\bThrottler` and found neither.
548
+ */
549
+ const nestThrottler = (0, detectContext_1.hasDep)(ctx, '@nestjs/throttler')
550
+ ? (await (0, textSearch_1.searchInFiles)(ctx.root, source, [/throttler/i], 20)).filter((hit) => authSurfaceFiles.has(hit.file))
551
+ : [];
552
+ for (const hit of nestThrottler.slice(0, 2)) {
553
+ evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line, claim: 'rate-limit' });
554
+ }
504
555
  const rateLimitNearAuth = coversAnAuthMount
505
556
  || limitedAuthRoutes.length > 0
557
+ || nestThrottler.length > 0
506
558
  || (boundLimiterUses ?? []).some((use) => authSurfaceFiles.has(use.file))
507
559
  || issuedRateLimits.some((match) => authSurfaceFiles.has(match.file));
508
560
  if (helmetDep)
@@ -639,6 +691,26 @@ async function detectSecurity(ctx) {
639
691
  corsLoose.push(...detected.loose);
640
692
  corsStrict.push(...detected.strict);
641
693
  }
694
+ /**
695
+ * NestJS turns it on with a method of its own.
696
+ *
697
+ * `app.enableCors()` is how a Nest application does this, and bare — with no
698
+ * argument — it is Nest's default, which is every origin. ghostfolio calls exactly
699
+ * that in `apps/api/src/main.ts`, and the one line the report cited was
700
+ * `allowedOrigins: [hostname]` in an MCP module twelve directories away: an
701
+ * allowlist, while the whole API answers anybody.
702
+ *
703
+ * `enableCors` is the framework's name for the method, so it is the anchor. An
704
+ * argument means somebody chose something and the choice is read the same way as
705
+ * everywhere else here — a literal `'*'` is open, anything else is a decision this
706
+ * search cannot settle and says so.
707
+ */
708
+ const nestCors = await (0, textSearch_1.searchInFiles)(ctx.root, source, [/\.enableCors\s*\(/], 5);
709
+ for (const hit of nestCors) {
710
+ const argument = /\.enableCors\s*\(\s*([^)]*)/.exec(hit.snippet)?.[1]?.trim() ?? '';
711
+ const wideOpen = argument === '' || /^["']\*["']/.test(argument) || /origin\s*:\s*["']\*["']/.test(argument);
712
+ (wideOpen ? corsLoose : corsStrict).push(hit);
713
+ }
642
714
  /**
643
715
  * Django's answer, which is a string in a list.
644
716
  *
@@ -404,7 +404,18 @@ exports.rules = [
404
404
  category: 'security',
405
405
  severity: 'medium',
406
406
  evaluate: ({ analysis }) => {
407
- const isExpress = analysis.stack.backend.includes('express');
407
+ /**
408
+ * The backends this check can actually read.
409
+ *
410
+ * It was `express` alone, and said so in its own unknown sentence — the
411
+ * coverage reading follows `app.use('/api', limiter)`, which is Express. Two
412
+ * readers have joined it since: a limit declared in a route's own options, and
413
+ * a Nest guard applied in the controller that declares the sign-in. A NestJS
414
+ * application that does not also declare express — which is most of them, since
415
+ * `@nestjs/platform-express` is the adapter rather than the framework — got no
416
+ * verdict at all, on a capability this analyzer can now read there.
417
+ */
418
+ const readableBackend = ['express', 'nestjs'].some((name) => analysis.stack.backend.includes(name));
408
419
  const auth = analysis.detectors['auth.core'];
409
420
  const sec = analysis.detectors['security.core'];
410
421
  const hasAuth = Boolean(auth?.present);
@@ -427,7 +438,7 @@ exports.rules = [
427
438
  * showed it.
428
439
  */
429
440
  const coverageUnasked = Boolean(sec?.details?.rateLimitCoverageUnasked);
430
- const status = !isExpress || !hasAuth
441
+ const status = !readableBackend || !hasAuth
431
442
  ? 'unknown'
432
443
  : nearAuth ? 'passed' : coverageUnasked && hasRate ? 'unknown' : hasRate ? 'partial' : 'missing';
433
444
  return mkFinding({
@@ -452,7 +463,7 @@ exports.rules = [
452
463
  ? 'Not assessed: this project throttles something, and whether it reaches the login is read from where the limiter is mounted — which needs the optional `typescript` peer dependency. Install it and re-run.'
453
464
  : !hasAuth
454
465
  ? 'Nothing here authenticates anybody, so there is no login surface to throttle.'
455
- : 'This check reads Express middleware, and this project does not use it — any throttling it has is somewhere this cannot see.',
466
+ : 'This check reads Express and NestJS, and this project uses neither — any throttling it has is somewhere this cannot see.',
456
467
  recommendation: 'Apply express-rate-limit (or equivalent) to login/register/password reset endpoints.',
457
468
  /**
458
469
  * 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": "1.10.0",
3
+ "version": "1.11.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": {