@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
- const stripeContextHits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
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 nodeOrPythonIsTheProduct = (0, languageShare_1.languageShare)(ctx, /\.(ts|tsx|js|jsx|mjs|cjs|py)$/) >= languageShare_1.MINIMUM_LANGUAGE_SHARE;
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.7.0",
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": {