@produtype/core 0.36.0 → 0.37.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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectEnv = detectEnv;
4
4
  const textSearch_1 = require("../utils/textSearch");
5
+ const lookupTables_1 = require("./structural/lookupTables");
5
6
  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;
6
7
  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;
7
8
  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;
@@ -83,9 +84,21 @@ async function detectEnv(ctx) {
83
84
  * the thing actually being claimed.
84
85
  */
85
86
  const fallbackHits = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [FALLBACK_SECRET_RE, ENV_FALLBACK_RE, GENERIC_SECRET_ASSIGNMENT_RE], 30);
87
+ /**
88
+ * Entries in a lookup table, which text cannot tell from assignments.
89
+ *
90
+ * `admin_session_secret: 'core'` is one property of a sixty-entry map from settings to
91
+ * the group they belong to, and Ghost was told it was a hardcoded secret. The table is
92
+ * visible only in the shape, so this is one of the places a syntax tree earns its
93
+ * keep. Absent the optional parser the map is null and nothing is excused, which is
94
+ * the behaviour this product had yesterday.
95
+ */
96
+ const lookupTables = await (0, lookupTables_1.readLookupTableLines)(ctx.root, sourceFiles);
97
+ const isTableEntry = (match) => lookupTables?.get(match.file)?.has(match.line) === true;
86
98
  const weakHits = fallbackHits.filter((m) => WEAK_SECRET_VALUE_RE.test(m.snippet)
87
99
  && SECRET_ASSIGNMENT_CONTEXT_RE.test(m.snippet)
88
- && !namesItself(m.snippet));
100
+ && !namesItself(m.snippet)
101
+ && !isTableEntry(m));
89
102
  for (const m of weakHits) {
90
103
  const hitEvidence = { type: 'snippet', value: m.snippet, file: m.file, line: m.line };
91
104
  evidence.push(hitEvidence);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Which lines of which files are table entries, or null when the question cannot be asked.
3
+ *
4
+ * `null` means the optional TypeScript dependency is absent, not that there are no
5
+ * tables — the caller keeps whatever it found rather than discarding it.
6
+ */
7
+ export declare function readLookupTableLines(root: string, files: string[]): Promise<Map<string, Set<number>> | null>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readLookupTableLines = readLookupTableLines;
4
+ const readTextFileSafe_1 = require("../../utils/readTextFileSafe");
5
+ const loadTypeScript_1 = require("./loadTypeScript");
6
+ /**
7
+ * Lines that are entries in a lookup table rather than assignments.
8
+ *
9
+ * Ghost keeps a map of every setting to the group it belongs to:
10
+ *
11
+ * admin_session_secret: 'core',
12
+ * theme_session_secret: 'core',
13
+ * members_email_auth_secret: 'core',
14
+ *
15
+ * Read as text this is a secret-named key assigned a short string, which is exactly
16
+ * the shape of a hardcoded fallback — and Ghost was told it had a critical. Read as a
17
+ * tree it is one property of an object with sixty of them, and what it says is "this
18
+ * setting is in the core group".
19
+ *
20
+ * Nothing in the words tells the two apart. The table is visible only in the shape.
21
+ */
22
+ /**
23
+ * How many string-valued properties make an object a table.
24
+ *
25
+ * Low enough to catch a modest map, high enough that a small configuration object —
26
+ * `{ secret: 'dev-secret', port: 3000 }`, which is a real finding — is not excused.
27
+ * A genuine fallback secret sits in an object with a handful of siblings at most.
28
+ */
29
+ const TABLE_SIZE = 8;
30
+ const READABLE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
31
+ const MAX_READABLE_LINE = 500;
32
+ function tableLines(ts, file, text) {
33
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
34
+ const lines = new Set();
35
+ const visit = (node) => {
36
+ if (ts.isObjectLiteralExpression(node)) {
37
+ const properties = node.properties.filter(ts.isPropertyAssignment);
38
+ const stringValued = properties.filter((property) => ts.isStringLiteral(property.initializer)
39
+ || ts.isNoSubstitutionTemplateLiteral(property.initializer));
40
+ /**
41
+ * Every property a string, and plenty of them. A mixed object — some strings,
42
+ * some calls, some numbers — is configuration, and configuration is where a
43
+ * fallback secret actually lives.
44
+ */
45
+ if (stringValued.length >= TABLE_SIZE && stringValued.length === properties.length) {
46
+ for (const property of stringValued) {
47
+ lines.add(source.getLineAndCharacterOfPosition(property.getStart(source)).line + 1);
48
+ }
49
+ }
50
+ }
51
+ ts.forEachChild(node, visit);
52
+ };
53
+ visit(source);
54
+ return lines;
55
+ }
56
+ /**
57
+ * Which lines of which files are table entries, or null when the question cannot be asked.
58
+ *
59
+ * `null` means the optional TypeScript dependency is absent, not that there are no
60
+ * tables — the caller keeps whatever it found rather than discarding it.
61
+ */
62
+ async function readLookupTableLines(root, files) {
63
+ const ts = await (0, loadTypeScript_1.loadTypeScript)();
64
+ if (!ts)
65
+ return null;
66
+ const found = new Map();
67
+ for (const file of files.filter((candidate) => READABLE.test(candidate))) {
68
+ const text = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
69
+ if (!text || text.split(/\r?\n/).some((line) => line.length > MAX_READABLE_LINE))
70
+ continue;
71
+ try {
72
+ const lines = tableLines(ts, file, text);
73
+ if (lines.size > 0)
74
+ found.set(file, lines);
75
+ }
76
+ catch {
77
+ // A file the parser refuses keeps whatever the text layer decided about it.
78
+ }
79
+ }
80
+ return found;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.36.0",
3
+ "version": "0.37.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": {