@produtype/core 0.80.0 → 0.82.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.
|
@@ -267,20 +267,38 @@ async function detectBackend(ctx) {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Django, which four filenames were enough to declare.
|
|
272
|
+
*
|
|
273
|
+
* Any two of `manage.py`, a file ending in `settings.py`, a file ending in `urls.py`
|
|
274
|
+
* and the dependency named Django, and three of those four are names other projects
|
|
275
|
+
* use. redash is a Flask application: it keeps a `manage.py`, and
|
|
276
|
+
* `redash/handlers/settings.py` is the HTTP handler for a user's settings page. Its
|
|
277
|
+
* report said "Backend: flask, django".
|
|
278
|
+
*
|
|
279
|
+
* The dependency is the fact — nobody runs Django without installing it — and the
|
|
280
|
+
* filenames only corroborate it.
|
|
281
|
+
*
|
|
282
|
+
* A fallback for the unreadable-manifest case was written and deleted: a settings
|
|
283
|
+
* file declaring `INSTALLED_APPS` would have stood on its own, and no repository
|
|
284
|
+
* measured reaches it, because every Django project declares Django. The mutation
|
|
285
|
+
* run said so — removing it failed nothing. A rule nothing measures is a rule to
|
|
286
|
+
* delete.
|
|
287
|
+
*/
|
|
271
288
|
const djangoSignals = [];
|
|
289
|
+
const djangoDep = (0, detectContext_1.hasRuntimePyDep)(ctx, 'django');
|
|
290
|
+
if (djangoDep)
|
|
291
|
+
djangoSignals.push({ type: 'dependency', value: 'django' });
|
|
272
292
|
if (ctx.files.all.some((f) => f.endsWith('manage.py') || f === 'manage.py')) {
|
|
273
293
|
djangoSignals.push({ type: 'file', value: 'manage.py' });
|
|
274
294
|
}
|
|
275
295
|
const settingsFile = ctx.files.all.find((f) => f.endsWith('settings.py'));
|
|
276
296
|
if (settingsFile)
|
|
277
297
|
djangoSignals.push({ type: 'file', value: settingsFile });
|
|
278
|
-
if ((0, detectContext_1.hasRuntimePyDep)(ctx, 'django'))
|
|
279
|
-
djangoSignals.push({ type: 'dependency', value: 'django' });
|
|
280
298
|
if (ctx.files.all.some((f) => f.endsWith('urls.py'))) {
|
|
281
299
|
djangoSignals.push({ type: 'file', value: ctx.files.all.find((f) => f.endsWith('urls.py')) });
|
|
282
300
|
}
|
|
283
|
-
if (djangoSignals.length >= 2) {
|
|
301
|
+
if (djangoDep && djangoSignals.length >= 2) {
|
|
284
302
|
frameworks.push('django');
|
|
285
303
|
evidence.push(...djangoSignals);
|
|
286
304
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.detectSecurity = detectSecurity;
|
|
4
4
|
const detectContext_1 = require("./detectContext");
|
|
5
5
|
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
6
|
+
const djangoSettings_1 = require("./djangoSettings");
|
|
6
7
|
const textSearch_1 = require("../utils/textSearch");
|
|
7
8
|
const absenceEvidence_1 = require("./absenceEvidence");
|
|
8
9
|
const valuesFromPackage_1 = require("./structural/valuesFromPackage");
|
|
@@ -111,35 +112,6 @@ function detectCorsConfig(text, file, boundNames) {
|
|
|
111
112
|
}
|
|
112
113
|
return { loose, strict };
|
|
113
114
|
}
|
|
114
|
-
/**
|
|
115
|
-
* A settings file that is Django's, rather than one that shares its name.
|
|
116
|
-
*
|
|
117
|
-
* `diet_hub/api/settings.py` is a FastAPI router that lets an administrator change
|
|
118
|
-
* application options from the browser. It was read as Django's configuration, and on
|
|
119
|
-
* the strength of the filename alone the project was credited with having security
|
|
120
|
-
* middleware it does not have — the one direction of error that matters here, because
|
|
121
|
-
* it hides a missing control rather than inventing a present one.
|
|
122
|
-
*
|
|
123
|
-
* Two conditions, because either alone is wrong. A repository can depend on Django and
|
|
124
|
-
* still own a dozen files called settings.py; a file can declare INSTALLED_APPS in a
|
|
125
|
-
* tutorial that the product does not run. And the first file named settings.py is not
|
|
126
|
-
* the right one: a project with `settings/base.py` and `settings/production.py` has
|
|
127
|
-
* several, so every candidate is examined and the first that is Django's is used.
|
|
128
|
-
*/
|
|
129
|
-
async function findDjangoSettings(ctx) {
|
|
130
|
-
if (!ctx.pythonDeps.includes('django'))
|
|
131
|
-
return null;
|
|
132
|
-
const candidates = ctx.files.all.filter((f) => /(^|\/)settings(_[a-z]+)?\.py$/i.test(f) || /(^|\/)settings\/[a-z_]+\.py$/i.test(f));
|
|
133
|
-
for (const file of candidates) {
|
|
134
|
-
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
135
|
-
if (!text)
|
|
136
|
-
continue;
|
|
137
|
-
if (/^\s*INSTALLED_APPS\s*=/m.test(text) || /^\s*MIDDLEWARE\s*=/m.test(text) || /DJANGO_SETTINGS_MODULE/.test(text)) {
|
|
138
|
-
return { file, text };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
115
|
const RATE_LIMIT_PACKAGES = [
|
|
144
116
|
'express-rate-limit',
|
|
145
117
|
'@upstash/ratelimit',
|
|
@@ -165,6 +137,26 @@ async function detectSecurity(ctx) {
|
|
|
165
137
|
/X-Frame-Options/i,
|
|
166
138
|
/SECURE_HSTS_SECONDS/,
|
|
167
139
|
/securityHeaders/i,
|
|
140
|
+
/**
|
|
141
|
+
* Django's spelling of the same decisions.
|
|
142
|
+
*
|
|
143
|
+
* The list held the HTTP header names and one Django setting, so a project that
|
|
144
|
+
* writes `X_FRAME_OPTIONS = "SAMEORIGIN"` — the setting, with underscores, which
|
|
145
|
+
* is the only way to say it in Django — matched nothing. paperless-ngx sets it
|
|
146
|
+
* and was told at `high` to add security headers.
|
|
147
|
+
*
|
|
148
|
+
* Only the settings that choose a policy. `SECURE_PROXY_SSL_HEADER` is not one
|
|
149
|
+
* of them: it tells Django how to tell it is behind HTTPS, and every deployment
|
|
150
|
+
* behind a proxy needs it whether or not anybody thought about headers. And
|
|
151
|
+
* `SecurityMiddleware` itself stays out, for the reason recorded further down —
|
|
152
|
+
* `django-admin startproject` writes it into every new project.
|
|
153
|
+
*/
|
|
154
|
+
/^\s*X_FRAME_OPTIONS\s*=/m,
|
|
155
|
+
/^\s*SECURE_CONTENT_TYPE_NOSNIFF\s*=/m,
|
|
156
|
+
/^\s*SECURE_BROWSER_XSS_FILTER\s*=/m,
|
|
157
|
+
/^\s*SECURE_REFERRER_POLICY\s*=/m,
|
|
158
|
+
/^\s*SECURE_CROSS_ORIGIN_OPENER_POLICY\s*=/m,
|
|
159
|
+
/^\s*CSP_DEFAULT_SRC\s*=/m,
|
|
168
160
|
], 20);
|
|
169
161
|
const helmet = helmetDep || headerSignals.length > 0;
|
|
170
162
|
/**
|
|
@@ -319,9 +311,30 @@ async function detectSecurity(ctx) {
|
|
|
319
311
|
if (at !== -1) {
|
|
320
312
|
const window = lines.slice(at, Math.min(lines.length, at + 12)).join('\n');
|
|
321
313
|
if (STARLETTE_CORS_CALL.test(window)) {
|
|
322
|
-
const allowList = /allow_origins\s*=\s*\[([^\]]*)\]/.exec(window);
|
|
323
314
|
const hit = { file, line: at + 1, snippet: lines[at].trim().slice(0, 200) };
|
|
324
|
-
|
|
315
|
+
const allowList = /allow_origins\s*=\s*\[([^\]]*)\]/.exec(window);
|
|
316
|
+
if (allowList) {
|
|
317
|
+
if (/^\s*["']\*["']\s*,?\s*$/.test(allowList[1]))
|
|
318
|
+
corsLoose.push(hit);
|
|
319
|
+
else
|
|
320
|
+
corsStrict.push(hit);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* `allow_origins=allowed_origins`, which is how a real application writes it.
|
|
325
|
+
*
|
|
326
|
+
* mealie builds the list from its settings and passes the name, and calling
|
|
327
|
+
* that "no explicit origin restrictions" at `high` reads as advice to add
|
|
328
|
+
* the allowlist it has. The wildcard is written literally when it is meant —
|
|
329
|
+
* `allow_origins=["*"]` — so a name is followed to its assignment, and a
|
|
330
|
+
* value assembled somewhere this cannot see is a configuration rather than a
|
|
331
|
+
* wildcard.
|
|
332
|
+
*/
|
|
333
|
+
const named = /allow_origins\s*=\s*([A-Za-z_][\w.]*)/.exec(window);
|
|
334
|
+
const assigned = named
|
|
335
|
+
? new RegExp(`^\\s*${named[1].split('.').pop()}\\s*=\\s*\\[([^\\]]*)\\]`, 'm').exec(text)
|
|
336
|
+
: null;
|
|
337
|
+
if (assigned && /^\s*["']\*["']\s*,?\s*$/.test(assigned[1]))
|
|
325
338
|
corsLoose.push(hit);
|
|
326
339
|
else
|
|
327
340
|
corsStrict.push(hit);
|
|
@@ -353,7 +366,7 @@ async function detectSecurity(ctx) {
|
|
|
353
366
|
* `CORS_ORIGIN_ALLOW_ALL`, which is what it was called before version 3.5 — is the
|
|
354
367
|
* one line that opens it.
|
|
355
368
|
*/
|
|
356
|
-
const django = await findDjangoSettings(ctx);
|
|
369
|
+
const django = await (0, djangoSettings_1.findDjangoSettings)(ctx);
|
|
357
370
|
if (django) {
|
|
358
371
|
const middlewareLine = django.text
|
|
359
372
|
.split(/\r?\n/)
|
|
@@ -412,7 +425,7 @@ async function detectSecurity(ctx) {
|
|
|
412
425
|
* this repository keeps as a fixture precisely because it is unprotected. What is
|
|
413
426
|
* left is the `SECURE_*` settings above: lines somebody chose to write.
|
|
414
427
|
*/
|
|
415
|
-
const djangoSettings = await findDjangoSettings(ctx);
|
|
428
|
+
const djangoSettings = await (0, djangoSettings_1.findDjangoSettings)(ctx);
|
|
416
429
|
let djangoDebugTrue = false;
|
|
417
430
|
let djangoSecureCookies = true;
|
|
418
431
|
if (djangoSettings) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DetectContext } from './detectContext';
|
|
2
|
+
/**
|
|
3
|
+
* A settings file that is Django's, rather than one that shares its name.
|
|
4
|
+
*
|
|
5
|
+
* `diet_hub/api/settings.py` is a FastAPI router that lets an administrator change
|
|
6
|
+
* application options from the browser. It was read as Django's configuration, and on
|
|
7
|
+
* the strength of the filename alone the project was credited with having security
|
|
8
|
+
* middleware it does not have — the one direction of error that matters here, because
|
|
9
|
+
* it hides a missing control rather than inventing a present one.
|
|
10
|
+
*
|
|
11
|
+
* Two conditions, because either alone is wrong. A repository can depend on Django and
|
|
12
|
+
* still own a dozen files called settings.py; a file can declare INSTALLED_APPS in a
|
|
13
|
+
* tutorial that the product does not run. And the first file named settings.py is not
|
|
14
|
+
* the right one: a project with `settings/base.py` and `settings/production.py` has
|
|
15
|
+
* several, so every candidate is examined and the first that is Django's is used.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findDjangoSettings(ctx: DetectContext): Promise<{
|
|
18
|
+
file: string;
|
|
19
|
+
text: string;
|
|
20
|
+
} | null>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findDjangoSettings = findDjangoSettings;
|
|
4
|
+
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
5
|
+
/**
|
|
6
|
+
* A settings file that is Django's, rather than one that shares its name.
|
|
7
|
+
*
|
|
8
|
+
* `diet_hub/api/settings.py` is a FastAPI router that lets an administrator change
|
|
9
|
+
* application options from the browser. It was read as Django's configuration, and on
|
|
10
|
+
* the strength of the filename alone the project was credited with having security
|
|
11
|
+
* middleware it does not have — the one direction of error that matters here, because
|
|
12
|
+
* it hides a missing control rather than inventing a present one.
|
|
13
|
+
*
|
|
14
|
+
* Two conditions, because either alone is wrong. A repository can depend on Django and
|
|
15
|
+
* still own a dozen files called settings.py; a file can declare INSTALLED_APPS in a
|
|
16
|
+
* tutorial that the product does not run. And the first file named settings.py is not
|
|
17
|
+
* the right one: a project with `settings/base.py` and `settings/production.py` has
|
|
18
|
+
* several, so every candidate is examined and the first that is Django's is used.
|
|
19
|
+
*/
|
|
20
|
+
async function findDjangoSettings(ctx) {
|
|
21
|
+
if (!ctx.pythonDeps.includes('django'))
|
|
22
|
+
return null;
|
|
23
|
+
const candidates = ctx.files.all.filter((f) => /(^|\/)settings(_[a-z]+)?\.py$/i.test(f) || /(^|\/)settings\/[a-z_]+\.py$/i.test(f));
|
|
24
|
+
for (const file of candidates) {
|
|
25
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
26
|
+
if (!text)
|
|
27
|
+
continue;
|
|
28
|
+
if (/^\s*INSTALLED_APPS\s*=/m.test(text) || /^\s*MIDDLEWARE\s*=/m.test(text) || /DJANGO_SETTINGS_MODULE/.test(text)) {
|
|
29
|
+
return { file, text };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.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": {
|