@produtype/core 0.18.1 → 0.19.1

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.
@@ -4,6 +4,40 @@ exports.detectAuth = detectAuth;
4
4
  const detectContext_1 = require("./detectContext");
5
5
  const textSearch_1 = require("../utils/textSearch");
6
6
  const absenceEvidence_1 = require("./absenceEvidence");
7
+ /**
8
+ * In a product that talks to a model, `role` usually means who is speaking.
9
+ *
10
+ * `m.role === "assistant"` is a chat transcript, not an authorization check, and three
11
+ * repositories in the verification corpus were credited with a role model on the
12
+ * strength of one line of chat UI. One of them — an AI product with no roles anywhere —
13
+ * had that single line as the only evidence, and the report told its owner their
14
+ * authorization depth was "basic role checks" rather than nothing at all.
15
+ *
16
+ * Only the literals that cannot be an authorization role: `assistant`, `system`, `tool`,
17
+ * `function`, `developer`, `model`, `bot`. A comparison against `'user'` or `'admin'` is
18
+ * left alone — "user" is a perfectly ordinary role name, and a rule that dropped it
19
+ * would blind the detector to every two-role application.
20
+ *
21
+ * A line that pairs `role === 'user'` with nothing recognisable still gets through. That
22
+ * is the honest limit of what the shape of a line can tell.
23
+ */
24
+ /**
25
+ * Showing somebody their role is not checking it.
26
+ *
27
+ * `{m.role === 'editor' ? <Pencil /> : <Eye />}` picks an icon. `{{ msg.role === 'user'
28
+ * ? '👤' : '🤖' }}` picks an avatar. Both are the interface displaying a value, and
29
+ * neither decides whether anything is allowed — which is what this detector claims when
30
+ * it fires.
31
+ *
32
+ * It costs nothing where a role model exists: an application that renders a role almost
33
+ * always guards on it somewhere too, and those lines are untouched. Where the rendered
34
+ * line was the *only* evidence, the claim rested on a label.
35
+ */
36
+ const ROLE_IN_MARKUP = /[<{][^<>{}]*\brole\s*===?=?/;
37
+ const CHAT_TURN_ROLE = /role\s*===?=?\s*["'`](?:assistant|system|tool|function|developer|model|bot)["'`]|["'`](?:assistant|system)["'`]\s*===?=?\s*\w*\.?role/i;
38
+ function excludeChatTurnRoles(matches) {
39
+ return matches.filter((match) => !CHAT_TURN_ROLE.test(match.snippet) && !ROLE_IN_MARKUP.test(match.snippet));
40
+ }
7
41
  function depEvidence(deps) {
8
42
  return deps.map((d) => ({ type: 'dependency', value: d }));
9
43
  }
@@ -109,7 +143,7 @@ async function detectAuth(ctx) {
109
143
  ], 20);
110
144
  const emailVerificationSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/verify\s*email/i, /email[_-]?verification/i, /confirm\s*email/i, /isEmailVerified/i], 20);
111
145
  const sessionSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/session/i, /cookie/i, /jwt/i, /refresh\s*token/i, /httpOnly/i], 25);
112
- const roleSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
146
+ const roleSignals = excludeChatTurnRoles(await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
113
147
  /requireRole/i,
114
148
  /isAdmin/i,
115
149
  /SUPER_ADMIN/i,
@@ -117,7 +151,7 @@ async function detectAuth(ctx) {
117
151
  /user\.role/i,
118
152
  /role\s*===/i,
119
153
  /roles\.includes\(/i,
120
- ], 20);
154
+ ], 20));
121
155
  const permissionSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/requirePermission/i, /permission_classes/i, /permissions\.py/i, /authorize\(/i, /\bcan\(/i], 20);
122
156
  const resourceLevelSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
123
157
  /requirePermission/i,
@@ -745,16 +745,33 @@ exports.rules = [
745
745
  severity: 'low',
746
746
  evaluate: ({ analysis }) => {
747
747
  const dep = analysis.detectors['deployment.readiness'];
748
- const status = statusFromFlags(Boolean(dep?.present), dep?.complete);
748
+ /**
749
+ * Deploying is something you do to a service.
750
+ *
751
+ * A package is published to a registry, a phone application is released to a
752
+ * store, and a page is hosted by whatever serves it — none of which this
753
+ * repository has to contain, and none of which this analyzer reads. Thirty-nine of
754
+ * the seventy-eight repositories in the verification corpus were being told they
755
+ * had "no meaningful deployment artifacts": libraries, browser games, a Flutter
756
+ * app. The same sentence this product already refuses to say about a Dockerfile.
757
+ *
758
+ * Where the repository does carry deployment artifacts the question is live again
759
+ * whatever it is — somebody wrote a compose file for a reason — so this only
760
+ * applies when there are none at all.
761
+ */
762
+ const deployedFromHere = isAServer(analysis) || Boolean(dep?.present);
763
+ const status = deployedFromHere ? statusFromFlags(Boolean(dep?.present), dep?.complete) : 'unknown';
749
764
  return mkFinding({
750
765
  id: 'deployment.readiness',
751
766
  title: 'Deployment readiness baseline',
752
767
  category: 'deployment',
753
768
  status,
754
769
  severity: sevForStatus(status, 'low'),
755
- description: status === 'missing'
756
- ? 'No meaningful deployment artifacts or prod/runtime signals detected.'
757
- : 'Deployment artifacts or runtime production signals detected.',
770
+ description: !deployedFromHere
771
+ ? 'Nothing in this repository is deployed as a running service, so how it reaches people — a registry, a store, a host — is decided outside it.'
772
+ : status === 'missing'
773
+ ? 'No meaningful deployment artifacts or prod/runtime signals detected.'
774
+ : 'Deployment artifacts or runtime production signals detected.',
758
775
  /**
759
776
  * Graceful shutdown is left out where there is no signal to catch: PHP-FPM and
760
777
  * CGI hand each request to a worker that exits when it is done, and telling a
@@ -49,6 +49,26 @@ function declaresRatherThanDoes(line) {
49
49
  const trimmed = line.trim();
50
50
  return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed);
51
51
  }
52
+ /**
53
+ * The longest line this analyzer will cite.
54
+ *
55
+ * The product's promise is that every finding points at a line somebody can open and
56
+ * argue with. A bundled `index-BVY8w6Ig.js` is one line of forty thousand characters:
57
+ * the snippet shown is the first two hundred of it, which names no function, no file the
58
+ * author wrote, and nothing they can change. Two repositories in the verification corpus
59
+ * were citing exactly that, and one of them was the only evidence behind a claim about
60
+ * their authorization model.
61
+ *
62
+ * Skipping the line rather than the file, because a file is not minified — a line is.
63
+ * Real source stays well under this: of twelve thousand files analysed, eleven thousand
64
+ * have no line over two hundred characters, and the few hundred above five hundred are
65
+ * bundles, embedded data URIs and generated blobs.
66
+ *
67
+ * The cost is honest and small: a problem that exists only inside generated output is no
68
+ * longer reported. That output is not what anybody edits, and a finding nobody can act
69
+ * on is not worth the one it displaces.
70
+ */
71
+ const MAX_CITABLE_LINE = 500;
52
72
  /**
53
73
  * Search a list of relative file paths for any of the given needles
54
74
  * (string or RegExp). Returns at most `limit` matches.
@@ -64,6 +84,8 @@ async function searchInFiles(root, files, needles, limit = 25) {
64
84
  const lines = text.split(/\r?\n/);
65
85
  for (let i = 0; i < lines.length; i++) {
66
86
  const line = lines[i];
87
+ if (line.length > MAX_CITABLE_LINE)
88
+ continue;
67
89
  if (declaresRatherThanDoes(line))
68
90
  continue;
69
91
  for (const n of needles) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.18.1",
3
+ "version": "0.19.1",
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": {