@produtype/core 1.9.0 → 1.9.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.
|
@@ -55,7 +55,9 @@ async function detectBilling(ctx) {
|
|
|
55
55
|
/stripeSubscriptionId/i,
|
|
56
56
|
/\/webhooks?\/stripe/i,
|
|
57
57
|
/\/stripe\/webhooks?/i,
|
|
58
|
-
], 30
|
|
58
|
+
], 30,
|
|
59
|
+
/** The budget counts real signals, not entries in Laravel's table of slots. */
|
|
60
|
+
(match) => !LARAVEL_SERVICE_SLOTS.test(match.file)));
|
|
59
61
|
/**
|
|
60
62
|
* The processor, declared wherever this project declares its dependencies.
|
|
61
63
|
*
|
|
@@ -11,6 +11,10 @@ const readingDepth_1 = require("./readingDepth");
|
|
|
11
11
|
const developmentOnly_1 = require("./developmentOnly");
|
|
12
12
|
/** Lines that decide which origins may call this server. */
|
|
13
13
|
const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /allowedOrigins/i];
|
|
14
|
+
/** A header name used as a key into a headers object: a lookup, not a decision. */
|
|
15
|
+
const READS_A_HEADER = /\[\s*(['"`])[^'"`]+\1\s*\](?!\s*=[^=])/;
|
|
16
|
+
/** The calls that put one on a response, in the frameworks this reads. */
|
|
17
|
+
const SETS_A_HEADER = /put_resp_header|setHeader|set_header|add_header|headers\.(?:set|append)|writeHead/i;
|
|
14
18
|
/** The cloud SDKs' names for a bucket's own cross-origin rules. */
|
|
15
19
|
const CLOUD_STORAGE_CORS = /\bStorageCorsRule\b|\bCorsRules\b|\bCORSRule\b|\bCORSConfiguration\b|\bsetCorsConfiguration\b/;
|
|
16
20
|
/**
|
|
@@ -227,7 +231,12 @@ async function detectSecurity(ctx) {
|
|
|
227
231
|
*/
|
|
228
232
|
/config\.content_security_policy\b/,
|
|
229
233
|
/^\s*config\.force_ssl\s*=\s*true/m,
|
|
230
|
-
], 20
|
|
234
|
+
], 20,
|
|
235
|
+
/**
|
|
236
|
+
* The filter runs inside the search, so the budget is spent on lines that set a
|
|
237
|
+
* header rather than on lines that merely name one — see the rule below.
|
|
238
|
+
*/
|
|
239
|
+
(match) => !READS_A_HEADER.test(match.snippet) || SETS_A_HEADER.test(match.snippet));
|
|
231
240
|
/**
|
|
232
241
|
* Reading a header is not setting one.
|
|
233
242
|
*
|
|
@@ -242,9 +251,6 @@ async function detectSecurity(ctx) {
|
|
|
242
251
|
* `headers.set`, `add_header` — or an assignment to that subscript, and a line doing
|
|
243
252
|
* either is left alone.
|
|
244
253
|
*/
|
|
245
|
-
const READS_A_HEADER = /\[\s*(['"`])[^'"`]+\1\s*\](?!\s*=[^=])/;
|
|
246
|
-
const SETS_A_HEADER = /put_resp_header|setHeader|set_header|add_header|headers\.(?:set|append)|writeHead/i;
|
|
247
|
-
const headerSignalsThatSet = headerSignals.filter((hit) => !READS_A_HEADER.test(hit.snippet) || SETS_A_HEADER.test(hit.snippet));
|
|
248
254
|
/**
|
|
249
255
|
* Spring Security, which writes the headers without being asked.
|
|
250
256
|
*
|
|
@@ -283,7 +289,7 @@ async function detectSecurity(ctx) {
|
|
|
283
289
|
claim: 'headers',
|
|
284
290
|
});
|
|
285
291
|
}
|
|
286
|
-
const helmet = helmetDep ||
|
|
292
|
+
const helmet = helmetDep || headerSignals.length > 0 || springFilterChain.length > 0;
|
|
287
293
|
/**
|
|
288
294
|
* The packages the ecosystem names, as distinct from the variables authors do.
|
|
289
295
|
* Shared between the dependency check below and the binding walk further down.
|
|
@@ -381,7 +387,9 @@ async function detectSecurity(ctx) {
|
|
|
381
387
|
/HttpStatus\.TOO_MANY_REQUESTS/,
|
|
382
388
|
/HttpStatusCode\.TooManyRequests/,
|
|
383
389
|
/Status429TooManyRequests/,
|
|
384
|
-
], 20
|
|
390
|
+
], 20,
|
|
391
|
+
/** The budget counts refusals issued, not 429s this project received. */
|
|
392
|
+
(match) => !COMPARES_A_STATUS.test(match.snippet));
|
|
385
393
|
/**
|
|
386
394
|
* Still issuing, not receiving — the constants need the same test the numbers got.
|
|
387
395
|
*
|
|
@@ -390,7 +398,7 @@ async function detectSecurity(ctx) {
|
|
|
390
398
|
* refused by somebody else's, which is what nocodb's webhook invoker does. A
|
|
391
399
|
* comparison is the reading direction; an argument is the writing one.
|
|
392
400
|
*/
|
|
393
|
-
const issuedRateLimits = rateLimitSignals
|
|
401
|
+
const issuedRateLimits = rateLimitSignals;
|
|
394
402
|
const rateLimit = rateLimitDep || issuedRateLimits.length > 0;
|
|
395
403
|
/**
|
|
396
404
|
* Rate limiting where the brute force happens.
|
|
@@ -501,7 +509,7 @@ async function detectSecurity(ctx) {
|
|
|
501
509
|
evidence.push({ type: 'dependency', value: 'helmet', claim: 'headers' });
|
|
502
510
|
if (rateLimitDep)
|
|
503
511
|
evidence.push({ type: 'dependency', value: 'rate limiting package', claim: 'rate-limit' });
|
|
504
|
-
for (const m of
|
|
512
|
+
for (const m of headerSignals)
|
|
505
513
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'headers' });
|
|
506
514
|
for (const m of issuedRateLimits)
|
|
507
515
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'rate-limit' });
|
|
@@ -167,7 +167,21 @@ async function scanFiles(opts) {
|
|
|
167
167
|
ignore,
|
|
168
168
|
suppressErrors: true,
|
|
169
169
|
});
|
|
170
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Sorted, because every search below has a cap and the cap counts in this order.
|
|
172
|
+
*
|
|
173
|
+
* fast-glob returns what the filesystem hands it, which is the directory entry order
|
|
174
|
+
* on that machine. Every detector here reads the first N matches and stops, so the
|
|
175
|
+
* same repository could be read differently on two computers, or after a fresh clone
|
|
176
|
+
* — in a tool whose first word is "deterministic".
|
|
177
|
+
*
|
|
178
|
+
* It was found in n8n, where the four searches that decide a project's
|
|
179
|
+
* authentication surface filled their budget with third-party credential
|
|
180
|
+
* definitions and never reached `auth.controller.ts`. Which files won that race was
|
|
181
|
+
* nobody's decision. Sorting does not make the budget large enough; it makes the
|
|
182
|
+
* answer the same every time, which is the part that was promised.
|
|
183
|
+
*/
|
|
184
|
+
return withoutVirtualenvs(entries.map((e) => e.split(path.sep).join('/'))).sort();
|
|
171
185
|
}
|
|
172
186
|
/**
|
|
173
187
|
* Quick check: does at least one path matching glob exist?
|
|
@@ -28,5 +28,19 @@ export declare function matchLines(text: string, needles: Array<string | RegExp>
|
|
|
28
28
|
* Search a list of relative file paths for any of the given needles
|
|
29
29
|
* (string or RegExp). Returns at most `limit` matches.
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A budget counts answers, not candidates.
|
|
33
|
+
*
|
|
34
|
+
* Several detectors search with a cap and then filter what came back — the header
|
|
35
|
+
* search drops lines that *read* a header rather than set one, the rate-limit search
|
|
36
|
+
* drops a 429 this project received rather than issued. The cap was spent on the
|
|
37
|
+
* candidates, so a repository with enough noise never handed the filter anything to
|
|
38
|
+
* keep: seventy files reading `content-security-policy` filled a budget of twenty,
|
|
39
|
+
* and the file setting one was never opened.
|
|
40
|
+
*
|
|
41
|
+
* Giving the filter to the search fixes it at the root. `keep` runs per match, and
|
|
42
|
+
* only a match it keeps costs budget, so the limit means what it says — "up to this
|
|
43
|
+
* many findings" — rather than "up to this many lines that might have been findings".
|
|
44
|
+
*/
|
|
45
|
+
export declare function searchInFiles(root: string, files: string[], needles: Array<string | RegExp>, limit?: number, keep?: (match: TextMatch) => boolean): Promise<TextMatch[]>;
|
|
32
46
|
export declare function anyIncludes(haystack: string, needles: string[]): boolean;
|
package/dist/utils/textSearch.js
CHANGED
|
@@ -137,7 +137,21 @@ function matchLines(text, needles, file = '') {
|
|
|
137
137
|
* Search a list of relative file paths for any of the given needles
|
|
138
138
|
* (string or RegExp). Returns at most `limit` matches.
|
|
139
139
|
*/
|
|
140
|
-
|
|
140
|
+
/**
|
|
141
|
+
* A budget counts answers, not candidates.
|
|
142
|
+
*
|
|
143
|
+
* Several detectors search with a cap and then filter what came back — the header
|
|
144
|
+
* search drops lines that *read* a header rather than set one, the rate-limit search
|
|
145
|
+
* drops a 429 this project received rather than issued. The cap was spent on the
|
|
146
|
+
* candidates, so a repository with enough noise never handed the filter anything to
|
|
147
|
+
* keep: seventy files reading `content-security-policy` filled a budget of twenty,
|
|
148
|
+
* and the file setting one was never opened.
|
|
149
|
+
*
|
|
150
|
+
* Giving the filter to the search fixes it at the root. `keep` runs per match, and
|
|
151
|
+
* only a match it keeps costs budget, so the limit means what it says — "up to this
|
|
152
|
+
* many findings" — rather than "up to this many lines that might have been findings".
|
|
153
|
+
*/
|
|
154
|
+
async function searchInFiles(root, files, needles, limit = 25, keep) {
|
|
141
155
|
const matches = [];
|
|
142
156
|
for (const file of files) {
|
|
143
157
|
if (matches.length >= limit)
|
|
@@ -160,7 +174,9 @@ async function searchInFiles(root, files, needles, limit = 25) {
|
|
|
160
174
|
for (const n of needles) {
|
|
161
175
|
const hit = typeof n === 'string' ? line.includes(n) : n.test(line);
|
|
162
176
|
if (hit) {
|
|
163
|
-
|
|
177
|
+
const match = { file, line: i + 1, snippet: line.trim().slice(0, 200) };
|
|
178
|
+
if (!keep || keep(match))
|
|
179
|
+
matches.push(match);
|
|
164
180
|
break;
|
|
165
181
|
}
|
|
166
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.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": {
|