@contrast/cli 1.24.0 → 1.25.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/README.md +26 -0
- package/lib/rewrite.js +193 -0
- package/package.json +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @contrast/cli
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @contrast/cli # optional, `npx` will install the package otherwise.
|
|
7
|
+
npx -p @contrast/cli <command>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
### rewrite
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Usage: npx -p @contrast/cli rewrite [options] <entrypoint>
|
|
16
|
+
|
|
17
|
+
Rewrites application files, caching them so that rewriting does not need to occur when the application runs.
|
|
18
|
+
|
|
19
|
+
Arguments:
|
|
20
|
+
entrypoint The entrypoint for the application
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-V, --version output the version number
|
|
24
|
+
-a, --assess enable assess mode
|
|
25
|
+
-h, --help display help for command
|
|
26
|
+
```
|
package/lib/rewrite.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright: 2024 Contrast Security, Inc
|
|
4
|
+
* Contact: support@contrastsecurity.com
|
|
5
|
+
* License: Commercial
|
|
6
|
+
|
|
7
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
8
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
9
|
+
* made available through public repositories, use of this Software is subject to
|
|
10
|
+
* the applicable End User Licensing Agreement found at
|
|
11
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
12
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
13
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
14
|
+
* way not consistent with the End User License Agreement.
|
|
15
|
+
*/
|
|
16
|
+
// @ts-check
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { readFile } = require('node:fs/promises');
|
|
20
|
+
const { createRequire } = require('node:module');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const swc = require('@swc/core');
|
|
23
|
+
const { Visitor } = require('@swc/core/Visitor');
|
|
24
|
+
const { findPackageJson } = require('@contrast/find-package-json');
|
|
25
|
+
const { program } = require('commander');
|
|
26
|
+
const { version } = require('../package.json');
|
|
27
|
+
|
|
28
|
+
const JS_FILE_REGEX = /\.[cm]?js$/;
|
|
29
|
+
|
|
30
|
+
/** @type {any} */
|
|
31
|
+
const core = {};
|
|
32
|
+
require('@contrast/core/lib/messages')(core);
|
|
33
|
+
const config = require('@contrast/config')(core);
|
|
34
|
+
if (core.config._errors?.length) throw core.config._errors[0];
|
|
35
|
+
|
|
36
|
+
const logger = require('@contrast/logger').default(core, { name: 'contrast:rewriter:cli' });
|
|
37
|
+
|
|
38
|
+
if (!config.agent.node.rewrite.enable || !config.agent.node.rewrite.cache.enable) {
|
|
39
|
+
logger.warn({ 'config.agent.node.rewrite': config.agent.node.rewrite }, 'rewriter config');
|
|
40
|
+
throw new Error('Rewriting disabled.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const appInfo = require('@contrast/core/lib/app-info')(core);
|
|
44
|
+
if (appInfo._errors?.length) throw appInfo._errors[0];
|
|
45
|
+
|
|
46
|
+
const rewriter = require('@contrast/rewriter')(core);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Keeps track of visited files so we don't bother rewriting multiple times.
|
|
50
|
+
* @type {Set<string>}
|
|
51
|
+
*/
|
|
52
|
+
const visited = new Set();
|
|
53
|
+
|
|
54
|
+
/** @param {string} filename absolute path */
|
|
55
|
+
async function isModuleFile(filename) {
|
|
56
|
+
// if the file extension specifies the type, there's no need to do extra IO
|
|
57
|
+
const ext = path.extname(filename);
|
|
58
|
+
if (ext === '.mjs') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ext === '.cjs') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// check for type: 'module' in a file's package json, otherwise assume CJS.
|
|
67
|
+
try {
|
|
68
|
+
const pkg = await findPackageJson({ cwd: filename });
|
|
69
|
+
if (pkg && JSON.parse((await readFile(pkg)).toString()).type === 'module') {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class RewriteVisitor extends Visitor {
|
|
80
|
+
/** @param {string} filename */
|
|
81
|
+
constructor(filename) {
|
|
82
|
+
super();
|
|
83
|
+
visited.add(filename);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a local `require` function so we can resolve relative filenames.
|
|
87
|
+
* @type {NodeRequire}
|
|
88
|
+
*/
|
|
89
|
+
this.require = createRequire(filename);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Visit `import ... from 'source'`, recursively rewriting the resolved path
|
|
94
|
+
* of `source`.
|
|
95
|
+
* @param {swc.ImportDeclaration} n
|
|
96
|
+
*/
|
|
97
|
+
visitImportDeclaration(n) {
|
|
98
|
+
try {
|
|
99
|
+
const filename = this.require.resolve(n.source.value);
|
|
100
|
+
if (path.isAbsolute(filename)) {
|
|
101
|
+
rewriteFile(filename);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger.debug({ n, err }, 'unable to resolve %s', n.source.value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return super.visitImportDeclaration(n);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Visits `import(...)` or `require(...)` call expressions, recursively
|
|
112
|
+
* rewriting the resolved path of the first argument if it's a string literal.
|
|
113
|
+
* @param {swc.CallExpression} n
|
|
114
|
+
*/
|
|
115
|
+
visitCallExpression(n) {
|
|
116
|
+
if (n.callee.type === 'Import' || (n.callee.type === 'Identifier' && n.callee.value === 'require')) {
|
|
117
|
+
const { expression } = n.arguments[0];
|
|
118
|
+
if (expression.type === 'StringLiteral') {
|
|
119
|
+
try {
|
|
120
|
+
const filename = this.require.resolve(expression.value);
|
|
121
|
+
if (path.isAbsolute(filename)) {
|
|
122
|
+
rewriteFile(filename);
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.debug({ n, err }, 'unable to resolve %s', expression.value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return super.visitCallExpression(n);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @param {string} filename */
|
|
135
|
+
async function rewriteFile(filename) {
|
|
136
|
+
if (!JS_FILE_REGEX.test(filename) || visited.has(filename)) return;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const content = (await readFile(filename)).toString();
|
|
140
|
+
const isModule = await isModuleFile(filename);
|
|
141
|
+
|
|
142
|
+
const result = await rewriter.rewrite(content, {
|
|
143
|
+
filename,
|
|
144
|
+
isModule,
|
|
145
|
+
inject: true,
|
|
146
|
+
wrap: !isModule,
|
|
147
|
+
trim: false,
|
|
148
|
+
});
|
|
149
|
+
rewriter.cache.write(filename, result);
|
|
150
|
+
|
|
151
|
+
/** @type {swc.Module | swc.Script} */
|
|
152
|
+
const program = await swc.parse(content, {
|
|
153
|
+
// @ts-expect-error swc types expect a literal, not an arbitrary boolean.
|
|
154
|
+
isModule
|
|
155
|
+
});
|
|
156
|
+
const visitor = new RewriteVisitor(filename);
|
|
157
|
+
visitor.visitProgram(program);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.debug({ err }, 'unable to parse or rewrite %s', filename);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} filename
|
|
165
|
+
* @param {object} opts
|
|
166
|
+
* @param {boolean=} opts.assess
|
|
167
|
+
*/
|
|
168
|
+
async function action(filename, opts) {
|
|
169
|
+
if (config.assess.enable || opts.assess) rewriter.install('assess');
|
|
170
|
+
// If we're rewriting, we're always at least in protect mode.
|
|
171
|
+
rewriter.install('protect');
|
|
172
|
+
|
|
173
|
+
logger.info(
|
|
174
|
+
'Caching rewriter results to %s',
|
|
175
|
+
path.join(
|
|
176
|
+
rewriter.cache.cacheDir,
|
|
177
|
+
rewriter.modes.has('assess') ? 'assess' : 'protect'
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
logger.debug({ config }, 'Agent configuration');
|
|
181
|
+
return rewriteFile(filename);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (require.main === module) {
|
|
185
|
+
program
|
|
186
|
+
.name('npx -p @contrast/cli rewrite')
|
|
187
|
+
.version(version)
|
|
188
|
+
.description('Rewrites application files, caching them so that rewriting does not need to occur when the application runs.')
|
|
189
|
+
.argument('<entrypoint>', 'The entrypoint for the application', entrypoint => path.resolve(entrypoint))
|
|
190
|
+
.option('-a, --assess', 'enable assess mode')
|
|
191
|
+
.action(action)
|
|
192
|
+
.parse(process.argv);
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "A collection of agent related CLI utilities",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
],
|
|
10
10
|
"bin": {
|
|
11
11
|
"config-diagnostics": "lib/config-diagnostics.js",
|
|
12
|
+
"rewrite": "lib/rewrite.js",
|
|
12
13
|
"system-diagnostics": "lib/system-diagnostics.js"
|
|
13
14
|
},
|
|
14
15
|
"engines": {
|
|
@@ -19,11 +20,14 @@
|
|
|
19
20
|
"test": "../scripts/test.sh"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
23
|
+
"@contrast/find-package-json": "^1.1.0",
|
|
24
|
+
"@contrast/rewriter": "1.7.0",
|
|
22
25
|
"@contrast/config": "1.27.0",
|
|
23
26
|
"@contrast/core": "1.31.0",
|
|
24
27
|
"@contrast/logger": "1.8.0",
|
|
25
28
|
"@contrast/reporter": "1.26.0",
|
|
26
29
|
"@contrast/scopes": "^1.4.0",
|
|
30
|
+
"@swc/core": "1.3.39",
|
|
27
31
|
"commander": "^9.4.1"
|
|
28
32
|
}
|
|
29
33
|
}
|