@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.
@@ -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
- launchReady: boolean;
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
- launchReady: false,
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
- strengths: buildStrengths(args.categoryScores, args.findings),
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
  '',
@@ -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.47.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": {