@produtype/core 0.47.0 → 0.49.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.
- package/dist/analyzer/detectEnv.js +10 -0
- package/dist/analyzer/developmentOnly.d.ts +20 -0
- package/dist/analyzer/developmentOnly.js +46 -0
- package/dist/report/executiveSummary.d.ts +2 -1
- package/dist/report/executiveSummary.js +18 -2
- package/dist/report/markdownReport.js +1 -1
- package/dist/utils/textSearch.js +12 -0
- package/package.json +1 -1
|
@@ -13,6 +13,15 @@ const ENV_FALLBACK_RE = /(process\.env\.(JWT_SECRET|SECRET_KEY|SESSION_SECRET)\s
|
|
|
13
13
|
*/
|
|
14
14
|
const SECRET_ASSIGNMENT_CONTEXT_RE = /(JWT_SECRET|SECRET_KEY|SESSION_SECRET|API_KEY|jwt_secret|secret_key|session_secret|api_key)\s*[:=]|process\.env\.[A-Z0-9_]*(SECRET|API_KEY)/;
|
|
15
15
|
const GENERIC_SECRET_ASSIGNMENT_RE = /(JWT_SECRET|SECRET_KEY|SESSION_SECRET|API_KEY)\s*[:=]\s*['"][^'"]+['"]/i;
|
|
16
|
+
/**
|
|
17
|
+
* A value that exists to hide a secret, not to be one.
|
|
18
|
+
*
|
|
19
|
+
* `config.global_settings.jwt_secret = "***"` is windmill's own redaction, in the
|
|
20
|
+
* command that prints an instance's settings, and it was reported as a hardcoded
|
|
21
|
+
* secret. The weak-value test runs on the whole line, and the line says "secret"
|
|
22
|
+
* because the identifier does — so the value itself was never looked at.
|
|
23
|
+
*/
|
|
24
|
+
const REDACTED_VALUE_RE = /[:=]\s*['"`](\*{2,}|x{3,}|<?\[?redacted\]?>?|hidden|\.{3,})['"`]/i;
|
|
16
25
|
function classifySecretFallback(snippet) {
|
|
17
26
|
if (/JWT_SECRET/i.test(snippet))
|
|
18
27
|
return 'jwt';
|
|
@@ -119,6 +128,7 @@ async function detectEnv(ctx) {
|
|
|
119
128
|
&& SECRET_ASSIGNMENT_CONTEXT_RE.test(m.snippet)
|
|
120
129
|
&& !namesItself(m.snippet)
|
|
121
130
|
&& !valueIsAnIdentifier(m.snippet)
|
|
131
|
+
&& !REDACTED_VALUE_RE.test(m.snippet)
|
|
122
132
|
&& !isTableEntry(m));
|
|
123
133
|
for (const m of weakHits) {
|
|
124
134
|
const hitEvidence = { type: 'snippet', value: m.snippet, file: m.file, line: m.line };
|
|
@@ -1 +1,21 @@
|
|
|
1
1
|
export declare function isDevelopmentOnlyFile(file: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* The lines of a Rust file that only exist to test it.
|
|
4
|
+
*
|
|
5
|
+
* windmill was reported with a `critical` hardcoded secret:
|
|
6
|
+
* `secret_key: "wrong_secret".to_string()`, in `http_trigger_auth.rs`. It is the
|
|
7
|
+
* fixture of `test_github_authenticate_wrong_secret` — a test asserting that the
|
|
8
|
+
* wrong secret is rejected — and the whole block sits under `#[cfg(test)]`, four
|
|
9
|
+
* hundred lines below the code it tests.
|
|
10
|
+
*
|
|
11
|
+
* The path filter cannot help here and never could: Rust keeps its unit tests inside
|
|
12
|
+
* the file they test, so there is no `tests/` directory to exclude. The language says
|
|
13
|
+
* plainly which lines are test-only, in an attribute written for exactly this purpose,
|
|
14
|
+
* and nothing was reading it.
|
|
15
|
+
*
|
|
16
|
+
* Brace depth rather than a parser: `#[cfg(test)]` is followed by a module, and a
|
|
17
|
+
* module ends where its braces balance. Strings containing braces would confuse it,
|
|
18
|
+
* and the cost of being confused is excluding a few more lines from a security scan
|
|
19
|
+
* of test code — which is where it was heading anyway.
|
|
20
|
+
*/
|
|
21
|
+
export declare function testOnlyLines(file: string, text: string): Set<number>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isDevelopmentOnlyFile = isDevelopmentOnlyFile;
|
|
4
|
+
exports.testOnlyLines = testOnlyLines;
|
|
4
5
|
/**
|
|
5
6
|
* A file the product does not run in production.
|
|
6
7
|
*
|
|
@@ -21,3 +22,48 @@ const DEVELOPMENT_ONLY_PATHS = [
|
|
|
21
22
|
function isDevelopmentOnlyFile(file) {
|
|
22
23
|
return DEVELOPMENT_ONLY_PATHS.some((pattern) => pattern.test(file));
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The lines of a Rust file that only exist to test it.
|
|
27
|
+
*
|
|
28
|
+
* windmill was reported with a `critical` hardcoded secret:
|
|
29
|
+
* `secret_key: "wrong_secret".to_string()`, in `http_trigger_auth.rs`. It is the
|
|
30
|
+
* fixture of `test_github_authenticate_wrong_secret` — a test asserting that the
|
|
31
|
+
* wrong secret is rejected — and the whole block sits under `#[cfg(test)]`, four
|
|
32
|
+
* hundred lines below the code it tests.
|
|
33
|
+
*
|
|
34
|
+
* The path filter cannot help here and never could: Rust keeps its unit tests inside
|
|
35
|
+
* the file they test, so there is no `tests/` directory to exclude. The language says
|
|
36
|
+
* plainly which lines are test-only, in an attribute written for exactly this purpose,
|
|
37
|
+
* and nothing was reading it.
|
|
38
|
+
*
|
|
39
|
+
* Brace depth rather than a parser: `#[cfg(test)]` is followed by a module, and a
|
|
40
|
+
* module ends where its braces balance. Strings containing braces would confuse it,
|
|
41
|
+
* and the cost of being confused is excluding a few more lines from a security scan
|
|
42
|
+
* of test code — which is where it was heading anyway.
|
|
43
|
+
*/
|
|
44
|
+
function testOnlyLines(file, text) {
|
|
45
|
+
const testOnly = new Set();
|
|
46
|
+
if (!/\.rs$/i.test(file))
|
|
47
|
+
return testOnly;
|
|
48
|
+
const lines = text.split(/\r?\n/);
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
if (!/^\s*#\[cfg\(test\)\]/.test(lines[i]))
|
|
51
|
+
continue;
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let opened = false;
|
|
54
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
55
|
+
testOnly.add(j + 1);
|
|
56
|
+
for (const character of lines[j]) {
|
|
57
|
+
if (character === '{') {
|
|
58
|
+
depth++;
|
|
59
|
+
opened = true;
|
|
60
|
+
}
|
|
61
|
+
else if (character === '}')
|
|
62
|
+
depth--;
|
|
63
|
+
}
|
|
64
|
+
if (opened && depth <= 0)
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return testOnly;
|
|
69
|
+
}
|
|
@@ -5,7 +5,8 @@ export interface ExecutiveSummary {
|
|
|
5
5
|
/** One sentence a non-technical reader can act on. */
|
|
6
6
|
verdict: string;
|
|
7
7
|
/** Whether the product can plausibly be launched as the selected profile. */
|
|
8
|
-
|
|
8
|
+
/** `null` where the report could not judge the project at all. */
|
|
9
|
+
launchReady: boolean | null;
|
|
9
10
|
/** The blocking themes, in plain language, worst first. */
|
|
10
11
|
topRisks: string[];
|
|
11
12
|
/** What the repository already does well, so the report is not only negative. */
|
|
@@ -123,7 +123,15 @@ function buildVerdict(args) {
|
|
|
123
123
|
verdict: reason
|
|
124
124
|
? `ProdKit could not judge this project. ${reason}`
|
|
125
125
|
: 'ProdKit could not recognise this project well enough to judge it. Check that the path points at application source.',
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* `null`, not `false`.
|
|
128
|
+
*
|
|
129
|
+
* The verdict directly above says the project could not be judged, and the flag
|
|
130
|
+
* beside it said "not launch ready" — which the interface rendered as an amber
|
|
131
|
+
* badge reading exactly that. One of the two is a verdict about the product and
|
|
132
|
+
* the other is the absence of one; they cannot both be shown.
|
|
133
|
+
*/
|
|
134
|
+
launchReady: null,
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
if (!args.profile) {
|
|
@@ -202,7 +210,15 @@ function buildExecutiveSummary(args) {
|
|
|
202
210
|
verdict,
|
|
203
211
|
launchReady,
|
|
204
212
|
topRisks,
|
|
205
|
-
|
|
213
|
+
/**
|
|
214
|
+
* A report that refuses to judge does not hand out compliments.
|
|
215
|
+
*
|
|
216
|
+
* A Python transcription script whose verdict read "ProdKit could not judge this
|
|
217
|
+
* project" listed three strengths underneath it, each of them a single check in a
|
|
218
|
+
* category nothing else had touched. The third place the same refusal sat next to
|
|
219
|
+
* the same praise.
|
|
220
|
+
*/
|
|
221
|
+
strengths: args.inconclusive ? [] : buildStrengths(args.categoryScores, args.findings),
|
|
206
222
|
estimatedEffort: estimateEffort(args.findings, args.profile),
|
|
207
223
|
scoreExplanation,
|
|
208
224
|
};
|
|
@@ -109,7 +109,7 @@ function executiveSummarySection(report) {
|
|
|
109
109
|
'',
|
|
110
110
|
summary.verdict,
|
|
111
111
|
'',
|
|
112
|
-
`- Launch ready: ${summary.launchReady ? 'yes' : 'no'}`,
|
|
112
|
+
`- Launch ready: ${summary.launchReady === null ? 'not judged' : summary.launchReady ? 'yes' : 'no'}`,
|
|
113
113
|
`- Score: ${summary.scoreExplanation}`,
|
|
114
114
|
`- Estimated effort: ${summary.estimatedEffort}`,
|
|
115
115
|
'',
|
package/dist/utils/textSearch.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.matchLines = matchLines;
|
|
|
5
5
|
exports.searchInFiles = searchInFiles;
|
|
6
6
|
exports.anyIncludes = anyIncludes;
|
|
7
7
|
const readTextFileSafe_1 = require("./readTextFileSafe");
|
|
8
|
+
const developmentOnly_1 = require("../analyzer/developmentOnly");
|
|
8
9
|
/**
|
|
9
10
|
* A line that declares a pattern rather than doing anything.
|
|
10
11
|
*
|
|
@@ -96,8 +97,16 @@ const MAX_CITABLE_LINE = 500;
|
|
|
96
97
|
function matchLines(text, needles, file = '') {
|
|
97
98
|
const matches = [];
|
|
98
99
|
const lines = text.split(/\r?\n/);
|
|
100
|
+
/**
|
|
101
|
+
* Rust keeps its unit tests in the file they test, so no path filter can exclude
|
|
102
|
+
* them. windmill was reported with a `critical` hardcoded secret that was the
|
|
103
|
+
* fixture of a test asserting the wrong secret is rejected.
|
|
104
|
+
*/
|
|
105
|
+
const testOnly = (0, developmentOnly_1.testOnlyLines)(file, text);
|
|
99
106
|
for (let i = 0; i < lines.length; i++) {
|
|
100
107
|
const line = lines[i];
|
|
108
|
+
if (testOnly.has(i + 1))
|
|
109
|
+
continue;
|
|
101
110
|
if (line.length > MAX_CITABLE_LINE)
|
|
102
111
|
continue;
|
|
103
112
|
if (declaresRatherThanDoes(line))
|
|
@@ -125,8 +134,11 @@ async function searchInFiles(root, files, needles, limit = 25) {
|
|
|
125
134
|
if (!text)
|
|
126
135
|
continue;
|
|
127
136
|
const lines = text.split(/\r?\n/);
|
|
137
|
+
const testOnly = (0, developmentOnly_1.testOnlyLines)(file, text);
|
|
128
138
|
for (let i = 0; i < lines.length; i++) {
|
|
129
139
|
const line = lines[i];
|
|
140
|
+
if (testOnly.has(i + 1))
|
|
141
|
+
continue;
|
|
130
142
|
if (line.length > MAX_CITABLE_LINE)
|
|
131
143
|
continue;
|
|
132
144
|
if (declaresRatherThanDoes(line))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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": {
|