@produtype/core 0.64.0 → 0.66.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/detectAuth.js +26 -2
- package/dist/analyzer/detectEnv.js +15 -0
- package/dist/analyzer/structural/ownershipChecks.d.ts +6 -0
- package/dist/analyzer/structural/ownershipChecks.js +155 -0
- package/dist/analyzer/structural/secretArguments.d.ts +8 -0
- package/dist/analyzer/structural/secretArguments.js +171 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ const roleChecks_1 = require("./structural/roleChecks");
|
|
|
7
7
|
const absenceEvidence_1 = require("./absenceEvidence");
|
|
8
8
|
const fileNames_1 = require("./fileNames");
|
|
9
9
|
const valuesFromPackage_1 = require("./structural/valuesFromPackage");
|
|
10
|
+
const ownershipChecks_1 = require("./structural/ownershipChecks");
|
|
10
11
|
/**
|
|
11
12
|
* In a product that talks to a model, `role` usually means who is speaking.
|
|
12
13
|
*
|
|
@@ -360,6 +361,21 @@ async function detectAuth(ctx) {
|
|
|
360
361
|
file: use.file,
|
|
361
362
|
line: use.line,
|
|
362
363
|
}));
|
|
364
|
+
/**
|
|
365
|
+
* The same question asked of the shape rather than of the vocabulary.
|
|
366
|
+
*
|
|
367
|
+
* This capability is the one with no package to anchor on — there is no
|
|
368
|
+
* `npm install authorization` — so every reading of it was a list of the words
|
|
369
|
+
* people happen to use. An Italian application guarding every route with
|
|
370
|
+
* `if (nota.proprietario !== richiesta.utente.id)` was told it had no per-record
|
|
371
|
+
* checks, under a recommendation to add what it already had.
|
|
372
|
+
*
|
|
373
|
+
* Express supplies the anchor it lacks: a router comes from the package, `.get(path,
|
|
374
|
+
* handler)` is the framework saying "this is a request handler", and the handler's
|
|
375
|
+
* first parameter is the request whatever its author called it. The rest is
|
|
376
|
+
* structure and needs no vocabulary at all.
|
|
377
|
+
*/
|
|
378
|
+
const structuralOwnership = await (0, ownershipChecks_1.readOwnershipChecks)(ctx.root, sourceFiles);
|
|
363
379
|
const hasAuth = authDeps.length > 0 || routeSignals.length > 0;
|
|
364
380
|
const hasAuthz = permissionSignals.length > 0 || roleSignals.length > 0;
|
|
365
381
|
const b2bHint = b2bSignals.length > 0;
|
|
@@ -434,8 +450,16 @@ async function detectAuth(ctx) {
|
|
|
434
450
|
key: 'authz.resourceLevel',
|
|
435
451
|
// Route-level permission checks no longer stand in for per-record ones: with the
|
|
436
452
|
// needles above narrowed, this clause could only reintroduce what they removed.
|
|
437
|
-
present: resourceLevelSignals.length > 0,
|
|
438
|
-
evidence: (0, absenceEvidence_1.evidenceOrSearch)(
|
|
453
|
+
present: resourceLevelSignals.length > 0 || (structuralOwnership ?? []).length > 0,
|
|
454
|
+
evidence: (0, absenceEvidence_1.evidenceOrSearch)([
|
|
455
|
+
...snippetEvidence(resourceLevelSignals),
|
|
456
|
+
...(structuralOwnership ?? []).slice(0, 6).map((check) => ({
|
|
457
|
+
type: 'snippet',
|
|
458
|
+
value: check.snippet,
|
|
459
|
+
file: check.file,
|
|
460
|
+
line: check.line,
|
|
461
|
+
})),
|
|
462
|
+
], 'a check that the row belongs to the caller', ['requirePermission', 'permission_classes', 'authorize(', 'canAccess(', 'hasAccessTo(', 'ownerId', 'createdBy', 'req.user.id', 'userId ===']),
|
|
439
463
|
},
|
|
440
464
|
{
|
|
441
465
|
key: 'tenancy.organization',
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.detectEnv = detectEnv;
|
|
4
4
|
const textSearch_1 = require("../utils/textSearch");
|
|
5
5
|
const lookupTables_1 = require("./structural/lookupTables");
|
|
6
|
+
const secretArguments_1 = require("./structural/secretArguments");
|
|
6
7
|
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;
|
|
7
8
|
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;
|
|
8
9
|
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;
|
|
@@ -124,6 +125,20 @@ async function detectEnv(ctx) {
|
|
|
124
125
|
*/
|
|
125
126
|
const lookupTables = await (0, lookupTables_1.readLookupTableLines)(ctx.root, sourceFiles);
|
|
126
127
|
const isTableEntry = (match) => lookupTables?.get(match.file)?.has(match.line) === true;
|
|
128
|
+
/**
|
|
129
|
+
* The other end of the value, where the library says what it is.
|
|
130
|
+
*
|
|
131
|
+
* Everything above starts from the name being assigned, and the name is the
|
|
132
|
+
* author's: an Italian application signing with `const chiave =
|
|
133
|
+
* process.env.CHIAVE_FIRMA || 'cambiami'` came back `passed`. `jwt.sign(payload,
|
|
134
|
+
* secret)` defines its second argument, and arriving there is what makes a literal
|
|
135
|
+
* a secret whatever it was called on the way.
|
|
136
|
+
*/
|
|
137
|
+
const secretArguments = await (0, secretArguments_1.readHardcodedSecretArguments)(ctx.root, sourceFiles);
|
|
138
|
+
for (const hit of secretArguments ?? []) {
|
|
139
|
+
weakSecretEvidence.push({ type: 'snippet', value: `${hit.snippet} — reaches ${hit.sink}`, file: hit.file, line: hit.line });
|
|
140
|
+
weakSecretByType.unknown.push({ type: 'snippet', value: `${hit.snippet} — reaches ${hit.sink}`, file: hit.file, line: hit.line });
|
|
141
|
+
}
|
|
127
142
|
const weakHits = fallbackHits.filter((m) => WEAK_SECRET_VALUE_RE.test(m.snippet)
|
|
128
143
|
&& SECRET_ASSIGNMENT_CONTEXT_RE.test(m.snippet)
|
|
129
144
|
&& !namesItself(m.snippet)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readOwnershipChecks = readOwnershipChecks;
|
|
4
|
+
const readTextFileSafe_1 = require("../../utils/readTextFileSafe");
|
|
5
|
+
const loadTypeScript_1 = require("./loadTypeScript");
|
|
6
|
+
/**
|
|
7
|
+
* A route handler comparing a field of something it fetched against a field of the
|
|
8
|
+
* caller.
|
|
9
|
+
*
|
|
10
|
+
* This is the one capability on the list with no package to anchor on. There is no
|
|
11
|
+
* `npm install authorization`: ownership is written by hand, in whatever words the
|
|
12
|
+
* author has, and every reading of it here was a list of those words — `ownerId`,
|
|
13
|
+
* `createdBy`, `req.user.id`. An Italian application guarding every route with
|
|
14
|
+
*
|
|
15
|
+
* if (nota.proprietario !== richiesta.utente.id) return risposta.sendStatus(404);
|
|
16
|
+
*
|
|
17
|
+
* was told it had no per-record checks, under a recommendation to add the thing it
|
|
18
|
+
* already had.
|
|
19
|
+
*
|
|
20
|
+
* The anchor that survives is the framework's contract. `express()` and
|
|
21
|
+
* `express.Router()` come from a package; a router's `.get(path, handler)` is Express
|
|
22
|
+
* saying "this is a request handler"; and the handler's first parameter is the
|
|
23
|
+
* request, whatever its author called it. From there the question is structural and
|
|
24
|
+
* needs no vocabulary at all: does this handler compare a property reached through
|
|
25
|
+
* that parameter against a property of something else?
|
|
26
|
+
*
|
|
27
|
+
* `nota.proprietario !== richiesta.utente.id` answers yes. `richiesta.method !== 'GET'`
|
|
28
|
+
* does not — the other side is a literal, and comparing the request to a constant is
|
|
29
|
+
* routing, not authorisation. Neither does a comparison with the request on both
|
|
30
|
+
* sides, which is a request talking to itself rather than to a row.
|
|
31
|
+
*/
|
|
32
|
+
const READABLE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
33
|
+
/** Express methods that take a handler whose first parameter is the request. */
|
|
34
|
+
const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all']);
|
|
35
|
+
function expressBindings(ts, source) {
|
|
36
|
+
const fromPackage = new Set();
|
|
37
|
+
const routers = new Set();
|
|
38
|
+
const visit = (node) => {
|
|
39
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier) && node.moduleSpecifier.text === 'express') {
|
|
40
|
+
if (node.importClause?.name)
|
|
41
|
+
fromPackage.add(node.importClause.name.text);
|
|
42
|
+
}
|
|
43
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isIdentifier(node.name)) {
|
|
44
|
+
const initializer = node.initializer;
|
|
45
|
+
/** `const express = require('express')` */
|
|
46
|
+
if (ts.isCallExpression(initializer)
|
|
47
|
+
&& ts.isIdentifier(initializer.expression)
|
|
48
|
+
&& initializer.expression.text === 'require'
|
|
49
|
+
&& initializer.arguments.length === 1
|
|
50
|
+
&& ts.isStringLiteralLike(initializer.arguments[0])
|
|
51
|
+
&& initializer.arguments[0].text === 'express') {
|
|
52
|
+
fromPackage.add(node.name.text);
|
|
53
|
+
}
|
|
54
|
+
/** `const app = express()` and `const rotte = express.Router()` */
|
|
55
|
+
if (ts.isCallExpression(initializer)) {
|
|
56
|
+
const callee = initializer.expression;
|
|
57
|
+
if (ts.isIdentifier(callee) && fromPackage.has(callee.text))
|
|
58
|
+
routers.add(node.name.text);
|
|
59
|
+
if (ts.isPropertyAccessExpression(callee)
|
|
60
|
+
&& callee.name.text === 'Router'
|
|
61
|
+
&& ts.isIdentifier(callee.expression)
|
|
62
|
+
&& fromPackage.has(callee.expression.text)) {
|
|
63
|
+
routers.add(node.name.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
ts.forEachChild(node, visit);
|
|
68
|
+
};
|
|
69
|
+
visit(source);
|
|
70
|
+
return routers;
|
|
71
|
+
}
|
|
72
|
+
/** The identifier a property access is reached through: `a.b.c` is rooted at `a`. */
|
|
73
|
+
function rootOf(ts, node) {
|
|
74
|
+
let current = node;
|
|
75
|
+
while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
|
76
|
+
current = current.expression;
|
|
77
|
+
}
|
|
78
|
+
return ts.isIdentifier(current) ? current.text : null;
|
|
79
|
+
}
|
|
80
|
+
function isPropertyPath(ts, node) {
|
|
81
|
+
return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node);
|
|
82
|
+
}
|
|
83
|
+
async function readOwnershipChecks(root, files) {
|
|
84
|
+
const ts = await (0, loadTypeScript_1.loadTypeScript)();
|
|
85
|
+
if (!ts)
|
|
86
|
+
return null;
|
|
87
|
+
const checks = [];
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
if (!READABLE.test(file))
|
|
90
|
+
continue;
|
|
91
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
|
|
92
|
+
if (!text)
|
|
93
|
+
continue;
|
|
94
|
+
if (!text.includes('express'))
|
|
95
|
+
continue;
|
|
96
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
97
|
+
const routers = expressBindings(ts, source);
|
|
98
|
+
if (routers.size === 0)
|
|
99
|
+
continue;
|
|
100
|
+
const lines = text.split(/\r?\n/);
|
|
101
|
+
const visit = (node) => {
|
|
102
|
+
if (ts.isCallExpression(node)) {
|
|
103
|
+
const callee = node.expression;
|
|
104
|
+
const isRoute = ts.isPropertyAccessExpression(callee)
|
|
105
|
+
&& ROUTE_METHODS.has(callee.name.text)
|
|
106
|
+
&& ts.isIdentifier(callee.expression)
|
|
107
|
+
&& routers.has(callee.expression.text);
|
|
108
|
+
if (isRoute) {
|
|
109
|
+
for (const argument of node.arguments) {
|
|
110
|
+
if (!ts.isArrowFunction(argument) && !ts.isFunctionExpression(argument))
|
|
111
|
+
continue;
|
|
112
|
+
/**
|
|
113
|
+
* The request's position, not its name. Express hands an error-first
|
|
114
|
+
* middleware four parameters and puts the request second; every other
|
|
115
|
+
* handler takes it first.
|
|
116
|
+
*/
|
|
117
|
+
const parameters = argument.parameters;
|
|
118
|
+
const request = parameters.length === 4 ? parameters[1] : parameters[0];
|
|
119
|
+
if (!request || !ts.isIdentifier(request.name))
|
|
120
|
+
continue;
|
|
121
|
+
const requestName = request.name.text;
|
|
122
|
+
const inspect = (inner) => {
|
|
123
|
+
if (ts.isBinaryExpression(inner)
|
|
124
|
+
&& [
|
|
125
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
126
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
127
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
128
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
129
|
+
].includes(inner.operatorToken.kind)) {
|
|
130
|
+
const left = inner.left;
|
|
131
|
+
const right = inner.right;
|
|
132
|
+
const leftRoot = rootOf(ts, left);
|
|
133
|
+
const rightRoot = rootOf(ts, right);
|
|
134
|
+
const fromRequest = (root, side) => root === requestName && isPropertyPath(ts, side);
|
|
135
|
+
const fromElsewhere = (root, side) => root !== null && root !== requestName && isPropertyPath(ts, side);
|
|
136
|
+
const compares = (fromRequest(leftRoot, left) && fromElsewhere(rightRoot, right))
|
|
137
|
+
|| (fromRequest(rightRoot, right) && fromElsewhere(leftRoot, left));
|
|
138
|
+
if (compares) {
|
|
139
|
+
const line = source.getLineAndCharacterOfPosition(inner.getStart(source)).line;
|
|
140
|
+
checks.push({ file, line: line + 1, snippet: (lines[line] ?? '').trim().slice(0, 200) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
ts.forEachChild(inner, inspect);
|
|
144
|
+
};
|
|
145
|
+
if (argument.body)
|
|
146
|
+
inspect(argument.body);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
ts.forEachChild(node, visit);
|
|
151
|
+
};
|
|
152
|
+
visit(source);
|
|
153
|
+
}
|
|
154
|
+
return checks;
|
|
155
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface HardcodedSecretArgument {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
snippet: string;
|
|
5
|
+
/** The call that defines this value as a secret, for the sentence shown to a reader. */
|
|
6
|
+
sink: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function readHardcodedSecretArguments(root: string, files: string[]): Promise<HardcodedSecretArgument[] | null>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readHardcodedSecretArguments = readHardcodedSecretArguments;
|
|
4
|
+
const readTextFileSafe_1 = require("../../utils/readTextFileSafe");
|
|
5
|
+
const loadTypeScript_1 = require("./loadTypeScript");
|
|
6
|
+
/**
|
|
7
|
+
* A literal reaching a parameter the library defines as a secret.
|
|
8
|
+
*
|
|
9
|
+
* Every other reading of "is this a hardcoded secret" starts from the name of the
|
|
10
|
+
* thing being assigned — `JWT_SECRET`, `SECRET_KEY`, `API_KEY`. That name is the
|
|
11
|
+
* author's, and an Italian application signing its tokens with
|
|
12
|
+
*
|
|
13
|
+
* const chiave = process.env.CHIAVE_FIRMA || 'cambiami';
|
|
14
|
+
* jwt.sign({ sub: utente.id }, chiave);
|
|
15
|
+
*
|
|
16
|
+
* came back `passed`: "no weak fallback secret patterns detected", about a signing
|
|
17
|
+
* key hardcoded in the source. The check congratulated it.
|
|
18
|
+
*
|
|
19
|
+
* The anchor that does not move is the other end. `jsonwebtoken.sign(payload, secret)`
|
|
20
|
+
* says what its second argument is — that is the library's contract, not a convention
|
|
21
|
+
* — and `crypto.createHmac(algorithm, key)` says the same. Whatever the value is
|
|
22
|
+
* called on the way there, arriving there is what makes it a secret.
|
|
23
|
+
*
|
|
24
|
+
* One hop of resolution, matching the rest of the structural readers: a literal
|
|
25
|
+
* passed directly, or an identifier declared in the same file whose initializer is a
|
|
26
|
+
* literal or falls back to one. Two hops would reach further and be wrong more often,
|
|
27
|
+
* and the text search remains underneath either way.
|
|
28
|
+
*/
|
|
29
|
+
const READABLE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
30
|
+
const SECRET_SINKS = [
|
|
31
|
+
{ package: 'jsonwebtoken', method: 'sign', argument: 1 },
|
|
32
|
+
{ package: 'jsonwebtoken', method: 'verify', argument: 1 },
|
|
33
|
+
{ package: 'crypto', method: 'createHmac', argument: 1 },
|
|
34
|
+
{ package: 'node:crypto', method: 'createHmac', argument: 1 },
|
|
35
|
+
{ package: 'crypto', method: 'createCipheriv', argument: 1 },
|
|
36
|
+
{ package: 'node:crypto', method: 'createCipheriv', argument: 1 },
|
|
37
|
+
{ package: 'express-session', option: 'secret' },
|
|
38
|
+
{ package: 'cookie-session', option: 'secret' },
|
|
39
|
+
{ package: 'iron-session', option: 'password' },
|
|
40
|
+
];
|
|
41
|
+
function bindingsFor(ts, source, specifier) {
|
|
42
|
+
const bound = new Set();
|
|
43
|
+
const visit = (node) => {
|
|
44
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
|
45
|
+
if (node.moduleSpecifier.text === specifier) {
|
|
46
|
+
const clause = node.importClause;
|
|
47
|
+
if (clause?.name)
|
|
48
|
+
bound.add(clause.name.text);
|
|
49
|
+
if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings))
|
|
50
|
+
bound.add(clause.namedBindings.name.text);
|
|
51
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
52
|
+
for (const element of clause.namedBindings.elements)
|
|
53
|
+
bound.add(element.name.text);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (ts.isVariableDeclaration(node)
|
|
58
|
+
&& node.initializer
|
|
59
|
+
&& ts.isCallExpression(node.initializer)
|
|
60
|
+
&& ts.isIdentifier(node.initializer.expression)
|
|
61
|
+
&& node.initializer.expression.text === 'require'
|
|
62
|
+
&& node.initializer.arguments.length === 1
|
|
63
|
+
&& ts.isStringLiteralLike(node.initializer.arguments[0])
|
|
64
|
+
&& node.initializer.arguments[0].text === specifier) {
|
|
65
|
+
if (ts.isIdentifier(node.name))
|
|
66
|
+
bound.add(node.name.text);
|
|
67
|
+
if (ts.isObjectBindingPattern(node.name)) {
|
|
68
|
+
for (const element of node.name.elements) {
|
|
69
|
+
if (ts.isIdentifier(element.name))
|
|
70
|
+
bound.add(element.name.text);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
ts.forEachChild(node, visit);
|
|
75
|
+
};
|
|
76
|
+
visit(source);
|
|
77
|
+
return bound;
|
|
78
|
+
}
|
|
79
|
+
/** The literal an expression is, or falls back to, following one declaration. */
|
|
80
|
+
function literalBehind(ts, source, expression) {
|
|
81
|
+
if (ts.isStringLiteralLike(expression))
|
|
82
|
+
return true;
|
|
83
|
+
if (ts.isBinaryExpression(expression)
|
|
84
|
+
&& (expression.operatorToken.kind === ts.SyntaxKind.BarBarToken
|
|
85
|
+
|| expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) {
|
|
86
|
+
return literalBehind(ts, source, expression.right);
|
|
87
|
+
}
|
|
88
|
+
if (ts.isIdentifier(expression)) {
|
|
89
|
+
let found = false;
|
|
90
|
+
const visit = (node) => {
|
|
91
|
+
if (!found
|
|
92
|
+
&& ts.isVariableDeclaration(node)
|
|
93
|
+
&& ts.isIdentifier(node.name)
|
|
94
|
+
&& node.name.text === expression.text
|
|
95
|
+
&& node.initializer
|
|
96
|
+
&& !ts.isIdentifier(node.initializer)) {
|
|
97
|
+
found = literalBehind(ts, source, node.initializer);
|
|
98
|
+
}
|
|
99
|
+
ts.forEachChild(node, visit);
|
|
100
|
+
};
|
|
101
|
+
visit(source);
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
async function readHardcodedSecretArguments(root, files) {
|
|
107
|
+
const ts = await (0, loadTypeScript_1.loadTypeScript)();
|
|
108
|
+
if (!ts)
|
|
109
|
+
return null;
|
|
110
|
+
const found = [];
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
if (!READABLE.test(file))
|
|
113
|
+
continue;
|
|
114
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
|
|
115
|
+
if (!text)
|
|
116
|
+
continue;
|
|
117
|
+
if (!SECRET_SINKS.some((sink) => text.includes(sink.package.replace('node:', ''))))
|
|
118
|
+
continue;
|
|
119
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
120
|
+
for (const sink of SECRET_SINKS) {
|
|
121
|
+
const bound = bindingsFor(ts, source, sink.package);
|
|
122
|
+
if (bound.size === 0)
|
|
123
|
+
continue;
|
|
124
|
+
const visit = (node) => {
|
|
125
|
+
if (ts.isCallExpression(node)) {
|
|
126
|
+
const callee = node.expression;
|
|
127
|
+
const named = sink.method
|
|
128
|
+
? ts.isPropertyAccessExpression(callee)
|
|
129
|
+
&& callee.name.text === sink.method
|
|
130
|
+
&& ts.isIdentifier(callee.expression)
|
|
131
|
+
&& bound.has(callee.expression.text)
|
|
132
|
+
: ts.isIdentifier(callee) && bound.has(callee.text);
|
|
133
|
+
if (named) {
|
|
134
|
+
const report = (expression) => {
|
|
135
|
+
if (!literalBehind(ts, source, expression))
|
|
136
|
+
return;
|
|
137
|
+
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line;
|
|
138
|
+
found.push({
|
|
139
|
+
file,
|
|
140
|
+
line: line + 1,
|
|
141
|
+
snippet: (text.split(/\r?\n/)[line] ?? '').trim().slice(0, 200),
|
|
142
|
+
sink: sink.method ? `${sink.package}.${sink.method}()` : `${sink.package}()`,
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
if (sink.argument !== undefined) {
|
|
146
|
+
const argument = node.arguments[sink.argument];
|
|
147
|
+
if (argument)
|
|
148
|
+
report(argument);
|
|
149
|
+
}
|
|
150
|
+
if (sink.option) {
|
|
151
|
+
for (const argument of node.arguments) {
|
|
152
|
+
if (!ts.isObjectLiteralExpression(argument))
|
|
153
|
+
continue;
|
|
154
|
+
for (const property of argument.properties) {
|
|
155
|
+
if (ts.isPropertyAssignment(property)
|
|
156
|
+
&& ts.isIdentifier(property.name)
|
|
157
|
+
&& property.name.text === sink.option) {
|
|
158
|
+
report(property.initializer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
ts.forEachChild(node, visit);
|
|
166
|
+
};
|
|
167
|
+
visit(source);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return found;
|
|
171
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.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": {
|