@produtype/core 1.24.0 → 1.26.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.
|
@@ -556,6 +556,36 @@ async function detectAuth(ctx) {
|
|
|
556
556
|
present: apiKeySignals.length > 0,
|
|
557
557
|
evidence: (0, absenceEvidence_1.evidenceOrSearch)(snippetEvidence(apiKeySignals), 'keys this product issues and checks', ['x-api-key', 'api_keys.find', 'api_keys.where', 'hashedApiKey', 'token scope']),
|
|
558
558
|
},
|
|
559
|
+
{
|
|
560
|
+
/**
|
|
561
|
+
* Whether this project's routes are in a language these patterns can read.
|
|
562
|
+
*
|
|
563
|
+
* A gestionale with `/accedi`, `/recupero-password` and `/conferma-email` —
|
|
564
|
+
* bcrypt, jsonwebtoken, the whole flow present — was told at `high` that it has
|
|
565
|
+
* no password reset and no erasure. The searches look for "forgot password",
|
|
566
|
+
* "password_reset" and "reset token", and `recuperoPassword` is none of those.
|
|
567
|
+
* Adding the Italian words would fix Italian and leave Japanese, Spanish and
|
|
568
|
+
* every other language exactly where they were, which is the guessing this
|
|
569
|
+
* analyzer exists to stop.
|
|
570
|
+
*
|
|
571
|
+
* What can be known is whether we ever read a route name at all. `routeSignals`
|
|
572
|
+
* matches `/login`, `/register`, `/signin` and their relatives; where a project
|
|
573
|
+
* authenticates — proved by a hashing package, which is not a word anybody
|
|
574
|
+
* chose — and *none* of those ever matched, its routes are named in something
|
|
575
|
+
* else. The reset search never had a chance, and the difference between "you
|
|
576
|
+
* have no reset" and "we could not read your routes" is the whole difference
|
|
577
|
+
* between a finding and a guess.
|
|
578
|
+
*
|
|
579
|
+
* A framework that supplies the flow is checked before this: Django's
|
|
580
|
+
* `auth.urls`, Devise and `Auth::routes(` are read whatever the routes around
|
|
581
|
+
* them are called.
|
|
582
|
+
*/
|
|
583
|
+
key: 'auth.routesAreReadable',
|
|
584
|
+
present: routeSignals.length > 0,
|
|
585
|
+
evidence: routeSignals.length > 0
|
|
586
|
+
? snippetEvidence(routeSignals.slice(0, 3))
|
|
587
|
+
: (0, absenceEvidence_1.searchedFor)('a route named in English', ['/login', '/register', '/signin', '/signup', '/logout']),
|
|
588
|
+
},
|
|
559
589
|
{
|
|
560
590
|
key: 'auth.passwordReset',
|
|
561
591
|
present: passwordResetSignals.length > 0 || passwordResetFiles.length > 0,
|
|
@@ -8,7 +8,33 @@ function toEvidence(matches) {
|
|
|
8
8
|
return matches.map((m) => ({ type: 'snippet', value: m.snippet, file: m.file, line: m.line }));
|
|
9
9
|
}
|
|
10
10
|
async function detectGdpr(ctx) {
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The consent vendors, who name themselves.
|
|
13
|
+
*
|
|
14
|
+
* `cookie-consent`, `consentGiven` and `gdprConsent` are four spellings of one
|
|
15
|
+
* English word, and a site whose banner says `consensoCookie` has none of them —
|
|
16
|
+
* the same failure the password reset had, in a capability where the whole audience
|
|
17
|
+
* is European and half of it does not write in English.
|
|
18
|
+
*
|
|
19
|
+
* What the audience does have is a vendor. Iubenda, Cookiebot, OneTrust,
|
|
20
|
+
* Usercentrics, CookieHub and Klaro all publish a banner you embed, and most sites
|
|
21
|
+
* embed the script rather than install a package — so the domain the script is
|
|
22
|
+
* fetched from is the anchor, beside the package for the ones that ship on npm.
|
|
23
|
+
* `cdn.iubenda.com` is Iubenda's; nobody else's site serves from it.
|
|
24
|
+
*/
|
|
25
|
+
const CONSENT_VENDORS = [
|
|
26
|
+
/cdn\.iubenda\.com|iubenda\.com\/(?:privacy-policy|cookie-policy)/i,
|
|
27
|
+
/consent\.cookiebot\.com|cookiebot/i,
|
|
28
|
+
/cdn\.cookielaw\.org|onetrust|OptanonWrapper/i,
|
|
29
|
+
/app\.usercentrics\.eu|usercentrics/i,
|
|
30
|
+
/cookiehub|cdn\.cookiehub\.eu/i,
|
|
31
|
+
/vanilla-cookieconsent|\bklaro\b|tarteaucitron|osano|termly/i,
|
|
32
|
+
/@axeptio|axeptio\.eu/i,
|
|
33
|
+
];
|
|
34
|
+
const consent = [
|
|
35
|
+
...await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/cookie[-_ ]?consent/i, /consentGiven/i, /privacyConsent/i, /gdprConsent/i], 20),
|
|
36
|
+
...await (0, textSearch_1.searchInFiles)(ctx.root, [...ctx.files.source, ...ctx.files.all.filter((f) => /\.html?$/i.test(f))], CONSENT_VENDORS, 10),
|
|
37
|
+
];
|
|
12
38
|
/**
|
|
13
39
|
* The same duty, in the words each ecosystem actually uses.
|
|
14
40
|
*
|
|
@@ -102,7 +128,7 @@ async function detectGdpr(ctx) {
|
|
|
102
128
|
* produced it: these are the same terms the patterns above match.
|
|
103
129
|
*/
|
|
104
130
|
const TERMS = {
|
|
105
|
-
consent: ['a consent record', ['cookie-consent', 'consentGiven', 'privacyConsent', 'gdprConsent']],
|
|
131
|
+
consent: ['a consent record', ['cookie-consent', 'consentGiven', 'privacyConsent', 'gdprConsent', 'Iubenda, Cookiebot, OneTrust, Usercentrics, CookieHub, Klaro or Axeptio']],
|
|
106
132
|
export: ['an export of personal data', ['/gdpr/export', 'exportUserData', 'personalDataExport', 'dataSubject', 'rightToAccess', '"data portability"']],
|
|
107
133
|
erasure: ['an erasure flow', ['erasure', '"delete account"', '"right to be forgotten"', '"delete user data"', '"delete personal data"']],
|
|
108
134
|
retention: ['a retention or purge policy', ['gdpr/privacy near retention/purge/delete', 'retention/purge/delete near personal data', '"delete personal data older than"']],
|
|
@@ -3,6 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.toFindingId = toFindingId;
|
|
4
4
|
exports.evaluateExpectedCapabilities = evaluateExpectedCapabilities;
|
|
5
5
|
const productProfiles_1 = require("./productProfiles");
|
|
6
|
+
/**
|
|
7
|
+
* Whether this project's routes are named in a language these searches can read.
|
|
8
|
+
*
|
|
9
|
+
* A gestionale with `/accedi`, `/recupero-password` and `/conferma-email` — bcrypt,
|
|
10
|
+
* jsonwebtoken, the whole flow present — was told at `high` that it has no password
|
|
11
|
+
* reset and no email verification. The searches look for "forgot password",
|
|
12
|
+
* "password_reset" and "verify email", and `recuperoPassword` is none of those.
|
|
13
|
+
* Adding the Italian words would fix Italian and leave Japanese, Spanish and every
|
|
14
|
+
* other language where they were, which is the guessing this analyzer exists to stop.
|
|
15
|
+
*
|
|
16
|
+
* What can be known is whether a route name was ever read at all. Where a project
|
|
17
|
+
* authenticates — proved by a password-hashing package, which is nobody's choice of
|
|
18
|
+
* word — and none of `/login`, `/register`, `/signin` ever matched, its routes are
|
|
19
|
+
* named in something else. The reset search never had a chance, and "we could not
|
|
20
|
+
* read your routes" is a different sentence from "you have no reset".
|
|
21
|
+
*
|
|
22
|
+
* Blindness may turn a verdict into no verdict; it may never turn it into the
|
|
23
|
+
* opposite verdict. The same rule the report applies to a language it cannot parse,
|
|
24
|
+
* applied to a vocabulary it cannot read.
|
|
25
|
+
*/
|
|
26
|
+
function routesWentUnread(analysis) {
|
|
27
|
+
return detector(analysis, 'auth.core')?.present === true
|
|
28
|
+
&& detector(analysis, 'auth.routesAreReadable')?.present === false;
|
|
29
|
+
}
|
|
6
30
|
function detector(analysis, key) {
|
|
7
31
|
return analysis.detectors[key];
|
|
8
32
|
}
|
|
@@ -35,10 +59,12 @@ function deriveStatus(analysis, capability) {
|
|
|
35
59
|
const managedOnly = detector(analysis, 'auth.externalIdentityOnly')?.present === true;
|
|
36
60
|
if (managedOnly && detector(analysis, 'auth.core')?.present === true)
|
|
37
61
|
return 'not_applicable';
|
|
38
|
-
return 'missing';
|
|
62
|
+
return routesWentUnread(analysis) ? 'unknown' : 'missing';
|
|
39
63
|
}
|
|
40
64
|
case 'auth.email-verification': {
|
|
41
|
-
|
|
65
|
+
if (detector(analysis, 'auth.emailVerification')?.present)
|
|
66
|
+
return 'present';
|
|
67
|
+
return routesWentUnread(analysis) ? 'unknown' : 'missing';
|
|
42
68
|
}
|
|
43
69
|
case 'authz.roles': {
|
|
44
70
|
/**
|
|
@@ -75,11 +101,29 @@ function deriveStatus(analysis, capability) {
|
|
|
75
101
|
case 'gdpr.consent': {
|
|
76
102
|
return detector(analysis, 'gdpr.consent.route')?.present ? 'present' : 'missing';
|
|
77
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Article 20 and article 17 are both routes, and both were read by their English
|
|
106
|
+
* names.
|
|
107
|
+
*
|
|
108
|
+
* `exportUserData`, `UserExport`, `erasure`, `"delete account"` — a gestionale
|
|
109
|
+
* whose route is `POST /cancella-account` has none of them, and was told at
|
|
110
|
+
* `high` that it offers neither. The one anchor that survives translation is a
|
|
111
|
+
* `DELETE` on `/account` or `/users/me`, which is HTTP's verb and a conventional
|
|
112
|
+
* path, and it is already read.
|
|
113
|
+
*
|
|
114
|
+
* Where even that did not fire and no route name was ever read in this project,
|
|
115
|
+
* these are the same unasked question the password reset is: the search could not
|
|
116
|
+
* reach a vocabulary it does not know.
|
|
117
|
+
*/
|
|
78
118
|
case 'gdpr.export': {
|
|
79
|
-
|
|
119
|
+
if (detector(analysis, 'gdpr.export.route')?.present)
|
|
120
|
+
return 'present';
|
|
121
|
+
return routesWentUnread(analysis) ? 'unknown' : 'missing';
|
|
80
122
|
}
|
|
81
123
|
case 'gdpr.erasure': {
|
|
82
|
-
|
|
124
|
+
if (detector(analysis, 'gdpr.erasure.route')?.present)
|
|
125
|
+
return 'present';
|
|
126
|
+
return routesWentUnread(analysis) ? 'unknown' : 'missing';
|
|
83
127
|
}
|
|
84
128
|
case 'gdpr.retention': {
|
|
85
129
|
return detector(analysis, 'gdpr.retention.job')?.present ? 'present' : 'missing';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.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": {
|