@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
- // Django
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
- if (!allowList || /^\s*["']\*["']\s*,?\s*$/.test(allowList[1]))
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.80.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": {