@produtype/core 0.97.0 → 0.99.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.
|
@@ -39,7 +39,31 @@ async function detectBilling(ctx) {
|
|
|
39
39
|
/\/webhooks?\/stripe/i,
|
|
40
40
|
/\/stripe\/webhooks?/i,
|
|
41
41
|
], 30);
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The processor, declared wherever this project declares its dependencies.
|
|
44
|
+
*
|
|
45
|
+
* `hasDep` reads `package.json` and nothing else, so a product that charges people
|
|
46
|
+
* in any other language had to be caught by the `STRIPE_` variable search instead.
|
|
47
|
+
* pretix is a ticketing platform with `stripe==7.9.*`, `paypalrestsdk` and
|
|
48
|
+
* `paypal-checkout-serversdk` in its pyproject, and it was told at `high` that it
|
|
49
|
+
* has no way to charge for the product.
|
|
50
|
+
*
|
|
51
|
+
* Only processors, not billing vocabulary: the `movie-like-billing-no-stripe`
|
|
52
|
+
* fixture exists to hold that line, and a route called `/api/billing/plans` is
|
|
53
|
+
* still not a payment integration.
|
|
54
|
+
*/
|
|
55
|
+
const PROCESSOR_PACKAGES = ['stripe', 'braintree', 'paddle', 'lemonsqueezy', 'mollie', 'razorpay', 'adyen'];
|
|
56
|
+
const processorDeps = [
|
|
57
|
+
...(0, detectContext_1.hasAnyPyDep)(ctx, [...PROCESSOR_PACKAGES, 'paypalrestsdk', 'paypal-checkout-serversdk', 'mollie-api-python']),
|
|
58
|
+
...(0, detectContext_1.hasAnyRubyDep)(ctx, [...PROCESSOR_PACKAGES, 'paypal-sdk-rest']),
|
|
59
|
+
...(0, detectContext_1.hasAnyPhpDep)(ctx, ['stripe/stripe-php', 'paypal/rest-api-sdk-php', 'mollie/mollie-api-php', 'braintree/braintree_php']),
|
|
60
|
+
...(0, detectContext_1.hasAnyGradleDep)(ctx, ['com.stripe:stripe-java', 'com.braintreepayments']),
|
|
61
|
+
...(0, detectContext_1.hasAnyDotnetDep)(ctx, ['Stripe.net', 'Braintree', 'PayPalCheckoutSdk']),
|
|
62
|
+
...(0, detectContext_1.hasAnyRustDep)(ctx, ['stripe-rust', 'async-stripe']),
|
|
63
|
+
];
|
|
64
|
+
for (const dep of processorDeps)
|
|
65
|
+
evidence.push({ type: 'dependency', value: dep });
|
|
66
|
+
const hasStrongStripeSignal = hasStripeDep || processorDeps.length > 0 || stripeContextHits.length > 0;
|
|
43
67
|
const webhookRouteHits = hasStrongStripeSignal
|
|
44
68
|
? await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
45
69
|
/\/webhooks?\b/i,
|
|
@@ -5,6 +5,7 @@ const textSearch_1 = require("../utils/textSearch");
|
|
|
5
5
|
const lookupTables_1 = require("./structural/lookupTables");
|
|
6
6
|
const secretArguments_1 = require("./structural/secretArguments");
|
|
7
7
|
const readingDepth_1 = require("./readingDepth");
|
|
8
|
+
const djangoSettings_1 = require("./djangoSettings");
|
|
8
9
|
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;
|
|
9
10
|
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;
|
|
10
11
|
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;
|
|
@@ -148,12 +149,25 @@ async function detectEnv(ctx) {
|
|
|
148
149
|
* argue with — silence is not.
|
|
149
150
|
*/
|
|
150
151
|
const secretSinksUnasked = (0, readingDepth_1.wentUnasked)(secretArguments, sourceFiles) && (await (0, secretArguments_1.anyFileReachesASecretSink)(ctx.root, sourceFiles));
|
|
152
|
+
/**
|
|
153
|
+
* A settings module the application does not run is not where its secret lives.
|
|
154
|
+
*
|
|
155
|
+
* pretix keeps `SECRET_KEY = "build-time-secret-key"` in `_build_settings.py`, a
|
|
156
|
+
* module named only by its packaging script, while `manage.py` and `wsgi.py` both
|
|
157
|
+
* name `pretix.settings`. That literal was the one `critical` in its report.
|
|
158
|
+
*
|
|
159
|
+
* Only applied when the entry points say something: with no `manage.py` to read,
|
|
160
|
+
* nothing is excused and the behaviour is what it was.
|
|
161
|
+
*/
|
|
162
|
+
const runningSettings = await (0, djangoSettings_1.settingsModulesTheAppRuns)(ctx);
|
|
163
|
+
const isAnotherSettingsModule = (file) => runningSettings.length > 0 && (0, djangoSettings_1.looksLikeDjangoSettings)(file) && !runningSettings.includes(file);
|
|
151
164
|
const weakHits = fallbackHits.filter((m) => WEAK_SECRET_VALUE_RE.test(m.snippet)
|
|
152
165
|
&& SECRET_ASSIGNMENT_CONTEXT_RE.test(m.snippet)
|
|
153
166
|
&& !namesItself(m.snippet)
|
|
154
167
|
&& !valueIsAnIdentifier(m.snippet)
|
|
155
168
|
&& !REDACTED_VALUE_RE.test(m.snippet)
|
|
156
|
-
&& !isTableEntry(m)
|
|
169
|
+
&& !isTableEntry(m)
|
|
170
|
+
&& !isAnotherSettingsModule(m.file));
|
|
157
171
|
for (const m of weakHits) {
|
|
158
172
|
const hitEvidence = { type: 'snippet', value: m.snippet, file: m.file, line: m.line };
|
|
159
173
|
evidence.push(hitEvidence);
|
|
@@ -329,6 +329,17 @@ async function detectSecurity(ctx) {
|
|
|
329
329
|
* issuing the refusal rather than reading one: a constant is what you construct
|
|
330
330
|
* a response from, where receiving is a comparison against `.status`.
|
|
331
331
|
*/
|
|
332
|
+
/**
|
|
333
|
+
* Laravel's, which is a middleware alias the framework registers.
|
|
334
|
+
*
|
|
335
|
+
* `Route::middleware(['throttle:oauth2-socialite'])` is monica applying the
|
|
336
|
+
* limiter that ships with the framework, and `RateLimiter::for('login', ...)`
|
|
337
|
+
* is where the limit is defined. Neither is a package to depend on — the
|
|
338
|
+
* limiter is part of Laravel — so a list of packages could never find them, and
|
|
339
|
+
* monica was told at `high` that it does not throttle.
|
|
340
|
+
*/
|
|
341
|
+
/['"]throttle:[\w.-]+['"]/,
|
|
342
|
+
/\bRateLimiter::for\s*\(/,
|
|
332
343
|
/StatusCode::TOO_MANY_REQUESTS/,
|
|
333
344
|
/http\.StatusTooManyRequests/,
|
|
334
345
|
/HttpStatus\.TOO_MANY_REQUESTS/,
|
|
@@ -18,3 +18,23 @@ export declare function findDjangoSettings(ctx: DetectContext): Promise<{
|
|
|
18
18
|
file: string;
|
|
19
19
|
text: string;
|
|
20
20
|
} | null>;
|
|
21
|
+
/**
|
|
22
|
+
* The settings module the application actually runs, named by Django's own entry points.
|
|
23
|
+
*
|
|
24
|
+
* `manage.py`, `wsgi.py` and `asgi.py` each set `DJANGO_SETTINGS_MODULE`, and that is
|
|
25
|
+
* the framework saying which configuration is the project's. Everything else called
|
|
26
|
+
* `settings` is somebody's variant: a test harness, a build-time module, a sample.
|
|
27
|
+
*
|
|
28
|
+
* pretix is the measured case. `src/pretix/_build_settings.py` holds
|
|
29
|
+
* `SECRET_KEY = "build-time-secret-key"` and is named only by `src/pretix/_build.py`,
|
|
30
|
+
* a packaging script; `manage.py` and `wsgi.py` both name `pretix.settings`. That
|
|
31
|
+
* literal was the single `critical` in pretix's report — the severity that bars a
|
|
32
|
+
* report from the top band — raised against a line that never serves a request.
|
|
33
|
+
*
|
|
34
|
+
* A name rule could not do this: `configuration_testing.py` was caught by one in
|
|
35
|
+
* 0.80.0, and `_build_settings.py` would need "build" to mean something, which it does
|
|
36
|
+
* not. The entry point is a fact, not a word.
|
|
37
|
+
*/
|
|
38
|
+
export declare function settingsModulesTheAppRuns(ctx: DetectContext): Promise<string[]>;
|
|
39
|
+
/** Whether a path is the sort of file a Django project keeps its configuration in. */
|
|
40
|
+
export declare function looksLikeDjangoSettings(file: string): boolean;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.findDjangoSettings = findDjangoSettings;
|
|
4
|
+
exports.settingsModulesTheAppRuns = settingsModulesTheAppRuns;
|
|
5
|
+
exports.looksLikeDjangoSettings = looksLikeDjangoSettings;
|
|
4
6
|
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
5
7
|
/**
|
|
6
8
|
* A settings file that is Django's, rather than one that shares its name.
|
|
@@ -31,3 +33,71 @@ async function findDjangoSettings(ctx) {
|
|
|
31
33
|
}
|
|
32
34
|
return null;
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The settings module the application actually runs, named by Django's own entry points.
|
|
38
|
+
*
|
|
39
|
+
* `manage.py`, `wsgi.py` and `asgi.py` each set `DJANGO_SETTINGS_MODULE`, and that is
|
|
40
|
+
* the framework saying which configuration is the project's. Everything else called
|
|
41
|
+
* `settings` is somebody's variant: a test harness, a build-time module, a sample.
|
|
42
|
+
*
|
|
43
|
+
* pretix is the measured case. `src/pretix/_build_settings.py` holds
|
|
44
|
+
* `SECRET_KEY = "build-time-secret-key"` and is named only by `src/pretix/_build.py`,
|
|
45
|
+
* a packaging script; `manage.py` and `wsgi.py` both name `pretix.settings`. That
|
|
46
|
+
* literal was the single `critical` in pretix's report — the severity that bars a
|
|
47
|
+
* report from the top band — raised against a line that never serves a request.
|
|
48
|
+
*
|
|
49
|
+
* A name rule could not do this: `configuration_testing.py` was caught by one in
|
|
50
|
+
* 0.80.0, and `_build_settings.py` would need "build" to mean something, which it does
|
|
51
|
+
* not. The entry point is a fact, not a word.
|
|
52
|
+
*/
|
|
53
|
+
async function settingsModulesTheAppRuns(ctx) {
|
|
54
|
+
const entryPoints = ctx.files.all.filter((file) => /(^|\/)(manage|wsgi|asgi)\.py$/.test(file));
|
|
55
|
+
const modules = new Set();
|
|
56
|
+
for (const file of entryPoints.slice(0, 6)) {
|
|
57
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
58
|
+
if (!text)
|
|
59
|
+
continue;
|
|
60
|
+
for (const match of text.matchAll(/DJANGO_SETTINGS_MODULE["'\s,]+["']([\w.]+)["']/g)) {
|
|
61
|
+
modules.add(match[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (modules.size === 0)
|
|
65
|
+
return [];
|
|
66
|
+
const paths = [];
|
|
67
|
+
for (const module of modules) {
|
|
68
|
+
const asPath = module.replace(/\./g, '/');
|
|
69
|
+
for (const file of ctx.files.all) {
|
|
70
|
+
if (file.endsWith(`${asPath}.py`) || file.endsWith(`${asPath}/__init__.py`))
|
|
71
|
+
paths.push(file);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A settings package is loaded whole, through its own imports.
|
|
76
|
+
*
|
|
77
|
+
* `config.settings.production` starts with `from .base import *`, so `base.py` is as
|
|
78
|
+
* much the running configuration as the module named. The first version of this rule
|
|
79
|
+
* excused `base.py` and a fixture written for exactly that shape — a `SECRET_KEY =
|
|
80
|
+
* "changeme"` in the base module — went from `missing` to `passed` in the corpus
|
|
81
|
+
* diff, which is how the mistake surfaced within a minute of making it.
|
|
82
|
+
*
|
|
83
|
+
* Every module beside the named one, inside a directory called `settings`, counts.
|
|
84
|
+
* That is the shape of a split configuration, and it does not reach a sibling of a
|
|
85
|
+
* plain `settings.py` — which is where pretix keeps the build-time module this rule
|
|
86
|
+
* exists to exclude.
|
|
87
|
+
*/
|
|
88
|
+
const withSiblings = new Set(paths);
|
|
89
|
+
for (const file of paths) {
|
|
90
|
+
const directory = file.slice(0, file.lastIndexOf('/'));
|
|
91
|
+
if (!/(^|\/)settings$/.test(directory))
|
|
92
|
+
continue;
|
|
93
|
+
for (const candidate of ctx.files.all) {
|
|
94
|
+
if (candidate.startsWith(`${directory}/`) && candidate.endsWith('.py'))
|
|
95
|
+
withSiblings.add(candidate);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...withSiblings];
|
|
99
|
+
}
|
|
100
|
+
/** Whether a path is the sort of file a Django project keeps its configuration in. */
|
|
101
|
+
function looksLikeDjangoSettings(file) {
|
|
102
|
+
return /(^|\/)[\w-]*settings[\w-]*\.py$/i.test(file) || /(^|\/)settings\/[\w-]+\.py$/i.test(file);
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.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": {
|