@produtype/core 0.90.0 → 0.92.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.
@@ -157,6 +157,23 @@ function parsePyprojectSections(text, runtimeOnly) {
157
157
  }
158
158
  return deps;
159
159
  }
160
+ /**
161
+ * Everything after `src/main/java` is a package name, not a project layout.
162
+ *
163
+ * spring-petclinic lives in `org.springframework.samples.petclinic`, so every one of
164
+ * its thirty Java files sits under a path segment called `samples` — and the rule that
165
+ * removes sample code removed the whole application. The report saw a project with no
166
+ * Java in it at all: twelve files, an HTML front end, no backend, and a profile of
167
+ * `client-app` at high confidence for a Spring Boot server.
168
+ *
169
+ * Maven and Gradle both fix that prefix, so the segments after it are the author's
170
+ * package and mean nothing about the layout. A real sample directory sits beside
171
+ * `src`, not inside its source root — ktor-documentation keeps its under
172
+ * `codeSnippets/snippets/`, which this still removes.
173
+ */
174
+ function isAPackageName(file) {
175
+ return /(^|\/)src\/(main|test)\/(java|kotlin|scala|groovy)\//.test(file);
176
+ }
160
177
  function isTestOrExamplePath(file) {
161
178
  // `__mocks__` was missing, and a mock is the most misleading file in a repository:
162
179
  // `application_fee_percent: null` inside a Stripe fixture made an open-source CRM read
@@ -219,7 +236,7 @@ function isTestOrExamplePath(file) {
219
236
  * spell it. A repository whose every source file is a sample now has no source
220
237
  * files, and says so — which is the honest answer for one.
221
238
  */
222
- || /(^|\/)(samples?|snippets?|codesnippets)(\/|$)/i.test(file)
239
+ || (/(^|\/)(samples?|snippets?|codesnippets)(\/|$)/i.test(file) && !isAPackageName(file))
223
240
  /**
224
241
  * Somebody else's code, vendored in.
225
242
  *
@@ -60,7 +60,25 @@ 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|livez|alive)\b/i.test(h.snippet));
63
+ /**
64
+ * Spring Boot Actuator, which is the endpoint rather than a route to it.
65
+ *
66
+ * Adding `spring-boot-starter-actuator` publishes `/actuator/health` — exposed by
67
+ * default, with no route written anywhere — so a project that has it cannot be
68
+ * found by searching for a path. spring-petclinic declares it in both its pom and
69
+ * its build.gradle and was reported as having no health endpoint.
70
+ *
71
+ * A deliberate dependency, unlike two things this file and its neighbour decline to
72
+ * count: Dropwizard's admin `/healthcheck`, which every Dropwizard application has
73
+ * for being Dropwizard, and Django's `SecurityMiddleware`, which `startproject`
74
+ * writes into every new project. Actuator is in no Spring application by default.
75
+ */
76
+ const actuator = (0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-boot-starter-actuator']);
77
+ for (const dep of actuator)
78
+ evidence.push({ type: 'dependency', value: dep, claim: 'health' });
79
+ const hasHealth = healthFiles.length > 0
80
+ || actuator.length > 0
81
+ || hits.some((h) => /\/(health|healthz|readyz|livez|alive)\b/i.test(h.snippet));
64
82
  const hasReqId = hits.some((h) => /x-request-id|correlation-id/i.test(h.snippet));
65
83
  // Structured logging without a logging library is still structured logging. What
66
84
  // matters is that entries are machine-readable and correlated, not which package
@@ -196,7 +196,45 @@ async function detectSecurity(ctx) {
196
196
  /^\s*SECURE_CROSS_ORIGIN_OPENER_POLICY\s*=/m,
197
197
  /^\s*CSP_DEFAULT_SRC\s*=/m,
198
198
  ], 20);
199
- const helmet = helmetDep || headerSignals.length > 0;
199
+ /**
200
+ * Spring Security, which writes the headers without being asked.
201
+ *
202
+ * Adding `spring-boot-starter-security` and configuring an `HttpSecurity` chain
203
+ * gives every response `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`
204
+ * and a no-store `Cache-Control` — the framework's defaults, applied whether or not
205
+ * anybody writes a line about headers. shopizer does exactly that and was told it
206
+ * has none.
207
+ *
208
+ * Unlike Django's `SecurityMiddleware`, which is deliberately not counted a few
209
+ * lines down: `django-admin startproject` writes that into every new project, so it
210
+ * distinguishes nothing, and its HSTS and nosniff behaviour still waits on
211
+ * `SECURE_*` settings. This starter is not in every Spring project — petclinic has
212
+ * no security at all — and its defaults need no settings.
213
+ *
214
+ * The chain is what is required. A project can pull the starter for method-level
215
+ * authorization in something that serves no requests, and then there is no filter
216
+ * chain and no headers — which is what the second fixture holds.
217
+ *
218
+ * The dependency check in front of it is a pre-filter and nothing more: it keeps
219
+ * this search off every file of every non-Spring repository. Removing it fails no
220
+ * test, and the comment says so rather than implying a safety it does not provide.
221
+ */
222
+ const springSecurity = (0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-boot-starter-security', 'spring-security-config']);
223
+ const springFilterChain = springSecurity.length > 0
224
+ ? await (0, textSearch_1.searchInFiles)(ctx.root, source, [/HttpSecurity\s+\w+|\bhttp\s*\n?\s*\.\s*(?:authorizeHttpRequests|authorizeRequests|securityMatcher|antMatcher)/], 2)
225
+ : [];
226
+ if (springFilterChain.length > 0) {
227
+ for (const dep of springSecurity)
228
+ evidence.push({ type: 'dependency', value: dep, claim: 'headers' });
229
+ evidence.push({
230
+ type: 'snippet',
231
+ value: springFilterChain[0].snippet,
232
+ file: springFilterChain[0].file,
233
+ line: springFilterChain[0].line,
234
+ claim: 'headers',
235
+ });
236
+ }
237
+ const helmet = helmetDep || headerSignals.length > 0 || springFilterChain.length > 0;
200
238
  /**
201
239
  * The packages the ecosystem names, as distinct from the variables authors do.
202
240
  * Shared between the dependency check below and the binding walk further down.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.90.0",
3
+ "version": "0.92.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": {