@produtype/core 1.6.0 → 1.7.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.
|
@@ -306,7 +306,7 @@ function isTestOrExamplePath(file) {
|
|
|
306
306
|
* `.vue`, `.svelte` and `.astro` were missing for the same reason: a single-file
|
|
307
307
|
* component holds the logic, not just the markup.
|
|
308
308
|
*/
|
|
309
|
-
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go|rb|java|cs|rs|kt|swift|dart|html?|vue|svelte|astro)$/;
|
|
309
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go|rb|java|cs|rs|kt|swift|dart|ex|exs|html?|vue|svelte|astro)$/;
|
|
310
310
|
function pickSource(files) {
|
|
311
311
|
return files.filter((f) => SOURCE_EXTENSIONS.test(f) && !isTestOrExamplePath(f));
|
|
312
312
|
}
|
|
@@ -321,7 +321,6 @@ function pickSource(files) {
|
|
|
321
321
|
* Being wrong is recoverable. Being wrong while announcing no reservations is not.
|
|
322
322
|
*/
|
|
323
323
|
const KNOWN_UNREADABLE = [
|
|
324
|
-
[/\.(ex|exs)$/, 'Elixir'],
|
|
325
324
|
[/\.(scala|sc)$/, 'Scala'],
|
|
326
325
|
[/\.(clj|cljs)$/, 'Clojure'],
|
|
327
326
|
[/\.(cpp|cc|hpp)$/, 'C++'],
|
|
@@ -193,6 +193,7 @@ exports.LANGUAGES = [
|
|
|
193
193
|
{ id: 'kotlin', label: 'Kotlin', extensions: /\.(kt|kts)$/ },
|
|
194
194
|
{ id: 'swift', label: 'Swift', extensions: /\.swift$/ },
|
|
195
195
|
{ id: 'dart', label: 'Dart', extensions: /\.dart$/ },
|
|
196
|
+
{ id: 'elixir', label: 'Elixir', extensions: /\.exs?$/ },
|
|
196
197
|
];
|
|
197
198
|
/** Display names for ids the tables above produce. */
|
|
198
199
|
const LABELS = {
|
|
@@ -30,9 +30,11 @@ const LOGGING_PACKAGES = [
|
|
|
30
30
|
'debug',
|
|
31
31
|
'npmlog',
|
|
32
32
|
];
|
|
33
|
+
/** Elixir reaches for a backend rather than a logger: Logger itself is in OTP. */
|
|
34
|
+
const ELIXIR_LOGGING_PACKAGES = ['logger_json', 'logger_file_backend', 'sentry'];
|
|
33
35
|
async function detectObservability(ctx) {
|
|
34
36
|
const evidence = [];
|
|
35
|
-
const logDeps = (0, detectContext_1.hasAnyDep)(ctx, LOGGING_PACKAGES);
|
|
37
|
+
const logDeps = [...(0, detectContext_1.hasAnyDep)(ctx, LOGGING_PACKAGES), ...(0, detectContext_1.hasAnyElixirDep)(ctx, ELIXIR_LOGGING_PACKAGES)];
|
|
36
38
|
const sentryDeps = (0, detectContext_1.hasAnyDep)(ctx, ['@sentry/node', 'sentry-sdk']);
|
|
37
39
|
for (const d of logDeps)
|
|
38
40
|
evidence.push({ type: 'dependency', value: d, claim: 'logging' });
|
|
@@ -100,6 +102,13 @@ async function detectObservability(ctx) {
|
|
|
100
102
|
/LoggerFactory\.getLogger|org\.slf4j/,
|
|
101
103
|
/Rails\.logger/,
|
|
102
104
|
/\btracing::(info|warn|error|debug)!|\blog::(info|warn|error)!/,
|
|
105
|
+
/**
|
|
106
|
+
* Elixir's, which is one module in the standard library.
|
|
107
|
+
*
|
|
108
|
+
* `Logger.info(...)` is how every Phoenix application logs, and `Logger` is
|
|
109
|
+
* OTP's name rather than anybody's variable.
|
|
110
|
+
*/
|
|
111
|
+
/\bLogger\.(info|warning|warn|error|debug|notice)\(/,
|
|
103
112
|
], 15);
|
|
104
113
|
for (const m of structuredHits)
|
|
105
114
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'logging' });
|
|
@@ -140,7 +149,7 @@ async function detectObservability(ctx) {
|
|
|
140
149
|
evidence.push(...(0, absenceEvidence_1.searchedFor)('a health endpoint', ['/health', '/healthz', '/readyz', 'a health, healthz, readyz, liveness or readiness route file'], 'health'));
|
|
141
150
|
}
|
|
142
151
|
if (!hasAnyLogging) {
|
|
143
|
-
evidence.push(...(0, absenceEvidence_1.searchedFor)('logging', ['winston', 'pino', 'morgan', 'bunyan', 'Monolog', 'slog', 'zap', 'logrus', 'slf4j', 'Rails.logger', 'tracing::', 'logger.info/warn/error/debug', 'error_log(', 'JSON.stringify with a level field'], 'logging'));
|
|
152
|
+
evidence.push(...(0, absenceEvidence_1.searchedFor)('logging', ['winston', 'pino', 'morgan', 'bunyan', 'Monolog', 'slog', 'zap', 'logrus', 'slf4j', 'Rails.logger', 'tracing::', 'logger.info/warn/error/debug', 'Logger.info', 'error_log(', 'JSON.stringify with a level field'], 'logging'));
|
|
144
153
|
}
|
|
145
154
|
return {
|
|
146
155
|
key: 'observability.core',
|
|
@@ -211,6 +211,23 @@ async function detectSecurity(ctx) {
|
|
|
211
211
|
/config\.content_security_policy\b/,
|
|
212
212
|
/^\s*config\.force_ssl\s*=\s*true/m,
|
|
213
213
|
], 20);
|
|
214
|
+
/**
|
|
215
|
+
* Reading a header is not setting one.
|
|
216
|
+
*
|
|
217
|
+
* plausible ships `tracker/installation_support/check-disallowed-by-csp.js`, whose
|
|
218
|
+
* whole job is to look at somebody else's `content-security-policy` and tell a user
|
|
219
|
+
* why the tracker was blocked. Its line `responseHeaders?.['content-security-policy']`
|
|
220
|
+
* was the evidence behind "security headers: passed" — a tool that inspects other
|
|
221
|
+
* people's headers credited with setting its own.
|
|
222
|
+
*
|
|
223
|
+
* The shape is the anchor, not the file: a header name used as a key into a headers
|
|
224
|
+
* object is a lookup. Setting one is a call — `put_resp_header`, `setHeader`,
|
|
225
|
+
* `headers.set`, `add_header` — or an assignment to that subscript, and a line doing
|
|
226
|
+
* either is left alone.
|
|
227
|
+
*/
|
|
228
|
+
const READS_A_HEADER = /\[\s*(['"`])[^'"`]+\1\s*\](?!\s*=[^=])/;
|
|
229
|
+
const SETS_A_HEADER = /put_resp_header|setHeader|set_header|add_header|headers\.(?:set|append)|writeHead/i;
|
|
230
|
+
const headerSignalsThatSet = headerSignals.filter((hit) => !READS_A_HEADER.test(hit.snippet) || SETS_A_HEADER.test(hit.snippet));
|
|
214
231
|
/**
|
|
215
232
|
* Spring Security, which writes the headers without being asked.
|
|
216
233
|
*
|
|
@@ -249,7 +266,7 @@ async function detectSecurity(ctx) {
|
|
|
249
266
|
claim: 'headers',
|
|
250
267
|
});
|
|
251
268
|
}
|
|
252
|
-
const helmet = helmetDep ||
|
|
269
|
+
const helmet = helmetDep || headerSignalsThatSet.length > 0 || springFilterChain.length > 0;
|
|
253
270
|
/**
|
|
254
271
|
* The packages the ecosystem names, as distinct from the variables authors do.
|
|
255
272
|
* Shared between the dependency check below and the binding walk further down.
|
|
@@ -406,7 +423,7 @@ async function detectSecurity(ctx) {
|
|
|
406
423
|
evidence.push({ type: 'dependency', value: 'helmet', claim: 'headers' });
|
|
407
424
|
if (rateLimitDep)
|
|
408
425
|
evidence.push({ type: 'dependency', value: 'rate limiting package', claim: 'rate-limit' });
|
|
409
|
-
for (const m of
|
|
426
|
+
for (const m of headerSignalsThatSet)
|
|
410
427
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'headers' });
|
|
411
428
|
for (const m of issuedRateLimits)
|
|
412
429
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'rate-limit' });
|
|
@@ -580,6 +597,33 @@ async function detectSecurity(ctx) {
|
|
|
580
597
|
const cited = anyOrigin[0] ?? chosen[0] ?? aspNetCors[0];
|
|
581
598
|
target.push({ file: cited.file, line: cited.line, snippet: cited.snippet.trim().slice(0, 200) });
|
|
582
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Elixir's, which is a plug in the endpoint or the router.
|
|
602
|
+
*
|
|
603
|
+
* `cors_plug` and `corsica` are the two packages, and both are used the same way:
|
|
604
|
+
* `plug CORSPlug, origin: ["https://app.example.com"]` or `plug Corsica, origins:
|
|
605
|
+
* "*"`. The module name comes from the package, so it is the anchor; what the
|
|
606
|
+
* author chose is the value of `origin`.
|
|
607
|
+
*
|
|
608
|
+
* `plug` takes parentheses as readily as not — plausible writes `plug(CORSPlug)` in
|
|
609
|
+
* its endpoint, and a rule that required a space after the keyword could not see
|
|
610
|
+
* it. Both forms are ordinary Elixir and the formatter leaves either alone.
|
|
611
|
+
*
|
|
612
|
+
* `"*"` is the wide-open one. A list, a function or a regex is a decision somebody
|
|
613
|
+
* made, and the same reflection-versus-allowlist question the other frameworks are
|
|
614
|
+
* asked. Where the plug names no origin at all, cors_plug's own default is `"*"`,
|
|
615
|
+
* so silence is the open branch rather than the strict one.
|
|
616
|
+
*/
|
|
617
|
+
const elixirCorsPackages = (0, detectContext_1.hasAnyElixirDep)(ctx, ['cors_plug', 'corsica']);
|
|
618
|
+
if (elixirCorsPackages.length > 0) {
|
|
619
|
+
const elixirSource = source.filter((f) => /\.exs?$/i.test(f));
|
|
620
|
+
const plugged = await (0, textSearch_1.searchInFiles)(ctx.root, elixirSource, [/\bplug[\s(]+(?:CORSPlug|Corsica)\b/], 5);
|
|
621
|
+
for (const hit of plugged) {
|
|
622
|
+
const origin = /origins?:\s*(.+)$/.exec(hit.snippet);
|
|
623
|
+
const wideOpen = !origin || /^["']\*["']/.test(origin[1].trim());
|
|
624
|
+
(wideOpen ? corsLoose : corsStrict).push({ file: hit.file, line: hit.line, snippet: hit.snippet.trim().slice(0, 200) });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
583
627
|
const django = await (0, djangoSettings_1.findDjangoSettings)(ctx);
|
|
584
628
|
if (django) {
|
|
585
629
|
const middlewareLine = django.text
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The lines of a Python file that are
|
|
2
|
+
* The lines of a Python or Elixir file that are prose rather than code.
|
|
3
3
|
*
|
|
4
4
|
* This analyzer has skipped comments since the day it read its own prose about Stripe
|
|
5
5
|
* as evidence that it takes payments. The rule looks for a line marker — `#`, `//`,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.proseLines = proseLines;
|
|
4
4
|
/**
|
|
5
|
-
* The lines of a Python file that are
|
|
5
|
+
* The lines of a Python or Elixir file that are prose rather than code.
|
|
6
6
|
*
|
|
7
7
|
* This analyzer has skipped comments since the day it read its own prose about Stripe
|
|
8
8
|
* as evidence that it takes payments. The rule looks for a line marker — `#`, `//`,
|
|
@@ -20,6 +20,8 @@ exports.proseLines = proseLines;
|
|
|
20
20
|
* has nothing before it but indentation.
|
|
21
21
|
*/
|
|
22
22
|
function proseLines(file, text) {
|
|
23
|
+
if (/\.exs?$/i.test(file))
|
|
24
|
+
return elixirDocLines(text);
|
|
23
25
|
const prose = new Set();
|
|
24
26
|
if (!/\.py$/i.test(file))
|
|
25
27
|
return prose;
|
|
@@ -54,3 +56,37 @@ function proseLines(file, text) {
|
|
|
54
56
|
}
|
|
55
57
|
return prose;
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Elixir writes its prose as a module attribute, and the rest is a heredoc.
|
|
61
|
+
*
|
|
62
|
+
* `@moduledoc """ ... """` is the same trap as a Python docstring with a different
|
|
63
|
+
* marker: the interior lines begin with whatever the author was saying, and nothing
|
|
64
|
+
* about them says they are not code. Phoenix generators put one at the top of every
|
|
65
|
+
* controller, context and channel, so a repository has thousands.
|
|
66
|
+
*
|
|
67
|
+
* Anchored on Elixir's own attribute names — `@moduledoc`, `@doc`, `@typedoc`,
|
|
68
|
+
* `@shortdoc` — rather than on the heredoc, because `@query """SELECT ..."""` is data
|
|
69
|
+
* assigned to a name and a hardcoded secret could live in one. Same distinction the
|
|
70
|
+
* Python reader draws, made by a different marker.
|
|
71
|
+
*/
|
|
72
|
+
function elixirDocLines(text) {
|
|
73
|
+
const prose = new Set();
|
|
74
|
+
const lines = text.split(/\r?\n/);
|
|
75
|
+
let open = false;
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
if (open) {
|
|
79
|
+
prose.add(i + 1);
|
|
80
|
+
if (/"""/.test(line))
|
|
81
|
+
open = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const doc = /^\s*@(?:module|type|short)?doc\s+(?:~[A-Za-z])?"""/.exec(line);
|
|
85
|
+
if (!doc)
|
|
86
|
+
continue;
|
|
87
|
+
prose.add(i + 1);
|
|
88
|
+
/** A heredoc cannot close on its opening line, so the block is always open here. */
|
|
89
|
+
open = true;
|
|
90
|
+
}
|
|
91
|
+
return prose;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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": {
|