@produtype/core 0.89.0 → 0.91.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.
|
@@ -18,6 +18,8 @@ const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /all
|
|
|
18
18
|
* `ALLOWED_ORIGINS = ["*"]` are the same decision written in two frameworks, and only
|
|
19
19
|
* the first was being caught.
|
|
20
20
|
*/
|
|
21
|
+
/** `== StatusCode::TOO_MANY_REQUESTS` and its spellings: a status being read. */
|
|
22
|
+
const COMPARES_A_STATUS = /[=!]==?\s*(?:StatusCode::TOO_MANY_REQUESTS|http\.StatusTooManyRequests|HttpStatus\.TOO_MANY_REQUESTS|HttpStatusCode\.TooManyRequests|Status429TooManyRequests)|(?:StatusCode::TOO_MANY_REQUESTS|http\.StatusTooManyRequests|HttpStatus\.TOO_MANY_REQUESTS|HttpStatusCode\.TooManyRequests)\s*[=!]==?/;
|
|
21
23
|
const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i;
|
|
22
24
|
/**
|
|
23
25
|
* A line that names an origin somebody chose.
|
|
@@ -194,7 +196,45 @@ async function detectSecurity(ctx) {
|
|
|
194
196
|
/^\s*SECURE_CROSS_ORIGIN_OPENER_POLICY\s*=/m,
|
|
195
197
|
/^\s*CSP_DEFAULT_SRC\s*=/m,
|
|
196
198
|
], 20);
|
|
197
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Spring Security, which writes the headers without being asked.
|
|
201
|
+
*
|
|
202
|
+
* Adding `spring-boot-starter-security` and configuring an `HttpSecurity` chain
|
|
203
|
+
* gives every response `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`
|
|
204
|
+
* and a no-store `Cache-Control` — the framework's defaults, applied whether or not
|
|
205
|
+
* anybody writes a line about headers. shopizer does exactly that and was told it
|
|
206
|
+
* has none.
|
|
207
|
+
*
|
|
208
|
+
* Unlike Django's `SecurityMiddleware`, which is deliberately not counted a few
|
|
209
|
+
* lines down: `django-admin startproject` writes that into every new project, so it
|
|
210
|
+
* distinguishes nothing, and its HSTS and nosniff behaviour still waits on
|
|
211
|
+
* `SECURE_*` settings. This starter is not in every Spring project — petclinic has
|
|
212
|
+
* no security at all — and its defaults need no settings.
|
|
213
|
+
*
|
|
214
|
+
* The chain is what is required. A project can pull the starter for method-level
|
|
215
|
+
* authorization in something that serves no requests, and then there is no filter
|
|
216
|
+
* chain and no headers — which is what the second fixture holds.
|
|
217
|
+
*
|
|
218
|
+
* The dependency check in front of it is a pre-filter and nothing more: it keeps
|
|
219
|
+
* this search off every file of every non-Spring repository. Removing it fails no
|
|
220
|
+
* test, and the comment says so rather than implying a safety it does not provide.
|
|
221
|
+
*/
|
|
222
|
+
const springSecurity = (0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-boot-starter-security', 'spring-security-config']);
|
|
223
|
+
const springFilterChain = springSecurity.length > 0
|
|
224
|
+
? await (0, textSearch_1.searchInFiles)(ctx.root, source, [/HttpSecurity\s+\w+|\bhttp\s*\n?\s*\.\s*(?:authorizeHttpRequests|authorizeRequests|securityMatcher|antMatcher)/], 2)
|
|
225
|
+
: [];
|
|
226
|
+
if (springFilterChain.length > 0) {
|
|
227
|
+
for (const dep of springSecurity)
|
|
228
|
+
evidence.push({ type: 'dependency', value: dep, claim: 'headers' });
|
|
229
|
+
evidence.push({
|
|
230
|
+
type: 'snippet',
|
|
231
|
+
value: springFilterChain[0].snippet,
|
|
232
|
+
file: springFilterChain[0].file,
|
|
233
|
+
line: springFilterChain[0].line,
|
|
234
|
+
claim: 'headers',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const helmet = helmetDep || headerSignals.length > 0 || springFilterChain.length > 0;
|
|
198
238
|
/**
|
|
199
239
|
* The packages the ecosystem names, as distinct from the variables authors do.
|
|
200
240
|
* Shared between the dependency check below and the binding walk further down.
|
|
@@ -252,8 +292,36 @@ async function detectSecurity(ctx) {
|
|
|
252
292
|
/HTTPException\(\s*429/,
|
|
253
293
|
/['"`]Retry-After['"`]\s*[,:]/i,
|
|
254
294
|
/setHeader\(\s*['"`]Retry-After/i,
|
|
295
|
+
/**
|
|
296
|
+
* The same refusal, spelled the way each platform spells it.
|
|
297
|
+
*
|
|
298
|
+
* Every shape above is JavaScript or Python, so a service that throttles in any
|
|
299
|
+
* other language had to declare a package to be seen at all. crates.io writes
|
|
300
|
+
* its own limiter — `src/rate_limiter.rs`, no crate — and refuses with
|
|
301
|
+
* `StatusCode::TOO_MANY_REQUESTS`; it was told at `high` that it does not
|
|
302
|
+
* throttle.
|
|
303
|
+
*
|
|
304
|
+
* These are constants the standard library or the framework defines for one
|
|
305
|
+
* number in RFC 6585. Nobody picks the name, and each of them is the server
|
|
306
|
+
* issuing the refusal rather than reading one: a constant is what you construct
|
|
307
|
+
* a response from, where receiving is a comparison against `.status`.
|
|
308
|
+
*/
|
|
309
|
+
/StatusCode::TOO_MANY_REQUESTS/,
|
|
310
|
+
/http\.StatusTooManyRequests/,
|
|
311
|
+
/HttpStatus\.TOO_MANY_REQUESTS/,
|
|
312
|
+
/HttpStatusCode\.TooManyRequests/,
|
|
313
|
+
/Status429TooManyRequests/,
|
|
255
314
|
], 20);
|
|
256
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Still issuing, not receiving — the constants need the same test the numbers got.
|
|
317
|
+
*
|
|
318
|
+
* `res.status(429)` can only be a server refusing, but
|
|
319
|
+
* `if resp.status() == StatusCode::TOO_MANY_REQUESTS` is this project being
|
|
320
|
+
* refused by somebody else's, which is what nocodb's webhook invoker does. A
|
|
321
|
+
* comparison is the reading direction; an argument is the writing one.
|
|
322
|
+
*/
|
|
323
|
+
const issuedRateLimits = rateLimitSignals.filter((hit) => !COMPARES_A_STATUS.test(hit.snippet));
|
|
324
|
+
const rateLimit = rateLimitDep || issuedRateLimits.length > 0;
|
|
257
325
|
/**
|
|
258
326
|
* Rate limiting where the brute force happens.
|
|
259
327
|
*
|
|
@@ -297,14 +365,14 @@ async function detectSecurity(ctx) {
|
|
|
297
365
|
const coversAnAuthMount = mountedPaths.some((prefix) => authMountPaths.some((mount) => mount === prefix || mount.startsWith(`${prefix}/`)));
|
|
298
366
|
const rateLimitNearAuth = coversAnAuthMount
|
|
299
367
|
|| (boundLimiterUses ?? []).some((use) => authSurfaceFiles.has(use.file))
|
|
300
|
-
||
|
|
368
|
+
|| issuedRateLimits.some((match) => authSurfaceFiles.has(match.file));
|
|
301
369
|
if (helmetDep)
|
|
302
370
|
evidence.push({ type: 'dependency', value: 'helmet', claim: 'headers' });
|
|
303
371
|
if (rateLimitDep)
|
|
304
372
|
evidence.push({ type: 'dependency', value: 'rate limiting package', claim: 'rate-limit' });
|
|
305
373
|
for (const m of headerSignals)
|
|
306
374
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'headers' });
|
|
307
|
-
for (const m of
|
|
375
|
+
for (const m of issuedRateLimits)
|
|
308
376
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'rate-limit' });
|
|
309
377
|
/**
|
|
310
378
|
* Every identifier the `cors` package reaches, per file.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.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": {
|