@produtype/core 0.73.0 → 0.74.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.
@@ -8,6 +8,7 @@ const absenceEvidence_1 = require("./absenceEvidence");
8
8
  const fileNames_1 = require("./fileNames");
9
9
  const valuesFromPackage_1 = require("./structural/valuesFromPackage");
10
10
  const ownershipChecks_1 = require("./structural/ownershipChecks");
11
+ const readingDepth_1 = require("./readingDepth");
11
12
  /**
12
13
  * In a product that talks to a model, `role` usually means who is speaking.
13
14
  *
@@ -376,6 +377,7 @@ async function detectAuth(ctx) {
376
377
  * structure and needs no vocabulary at all.
377
378
  */
378
379
  const structuralOwnership = await (0, ownershipChecks_1.readOwnershipChecks)(ctx.root, sourceFiles);
380
+ const ownershipUnasked = (0, readingDepth_1.wentUnasked)(structuralOwnership, sourceFiles) && (await (0, ownershipChecks_1.anyFileImportsExpress)(ctx.root, sourceFiles));
379
381
  const hasAuth = authDeps.length > 0 || routeSignals.length > 0;
380
382
  const hasAuthz = permissionSignals.length > 0 || roleSignals.length > 0;
381
383
  const b2bHint = b2bSignals.length > 0;
@@ -451,6 +453,15 @@ async function detectAuth(ctx) {
451
453
  // Route-level permission checks no longer stand in for per-record ones: with the
452
454
  // needles above narrowed, this clause could only reintroduce what they removed.
453
455
  present: resourceLevelSignals.length > 0 || (structuralOwnership ?? []).length > 0,
456
+ /**
457
+ * The capability with no package to anchor on, read entirely from structure.
458
+ *
459
+ * Without the optional compiler the Express route walk does not happen, and the
460
+ * only thing left is the vocabulary this detector was written to stop relying on.
461
+ * `proprieta-in-italiano` turns from `passed` into a false `missing` — a
462
+ * recommendation to add a check the code already has.
463
+ */
464
+ unanswered: ownershipUnasked && resourceLevelSignals.length === 0,
454
465
  evidence: (0, absenceEvidence_1.evidenceOrSearch)([
455
466
  ...snippetEvidence(resourceLevelSignals),
456
467
  ...(structuralOwnership ?? []).slice(0, 6).map((check) => ({
@@ -4,6 +4,7 @@ exports.detectEnv = detectEnv;
4
4
  const textSearch_1 = require("../utils/textSearch");
5
5
  const lookupTables_1 = require("./structural/lookupTables");
6
6
  const secretArguments_1 = require("./structural/secretArguments");
7
+ const readingDepth_1 = require("./readingDepth");
7
8
  const WEAK_SECRET_VALUE_RE = /(changeme|your[_-]?secret|fallback-secret(?:-change-in-production)?|change[_-]in[_-]production|your_jwt_secret_key_change_in_production|local[-_]?secret|development[-_]?secret|dev[-_]?secret|not[_-]?for[_-]?production|test123|secret)/i;
8
9
  const FALLBACK_SECRET_RE = /(jwt_secret|secret_key|session_secret)\s*[:=]\s*['"][^'"]*(changeme|your[_-]?secret|fallback-secret(?:-change-in-production)?|change[_-]in[_-]production|your_jwt_secret_key_change_in_production|local[-_]?secret|development[-_]?secret|dev[-_]?secret|not[_-]?for[_-]?production|test123|secret)[^'"]*['"]/i;
9
10
  const ENV_FALLBACK_RE = /(process\.env\.(JWT_SECRET|SECRET_KEY|SESSION_SECRET)\s*(\|\||\?\?)\s*['"][^'"]*(changeme|your[_-]?secret|fallback-secret(?:-change-in-production)?|change[_-]in[_-]production|your_jwt_secret_key_change_in_production|local[-_]?secret|development[-_]?secret|dev[-_]?secret|not[_-]?for[_-]?production|test123|secret)[^'"]*['"])/i;
@@ -139,6 +140,14 @@ async function detectEnv(ctx) {
139
140
  weakSecretEvidence.push({ type: 'snippet', value: `${hit.snippet} — reaches ${hit.sink}`, file: hit.file, line: hit.line });
140
141
  weakSecretByType.unknown.push({ type: 'snippet', value: `${hit.snippet} — reaches ${hit.sink}`, file: hit.file, line: hit.line });
141
142
  }
143
+ /**
144
+ * Whether the sink side of the question went unasked.
145
+ *
146
+ * Only the reader that *finds* secrets counts here. `lookupTables` going missing makes
147
+ * this detector noisier, not blinder, and a noisy finding is one a reader can see and
148
+ * argue with — silence is not.
149
+ */
150
+ const secretSinksUnasked = (0, readingDepth_1.wentUnasked)(secretArguments, sourceFiles) && (await (0, secretArguments_1.anyFileReachesASecretSink)(ctx.root, sourceFiles));
142
151
  const weakHits = fallbackHits.filter((m) => WEAK_SECRET_VALUE_RE.test(m.snippet)
143
152
  && SECRET_ASSIGNMENT_CONTEXT_RE.test(m.snippet)
144
153
  && !namesItself(m.snippet)
@@ -167,26 +176,31 @@ async function detectEnv(ctx) {
167
176
  {
168
177
  key: 'env.secretFallback.jwt',
169
178
  present: weakSecretByType.jwt.length > 0,
179
+ unanswered: secretSinksUnasked && weakSecretByType.jwt.length === 0,
170
180
  evidence: weakSecretByType.jwt,
171
181
  },
172
182
  {
173
183
  key: 'env.secretFallback.session',
174
184
  present: weakSecretByType.session.length > 0,
185
+ unanswered: secretSinksUnasked && weakSecretByType.session.length === 0,
175
186
  evidence: weakSecretByType.session,
176
187
  },
177
188
  {
178
189
  key: 'env.secretFallback.app',
179
190
  present: weakSecretByType.app.length > 0,
191
+ unanswered: secretSinksUnasked && weakSecretByType.app.length === 0,
180
192
  evidence: weakSecretByType.app,
181
193
  },
182
194
  {
183
195
  key: 'env.secretFallback.apiKey',
184
196
  present: weakSecretByType.apiKey.length > 0,
197
+ unanswered: secretSinksUnasked && weakSecretByType.apiKey.length === 0,
185
198
  evidence: weakSecretByType.apiKey,
186
199
  },
187
200
  {
188
201
  key: 'env.secretFallback.unknown',
189
202
  present: weakSecretByType.unknown.length > 0,
203
+ unanswered: secretSinksUnasked && weakSecretByType.unknown.length === 0,
190
204
  evidence: weakSecretByType.unknown,
191
205
  details: { weakSecretEvidence },
192
206
  },
@@ -6,6 +6,7 @@ const readTextFileSafe_1 = require("../utils/readTextFileSafe");
6
6
  const textSearch_1 = require("../utils/textSearch");
7
7
  const absenceEvidence_1 = require("./absenceEvidence");
8
8
  const valuesFromPackage_1 = require("./structural/valuesFromPackage");
9
+ const readingDepth_1 = require("./readingDepth");
9
10
  const developmentOnly_1 = require("./developmentOnly");
10
11
  /** Lines that decide which origins may call this server. */
11
12
  const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /allowedOrigins/i];
@@ -371,6 +372,16 @@ async function detectSecurity(ctx) {
371
372
  helmet,
372
373
  rateLimit,
373
374
  rateLimitNearAuth,
375
+ /**
376
+ * Whether coverage could be established at all.
377
+ *
378
+ * Whether a limiter reaches the login is read from the value the package
379
+ * produces and the paths it is mounted on — a structural question. Without the
380
+ * optional compiler `mountedPaths` is empty and three fixtures that do throttle
381
+ * their sign-in drop from `passed` to `partial`, told that nothing shows the
382
+ * coverage they have.
383
+ */
384
+ rateLimitCoverageUnasked: (0, readingDepth_1.wentUnasked)(boundLimiterUses, source) && !rateLimitNearAuth,
374
385
  corsLoose: corsLoose.length > 0,
375
386
  corsStrict: corsStrict.length > 0,
376
387
  webhookSignature: webhookSig.length > 0,
@@ -50,3 +50,21 @@ export declare function readingDepths(sourceFiles: string[], unreadable: Array<{
50
50
  * worth a clause; telling them their Go is read by keyword is worth a different one.
51
51
  */
52
52
  export declare function describeReadingDepth(readings: LanguageReading[], parserAvailable: boolean): string | undefined;
53
+ /**
54
+ * Whether a parser would have had anything to say about this repository.
55
+ *
56
+ * A detector that loses its structural reader has lost nothing on a Go or Python
57
+ * project — no parser here was ever going to read those — so the missing compiler is
58
+ * only worth reporting where it would have changed the answer. Same list as the depths
59
+ * above, for the same reason: adding a language to the structural layer must move both
60
+ * claims in one commit.
61
+ */
62
+ export declare function parserCouldHaveRead(sourceFiles: string[]): boolean;
63
+ /**
64
+ * The question was asked and answered, or it was never asked.
65
+ *
66
+ * The structural readers already distinguish the two — `null` for "no parser", `[]` for
67
+ * "parsed, found nothing" — and every consumer flattened it with `?? []`. This puts the
68
+ * distinction back where a detector can act on it.
69
+ */
70
+ export declare function wentUnasked(readerResult: unknown | null, sourceFiles: string[]): boolean;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readingDepths = readingDepths;
4
4
  exports.describeReadingDepth = describeReadingDepth;
5
+ exports.parserCouldHaveRead = parserCouldHaveRead;
6
+ exports.wentUnasked = wentUnasked;
5
7
  const catalogue_1 = require("./catalogue");
6
8
  /**
7
9
  * The extensions a parser reads today.
@@ -89,3 +91,25 @@ function describeReadingDepth(readings, parserAvailable) {
89
91
  : []),
90
92
  ].join(' ');
91
93
  }
94
+ /**
95
+ * Whether a parser would have had anything to say about this repository.
96
+ *
97
+ * A detector that loses its structural reader has lost nothing on a Go or Python
98
+ * project — no parser here was ever going to read those — so the missing compiler is
99
+ * only worth reporting where it would have changed the answer. Same list as the depths
100
+ * above, for the same reason: adding a language to the structural layer must move both
101
+ * claims in one commit.
102
+ */
103
+ function parserCouldHaveRead(sourceFiles) {
104
+ return sourceFiles.some((file) => PARSED_EXTENSIONS.test(file));
105
+ }
106
+ /**
107
+ * The question was asked and answered, or it was never asked.
108
+ *
109
+ * The structural readers already distinguish the two — `null` for "no parser", `[]` for
110
+ * "parsed, found nothing" — and every consumer flattened it with `?? []`. This puts the
111
+ * distinction back where a detector can act on it.
112
+ */
113
+ function wentUnasked(readerResult, sourceFiles) {
114
+ return readerResult === null && parserCouldHaveRead(sourceFiles);
115
+ }
@@ -2,6 +2,18 @@ import type * as TypeScriptApi from 'typescript';
2
2
  export declare function loadTypeScript(): Promise<typeof TypeScriptApi | null>;
3
3
  /** For tests that need to observe both paths without reinstalling anything. */
4
4
  export declare function resetTypeScriptCache(): void;
5
+ /**
6
+ * Pretend the compiler is not installed, for tests of what this analyzer says when it
7
+ * cannot read structure.
8
+ *
9
+ * That path is the ordinary one for anybody running `npx prodkit` against their own
10
+ * repository, and it went untested for as long as it existed because the test machine
11
+ * always has the compiler. Mocking the module specifier does not work here: the import
12
+ * is dynamic and resolves before the mock registry answers, so the first analysis in a
13
+ * file quietly reads the real compiler. Seeding the same cache the loader reads is the
14
+ * one seam that behaves identically to the real absence.
15
+ */
16
+ export declare function pretendTypeScriptIsMissing(): void;
5
17
  /**
6
18
  * Whether the parser is there, for the report rather than for a reader.
7
19
  *
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadTypeScript = loadTypeScript;
4
4
  exports.resetTypeScriptCache = resetTypeScriptCache;
5
+ exports.pretendTypeScriptIsMissing = pretendTypeScriptIsMissing;
5
6
  exports.typeScriptIsAvailable = typeScriptIsAvailable;
6
7
  /**
7
8
  * The TypeScript compiler, loaded when something needs to read structure rather than text.
@@ -40,6 +41,20 @@ async function loadTypeScript() {
40
41
  function resetTypeScriptCache() {
41
42
  cached = undefined;
42
43
  }
44
+ /**
45
+ * Pretend the compiler is not installed, for tests of what this analyzer says when it
46
+ * cannot read structure.
47
+ *
48
+ * That path is the ordinary one for anybody running `npx prodkit` against their own
49
+ * repository, and it went untested for as long as it existed because the test machine
50
+ * always has the compiler. Mocking the module specifier does not work here: the import
51
+ * is dynamic and resolves before the mock registry answers, so the first analysis in a
52
+ * file quietly reads the real compiler. Seeding the same cache the loader reads is the
53
+ * one seam that behaves identically to the real absence.
54
+ */
55
+ function pretendTypeScriptIsMissing() {
56
+ cached = null;
57
+ }
43
58
  /**
44
59
  * Whether the parser is there, for the report rather than for a reader.
45
60
  *
@@ -4,3 +4,11 @@ export interface OwnershipCheck {
4
4
  snippet: string;
5
5
  }
6
6
  export declare function readOwnershipChecks(root: string, files: string[]): Promise<OwnershipCheck[] | null>;
7
+ /**
8
+ * Whether this reader would have had routes to walk.
9
+ *
10
+ * Same reasoning as the secret sinks: a repository that never imports Express has no
11
+ * route handler whose shape this reader could have read, so its absence changes no
12
+ * answer there and the report should not call the question unasked.
13
+ */
14
+ export declare function anyFileImportsExpress(root: string, files: string[]): Promise<boolean>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readOwnershipChecks = readOwnershipChecks;
4
+ exports.anyFileImportsExpress = anyFileImportsExpress;
4
5
  const readTextFileSafe_1 = require("../../utils/readTextFileSafe");
5
6
  const loadTypeScript_1 = require("./loadTypeScript");
6
7
  /**
@@ -153,3 +154,22 @@ async function readOwnershipChecks(root, files) {
153
154
  }
154
155
  return checks;
155
156
  }
157
+ /**
158
+ * Whether this reader would have had routes to walk.
159
+ *
160
+ * Same reasoning as the secret sinks: a repository that never imports Express has no
161
+ * route handler whose shape this reader could have read, so its absence changes no
162
+ * answer there and the report should not call the question unasked.
163
+ */
164
+ async function anyFileImportsExpress(root, files) {
165
+ for (const file of files) {
166
+ if (!READABLE.test(file))
167
+ continue;
168
+ const text = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
169
+ if (!text)
170
+ continue;
171
+ if (/(from\s+['"]express['"]|require\(\s*['"]express['"]\s*\))/.test(text))
172
+ return true;
173
+ }
174
+ return false;
175
+ }
@@ -6,3 +6,17 @@ export interface HardcodedSecretArgument {
6
6
  sink: string;
7
7
  }
8
8
  export declare function readHardcodedSecretArguments(root: string, files: string[]): Promise<HardcodedSecretArgument[] | null>;
9
+ /**
10
+ * Whether this reader would have had anything to read.
11
+ *
12
+ * `unanswered` has to mean "the answer depends on the reader that did not run", not
13
+ * "a reader did not run". The first version marked every JavaScript repository in the
14
+ * corpus — 79 of 133 — as not assessed for weak secrets, including the ones where a
15
+ * plain `const JWT_SECRET = 'changeme'` is found by name and the sink-following reader
16
+ * would have added nothing.
17
+ *
18
+ * The reader's own first act is this test: a file that never mentions a signing or
19
+ * ciphering package has no sink to follow. Where no file does, the missing compiler
20
+ * costs nothing and the text answer stands on its own.
21
+ */
22
+ export declare function anyFileReachesASecretSink(root: string, files: string[]): Promise<boolean>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readHardcodedSecretArguments = readHardcodedSecretArguments;
4
+ exports.anyFileReachesASecretSink = anyFileReachesASecretSink;
4
5
  const readTextFileSafe_1 = require("../../utils/readTextFileSafe");
5
6
  const loadTypeScript_1 = require("./loadTypeScript");
6
7
  /**
@@ -169,3 +170,28 @@ async function readHardcodedSecretArguments(root, files) {
169
170
  }
170
171
  return found;
171
172
  }
173
+ /**
174
+ * Whether this reader would have had anything to read.
175
+ *
176
+ * `unanswered` has to mean "the answer depends on the reader that did not run", not
177
+ * "a reader did not run". The first version marked every JavaScript repository in the
178
+ * corpus — 79 of 133 — as not assessed for weak secrets, including the ones where a
179
+ * plain `const JWT_SECRET = 'changeme'` is found by name and the sink-following reader
180
+ * would have added nothing.
181
+ *
182
+ * The reader's own first act is this test: a file that never mentions a signing or
183
+ * ciphering package has no sink to follow. Where no file does, the missing compiler
184
+ * costs nothing and the text answer stands on its own.
185
+ */
186
+ async function anyFileReachesASecretSink(root, files) {
187
+ for (const file of files) {
188
+ if (!READABLE.test(file))
189
+ continue;
190
+ const text = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
191
+ if (!text)
192
+ continue;
193
+ if (SECRET_SINKS.some((sink) => text.includes(sink.package.replace('node:', ''))))
194
+ return true;
195
+ }
196
+ return false;
197
+ }
@@ -74,6 +74,25 @@ export interface DetectorResult {
74
74
  /** Stable feature key, e.g. 'security.helmet' */
75
75
  key: string;
76
76
  present: boolean;
77
+ /**
78
+ * The question behind this signal could not be asked.
79
+ *
80
+ * `present: false` has carried two meanings that a reader would never confuse: "I
81
+ * looked and it is not there", and "I could not look". The second happens whenever a
82
+ * signal rests on the optional TypeScript compiler and the compiler is absent — the
83
+ * ordinary case for `npx prodkit` against somebody else's repository.
84
+ *
85
+ * Measured by hiding `node_modules/typescript` and re-running the fixture corpus: 7
86
+ * of 133 repositories answer differently, and `segreto-in-italiano` reports a
87
+ * hardcoded signing secret as `passed` rather than `missing`. A verdict of "fine" is
88
+ * the one direction blindness must never produce.
89
+ *
90
+ * Set it only where the missing reader can change the answer, and only alongside
91
+ * `present: false`: a signal that found the thing found it, whatever else went
92
+ * unasked. Consumers turn it into `unknown`, which already means "not assessed"
93
+ * everywhere downstream — the plan skips it and the score leaves it out.
94
+ */
95
+ unanswered?: boolean;
77
96
  /** When present is true: was the implementation complete? */
78
97
  complete?: boolean;
79
98
  evidence: DetectorEvidence[];
@@ -252,7 +252,20 @@ exports.rules = [
252
252
  app?.present ? 'app' : null,
253
253
  apiKey?.present ? 'apiKey' : null,
254
254
  ].filter(Boolean).join(', ');
255
- const status = weak ? 'missing' : 'passed';
255
+ /**
256
+ * A pass this reading did not earn.
257
+ *
258
+ * `jwt.sign(payload, 'cambiami')` is found by following the literal into the
259
+ * signing call, which needs the optional TypeScript compiler. Without it the
260
+ * search comes back empty and this rule said "No weak fallback secret patterns
261
+ * detected" — the analyzer's most confident sentence, produced by not looking.
262
+ * Measured on `segreto-in-italiano`: `missing` becomes `passed`, 47 becomes 62.
263
+ *
264
+ * Finding one is still finding one, so `weak` wins: blindness can only ever turn
265
+ * a clean verdict into no verdict.
266
+ */
267
+ const unanswered = [jwt, session, app, apiKey, unknown].some((d) => d?.unanswered);
268
+ const status = weak ? 'missing' : unanswered ? 'unknown' : 'passed';
256
269
  return mkFinding({
257
270
  id: 'security.weak-secret',
258
271
  title: 'Weak/fallback secret values',
@@ -261,7 +274,9 @@ exports.rules = [
261
274
  severity: weak ? 'critical' : 'info',
262
275
  description: weak
263
276
  ? `Hardcoded fallback secrets detected (${weakTypes || 'unknown'} key context).`
264
- : 'No weak fallback secret patterns detected.',
277
+ : unanswered
278
+ ? 'Not assessed: finding a literal secret where the name gives nothing away means following it into the call that signs with it, and the optional `typescript` peer dependency is not installed. Install it and re-run.'
279
+ : 'No weak fallback secret patterns detected.',
265
280
  recommendation: 'Require strong secrets through environment variables with strict startup validation.',
266
281
  /**
267
282
  * A check that passes because it found nothing has to say what it looked for.
@@ -367,9 +382,18 @@ exports.rules = [
367
382
  * surface", and now it only says that when something shows it.
368
383
  */
369
384
  const nearAuth = Boolean(sec?.details?.rateLimitNearAuth);
385
+ /**
386
+ * `partial` is a verdict, and blindness has not earned one.
387
+ *
388
+ * Coverage is read from the value the limiter package produces and the prefixes
389
+ * it is mounted on, so without the optional compiler the answer is empty rather
390
+ * than negative. Three fixtures that do throttle their login were told nothing
391
+ * showed it.
392
+ */
393
+ const coverageUnasked = Boolean(sec?.details?.rateLimitCoverageUnasked);
370
394
  const status = !isExpress || !hasAuth
371
395
  ? 'unknown'
372
- : nearAuth ? 'passed' : hasRate ? 'partial' : 'missing';
396
+ : nearAuth ? 'passed' : coverageUnasked && hasRate ? 'unknown' : hasRate ? 'partial' : 'missing';
373
397
  return mkFinding({
374
398
  id: 'security.rate-limit-auth',
375
399
  title: 'Authentication rate limiting',
@@ -388,9 +412,11 @@ exports.rules = [
388
412
  ? 'Rate limiting is in place somewhere, but nothing here shows it covering sign-in.'
389
413
  : status === 'missing'
390
414
  ? 'No auth-focused rate limiting detected.'
391
- : !hasAuth
392
- ? 'Nothing here authenticates anybody, so there is no login surface to throttle.'
393
- : 'This check reads Express middleware, and this project does not use it — any throttling it has is somewhere this cannot see.',
415
+ : coverageUnasked && hasRate
416
+ ? 'Not assessed: this project throttles something, and whether it reaches the login is read from where the limiter is mounted — which needs the optional `typescript` peer dependency. Install it and re-run.'
417
+ : !hasAuth
418
+ ? 'Nothing here authenticates anybody, so there is no login surface to throttle.'
419
+ : 'This check reads Express middleware, and this project does not use it — any throttling it has is somewhere this cannot see.',
394
420
  recommendation: 'Apply express-rate-limit (or equivalent) to login/register/password reset endpoints.',
395
421
  /**
396
422
  * The claim is about rate limiting, so the evidence is about rate limiting.
@@ -586,13 +612,22 @@ exports.rules = [
586
612
  // A question about who may act on which record needs a system that has records
587
613
  // and callers. The auth detector alone was not enough of a gate: it answers from
588
614
  // strings, and a package that searches for `requireAuth` contains it.
615
+ /**
616
+ * Per-record authorization is read from the shape of a route handler, so the
617
+ * optional compiler is the whole reading. Without it `proprieta-in-italiano`
618
+ * turns from `passed` into a false `missing`, and the report recommends adding
619
+ * a check the code already makes.
620
+ */
621
+ const ownershipUnasked = Boolean(resourceLevel?.unanswered) && !hasPermissions && !hasRoles;
589
622
  const status = !hasAuth || !hasUserFacingSurface(analysis)
590
623
  ? 'unknown'
591
624
  : hasPermissions || hasResourceLevel
592
625
  ? 'passed'
593
626
  : hasRoles
594
627
  ? 'partial'
595
- : 'missing';
628
+ : ownershipUnasked
629
+ ? 'unknown'
630
+ : 'missing';
596
631
  return mkFinding({
597
632
  id: 'authz.resource-level',
598
633
  title: 'Authorization depth',
@@ -607,7 +642,9 @@ exports.rules = [
607
642
  ? 'No resource-level authorization signals detected.'
608
643
  : !hasAuth
609
644
  ? 'Nothing here authenticates anybody, so there are no callers to authorize.'
610
- : 'Nothing in this repository serves requests, so there are no records to guard.',
645
+ : ownershipUnasked
646
+ ? 'Not assessed: a check that compares the record to the caller is recognised by the shape of the route handler, and the optional `typescript` peer dependency is not installed. Install it and re-run.'
647
+ : 'Nothing in this repository serves requests, so there are no records to guard.',
611
648
  recommendation: 'Add policy/resource-level checks beyond coarse role gates.',
612
649
  evidence: [...(roles?.evidence ?? []), ...(permissions?.evidence ?? []), ...(resourceLevel?.evidence ?? [])],
613
650
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.73.0",
3
+ "version": "0.74.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": {