@produtype/core 1.17.2 → 1.19.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.
@@ -854,10 +854,26 @@ async function detectSecurity(ctx) {
854
854
  const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
855
855
  if (!text)
856
856
  continue;
857
+ /**
858
+ * The key has to be there, and "not found" is not "wide open".
859
+ *
860
+ * This read a missing `allowed_origins` as the permissive case, which is
861
+ * blindness turned into the loudest verdict there is. appwrite is not a Laravel
862
+ * application — it keeps its own `app/config/cors.php` holding methods and
863
+ * headers, with origins decided in `src/Appwrite/Network/Cors.php`, where they
864
+ * are matched by hostname and echoed back one at a time, never as a wildcard.
865
+ * The path matched, the key did not, and a product with a careful allowlist was
866
+ * reported at `high` as having opened itself to everybody.
867
+ *
868
+ * Laravel publishes that key with the file, so a `config/cors.php` without it is
869
+ * somebody else's file at the same path. This branch now says nothing about it.
870
+ */
857
871
  const origins = /'allowed_origins'\s*=>\s*\[([^\]]*)\]/.exec(text);
858
- const line = origins ? text.slice(0, origins.index).split('\n').length : 1;
859
- const hit = { file, line, snippet: (origins?.[0] ?? "config/cors.php").replace(/\s+/g, ' ').trim().slice(0, 200) };
860
- if (!origins || /^\s*'\*'\s*,?\s*$/.test(origins[1]))
872
+ if (!origins)
873
+ continue;
874
+ const line = text.slice(0, origins.index).split('\n').length;
875
+ const hit = { file, line, snippet: origins[0].replace(/\s+/g, ' ').trim().slice(0, 200) };
876
+ if (/^\s*'\*'\s*,?\s*$/.test(origins[1]))
861
877
  corsLoose.push(hit);
862
878
  else
863
879
  corsStrict.push(hit);
@@ -26,6 +26,44 @@ const blockComments_1 = require("../analyzer/blockComments");
26
26
  * and is left alone.
27
27
  */
28
28
  const PATTERN_DECLARATION = /^(?:const\s+\w+(?:\s*:[^=]+)?\s*=\s*)?\/(?:[^/\\]|\\.)+\/[gimsuy]*\s*[,;]?$/;
29
+ /**
30
+ * A header name on a line of its own says which header, not what value.
31
+ *
32
+ * saleor's ASGI handler writes the response headers as a list of tuples, so
33
+ * `b"access-control-allow-origin",` sits alone on line 50 and the origin it will
34
+ * carry is on line 51. Saleor only appends that pair when the caller's origin matched
35
+ * its allowlist, and answers 400 when it did not — a strict policy, reported at
36
+ * `high` as one with no restrictions, on the strength of a line that cannot show a
37
+ * policy because there is no value on it.
38
+ *
39
+ * The bare-string-in-a-list rule this file already records stays intact:
40
+ * `'django.contrib.auth',` in an INSTALLED_APPS array *is* the behaviour. A header
41
+ * name is not, for the reason above — it names the slot, and the slot is filled
42
+ * somewhere else.
43
+ */
44
+ const LONE_HEADER_NAME = /^[bru]?(['"`])([^'"`]+)\1\s*,?$/i;
45
+ function isALoneHeaderName(trimmed) {
46
+ const literal = LONE_HEADER_NAME.exec(trimmed);
47
+ return literal !== null && HTTP_HEADER_NAME.test(literal[2].replace(/\b[a-z]/g, (c) => c.toUpperCase()));
48
+ }
49
+ /**
50
+ * An entry that says what something is called.
51
+ *
52
+ * appwrite ships a catalogue of function templates, and one line of it is
53
+ * `'name' => 'ALLOWED_ORIGINS',` — an environment variable *the user's* function may
54
+ * set, described in a table beside its placeholder and its help text. After the other
55
+ * two citations were withdrawn it was carrying appwrite's whole `high` cross-origin
56
+ * finding on its own, about a product whose real policy matches origins by hostname
57
+ * and never answers with a wildcard.
58
+ *
59
+ * A string bound to `name`, `key`, `label` or `id` is what a thing is called. That is
60
+ * not the same as a bare string in a list, which this file deliberately still reads —
61
+ * `'django.contrib.auth',` in an INSTALLED_APPS array *is* the behaviour, and the
62
+ * measurement that established it cost a real Django project eight points for being
63
+ * legible. The difference is the key: one line says "this is called X", the other says
64
+ * "X".
65
+ */
66
+ const NAMES_SOMETHING = /^["']?(?:name|key|label|id|title|field|env|variable)["']?\s*(?:=>|:)\s*["'`][^"'`]+["'`],?$/i;
29
67
  /**
30
68
  * A constant that spells its own name.
31
69
  *
@@ -149,7 +187,7 @@ function declaresAType(trimmed) {
149
187
  }
150
188
  function declaresRatherThanDoes(line) {
151
189
  const trimmed = line.trim();
152
- return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed) || declaresAType(trimmed) || spellsItsOwnName(trimmed);
190
+ return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed) || declaresAType(trimmed) || spellsItsOwnName(trimmed) || NAMES_SOMETHING.test(trimmed) || isALoneHeaderName(trimmed);
153
191
  }
154
192
  /**
155
193
  * Whether this line may be quoted back to the reader as something the code does.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "1.17.2",
3
+ "version": "1.19.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": {