@produtype/core 1.12.0 → 1.13.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.
@@ -301,7 +301,64 @@ async function detectAuth(ctx) {
301
301
  * is the one that meant three different things in three repositories, and it is the
302
302
  * one the tree answers.
303
303
  */
304
- const unambiguousRoles = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/requireRole/i, /isAdmin/i, /SUPER_ADMIN/i, /roles\.includes\(/i], 20);
304
+ /**
305
+ * Spring says all of this with names it owns, and none of them were here.
306
+ *
307
+ * mall is a Spring Boot shop with a full authorization model — a filter chain built
308
+ * with `authorizeHttpRequests`, a `DynamicAuthorizationManager` comparing the
309
+ * caller's `GrantedAuthority` against the one a path requires, and a
310
+ * `UmsAdminRoleRelationDao` that loads an administrator's roles from the database —
311
+ * and it was told at `medium` that it has no role checks and no permission checks
312
+ * at all.
313
+ *
314
+ * Every word above is Spring Security's or the JSR's: `GrantedAuthority`,
315
+ * `@PreAuthorize`, `@Secured`, `@RolesAllowed`, `hasAuthority`, `hasAnyRole`,
316
+ * `authorizeHttpRequests` and the `antMatchers` it replaced. What mall chose was
317
+ * `Ums`, the prefix on its own classes, and that is exactly what a search must not
318
+ * depend on.
319
+ */
320
+ const SPRING_AUTHORIZATION = [
321
+ /@PreAuthorize\b/,
322
+ /@PostAuthorize\b/,
323
+ /@Secured\b/,
324
+ /@RolesAllowed\b/,
325
+ /\bGrantedAuthority\b/,
326
+ /\bhasAuthority\s*\(/,
327
+ /\bhasAnyAuthority\s*\(/,
328
+ /\bhasAnyRole\s*\(/,
329
+ /\bhasRole\s*\(/,
330
+ ];
331
+ /**
332
+ * `authorizeHttpRequests` is not on that list, and the corpus is why.
333
+ *
334
+ * It was, and `spring-security-defaults` — a fixture whose whole point is a chain
335
+ * with no roles in it, `requests.anyRequest().authenticated()` — started reading as
336
+ * having an authorization model. Every Spring Security setup writes that line: it
337
+ * says the request must be authenticated, which is the question one capability
338
+ * along. The same test Django's `SecurityMiddleware` and Rails' default headers
339
+ * failed — a thing every project has distinguishes nothing.
340
+ *
341
+ * What survives is the part somebody chose: which authority a path requires, and
342
+ * the annotation that says it on a method.
343
+ */
344
+ const unambiguousRoles = [
345
+ ...await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/requireRole/i, /isAdmin/i, /SUPER_ADMIN/i, /roles\.includes\(/i], 20),
346
+ ...await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, SPRING_AUTHORIZATION, 20,
347
+ /**
348
+ * An import is not a check.
349
+ *
350
+ * `import org.springframework.security.core.GrantedAuthority;` names the type
351
+ * and decides nothing; `grantedAuthorities.stream()` twenty files away is where
352
+ * the caller's authority is compared against the one the path requires. The
353
+ * first version of this cited the import, which is the same complaint pocketbase
354
+ * earned two releases ago — a reader is promised the line that decides.
355
+ *
356
+ * Filtered inside the search rather than after it, so the budget is spent on
357
+ * lines that check something: a Java project has one import per file and they
358
+ * would fill it.
359
+ */
360
+ (match) => !/^\s*import\b/.test(match.snippet)),
361
+ ];
305
362
  const comparedRoles = guardedRoles
306
363
  ?? excludeChatTurnRoles(await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/req\.user\.role/i, /user\.role/i, /role\s*===/i], 20));
307
364
  const roleSignals = [...unambiguousRoles, ...comparedRoles].slice(0, 20);
@@ -524,7 +581,7 @@ async function detectAuth(ctx) {
524
581
  {
525
582
  key: 'authz.roles',
526
583
  present: roleSignals.length > 0,
527
- evidence: (0, absenceEvidence_1.evidenceOrSearch)(snippetEvidence(roleSignals), 'role checks', ['role ===', 'hasRole', 'isAdmin', 'user.role', 'roles.includes', '@Roles', 'role_required']),
584
+ evidence: (0, absenceEvidence_1.evidenceOrSearch)(snippetEvidence(roleSignals), 'role checks', ['role ===', 'hasRole', 'isAdmin', 'user.role', 'roles.includes', '@Roles', 'role_required', '@PreAuthorize', '@Secured', 'GrantedAuthority', 'hasAuthority(', 'hasAnyRole(']),
528
585
  },
529
586
  {
530
587
  key: 'authz.permissions',
@@ -225,26 +225,53 @@ async function detectDatabase(ctx) {
225
225
  databases.add('sqlite');
226
226
  evidence.push({ type: 'file', value: 'sqlite db file detected' });
227
227
  }
228
- // docker-compose hints
229
- const composeFile = ctx.files.all.find((f) => /(^|\/)(docker-compose\.ya?ml|compose\.ya?ml)$/.test(f));
230
- if (composeFile) {
228
+ /**
229
+ * Every compose file, not the first one in the list.
230
+ *
231
+ * A repository has several: `docker-compose.yml` beside `.devcontainer/
232
+ * docker-compose.yml`, an override for tests, one per deployment shape. This read
233
+ * whichever came back first and ignored the rest, so a Postgres declared only in
234
+ * the production compose was invisible whenever a development one sorted ahead of
235
+ * it — and which one that is was the filesystem's business until this session
236
+ * sorted the scan.
237
+ *
238
+ * The devcontainer is not excluded here, and that is the difference from
239
+ * `docker.presence` next door. "How does this ship" is answered by the image the
240
+ * product is built into; "what does it store data in" is answered by the database
241
+ * it talks to, and in development that is the one the devcontainer starts. The same
242
+ * file, two questions, two answers.
243
+ */
244
+ const composeFiles = ctx.files.all.filter((f) => /(^|\/)(docker-compose[\w.-]*\.ya?ml|compose[\w.-]*\.ya?ml)$/.test(f)).slice(0, 8);
245
+ /**
246
+ * Each store cited once, from the first file that shows it.
247
+ *
248
+ * immich has seven compose files and all of them run Postgres and Redis, so reading
249
+ * every one turned two facts into eleven citations of the same two facts. A reader
250
+ * is promised a line they can open and argue with; eleven lines saying the same
251
+ * thing is not eleven times the argument.
252
+ */
253
+ const COMPOSE_SERVICES = [
254
+ ['postgres', /image:\s*postgres/i, /postgres:/i],
255
+ ['redis', /image:\s*redis/i, /redis:/i],
256
+ ['mysql', /image:\s*mysql/i, null],
257
+ ['mongodb', /image:\s*mongo/i, null],
258
+ ];
259
+ /**
260
+ * Counted for this reading only, not against `databases`: a store the dependencies
261
+ * already named still deserves the compose line beside it, and skipping on the set
262
+ * removed every compose citation from a project that declares its driver too.
263
+ */
264
+ const citedFromCompose = new Set();
265
+ for (const composeFile of composeFiles) {
231
266
  const text = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, composeFile)) ?? '';
232
- const lower = text.toLowerCase();
233
- if (/image:\s*postgres/i.test(text) || lower.includes('postgres:')) {
234
- databases.add('postgres');
235
- evidence.push({ type: 'file', value: 'postgres in docker-compose', file: composeFile });
236
- }
237
- if (/image:\s*redis/i.test(text) || lower.includes('redis:')) {
238
- databases.add('redis');
239
- evidence.push({ type: 'file', value: 'redis in docker-compose', file: composeFile });
240
- }
241
- if (/image:\s*mysql/i.test(text)) {
242
- databases.add('mysql');
243
- evidence.push({ type: 'file', value: 'mysql in docker-compose', file: composeFile });
244
- }
245
- if (/image:\s*mongo/i.test(text)) {
246
- databases.add('mongodb');
247
- evidence.push({ type: 'file', value: 'mongo in docker-compose', file: composeFile });
267
+ for (const [name, image, service] of COMPOSE_SERVICES) {
268
+ if (citedFromCompose.has(name))
269
+ continue;
270
+ if (!image.test(text) && !(service && service.test(text)))
271
+ continue;
272
+ databases.add(name);
273
+ citedFromCompose.add(name);
274
+ evidence.push({ type: 'file', value: `${name} in docker-compose`, file: composeFile });
248
275
  }
249
276
  }
250
277
  // DATABASE_URL env reference
@@ -97,11 +97,21 @@ async function detectJobs(ctx) {
97
97
  for (const name of processScripts) {
98
98
  evidence.push({ type: 'note', value: `package.json script "${name}" starts a separate process` });
99
99
  }
100
- // A compose service that runs one.
101
- const composeFile = ctx.files.all.find((file) => /(^|\/)(docker-compose\.ya?ml|compose\.ya?ml)$/.test(file));
102
- const composeText = composeFile ? ((await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, composeFile)) ?? '') : '';
103
- const composeWorker = /^\s{2}(worker|jobs?|scheduler|cron|consumer)[a-z0-9_-]*:/im.test(composeText);
104
- if (composeWorker && composeFile) {
100
+ /**
101
+ * A compose service that runs one — in any of the compose files, not the first.
102
+ *
103
+ * A repository has several, and this read whichever came back first: a worker
104
+ * declared in the production compose was invisible whenever a development one
105
+ * sorted ahead of it. The same first-match reading that made `docker.presence`
106
+ * answer "how does this ship" with a devcontainer, one detector along.
107
+ */
108
+ const composeFiles = ctx.files.all.filter((file) => /(^|\/)(docker-compose[\w.-]*\.ya?ml|compose[\w.-]*\.ya?ml)$/.test(file)).slice(0, 8);
109
+ let composeWorker = false;
110
+ for (const composeFile of composeFiles) {
111
+ const composeText = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, composeFile)) ?? '';
112
+ if (!/^\s{2}(worker|jobs?|scheduler|cron|consumer)[a-z0-9_-]*:/im.test(composeText))
113
+ continue;
114
+ composeWorker = true;
105
115
  evidence.push({ type: 'file', value: 'a worker service in compose', file: composeFile });
106
116
  }
107
117
  const declarations = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, JOB_DECLARATIONS, 20);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "1.12.0",
3
+ "version": "1.13.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": {