@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.
- package/dist/analyzer/detectAuth.js +36 -2
- package/dist/rules/rules.js +21 -4
- package/dist/utils/textSearch.js +22 -0
- package/package.json +1 -1
|
@@ -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,
|
package/dist/rules/rules.js
CHANGED
|
@@ -745,16 +745,33 @@ exports.rules = [
|
|
|
745
745
|
severity: 'low',
|
|
746
746
|
evaluate: ({ analysis }) => {
|
|
747
747
|
const dep = analysis.detectors['deployment.readiness'];
|
|
748
|
-
|
|
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:
|
|
756
|
-
? '
|
|
757
|
-
:
|
|
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
|
package/dist/utils/textSearch.js
CHANGED
|
@@ -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.
|
|
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": {
|