@produtype/core 1.7.0 → 1.8.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.
|
@@ -233,6 +233,20 @@ function isTestOrExamplePath(file) {
|
|
|
233
233
|
|| /\.(e2e|e2e-spec|cy|stories)\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(file)
|
|
234
234
|
|| /_spec\.rb$/i.test(file)
|
|
235
235
|
|| /_test\.(go|py|rb|java|cs|php)$/i.test(file)
|
|
236
|
+
|| /_test\.exs$/i.test(file)
|
|
237
|
+
/**
|
|
238
|
+
* Mix names its environments, and the file name is the environment.
|
|
239
|
+
*
|
|
240
|
+
* `config/test.exs` and `config/dev.exs` are compiled only under `MIX_ENV=test` and
|
|
241
|
+
* `MIX_ENV=dev`; a production release carries `config/prod.exs` and
|
|
242
|
+
* `config/runtime.exs` and neither of the other two. supabase/realtime keeps
|
|
243
|
+
* `metrics_jwt_secret: "test"` in `config/test.exs` and it was the one `critical`
|
|
244
|
+
* in its report — a literal written to be fake, read as production configuration.
|
|
245
|
+
*
|
|
246
|
+
* The same argument as `_test.go`: the name is the toolchain's, not the author's,
|
|
247
|
+
* and it says which build the file is part of.
|
|
248
|
+
*/
|
|
249
|
+
|| /(^|\/)config\/(test|dev)\.exs$/i.test(file)
|
|
236
250
|
|| /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
237
251
|
/**
|
|
238
252
|
* The other half of the convention.
|
|
@@ -12,7 +12,24 @@ async function detectBilling(ctx) {
|
|
|
12
12
|
const hasStripeDep = (0, detectContext_1.hasDep)(ctx, 'stripe');
|
|
13
13
|
if (hasStripeDep)
|
|
14
14
|
evidence.push({ type: 'dependency', value: 'stripe' });
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Laravel's table of service credentials, which is a list of slots.
|
|
17
|
+
*
|
|
18
|
+
* `config/services.php` ships with the framework and holds a block per service the
|
|
19
|
+
* skeleton knows about — mailgun, postmark, ses, and for years a `stripe` one
|
|
20
|
+
* reading `env('STRIPE_KEY')`. Firefly III still carries it, beside sparkpost,
|
|
21
|
+
* mandrill and pushover, and takes no payments at all: no processor package in its
|
|
22
|
+
* composer.json, no charge anywhere. It was reported at `medium` for an unhardened
|
|
23
|
+
* billing webhook, and the rest of that finding was its own webhooks feature —
|
|
24
|
+
* notifications a *user* registers to hear about their transactions, which point
|
|
25
|
+
* the other way entirely.
|
|
26
|
+
*
|
|
27
|
+
* An entry there is a slot the deployment may fill, not an integration this project
|
|
28
|
+
* has. A project that charges people declares `stripe/stripe-php` or
|
|
29
|
+
* `laravel/cashier`, and that is read from the manifest a few lines down.
|
|
30
|
+
*/
|
|
31
|
+
const LARAVEL_SERVICE_SLOTS = /(^|\/)config\/services\.php$/;
|
|
32
|
+
const stripeContextHits = (await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
16
33
|
/**
|
|
17
34
|
* The bare word is not here, on purpose.
|
|
18
35
|
*
|
|
@@ -38,7 +55,7 @@ async function detectBilling(ctx) {
|
|
|
38
55
|
/stripeSubscriptionId/i,
|
|
39
56
|
/\/webhooks?\/stripe/i,
|
|
40
57
|
/\/stripe\/webhooks?/i,
|
|
41
|
-
], 30);
|
|
58
|
+
], 30)).filter((hit) => !LARAVEL_SERVICE_SLOTS.test(hit.file));
|
|
42
59
|
/**
|
|
43
60
|
* The processor, declared wherever this project declares its dependencies.
|
|
44
61
|
*
|
|
@@ -25,6 +25,23 @@ const GENERIC_SECRET_ASSIGNMENT_RE = /(JWT_SECRET|SECRET_KEY|SESSION_SECRET|API_
|
|
|
25
25
|
* because the identifier does — so the value itself was never looked at.
|
|
26
26
|
*/
|
|
27
27
|
const REDACTED_VALUE_RE = /[:=]\s*['"`](\*{2,}|x{3,}|<?\[?redacted\]?>?|hidden|\.{3,})['"`]/i;
|
|
28
|
+
/**
|
|
29
|
+
* A sentence assigned to a secret-named identifier is not a secret.
|
|
30
|
+
*
|
|
31
|
+
* `secret_key: "must be #{@secret_key_size} bytes in Base 64 URL alphabet"` is
|
|
32
|
+
* Livebook's validation message, returned when somebody types a bad key, and it was
|
|
33
|
+
* the one `critical` in its report. The finding is "Weak/fallback secret values" and
|
|
34
|
+
* the value was never looked at: every test above runs on the whole line, and the
|
|
35
|
+
* line says "secret" because the identifier does. The redaction rule beside this one
|
|
36
|
+
* was the same discovery made once, for one shape, and patched there.
|
|
37
|
+
*
|
|
38
|
+
* A secret is a token. It has no spaces in it and it is not a template with a
|
|
39
|
+
* variable in the middle — an error message, a label and a sentence all do. Three
|
|
40
|
+
* Elixir products raised this `critical` on the day Elixir became readable, and
|
|
41
|
+
* `critical` is the severity that caps maturity, so it was the loudest thing in each
|
|
42
|
+
* of three reports about products that had done nothing wrong.
|
|
43
|
+
*/
|
|
44
|
+
const VALUE_IS_A_SENTENCE_RE = /[:=]\s*(['"`])[^'"`]*(?:\s|#\{|\$\{)[^'"`]*\1/;
|
|
28
45
|
function classifySecretFallback(snippet) {
|
|
29
46
|
if (/JWT_SECRET/i.test(snippet))
|
|
30
47
|
return 'jwt';
|
|
@@ -166,6 +183,7 @@ async function detectEnv(ctx) {
|
|
|
166
183
|
&& !namesItself(m.snippet)
|
|
167
184
|
&& !valueIsAnIdentifier(m.snippet)
|
|
168
185
|
&& !REDACTED_VALUE_RE.test(m.snippet)
|
|
186
|
+
&& !VALUE_IS_A_SENTENCE_RE.test(m.snippet)
|
|
169
187
|
&& !isTableEntry(m)
|
|
170
188
|
&& !isAnotherSettingsModule(m.file));
|
|
171
189
|
for (const m of weakHits) {
|
|
@@ -97,7 +97,7 @@ function resolveFromOtherManifests(ctx) {
|
|
|
97
97
|
const best = pool.reduce((winner, candidate) => candidate.depth < winner.depth || (candidate.depth === winner.depth && candidate.order < winner.order)
|
|
98
98
|
? candidate
|
|
99
99
|
: winner);
|
|
100
|
-
return { manager: best.manager, confidence: 'manifest', warnings: [], evidence: [best.file] };
|
|
100
|
+
return { manager: best.manager, confidence: 'manifest', warnings: [], evidence: [best.file], share: best.share };
|
|
101
101
|
}
|
|
102
102
|
function resolveWorkspaceManager(workspace) {
|
|
103
103
|
const warnings = [];
|
|
@@ -156,7 +156,24 @@ async function detectPackageManager(ctx) {
|
|
|
156
156
|
* prefer: a repository whose only manifest is a package.json is an npm repository
|
|
157
157
|
* however little JavaScript it has.
|
|
158
158
|
*/
|
|
159
|
-
const
|
|
159
|
+
const nodeOrPythonShare = (0, languageShare_1.languageShare)(ctx, /\.(ts|tsx|js|jsx|mjs|cjs|py)$/);
|
|
160
|
+
/**
|
|
161
|
+
* And clearing the line is not the same as winning.
|
|
162
|
+
*
|
|
163
|
+
* The test above asks whether Node is a real part of the repository, and the
|
|
164
|
+
* question is which manifest builds the *product*. Firefly III is 1748 PHP files
|
|
165
|
+
* and 221 of JavaScript — eleven per cent, comfortably over the line — and it is a
|
|
166
|
+
* Laravel application whose package.json runs Vite over its frontend assets. The
|
|
167
|
+
* report said "Package manager: npm (lockfile)" about a project installed with
|
|
168
|
+
* Composer.
|
|
169
|
+
*
|
|
170
|
+
* Where both manifests clear the line, the larger language decides, which is the
|
|
171
|
+
* comparison the other manifests already make among themselves. Below the line
|
|
172
|
+
* nothing changes, and a repository whose only manifest is a package.json is an npm
|
|
173
|
+
* repository however little JavaScript it has.
|
|
174
|
+
*/
|
|
175
|
+
const nodeOrPythonIsTheProduct = nodeOrPythonShare >= languageShare_1.MINIMUM_LANGUAGE_SHARE
|
|
176
|
+
&& nodeOrPythonShare >= (otherManifest?.share ?? 0);
|
|
160
177
|
const selected = fromWorkspaces
|
|
161
178
|
&& fromWorkspaces.manager !== 'unknown'
|
|
162
179
|
&& (nodeOrPythonIsTheProduct || !otherManifest)
|
|
@@ -11,6 +11,23 @@ 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
|
+
/** The cloud SDKs' names for a bucket's own cross-origin rules. */
|
|
15
|
+
const CLOUD_STORAGE_CORS = /\bStorageCorsRule\b|\bCorsRules\b|\bCORSRule\b|\bCORSConfiguration\b|\bsetCorsConfiguration\b/;
|
|
16
|
+
/**
|
|
17
|
+
* Whether this line sits inside one of those rules.
|
|
18
|
+
*
|
|
19
|
+
* The type is named where the object is opened and the origins are listed a few lines
|
|
20
|
+
* further in — bitwarden's `CorsRules.Add(new StorageCorsRule` is two lines above its
|
|
21
|
+
* `AllowedOrigins`. A short window rather than the whole file, because the names are
|
|
22
|
+
* specific enough to be decisive and a file-wide test would excuse a real policy that
|
|
23
|
+
* happens to share a file with a bucket's.
|
|
24
|
+
*/
|
|
25
|
+
const CLOUD_STORAGE_CORS_WINDOW = 5;
|
|
26
|
+
function namesACloudStorageRule(text, line) {
|
|
27
|
+
const lines = text.split(/\r?\n/);
|
|
28
|
+
const from = Math.max(0, line - 1 - CLOUD_STORAGE_CORS_WINDOW);
|
|
29
|
+
return lines.slice(from, line).some((candidate) => CLOUD_STORAGE_CORS.test(candidate));
|
|
30
|
+
}
|
|
14
31
|
/**
|
|
15
32
|
* The same line, allowing everyone.
|
|
16
33
|
*
|
|
@@ -454,6 +471,22 @@ async function detectSecurity(ctx) {
|
|
|
454
471
|
// allowlist made a wildcard read as restricted; and it cited line 1, which in
|
|
455
472
|
// every repository that triggered it was an import.
|
|
456
473
|
for (const m of (0, textSearch_1.matchLines)(text, ORIGIN_HANDLING, file)) {
|
|
474
|
+
/**
|
|
475
|
+
* A bucket's CORS is not the application's CORS.
|
|
476
|
+
*
|
|
477
|
+
* bitwarden's Aspire host configures the local Azurite storage emulator with
|
|
478
|
+
* `AllowedOrigins = [new BicepValue<string>("*")]` inside a `StorageCorsRule`,
|
|
479
|
+
* and that one line made a `high` finding out of an API whose actual policy is
|
|
480
|
+
* `SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))`
|
|
481
|
+
* — a function deciding, cited two lines below it in the same report.
|
|
482
|
+
*
|
|
483
|
+
* The subject is different, not the severity: a storage account, a bucket or a
|
|
484
|
+
* CDN distribution answers for the objects it serves, and this check is about
|
|
485
|
+
* the requests this application answers. `StorageCorsRule`, `CorsRules` and S3's
|
|
486
|
+
* `CORSRule` are type names from the cloud SDKs, so they are the anchor.
|
|
487
|
+
*/
|
|
488
|
+
if (namesACloudStorageRule(text, m.line))
|
|
489
|
+
continue;
|
|
457
490
|
if (WILDCARD_ORIGIN.test(m.snippet))
|
|
458
491
|
corsLoose.push(m);
|
|
459
492
|
else if (allowsAChosenOrigin(m.snippet, text))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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": {
|