@produtype/core 1.18.0 → 1.20.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.
@@ -5,6 +5,7 @@ const detectContext_1 = require("./detectContext");
5
5
  const textSearch_1 = require("../utils/textSearch");
6
6
  const absenceEvidence_1 = require("./absenceEvidence");
7
7
  const valuesFromPackage_1 = require("./structural/valuesFromPackage");
8
+ const readTextFileSafe_1 = require("../utils/readTextFileSafe");
8
9
  /**
9
10
  * Logging packages, which the ecosystem names and the author does not.
10
11
  *
@@ -78,7 +79,42 @@ async function detectObservability(ctx) {
78
79
  const actuator = (0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-boot-starter-actuator']);
79
80
  for (const dep of actuator)
80
81
  evidence.push({ type: 'dependency', value: dep, claim: 'health' });
81
- const hasHealth = healthFiles.length > 0
82
+ /**
83
+ * The probe a platform declares, which is the operator's half of the same answer.
84
+ *
85
+ * immich declares `healthcheck:` for its services in all four of its compose files
86
+ * and was told it has no health endpoint. Something is being probed — that is what
87
+ * the key means — and the endpoint it probes is inside the image, where no text
88
+ * search here will find it.
89
+ *
90
+ * `HEALTHCHECK` is Docker's instruction, `healthcheck:` is compose's key, and
91
+ * `livenessProbe:` and `readinessProbe:` are Kubernetes' field names. None is the
92
+ * author's word, and each one is a statement that this service answers a health
93
+ * question.
94
+ *
95
+ * `disable: true` is the one that says the opposite — compose's way of switching
96
+ * off an image's own check — so a block carrying it is not counted.
97
+ */
98
+ const deploymentManifests = ctx.files.all.filter((file) => /(^|\/)(docker-compose[\w.-]*\.ya?ml|compose[\w.-]*\.ya?ml|Dockerfile[\w.-]*)$/.test(file)
99
+ || /(^|\/)(k8s|kubernetes|helm|deploy|charts)\/.*\.ya?ml$/i.test(file)).slice(0, 12);
100
+ const declaredProbes = [];
101
+ for (const file of deploymentManifests) {
102
+ const text = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file)) ?? '';
103
+ const lines = text.split(/\r?\n/);
104
+ for (let i = 0; i < lines.length; i++) {
105
+ if (!/^\s*(?:HEALTHCHECK\s|healthcheck\s*:|livenessProbe\s*:|readinessProbe\s*:)/.test(lines[i]))
106
+ continue;
107
+ /** compose writes `healthcheck:` then `disable: true` to switch the image's own off. */
108
+ if (lines.slice(i + 1, i + 3).some((line) => /^\s*disable\s*:\s*true/.test(line)))
109
+ continue;
110
+ declaredProbes.push({ type: 'snippet', value: lines[i].trim().slice(0, 200), file, line: i + 1, claim: 'health' });
111
+ break;
112
+ }
113
+ }
114
+ for (const probe of declaredProbes.slice(0, 3))
115
+ evidence.push(probe);
116
+ const hasHealth = declaredProbes.length > 0
117
+ || healthFiles.length > 0
82
118
  || actuator.length > 0
83
119
  || hits.some((h) => /\/(health|healthz|readyz|livez|alive)\b/i.test(h.snippet));
84
120
  const hasReqId = hits.some((h) => /x-request-id|correlation-id/i.test(h.snippet));
@@ -146,7 +182,7 @@ async function detectObservability(ctx) {
146
182
  const hasStructuredLogging = logDeps.length > 0 || structuredHits.length > 0;
147
183
  const hasAnyLogging = hasStructuredLogging || plainLogging.length > 0;
148
184
  if (!hasHealth) {
149
- evidence.push(...(0, absenceEvidence_1.searchedFor)('a health endpoint', ['/health', '/healthz', '/readyz', 'a health, healthz, readyz, liveness or readiness route file'], 'health'));
185
+ evidence.push(...(0, absenceEvidence_1.searchedFor)('a health endpoint', ['/health', '/healthz', '/readyz', 'a health, healthz, readyz, liveness or readiness route file', 'a HEALTHCHECK, a compose healthcheck or a Kubernetes liveness probe'], 'health'));
150
186
  }
151
187
  if (!hasAnyLogging) {
152
188
  evidence.push(...(0, absenceEvidence_1.searchedFor)('logging', ['winston', 'pino', 'morgan', 'bunyan', 'Monolog', 'slog', 'zap', 'logrus', 'slf4j', 'Rails.logger', 'tracing::', 'logger.info/warn/error/debug', 'Logger.info', 'error_log(', 'JSON.stringify with a level field'], 'logging'));
@@ -26,6 +26,26 @@ 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
+ }
29
49
  /**
30
50
  * An entry that says what something is called.
31
51
  *
@@ -167,7 +187,7 @@ function declaresAType(trimmed) {
167
187
  }
168
188
  function declaresRatherThanDoes(line) {
169
189
  const trimmed = line.trim();
170
- return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed) || declaresAType(trimmed) || spellsItsOwnName(trimmed) || NAMES_SOMETHING.test(trimmed);
190
+ return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed) || declaresAType(trimmed) || spellsItsOwnName(trimmed) || NAMES_SOMETHING.test(trimmed) || isALoneHeaderName(trimmed);
171
191
  }
172
192
  /**
173
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.18.0",
3
+ "version": "1.20.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": {