@produtype/core 0.79.0 → 0.81.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.
|
@@ -189,6 +189,22 @@ function isTestOrExamplePath(file) {
|
|
|
189
189
|
* band — against a line written to be fake.
|
|
190
190
|
*/
|
|
191
191
|
|| /[-_]tests?\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
192
|
+
/**
|
|
193
|
+
* And the third spelling of it.
|
|
194
|
+
*
|
|
195
|
+
* netbox keeps `netbox/netbox/configuration_testing.py`, whose first three lines
|
|
196
|
+
* say it is a base configuration for testing and not intended for production. Its
|
|
197
|
+
* `SECRET_KEY = 'abcdefg...'` was the single `critical` in netbox's report — the
|
|
198
|
+
* severity that bars a report from the top band — raised against a line written to
|
|
199
|
+
* be thrown away.
|
|
200
|
+
*
|
|
201
|
+
* A directory called `testing/` has counted as tests here for a while, so the
|
|
202
|
+
* suffix is the same decision in the same ecosystem. It does cost a product file
|
|
203
|
+
* genuinely named `ab_testing.py`, which would go unread rather than be read
|
|
204
|
+
* wrongly — the cheaper of the two errors, and the same trade the `testing/`
|
|
205
|
+
* directory rule already makes.
|
|
206
|
+
*/
|
|
207
|
+
|| /[-_]testing\.py$/i.test(file)
|
|
192
208
|
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
193
209
|
/**
|
|
194
210
|
* Sample code, which is what a documentation repository is made of.
|
|
@@ -28,6 +28,21 @@ const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i
|
|
|
28
28
|
*/
|
|
29
29
|
const IMPORTS_FLASK_CORS = /from\s+flask_cors\s+import|import\s+flask_cors/;
|
|
30
30
|
const FLASK_CORS_CALL = /\bCORS\s*\(/;
|
|
31
|
+
/**
|
|
32
|
+
* Starlette's, which is how every FastAPI application does it.
|
|
33
|
+
*
|
|
34
|
+
* The full-stack FastAPI template — the one the framework's own organisation
|
|
35
|
+
* publishes — was told at `high` to add cross-origin handling, above
|
|
36
|
+
* `app.add_middleware(CORSMiddleware, allow_origins=[settings.FRONTEND_HOST])`. The
|
|
37
|
+
* class comes from the framework and `add_middleware` is its contract; neither is a
|
|
38
|
+
* word the author picked.
|
|
39
|
+
*
|
|
40
|
+
* The allowlist decides which case it is, read from the same call: `allow_origins`
|
|
41
|
+
* holding a bare `"*"` is the wide-open one, and anything else is a list somebody
|
|
42
|
+
* chose.
|
|
43
|
+
*/
|
|
44
|
+
const IMPORTS_STARLETTE_CORS = /from\s+(?:starlette|fastapi)\.middleware(?:\.cors)?\s+import[^\n]*CORSMiddleware/;
|
|
45
|
+
const STARLETTE_CORS_CALL = /add_middleware\s*\(\s*\n?\s*CORSMiddleware/;
|
|
31
46
|
/**
|
|
32
47
|
* The line with its quoted text removed.
|
|
33
48
|
*
|
|
@@ -150,6 +165,26 @@ async function detectSecurity(ctx) {
|
|
|
150
165
|
/X-Frame-Options/i,
|
|
151
166
|
/SECURE_HSTS_SECONDS/,
|
|
152
167
|
/securityHeaders/i,
|
|
168
|
+
/**
|
|
169
|
+
* Django's spelling of the same decisions.
|
|
170
|
+
*
|
|
171
|
+
* The list held the HTTP header names and one Django setting, so a project that
|
|
172
|
+
* writes `X_FRAME_OPTIONS = "SAMEORIGIN"` — the setting, with underscores, which
|
|
173
|
+
* is the only way to say it in Django — matched nothing. paperless-ngx sets it
|
|
174
|
+
* and was told at `high` to add security headers.
|
|
175
|
+
*
|
|
176
|
+
* Only the settings that choose a policy. `SECURE_PROXY_SSL_HEADER` is not one
|
|
177
|
+
* of them: it tells Django how to tell it is behind HTTPS, and every deployment
|
|
178
|
+
* behind a proxy needs it whether or not anybody thought about headers. And
|
|
179
|
+
* `SecurityMiddleware` itself stays out, for the reason recorded further down —
|
|
180
|
+
* `django-admin startproject` writes it into every new project.
|
|
181
|
+
*/
|
|
182
|
+
/^\s*X_FRAME_OPTIONS\s*=/m,
|
|
183
|
+
/^\s*SECURE_CONTENT_TYPE_NOSNIFF\s*=/m,
|
|
184
|
+
/^\s*SECURE_BROWSER_XSS_FILTER\s*=/m,
|
|
185
|
+
/^\s*SECURE_REFERRER_POLICY\s*=/m,
|
|
186
|
+
/^\s*SECURE_CROSS_ORIGIN_OPENER_POLICY\s*=/m,
|
|
187
|
+
/^\s*CSP_DEFAULT_SRC\s*=/m,
|
|
153
188
|
], 20);
|
|
154
189
|
const helmet = helmetDep || headerSignals.length > 0;
|
|
155
190
|
/**
|
|
@@ -298,6 +333,42 @@ async function detectSecurity(ctx) {
|
|
|
298
333
|
corsLoose.push(m);
|
|
299
334
|
}
|
|
300
335
|
}
|
|
336
|
+
if (IMPORTS_STARLETTE_CORS.test(text)) {
|
|
337
|
+
const lines = text.split(/\r?\n/);
|
|
338
|
+
const at = lines.findIndex((line) => /add_middleware\s*\(/.test(line) && (0, textSearch_1.isCitableLine)(line));
|
|
339
|
+
if (at !== -1) {
|
|
340
|
+
const window = lines.slice(at, Math.min(lines.length, at + 12)).join('\n');
|
|
341
|
+
if (STARLETTE_CORS_CALL.test(window)) {
|
|
342
|
+
const hit = { file, line: at + 1, snippet: lines[at].trim().slice(0, 200) };
|
|
343
|
+
const allowList = /allow_origins\s*=\s*\[([^\]]*)\]/.exec(window);
|
|
344
|
+
if (allowList) {
|
|
345
|
+
if (/^\s*["']\*["']\s*,?\s*$/.test(allowList[1]))
|
|
346
|
+
corsLoose.push(hit);
|
|
347
|
+
else
|
|
348
|
+
corsStrict.push(hit);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* `allow_origins=allowed_origins`, which is how a real application writes it.
|
|
353
|
+
*
|
|
354
|
+
* mealie builds the list from its settings and passes the name, and calling
|
|
355
|
+
* that "no explicit origin restrictions" at `high` reads as advice to add
|
|
356
|
+
* the allowlist it has. The wildcard is written literally when it is meant —
|
|
357
|
+
* `allow_origins=["*"]` — so a name is followed to its assignment, and a
|
|
358
|
+
* value assembled somewhere this cannot see is a configuration rather than a
|
|
359
|
+
* wildcard.
|
|
360
|
+
*/
|
|
361
|
+
const named = /allow_origins\s*=\s*([A-Za-z_][\w.]*)/.exec(window);
|
|
362
|
+
const assigned = named
|
|
363
|
+
? new RegExp(`^\\s*${named[1].split('.').pop()}\\s*=\\s*\\[([^\\]]*)\\]`, 'm').exec(text)
|
|
364
|
+
: null;
|
|
365
|
+
if (assigned && /^\s*["']\*["']\s*,?\s*$/.test(assigned[1]))
|
|
366
|
+
corsLoose.push(hit);
|
|
367
|
+
else
|
|
368
|
+
corsStrict.push(hit);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
301
372
|
if (!IMPORTS_CORS.test(text))
|
|
302
373
|
continue;
|
|
303
374
|
const boundNames = new Set((corsBindings ?? []).filter((use) => use.file === file).map((use) => use.name));
|
|
@@ -308,6 +379,41 @@ async function detectSecurity(ctx) {
|
|
|
308
379
|
corsLoose.push(...detected.loose);
|
|
309
380
|
corsStrict.push(...detected.strict);
|
|
310
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Django's answer, which is a string in a list.
|
|
384
|
+
*
|
|
385
|
+
* netbox installs `corsheaders.middleware.CorsMiddleware` and was reported as having
|
|
386
|
+
* no cross-origin configuration at all — then told, at `high`, to add the handling it
|
|
387
|
+
* has. Nothing here looked for it: the reading was built around a call expression,
|
|
388
|
+
* and django-cors-headers is applied by naming it in `MIDDLEWARE` and configured by
|
|
389
|
+
* setting names the package itself defines.
|
|
390
|
+
*
|
|
391
|
+
* The package name and its setting names are the anchor, and neither is the author's
|
|
392
|
+
* to choose. Installed with no allowlist the package denies every cross-origin
|
|
393
|
+
* request, so the middleware alone is the strict case; `CORS_ALLOW_ALL_ORIGINS` — and
|
|
394
|
+
* `CORS_ORIGIN_ALLOW_ALL`, which is what it was called before version 3.5 — is the
|
|
395
|
+
* one line that opens it.
|
|
396
|
+
*/
|
|
397
|
+
const django = await findDjangoSettings(ctx);
|
|
398
|
+
if (django) {
|
|
399
|
+
const middlewareLine = django.text
|
|
400
|
+
.split(/\r?\n/)
|
|
401
|
+
.findIndex((line) => /corsheaders\.middleware\.CorsMiddleware/.test(line));
|
|
402
|
+
if (middlewareLine !== -1) {
|
|
403
|
+
const openedUp = /CORS_(?:ALLOW_ALL_ORIGINS|ORIGIN_ALLOW_ALL)\s*=\s*True/.exec(django.text);
|
|
404
|
+
const hit = {
|
|
405
|
+
file: django.file,
|
|
406
|
+
line: middlewareLine + 1,
|
|
407
|
+
snippet: openedUp
|
|
408
|
+
? openedUp[0]
|
|
409
|
+
: django.text.split(/\r?\n/)[middlewareLine].trim().slice(0, 200),
|
|
410
|
+
};
|
|
411
|
+
if (openedUp)
|
|
412
|
+
corsLoose.push(hit);
|
|
413
|
+
else
|
|
414
|
+
corsStrict.push(hit);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
311
417
|
for (const m of corsLoose)
|
|
312
418
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'cors' });
|
|
313
419
|
for (const m of corsStrict)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.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": {
|