@produtype/core 0.38.0 → 0.40.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.
- package/dist/analyzer/detectSecurity.js +118 -20
- package/dist/analyzer/detectUploads.js +104 -19
- package/dist/rules/rules.js +9 -2
- package/dist/utils/textSearch.d.ts +21 -0
- package/dist/utils/textSearch.js +43 -0
- package/package.json +1 -1
|
@@ -5,6 +5,43 @@ const detectContext_1 = require("./detectContext");
|
|
|
5
5
|
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
6
6
|
const textSearch_1 = require("../utils/textSearch");
|
|
7
7
|
const absenceEvidence_1 = require("./absenceEvidence");
|
|
8
|
+
/** Lines that decide which origins may call this server. */
|
|
9
|
+
const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /allowedOrigins/i];
|
|
10
|
+
/**
|
|
11
|
+
* The same line, allowing everyone.
|
|
12
|
+
*
|
|
13
|
+
* Both spellings, because `'Access-Control-Allow-Origin', '*'` and
|
|
14
|
+
* `ALLOWED_ORIGINS = ["*"]` are the same decision written in two frameworks, and only
|
|
15
|
+
* the first was being caught.
|
|
16
|
+
*/
|
|
17
|
+
const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i;
|
|
18
|
+
/**
|
|
19
|
+
* Flask's answer, which was invisible.
|
|
20
|
+
*
|
|
21
|
+
* `\bcors\s*\(` is case-sensitive, so `CORS(app)` matched nothing, and the extension's
|
|
22
|
+
* default is to allow every origin — the one configuration this check exists to find.
|
|
23
|
+
* A Flask application that opens itself to the whole web in one line was reported as
|
|
24
|
+
* having no cross-origin configuration at all.
|
|
25
|
+
*/
|
|
26
|
+
const IMPORTS_FLASK_CORS = /from\s+flask_cors\s+import|import\s+flask_cors/;
|
|
27
|
+
const FLASK_CORS_CALL = /\bCORS\s*\(/;
|
|
28
|
+
/**
|
|
29
|
+
* The line with its quoted text removed.
|
|
30
|
+
*
|
|
31
|
+
* `cors(` inside a string literal is never the middleware being applied — it is prose
|
|
32
|
+
* about it. This tool's own remediation catalogue, "Replace cors() defaults with an
|
|
33
|
+
* explicit allowlist.", was being cited as a CORS configuration, and no rule about the
|
|
34
|
+
* shape of the line could help: a standalone string in a list is deliberately not
|
|
35
|
+
* skipped, because `'django.contrib.auth',` in INSTALLED_APPS is the behaviour itself.
|
|
36
|
+
*
|
|
37
|
+
* Narrow on purpose. It is applied to this one question, where a call is what is being
|
|
38
|
+
* looked for, and not to the file search, where a string is often the answer.
|
|
39
|
+
*/
|
|
40
|
+
function withoutStringLiterals(line) {
|
|
41
|
+
return line.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, '""');
|
|
42
|
+
}
|
|
43
|
+
/** The Express middleware, brought into the file that configures it. */
|
|
44
|
+
const IMPORTS_CORS = /(require\(['"]cors['"]\))|(from\s+['"]cors['"])|(import\s+['"]cors['"])/;
|
|
8
45
|
function detectCorsConfig(text, file) {
|
|
9
46
|
const loose = [];
|
|
10
47
|
const strict = [];
|
|
@@ -12,7 +49,9 @@ function detectCorsConfig(text, file) {
|
|
|
12
49
|
const corsVarRegex = /\bcors\(\s*([A-Za-z_$][\w$]*)\s*\)/;
|
|
13
50
|
for (let i = 0; i < lines.length; i++) {
|
|
14
51
|
const line = lines[i];
|
|
15
|
-
if (
|
|
52
|
+
if (!(0, textSearch_1.isCitableLine)(line))
|
|
53
|
+
continue;
|
|
54
|
+
if (!/\bcors\s*\(/.test(withoutStringLiterals(line)))
|
|
16
55
|
continue;
|
|
17
56
|
const snippet = line.trim().slice(0, 200);
|
|
18
57
|
if (/\bcors\(\s*\)/.test(line)) {
|
|
@@ -41,6 +80,35 @@ function detectCorsConfig(text, file) {
|
|
|
41
80
|
}
|
|
42
81
|
return { loose, strict };
|
|
43
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* A settings file that is Django's, rather than one that shares its name.
|
|
85
|
+
*
|
|
86
|
+
* `diet_hub/api/settings.py` is a FastAPI router that lets an administrator change
|
|
87
|
+
* application options from the browser. It was read as Django's configuration, and on
|
|
88
|
+
* the strength of the filename alone the project was credited with having security
|
|
89
|
+
* middleware it does not have — the one direction of error that matters here, because
|
|
90
|
+
* it hides a missing control rather than inventing a present one.
|
|
91
|
+
*
|
|
92
|
+
* Two conditions, because either alone is wrong. A repository can depend on Django and
|
|
93
|
+
* still own a dozen files called settings.py; a file can declare INSTALLED_APPS in a
|
|
94
|
+
* tutorial that the product does not run. And the first file named settings.py is not
|
|
95
|
+
* the right one: a project with `settings/base.py` and `settings/production.py` has
|
|
96
|
+
* several, so every candidate is examined and the first that is Django's is used.
|
|
97
|
+
*/
|
|
98
|
+
async function findDjangoSettings(ctx) {
|
|
99
|
+
if (!ctx.pythonDeps.includes('django'))
|
|
100
|
+
return null;
|
|
101
|
+
const candidates = ctx.files.all.filter((f) => /(^|\/)settings(_[a-z]+)?\.py$/i.test(f) || /(^|\/)settings\/[a-z_]+\.py$/i.test(f));
|
|
102
|
+
for (const file of candidates) {
|
|
103
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
104
|
+
if (!text)
|
|
105
|
+
continue;
|
|
106
|
+
if (/^\s*INSTALLED_APPS\s*=/m.test(text) || /^\s*MIDDLEWARE\s*=/m.test(text) || /DJANGO_SETTINGS_MODULE/.test(text)) {
|
|
107
|
+
return { file, text };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
44
112
|
async function detectSecurity(ctx) {
|
|
45
113
|
const evidence = [];
|
|
46
114
|
const source = ctx.files.source;
|
|
@@ -82,14 +150,35 @@ async function detectSecurity(ctx) {
|
|
|
82
150
|
continue;
|
|
83
151
|
// Framework-native CORS: an explicit allowlist checked against the Origin header,
|
|
84
152
|
// rather than the Express cors() middleware.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
153
|
+
//
|
|
154
|
+
// Line by line, because the file is the wrong unit for this question twice over.
|
|
155
|
+
// It decided loose-or-strict from whether a wildcard appeared anywhere in the
|
|
156
|
+
// file, so one permissive route made an allowlist read as permissive and one
|
|
157
|
+
// allowlist made a wildcard read as restricted; and it cited line 1, which in
|
|
158
|
+
// every repository that triggered it was an import.
|
|
159
|
+
for (const m of (0, textSearch_1.matchLines)(text, ORIGIN_HANDLING, file)) {
|
|
160
|
+
if (WILDCARD_ORIGIN.test(m.snippet))
|
|
161
|
+
corsLoose.push(m);
|
|
90
162
|
else
|
|
91
|
-
corsStrict.push(
|
|
163
|
+
corsStrict.push(m);
|
|
164
|
+
}
|
|
165
|
+
// Only where the middleware is actually imported. A sentence in this tool's own
|
|
166
|
+
// remediation catalogue — "Replace cors() defaults with an explicit allowlist." —
|
|
167
|
+
// was being read as a CORS configuration, and a table of strings is not a comment,
|
|
168
|
+
// so no rule about the shape of the line could tell them apart. Whether the file
|
|
169
|
+
// imports the package can.
|
|
170
|
+
if (IMPORTS_FLASK_CORS.test(text)) {
|
|
171
|
+
const lines = text.split(/\r?\n/);
|
|
172
|
+
for (const m of (0, textSearch_1.matchLines)(text, [FLASK_CORS_CALL], file).filter((m) => FLASK_CORS_CALL.test(withoutStringLiterals(m.snippet)))) {
|
|
173
|
+
const window = lines.slice(m.line - 1, m.line + 9).join('\n');
|
|
174
|
+
if (/\borigins\s*=/.test(window) || /\bresources\s*=/.test(window))
|
|
175
|
+
corsStrict.push(m);
|
|
176
|
+
else
|
|
177
|
+
corsLoose.push(m);
|
|
178
|
+
}
|
|
92
179
|
}
|
|
180
|
+
if (!IMPORTS_CORS.test(text))
|
|
181
|
+
continue;
|
|
93
182
|
if (!/\bcors\s*\(/.test(text))
|
|
94
183
|
continue;
|
|
95
184
|
const detected = detectCorsConfig(text, file);
|
|
@@ -125,27 +214,36 @@ async function detectSecurity(ctx) {
|
|
|
125
214
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'body-size' });
|
|
126
215
|
for (const m of contentTypeCheck)
|
|
127
216
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'content-type' });
|
|
128
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Django was credited for headers twice, and neither credit was earned.
|
|
219
|
+
*
|
|
220
|
+
* The first was the mere existence of a file named settings.py, which a FastAPI
|
|
221
|
+
* router satisfied. The second looked truer — `SecurityMiddleware` really does set
|
|
222
|
+
* those headers — but `django-admin startproject` writes that line into every new
|
|
223
|
+
* project, so it distinguishes nothing and would have credited the bare skeleton
|
|
224
|
+
* this repository keeps as a fixture precisely because it is unprotected. What is
|
|
225
|
+
* left is the `SECURE_*` settings above: lines somebody chose to write.
|
|
226
|
+
*/
|
|
227
|
+
const djangoSettings = await findDjangoSettings(ctx);
|
|
129
228
|
let djangoDebugTrue = false;
|
|
130
229
|
let djangoSecureCookies = true;
|
|
131
230
|
if (djangoSettings) {
|
|
132
|
-
const
|
|
133
|
-
|
|
231
|
+
const { file, text } = djangoSettings;
|
|
232
|
+
const debugOn = (0, textSearch_1.matchLines)(text, [/^\s*DEBUG\s*=\s*True\b/], file);
|
|
233
|
+
if (debugOn.length > 0) {
|
|
134
234
|
djangoDebugTrue = true;
|
|
135
|
-
|
|
235
|
+
for (const m of debugOn)
|
|
236
|
+
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'django-debug' });
|
|
136
237
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
evidence.push({ type: 'snippet', value: 'SESSION_COOKIE_SECURE = False', file: djangoSettings, claim: 'django-cookies' });
|
|
142
|
-
if (insecureCsrf)
|
|
143
|
-
evidence.push({ type: 'snippet', value: 'CSRF_COOKIE_SECURE = False', file: djangoSettings, claim: 'django-cookies' });
|
|
238
|
+
const insecure = (0, textSearch_1.matchLines)(text, [/^\s*SESSION_COOKIE_SECURE\s*=\s*False\b/, /^\s*CSRF_COOKIE_SECURE\s*=\s*False\b/], file);
|
|
239
|
+
djangoSecureCookies = insecure.length === 0;
|
|
240
|
+
for (const m of insecure)
|
|
241
|
+
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'django-cookies' });
|
|
144
242
|
}
|
|
145
243
|
return {
|
|
146
244
|
key: 'security.core',
|
|
147
|
-
present: helmet || rateLimit
|
|
148
|
-
complete: helmet &&
|
|
245
|
+
present: helmet || rateLimit,
|
|
246
|
+
complete: helmet && rateLimit,
|
|
149
247
|
evidence,
|
|
150
248
|
details: {
|
|
151
249
|
helmet,
|
|
@@ -25,6 +25,84 @@ function findExpressUploadsRoutes(text, file) {
|
|
|
25
25
|
}
|
|
26
26
|
return out;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The lines of a Python file that only run in development.
|
|
30
|
+
*
|
|
31
|
+
* `urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)` is the
|
|
32
|
+
* line Django's own documentation gives for serving uploaded files, and the
|
|
33
|
+
* documentation wraps it in `if settings.DEBUG:` — which is the whole point of the
|
|
34
|
+
* snippet. This detector already said so in a comment and then counted the line anyway,
|
|
35
|
+
* so a travel application was told its uploads were exposed to the public by a line
|
|
36
|
+
* that does not run in production.
|
|
37
|
+
*
|
|
38
|
+
* Block extent by indentation, which is how Python says it.
|
|
39
|
+
*/
|
|
40
|
+
function debugOnlyLines(text) {
|
|
41
|
+
const guarded = new Set();
|
|
42
|
+
const lines = text.split(/\r?\n/);
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const opener = lines[i].match(/^(\s*)if\s+(?:settings\.)?DEBUG\s*:/);
|
|
45
|
+
if (!opener)
|
|
46
|
+
continue;
|
|
47
|
+
const indent = opener[1].length;
|
|
48
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
49
|
+
const line = lines[j];
|
|
50
|
+
if (line.trim() === '') {
|
|
51
|
+
guarded.add(j + 1);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
55
|
+
if (lineIndent <= indent)
|
|
56
|
+
break;
|
|
57
|
+
guarded.add(j + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return guarded;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A URL pattern that serves uploaded files, as distinct from one that serves the
|
|
64
|
+
* stylesheet.
|
|
65
|
+
*
|
|
66
|
+
* `document_root=` alone matched `static(settings.STATIC_URL, document_root=STATIC_ROOT)`
|
|
67
|
+
* — CSS and JavaScript, which every Django project serves and nobody uploads. Two of
|
|
68
|
+
* the projects reported as exposing their uploads were exposing their stylesheets.
|
|
69
|
+
*/
|
|
70
|
+
const DJANGO_MEDIA_ROUTES = [
|
|
71
|
+
/static\s*\(\s*settings\.MEDIA_URL/i,
|
|
72
|
+
/document_root\s*=\s*settings\.MEDIA_ROOT/i,
|
|
73
|
+
/re_path\s*\(\s*r?['"][^'"]*media/i,
|
|
74
|
+
/url\s*\(\s*r?['"][^'"]*media/i,
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Checking the type of a file somebody uploaded.
|
|
78
|
+
*
|
|
79
|
+
* This was `/mime/i` and `/content-type/i`, which is every JSON response header in
|
|
80
|
+
* every project: `res.writeHead(404, { 'Content-Type': 'application/json' })` was
|
|
81
|
+
* cited as upload validation, and so was `X-Content-Type-Options: nosniff`. The flag
|
|
82
|
+
* came out true for all six projects in the corpus that handle uploads — a measurement
|
|
83
|
+
* that never varies is not one — and it decides whether upload protection reads
|
|
84
|
+
* `present` or `partial`, so two projects were credited for a header on a 404.
|
|
85
|
+
*/
|
|
86
|
+
const DJANGO_VIEW_PROTECTION = [
|
|
87
|
+
/@login_required/,
|
|
88
|
+
/LoginRequiredMixin/,
|
|
89
|
+
/PermissionRequiredMixin/,
|
|
90
|
+
/@user_passes_test/,
|
|
91
|
+
/@permission_required/,
|
|
92
|
+
];
|
|
93
|
+
/** A file that does something with an uploaded file, rather than one that logs people in. */
|
|
94
|
+
const HANDLES_FILES = /request\.FILES|FileField|ImageField|\bupload/i;
|
|
95
|
+
const UPLOAD_VALIDATION = [
|
|
96
|
+
/\bfileFilter\b/,
|
|
97
|
+
/file\.mimetype/i,
|
|
98
|
+
/files?\[[^\]]*\]\.mimetype/i,
|
|
99
|
+
/allowed(?:File|Mime|Content)?_?(?:Types|Extensions)/i,
|
|
100
|
+
/FileExtensionValidator/,
|
|
101
|
+
/validate_image_file_extension/,
|
|
102
|
+
/\bfileTypeFrom(?:Buffer|File|Stream)\b/,
|
|
103
|
+
/request\.FILES\[[^\]]*\]\.content_type/,
|
|
104
|
+
/\.content_type\s*(?:not\s+)?in\b/,
|
|
105
|
+
];
|
|
28
106
|
async function detectUploads(ctx) {
|
|
29
107
|
const evidence = [];
|
|
30
108
|
const uploadDeps = (0, detectContext_1.hasAnyDep)(ctx, ['multer', 'formidable', '@aws-sdk/client-s3', 'aws-sdk']);
|
|
@@ -65,28 +143,35 @@ async function detectUploads(ctx) {
|
|
|
65
143
|
* A real report on a real school platform called its uploads publicly exposed on the
|
|
66
144
|
* strength of those two lines, with no pattern serving media anywhere in the project.
|
|
67
145
|
*/
|
|
68
|
-
const djangoPublicSignals =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
146
|
+
const djangoPublicSignals = [];
|
|
147
|
+
const djangoProtectionSignals = [];
|
|
148
|
+
const validationSignals = [];
|
|
149
|
+
for (const file of source) {
|
|
150
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
151
|
+
if (!text)
|
|
152
|
+
continue;
|
|
153
|
+
validationSignals.push(...(0, textSearch_1.matchLines)(text, UPLOAD_VALIDATION, file));
|
|
154
|
+
if (!file.endsWith('.py'))
|
|
155
|
+
continue;
|
|
156
|
+
const developmentOnly = debugOnlyLines(text);
|
|
157
|
+
djangoPublicSignals.push(...(0, textSearch_1.matchLines)(text, DJANGO_MEDIA_ROUTES, file).filter((m) => !developmentOnly.has(m.line)));
|
|
158
|
+
/**
|
|
159
|
+
* Django's own way of protecting a view: a decorator or a mixin, not middleware on
|
|
160
|
+
* a route. The Express-shaped route scan cannot see either, so a project whose
|
|
161
|
+
* upload view is `@login_required` read as having no protection at all.
|
|
162
|
+
*
|
|
163
|
+
* Only in a file that handles files. This detector argues, three comments above,
|
|
164
|
+
* that authentication is not evidence about uploads — and then counted every
|
|
165
|
+
* `@login_required` in the project as upload protection: a photography business
|
|
166
|
+
* was credited for the decorators on its accounting views.
|
|
167
|
+
*/
|
|
168
|
+
if (HANDLES_FILES.test(text)) {
|
|
169
|
+
djangoProtectionSignals.push(...(0, textSearch_1.matchLines)(text, DJANGO_VIEW_PROTECTION, file));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
86
172
|
for (const m of djangoProtectionSignals) {
|
|
87
173
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'protection' });
|
|
88
174
|
}
|
|
89
|
-
const validationSignals = await (0, textSearch_1.searchInFiles)(ctx.root, source, [/file-type/i, /mime/i, /content-type/i], 15);
|
|
90
175
|
const protectedRoutes = routeSignals.filter((r) => r.protected);
|
|
91
176
|
const unprotectedRoutes = routeSignals.filter((r) => !r.protected);
|
|
92
177
|
for (const r of routeSignals) {
|
package/dist/rules/rules.js
CHANGED
|
@@ -390,9 +390,14 @@ exports.rules = [
|
|
|
390
390
|
const loose = Boolean(sec?.details?.corsLoose);
|
|
391
391
|
const strict = Boolean(sec?.details?.corsStrict);
|
|
392
392
|
// Nothing answers a cross-origin request where nothing answers a request.
|
|
393
|
+
//
|
|
394
|
+
// The permissive line decides, not the restricted one. While the file was the
|
|
395
|
+
// unit of judgement this hardly mattered; now that each line is read on its own,
|
|
396
|
+
// an allowlist on one route would otherwise absolve a wildcard on another — and
|
|
397
|
+
// a wildcard is open to the whole web no matter what the route beside it does.
|
|
393
398
|
const status = !hasUserFacingSurface(analysis)
|
|
394
399
|
? 'unknown'
|
|
395
|
-
:
|
|
400
|
+
: loose ? 'partial' : strict ? 'passed' : 'unknown';
|
|
396
401
|
return mkFinding({
|
|
397
402
|
id: 'security.cors-origin',
|
|
398
403
|
title: 'CORS origin restrictions',
|
|
@@ -402,7 +407,9 @@ exports.rules = [
|
|
|
402
407
|
description: status === 'passed'
|
|
403
408
|
? 'CORS appears configured with explicit origins.'
|
|
404
409
|
: status === 'partial'
|
|
405
|
-
?
|
|
410
|
+
? strict
|
|
411
|
+
? 'Some routes restrict origins and at least one allows any origin.'
|
|
412
|
+
: 'CORS middleware detected without explicit origin restrictions.'
|
|
406
413
|
: 'CORS configuration not detected.',
|
|
407
414
|
recommendation: 'Configure allowlist origins and avoid permissive defaults in production.',
|
|
408
415
|
evidence: evidenceForClaim(sec?.evidence, 'cors'),
|
|
@@ -3,6 +3,27 @@ export interface TextMatch {
|
|
|
3
3
|
line: number;
|
|
4
4
|
snippet: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Whether this line may be quoted back to the reader as something the code does.
|
|
8
|
+
*
|
|
9
|
+
* The rules above were reachable only through the file search, so any detector that
|
|
10
|
+
* walked lines itself got none of them. The CORS middleware walk was one: it cited
|
|
11
|
+
* `// app.use(cors({` from a route somebody had commented out, and — pointed at this
|
|
12
|
+
* repository — cited this tool's own prose about the Express middleware and the
|
|
13
|
+
* sentence in its remediation catalogue that advises replacing `cors()` defaults.
|
|
14
|
+
* Three citations, none of them code that runs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isCitableLine(line: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The lines of one already-read file that match, with the same hygiene the file
|
|
19
|
+
* search applies: no comments, no pattern tables, no minified lines.
|
|
20
|
+
*
|
|
21
|
+
* Exported because a detector that has the text in hand should not have to choose
|
|
22
|
+
* between re-reading the file and skipping those rules. The CORS branch made that
|
|
23
|
+
* choice the wrong way: it tested the whole file for a header name and cited line 1
|
|
24
|
+
* with the words "explicit origin handling" — a conclusion where a line was promised.
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchLines(text: string, needles: Array<string | RegExp>, file?: string): TextMatch[];
|
|
6
27
|
/**
|
|
7
28
|
* Search a list of relative file paths for any of the given needles
|
|
8
29
|
* (string or RegExp). Returns at most `limit` matches.
|
package/dist/utils/textSearch.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCitableLine = isCitableLine;
|
|
4
|
+
exports.matchLines = matchLines;
|
|
3
5
|
exports.searchInFiles = searchInFiles;
|
|
4
6
|
exports.anyIncludes = anyIncludes;
|
|
5
7
|
const readTextFileSafe_1 = require("./readTextFileSafe");
|
|
@@ -49,6 +51,19 @@ function declaresRatherThanDoes(line) {
|
|
|
49
51
|
const trimmed = line.trim();
|
|
50
52
|
return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed);
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether this line may be quoted back to the reader as something the code does.
|
|
56
|
+
*
|
|
57
|
+
* The rules above were reachable only through the file search, so any detector that
|
|
58
|
+
* walked lines itself got none of them. The CORS middleware walk was one: it cited
|
|
59
|
+
* `// app.use(cors({` from a route somebody had commented out, and — pointed at this
|
|
60
|
+
* repository — cited this tool's own prose about the Express middleware and the
|
|
61
|
+
* sentence in its remediation catalogue that advises replacing `cors()` defaults.
|
|
62
|
+
* Three citations, none of them code that runs.
|
|
63
|
+
*/
|
|
64
|
+
function isCitableLine(line) {
|
|
65
|
+
return line.length <= MAX_CITABLE_LINE && !declaresRatherThanDoes(line);
|
|
66
|
+
}
|
|
52
67
|
/**
|
|
53
68
|
* The longest line this analyzer will cite.
|
|
54
69
|
*
|
|
@@ -69,6 +84,34 @@ function declaresRatherThanDoes(line) {
|
|
|
69
84
|
* on is not worth the one it displaces.
|
|
70
85
|
*/
|
|
71
86
|
const MAX_CITABLE_LINE = 500;
|
|
87
|
+
/**
|
|
88
|
+
* The lines of one already-read file that match, with the same hygiene the file
|
|
89
|
+
* search applies: no comments, no pattern tables, no minified lines.
|
|
90
|
+
*
|
|
91
|
+
* Exported because a detector that has the text in hand should not have to choose
|
|
92
|
+
* between re-reading the file and skipping those rules. The CORS branch made that
|
|
93
|
+
* choice the wrong way: it tested the whole file for a header name and cited line 1
|
|
94
|
+
* with the words "explicit origin handling" — a conclusion where a line was promised.
|
|
95
|
+
*/
|
|
96
|
+
function matchLines(text, needles, file = '') {
|
|
97
|
+
const matches = [];
|
|
98
|
+
const lines = text.split(/\r?\n/);
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const line = lines[i];
|
|
101
|
+
if (line.length > MAX_CITABLE_LINE)
|
|
102
|
+
continue;
|
|
103
|
+
if (declaresRatherThanDoes(line))
|
|
104
|
+
continue;
|
|
105
|
+
for (const n of needles) {
|
|
106
|
+
const hit = typeof n === 'string' ? line.includes(n) : n.test(line);
|
|
107
|
+
if (hit) {
|
|
108
|
+
matches.push({ file, line: i + 1, snippet: line.trim().slice(0, 200) });
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return matches;
|
|
114
|
+
}
|
|
72
115
|
/**
|
|
73
116
|
* Search a list of relative file paths for any of the given needles
|
|
74
117
|
* (string or RegExp). Returns at most `limit` matches.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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": {
|