@produtype/core 0.86.0 → 0.88.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.
@@ -38,7 +38,7 @@ async function detectObservability(ctx) {
38
38
  evidence.push({ type: 'dependency', value: d, claim: 'logging' });
39
39
  for (const d of sentryDeps)
40
40
  evidence.push({ type: 'dependency', value: d, claim: 'logging' });
41
- const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/\/(health|healthz|readyz)\b/i, /x-request-id/i, /correlation-id/i, /error\s*handler/i, /RotatingFileHandler/i], 25);
41
+ const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/\/(health|healthz|readyz|livez|alive)\b/i, /x-request-id/i, /correlation-id/i, /error\s*handler/i, /RotatingFileHandler/i], 25);
42
42
  /**
43
43
  * One search, three answers, so the claim is decided per hit rather than per search.
44
44
  *
@@ -47,7 +47,7 @@ async function detectObservability(ctx) {
47
47
  * is what the line is evidence of.
48
48
  */
49
49
  for (const m of hits) {
50
- const claim = /\/(health|healthz|readyz)\b/i.test(m.snippet)
50
+ const claim = /\/(health|healthz|readyz|livez|alive)\b/i.test(m.snippet)
51
51
  ? 'health'
52
52
  : /x-request-id|correlation-id/i.test(m.snippet)
53
53
  ? 'request-id'
@@ -60,7 +60,7 @@ async function detectObservability(ctx) {
60
60
  const healthFiles = ctx.files.all.filter((file) => /(^|\/)(health|healthz|readyz|liveness|readiness)(\/route|\.[a-z]+)?$|(^|\/)(health|healthz|readyz)\//i.test(file));
61
61
  for (const file of healthFiles)
62
62
  evidence.push({ type: 'file', value: file, claim: 'health' });
63
- const hasHealth = healthFiles.length > 0 || hits.some((h) => /\/(health|healthz|readyz)\b/i.test(h.snippet));
63
+ const hasHealth = healthFiles.length > 0 || hits.some((h) => /\/(health|healthz|readyz|livez|alive)\b/i.test(h.snippet));
64
64
  const hasReqId = hits.some((h) => /x-request-id|correlation-id/i.test(h.snippet));
65
65
  // Structured logging without a logging library is still structured logging. What
66
66
  // matters is that entries are machine-readable and correlated, not which package
@@ -19,6 +19,42 @@ const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /all
19
19
  * the first was being caught.
20
20
  */
21
21
  const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i;
22
+ /**
23
+ * A line that names an origin somebody chose.
24
+ *
25
+ * The test is that the value is written down: a quoted string that is not `*`, or a
26
+ * list of them. Everything else — a variable, a concatenation, a method call — is a
27
+ * value the reader of this line cannot see, and a security check does not get to
28
+ * assume it is an allowlist.
29
+ */
30
+ function allowsAChosenOrigin(line, file) {
31
+ /**
32
+ * The line itself asks whether the origin belongs.
33
+ *
34
+ * `!ALLOWED_ORIGINS.includes(origin)` is the allowlist being enforced, and it is
35
+ * the strongest evidence there is — stronger than the declaration it checks
36
+ * against, which may live in an environment variable.
37
+ */
38
+ if (MEMBERSHIP_TEST.test(line))
39
+ return true;
40
+ const assigned = /(?:Access-Control-Allow-Origin|ALLOWED_ORIGINS|allowedOrigins)[^=:,]*[=:,]\s*(.+)$/i.exec(line);
41
+ if (!assigned)
42
+ return false;
43
+ // An origin written down: it has a scheme or a dotted host, which `","` does not.
44
+ if (/["'`](?:https?:\/\/|\*\.)[^"'`]*["'`]|["'`][^"'`]*\.[a-z]{2,}[^"'`]*["'`]/i.test(assigned[1]))
45
+ return true;
46
+ /**
47
+ * Or a value this line cannot see, checked somewhere else in the same file.
48
+ *
49
+ * A list kept in an environment variable is still an allowlist, so the test cannot
50
+ * be "is the value a literal". What separates it from reflection is that somebody
51
+ * asks whether the origin belongs before answering yes — and shopizer never does:
52
+ * `origin = request.getHeader("origin")` goes straight into the header.
53
+ */
54
+ return MEMBERSHIP_TEST.test(file);
55
+ }
56
+ /** `includes`, `contains`, `indexOf`, `has` — asking whether a value belongs. */
57
+ const MEMBERSHIP_TEST = /\.(includes|contains|indexOf|has)\s*\(/i;
22
58
  /**
23
59
  * Flask's answer, which was invisible.
24
60
  *
@@ -163,12 +199,24 @@ async function detectSecurity(ctx) {
163
199
  * The packages the ecosystem names, as distinct from the variables authors do.
164
200
  * Shared between the dependency check below and the binding walk further down.
165
201
  */
202
+ /**
203
+ * Two of these were checked against the wrong manifest for as long as they existed.
204
+ *
205
+ * `hasDep` reads `package.json` and nothing else, so `django-ratelimit` and
206
+ * `slowapi` — a Python package and a Python package — were being looked for among a
207
+ * project's npm dependencies, where they can never be. Written as though they
208
+ * worked, never able to match.
209
+ *
210
+ * Rust joins them, measured: vaultwarden declares `governor` and calls
211
+ * `check_limit_login(&ip.ip)` on its login route, and was told at `high` that it
212
+ * does not throttle authentication.
213
+ */
166
214
  const rateLimitDep = (0, detectContext_1.hasDep)(ctx, 'express-rate-limit') ||
167
215
  (0, detectContext_1.hasDep)(ctx, '@upstash/ratelimit') ||
168
216
  (0, detectContext_1.hasDep)(ctx, 'rate-limiter-flexible') ||
169
217
  (0, detectContext_1.hasDep)(ctx, 'next-rate-limit') ||
170
- (0, detectContext_1.hasDep)(ctx, 'django-ratelimit') ||
171
- (0, detectContext_1.hasDep)(ctx, 'slowapi');
218
+ (0, detectContext_1.hasAnyPyDep)(ctx, ['django-ratelimit', 'slowapi', 'flask-limiter']).length > 0 ||
219
+ (0, detectContext_1.hasAnyRustDep)(ctx, ['governor', 'tower_governor', 'tower-governor', 'actix-governor', 'ratelimit']).length > 0;
172
220
  /**
173
221
  * Throttling by what the protocol says, not by what the variable is called.
174
222
  *
@@ -287,8 +335,25 @@ async function detectSecurity(ctx) {
287
335
  for (const m of (0, textSearch_1.matchLines)(text, ORIGIN_HANDLING, file)) {
288
336
  if (WILDCARD_ORIGIN.test(m.snippet))
289
337
  corsLoose.push(m);
290
- else
338
+ else if (allowsAChosenOrigin(m.snippet, text))
291
339
  corsStrict.push(m);
340
+ /**
341
+ * Anything else sets the header to a value this cannot see.
342
+ *
343
+ * shopizer writes `origin = request.getHeader("origin")` and then
344
+ * `setHeader("Access-Control-Allow-Origin", origin)` — reflecting whatever the
345
+ * caller asked for, which allows every origin there is. The report called it
346
+ * "configured with explicit origins" and passed it: a clean verdict on the one
347
+ * shape this check exists to catch.
348
+ *
349
+ * An allowlist is a fixed set, so it is written down. A variable, a
350
+ * concatenation or a call cannot be shown to be one — it may be a value from
351
+ * configuration, and it may be the request's own header, and nothing in the line
352
+ * says which. `partial` is what that deserves: something is configured and
353
+ * nothing here shows it restricted.
354
+ */
355
+ else
356
+ corsLoose.push(m);
292
357
  }
293
358
  // Only where the middleware is actually imported. A sentence in this tool's own
294
359
  // remediation catalogue — "Replace cors() defaults with an explicit allowlist." —
@@ -36,8 +36,14 @@ const PATTERN_DECLARATION = /^(?:const\s+\w+(?:\s*:[^=]+)?\s*=\s*)?\/(?:[^/\\]|\
36
36
  * A signal that appears only in a comment was never evidence of behaviour, which is the
37
37
  * whole argument: the report cites a line and says "this is what your code does", and a
38
38
  * sentence about the code is not that.
39
+ *
40
+ * `#` is where this went wrong for a whole language. It opens a comment in Python, YAML
41
+ * and shell, and in Rust `#[get("/alive")]` is the route itself — so every Rust
42
+ * attribute was invisible to every search this analyzer makes. vaultwarden declares its
43
+ * liveness endpoint that way and was told it has none; the same blindness covered
44
+ * `#[post(...)]`, every derive and every `#[cfg]`.
39
45
  */
40
- const COMMENT_LINE = /^(?:\/\/|\/\*|\*\/?|#(?!!)|<!--|--\s)/;
46
+ const COMMENT_LINE = /^(?:\/\/|\/\*|\*\/?|#(?![![])(?!!)|<!--|--\s)/;
41
47
  /**
42
48
  * A table of strings is not a third rule, and that was a mistake worth recording.
43
49
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.86.0",
3
+ "version": "0.88.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": {