@produtype/core 1.23.0 → 1.25.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,
|
|
@@ -258,6 +258,33 @@ async function detectMobile(ctx) {
|
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Xcode has generated the Info.plist from build settings since version 13.
|
|
263
|
+
*
|
|
264
|
+
* damus declares five purpose strings and its `Info.plist` holds none of them:
|
|
265
|
+
* they are `INFOPLIST_KEY_NSCameraUsageDescription` and its four siblings in
|
|
266
|
+
* `project.pbxproj`, and again in `de.lproj/InfoPlist.strings` for every language
|
|
267
|
+
* it ships. The report told a careful application, at `high`, that it gives no
|
|
268
|
+
* reason for any permission it asks for.
|
|
269
|
+
*
|
|
270
|
+
* `INFOPLIST_KEY_` is Xcode's prefix and `InfoPlist.strings` is Apple's
|
|
271
|
+
* localisation file; the sentence after the equals sign is the author's. An empty
|
|
272
|
+
* one is treated the same way as an empty `<string>` in the plist — it passes the
|
|
273
|
+
* compiler and fails review.
|
|
274
|
+
*/
|
|
275
|
+
for (const file of ctx.files.all.filter((f) => /project\.pbxproj$|(^|\/)InfoPlist\.strings$/i.test(f)).slice(0, 8)) {
|
|
276
|
+
const text = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file)) ?? '';
|
|
277
|
+
const declared = [...text.matchAll(/(?:INFOPLIST_KEY_)?"?(NS\w*UsageDescription)"?\s*=\s*"([^"]*)"/g)];
|
|
278
|
+
const explained = declared.filter(([, , reason]) => reason.trim().length > 0);
|
|
279
|
+
if (!explained.length)
|
|
280
|
+
continue;
|
|
281
|
+
permissionsExplained = true;
|
|
282
|
+
permissionEvidence.push({
|
|
283
|
+
type: 'file',
|
|
284
|
+
value: `${explained.length} permission${explained.length === 1 ? '' : 's'} with a reason in ${file}`,
|
|
285
|
+
file,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
261
288
|
const runtimeRequests = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
262
289
|
/requestPermissions?\(/,
|
|
263
290
|
/ActivityCompat\.requestPermissions/,
|
|
@@ -317,6 +344,22 @@ async function detectMobile(ctx) {
|
|
|
317
344
|
*/
|
|
318
345
|
const connectivityChecks = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/Connectivity\(\)/, /connectivity_plus/, /NetInfo\./, /navigator\.onLine/, /NWPathMonitor/, /isReachable/], 3);
|
|
319
346
|
const platformStores = localStores.uses;
|
|
347
|
+
/**
|
|
348
|
+
* Watching the network is not storing anything, and it was the citation under
|
|
349
|
+
* "no local database".
|
|
350
|
+
*
|
|
351
|
+
* `NWPathMonitor`, `NetInfo` and `navigator.onLine` tell an application whether it
|
|
352
|
+
* is connected. They were pushed into this evidence list while counting for nothing
|
|
353
|
+
* in the verdict, so damus — which was found to have no store this can read — was
|
|
354
|
+
* shown `let network_monitor = NWPathMonitor()` as the reason. A line answering one
|
|
355
|
+
* question, offered as the evidence for another.
|
|
356
|
+
*
|
|
357
|
+
* Where a store *is* found they belong: knowing you are offline is part of working
|
|
358
|
+
* offline. Where none is, the search that came up empty is the honest evidence, and
|
|
359
|
+
* a reader who keeps their notes in something this cannot read can see what was
|
|
360
|
+
* looked for and say so.
|
|
361
|
+
*/
|
|
362
|
+
const hasALocalStore = offlineDeps.length > 0 || platformStores.length > 0;
|
|
320
363
|
const offlineEvidence = [
|
|
321
364
|
...offlineDeps.map((dep) => ({ type: 'dependency', value: dep })),
|
|
322
365
|
...platformStores.map((match) => ({
|
|
@@ -325,7 +368,7 @@ async function detectMobile(ctx) {
|
|
|
325
368
|
file: match.file,
|
|
326
369
|
line: match.line,
|
|
327
370
|
})),
|
|
328
|
-
...connectivityChecks.map((match) => ({
|
|
371
|
+
...(hasALocalStore ? connectivityChecks : []).map((match) => ({
|
|
329
372
|
type: 'snippet',
|
|
330
373
|
value: match.snippet,
|
|
331
374
|
file: match.file,
|
|
@@ -369,7 +412,7 @@ async function detectMobile(ctx) {
|
|
|
369
412
|
},
|
|
370
413
|
{
|
|
371
414
|
key: 'mobile.offline',
|
|
372
|
-
present:
|
|
415
|
+
present: hasALocalStore,
|
|
373
416
|
evidence: (0, absenceEvidence_1.evidenceOrSearch)(offlineEvidence, 'a local database the app can read with no network', ['sqflite', 'drift', 'hive', 'isar', 'objectbox', 'realm', 'androidx.room', 'sqldelight', 'grdb.swift', 'sqlite.swift', 'NSManagedObjectContext', 'SQLiteOpenHelper', 'getWritableDatabase(']),
|
|
374
417
|
},
|
|
375
418
|
{
|
|
@@ -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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.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": {
|