@contrast/cli 1.23.3 → 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.
Files changed (3) hide show
  1. package/README.md +26 -0
  2. package/lib/rewrite.js +193 -0
  3. package/package.json +8 -4
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.23.3",
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": {
22
- "@contrast/config": "1.26.2",
23
- "@contrast/core": "1.30.0",
23
+ "@contrast/find-package-json": "^1.1.0",
24
+ "@contrast/rewriter": "1.7.0",
25
+ "@contrast/config": "1.27.0",
26
+ "@contrast/core": "1.31.0",
24
27
  "@contrast/logger": "1.8.0",
25
- "@contrast/reporter": "1.25.1",
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
  }