@produtype/core 1.12.1 → 1.14.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
|
-
|
|
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',
|
|
@@ -726,6 +726,44 @@ async function detectSecurity(ctx) {
|
|
|
726
726
|
* `CORS_ORIGIN_ALLOW_ALL`, which is what it was called before version 3.5 — is the
|
|
727
727
|
* one line that opens it.
|
|
728
728
|
*/
|
|
729
|
+
/**
|
|
730
|
+
* Symfony's answer, which is a bundle and the file it reads.
|
|
731
|
+
*
|
|
732
|
+
* kimai scored 93 and `production_ready` while its API answered every origin on
|
|
733
|
+
* earth: `config/packages/nelmio_cors.yaml` sets `allow_origin: []` in its defaults
|
|
734
|
+
* — the bundle's own comment says "for security reasons we do not allow CORS by
|
|
735
|
+
* default" — and then `allow_origin: ['*']` under `^/api/`. The report said "CORS
|
|
736
|
+
* configuration not detected" and gave no verdict at all.
|
|
737
|
+
*
|
|
738
|
+
* `nelmio/cors-bundle` is what a Symfony project installs for this, `nelmio_cors` is
|
|
739
|
+
* the configuration key the bundle defines and `allow_origin` is its setting. None
|
|
740
|
+
* of the three is the author's to choose; the values are.
|
|
741
|
+
*
|
|
742
|
+
* Every `allow_origin` in the file is read, not the first. A bundle configures
|
|
743
|
+
* defaults and then overrides them per path, and it is the override that is
|
|
744
|
+
* reachable — the same reason the ASP.NET branch above reports the open one where a
|
|
745
|
+
* project ships both.
|
|
746
|
+
*/
|
|
747
|
+
for (const file of ctx.files.all.filter((f) => /(^|\/)config\/packages\/[\w.-]*nelmio_cors[\w.-]*\.ya?ml$/i.test(f))) {
|
|
748
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
749
|
+
if (!text)
|
|
750
|
+
continue;
|
|
751
|
+
const lines = text.split(/\r?\n/);
|
|
752
|
+
for (let i = 0; i < lines.length; i++) {
|
|
753
|
+
const origins = /^\s*allow_origin\s*:\s*(.*)$/.exec(lines[i]);
|
|
754
|
+
if (!origins)
|
|
755
|
+
continue;
|
|
756
|
+
const value = origins[1].trim();
|
|
757
|
+
/** An empty list is the bundle refusing every cross-origin request. */
|
|
758
|
+
if (value === '[]' || value === '')
|
|
759
|
+
continue;
|
|
760
|
+
const hit = { file, line: i + 1, snippet: lines[i].trim().slice(0, 200) };
|
|
761
|
+
if (/\[\s*['"]\*['"]\s*\]/.test(value) || value === "'*'" || value === '"*"')
|
|
762
|
+
corsLoose.push(hit);
|
|
763
|
+
else
|
|
764
|
+
corsStrict.push(hit);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
729
767
|
/**
|
|
730
768
|
* Laravel's answer, which is a file with a name the framework chose.
|
|
731
769
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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": {
|