@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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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,
|
package/dist/rules/rules.js
CHANGED
|
@@ -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
|
-
|
|
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 === '
|
|
377
|
-
? '
|
|
378
|
-
:
|
|
379
|
-
? '
|
|
380
|
-
:
|
|
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.
|
|
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": {
|