@contrast/rewriter 1.3.1 → 1.4.1

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 (2) hide show
  1. package/lib/index.js +92 -197
  2. package/package.json +5 -7
package/lib/index.js CHANGED
@@ -12,226 +12,121 @@
12
12
  * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
13
  * way not consistent with the End User License Agreement.
14
14
  */
15
+ // @ts-check
15
16
 
16
17
  'use strict';
17
18
 
19
+ const { transformSync } = require('@swc/core');
18
20
  const Module = require('module');
19
- const { default: traverse } = require('@babel/traverse');
20
- const parser = require('@babel/parser');
21
- const { default: generate } = require('@babel/generator');
22
- const t = require('@babel/types');
23
- const { expression, statement } = require('@babel/template');
24
21
 
22
+ const rewriterPath = require.resolve('@contrast/agent-swc-plugin');
23
+ const unwriterPath = require.resolve('@contrast/agent-swc-plugin-unwrite');
25
24
 
26
- /**
27
- * factory
28
- */
29
- module.exports = function(core) {
30
- const rewriter = new Rewriter(core);
31
- return core.rewriter = rewriter;
32
- };
25
+ // @ts-expect-error `wrapper` is missing from @types/node
26
+ const prefix = Module.wrapper[0];
27
+ // @ts-expect-error `wrapper` is missing from @types/node
28
+ const suffix = Module.wrapper[1].replace(/;$/, '.apply(this, arguments);');
33
29
 
30
+ /** @typedef {'assess' | 'protect'} Mode */
34
31
 
35
- /**
36
- * Babel will add a trailing semicolon in some cases. This will remove it if it
37
- * wasn't there to begin with.
38
- * @param {string} rewritten rewritten content
39
- * @param {string} orig original content before rewriting
40
- * @returns {string}
41
- */
42
- function removeAddedSemicolons(orig, rewritten) {
43
- if (
44
- rewritten.charCodeAt(rewritten.length - 1) == 59 &&
45
- orig.charCodeAt(orig.length - 1) != 59
46
- ) {
47
- rewritten = rewritten.substr(0, rewritten.length - 1);
48
- }
49
- return rewritten;
50
- }
51
-
52
- class Rewriter {
53
- constructor(deps) {
54
- const self = this;
55
- this.logger = deps.logger;
56
- this.visitors = [];
57
- this.installedModes = [];
58
- this.tokens = [];
59
- this.injections = [];
60
- this.methodLookups = {};
61
- this.rewriteTransforms = {
62
- enter(...args) {
63
- for (const v of self.visitors) {
64
- v(...args);
65
- }
66
- },
67
- Program: function Program(path, state) {
68
- if (state.wrap) {
69
- let [prefix, suffix] = Module.wrapper;
70
- prefix = prefix.trim();
71
- suffix = suffix.trim().replace(/;$/, '.apply(this, arguments);');
72
-
73
- path.node.body = [
74
- statement(`${prefix} %%body%% ${suffix}`)({
75
- body: path.node.body
76
- })
77
- ];
78
- }
79
-
80
- if (state.inject) {
81
- path.unshiftContainer('body', self.injections);
82
- }
83
- },
84
- CallExpression(path) {
85
- if (path.node.callee.name === 'eval') {
86
- path.node.arguments = [
87
- t.callExpression(expression('global.ContrastMethods.eval')(), path.node.arguments)
88
- ];
89
- }
90
- },
91
- BinaryExpression: function BinaryExpression(path) {
92
- const method = self.methodLookups[path.node.operator];
93
- if (method) {
94
- path.replaceWith(
95
- t.callExpression(
96
- expression('ContrastMethods.%%method%%')({ method }), [
97
- path.node.left,
98
- path.node.right
99
- ]
100
- )
101
- );
102
- }
103
- }
104
- };
105
- this.unwriteTransforms = {
106
- CallExpression(path) {
107
- const obj = path.node.callee.object;
108
- if (obj && obj.property && obj.property.name === 'ContrastMethods') {
109
- path.replaceWith(path.node.arguments[0]);
110
- }
111
- }
112
- };
113
- this.install = function(mode) {
114
- self.installedModes.push(mode);
115
- !self.methodLookups.eval && (self.methodLookups = {
116
- eval: 'eval'
117
- });
118
-
119
- !(self.injections.length === 2) && self.injections.push(
120
- statement(
121
- 'const %%id%% = global.%%id%% || (() => { throw new SyntaxError(%%errMessage%%); })();'
122
- )({
123
- id: 'ContrastMethods',
124
- errMessage: t.stringLiteral(
125
- 'ContrastMethods undefined during compilation'
126
- )
127
- }),
128
- statement(
129
- 'var %%name%% = global.ContrastMethods.%%id%% || %%name%%;'
130
- )({ name: 'Function', id: 'Function' }),
131
- );
132
-
133
- if (self.installedModes.includes('protect')) {
134
- // Protect doesn't have anything specific
135
- // for rewriting that should not be rewritten
136
- // in Assess (at least for now)
137
- }
138
- if (self.installedModes.includes('assess')) {
139
- Object.assign(self.methodLookups, {
140
- '+': 'plus',
141
- '===': 'tripleEqual',
142
- '!==': 'notTripleEqual',
143
- '==': 'doubleEqual',
144
- '!=': 'notDoubleEqual'
145
- });
146
- Object.assign(self.rewriteTransforms, {
147
- AssignmentExpression(path) {
148
- if (path.node.operator !== '+=') return;
149
- path.replaceWith(
150
- t.assignmentExpression(
151
- '=',
152
- path.node.left,
153
- t.callExpression(expression('global.ContrastMethods.plus')(), [path.node.left, path.node.right])
154
- )
155
- );
156
- }
157
- });
158
- }
32
+ const rewriter = {
33
+ /** @type {Set<Mode>} */
34
+ modes: new Set(),
159
35
 
160
- self.tokens = Object.keys(self.methodLookups);
161
- };
162
- }
36
+ /**
37
+ * Sets the rewriter to 'assess' or 'protect' mode, enabling different
38
+ * transforms.
39
+ * @param {Mode} mode
40
+ */
41
+ install(mode) {
42
+ this.modes.add(mode);
43
+ },
163
44
 
164
45
  /**
165
46
  * @param {string} content the source code
166
47
  * @param {object} opts
167
- * @param {string} opts.filename e.g. 'index.js'
168
- * @param {boolean} opts.inject whether to inject contrast methods
169
- * @param {string} opts.sourceType script or module
170
- * @param {boolean} opts.wrap whether to wrap code in module wrap IIFE
171
- * @returns {object}
48
+ * @param {string=} opts.filename e.g. 'index.js'
49
+ * @param {boolean=} opts.isModule if true, file is parsed as an ES module instead of a CJS script
50
+ * @param {boolean=} opts.inject if true, injects ContrastMethods on the global object
51
+ * @param {boolean=} opts.wrap if true, wraps the content with a modified module wrapper IIFE
52
+ * @returns {import("@swc/core").Output}
172
53
  */
173
- rewrite(content, opts = {
174
- inject: false,
175
- wrap: false,
176
- sourceType: 'script',
177
- }) {
178
- opts.filename = opts.filename || 'no filename';
179
- opts.sourceType = opts.sourceType || 'script';
180
-
181
- const state = {
182
- orig: String(content),
183
- deps: [],
184
- filename: opts.filename,
185
- ...opts
186
- };
54
+ rewrite(content, opts = {}) {
55
+ let shebang = '';
187
56
 
188
- if (this.tokens.every((token) => state.orig.indexOf(token) === -1)) {
189
- return { code: state.orig };
57
+ if (content.charAt(0) === '#') {
58
+ shebang = content.substring(0, content.indexOf('\n') + 1);
59
+ // see the test output: swc doesn't include the commented shebang in the generated code despite including comments otherwise
60
+ content = `//${content}`;
190
61
  }
191
62
 
192
- const ast = parser.parse(state.orig, {
193
- plugins: [
194
- 'classPrivateMethods',
195
- 'classPrivateProperties',
196
- 'classProperties'
197
- ],
198
- ranges: true,
199
- sourceType: state.sourceType,
200
- sourceFilename: state.filename,
201
- tokens: true
202
- });
203
-
204
- traverse(ast, this.rewriteTransforms, null, state);
205
- // TODO: Look into how effective this is
206
- traverse.cache.clear();
63
+ if (opts.wrap) {
64
+ content = `${shebang}${prefix}${content}${suffix}`;
65
+ }
207
66
 
208
- const result = generate(
209
- ast,
210
- {
211
- jsonCompatibleStrings: true,
212
- sourceMaps: true,
213
- sourceFileName: state.filename
67
+ const result = transformSync(content, {
68
+ filename: opts.filename,
69
+ isModule: opts.isModule,
70
+ jsc: {
71
+ target: 'es2019', // should work for node >14
72
+ experimental: {
73
+ plugins: [
74
+ [
75
+ rewriterPath,
76
+ {
77
+ assess: this.modes.has('assess'),
78
+ inject: this.modes.has('assess') && opts.inject,
79
+ },
80
+ ],
81
+ ],
82
+ },
214
83
  },
215
- state.orig
216
- );
84
+ sourceMaps: true,
85
+ });
217
86
 
218
- result.code = removeAddedSemicolons(content, result.code);
219
- result.deps = state.deps;
87
+ if (!opts.wrap) {
88
+ let carriageReturn = 0;
89
+ // swc always adds a newline, so we only need to check the input
90
+ if (!content.endsWith('\n')) {
91
+ result.code = result.code.substring(0, result.code.length - 1);
92
+ } else if (content.endsWith('\r\n')) {
93
+ // if EOL is \r\n, then we need to account for that when we check the
94
+ // negative index of the last semicolon below
95
+ carriageReturn = 1;
96
+ }
97
+ const resultSemicolonIdx = result.code.lastIndexOf(';');
98
+ const contentSemicolonIdx = content.lastIndexOf(';');
99
+ if (contentSemicolonIdx === -1 || resultSemicolonIdx - result.code.length !== contentSemicolonIdx - content.length + carriageReturn) {
100
+ result.code = result.code.substring(0, resultSemicolonIdx) + result.code.substring(resultSemicolonIdx + 1, result.code.length);
101
+ }
102
+ }
220
103
 
221
104
  return result;
222
- }
105
+ },
223
106
 
224
107
  /**
225
- * @param {string} code
226
- * @param {object} opts
108
+ * @param {string} content
227
109
  * @returns {string}
228
110
  */
229
- unwrite(code, opts) {
230
- const ast = parser.parse(code);
231
- traverse(ast, this.unwriteTransforms);
232
- const unwritten = generate(ast, { jsonCompatibleStrings: true }).code;
233
- return removeAddedSemicolons(code, unwritten);
234
- }
235
- }
236
-
237
- module.exports.Rewriter = Rewriter;
111
+ unwrite(content) {
112
+ return transformSync(content, {
113
+ jsc: {
114
+ target: 'es2019', // should work for node >14
115
+ experimental: {
116
+ plugins: [[unwriterPath, {}]],
117
+ },
118
+ },
119
+ }).code;
120
+ },
121
+ };
122
+
123
+ /** @typedef {typeof rewriter} Rewriter */
124
+
125
+ /**
126
+ * @param {{ rewriter: Rewriter }} core
127
+ * @returns {Rewriter}
128
+ */
129
+ module.exports = function init(core) {
130
+ core.rewriter = rewriter;
131
+ return rewriter;
132
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/rewriter",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "A transpilation tool mainly used for instrumentation",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -17,12 +17,10 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@babel/generator": "^7.17.9",
21
- "@babel/parser": "^7.17.9",
22
- "@babel/template": "^7.16.7",
23
- "@babel/traverse": "^7.17.9",
24
- "@babel/types": "^7.16.7",
20
+ "@contrast/agent-swc-plugin": "^1.1.0",
21
+ "@contrast/agent-swc-plugin-unwrite": "^1.1.0",
25
22
  "@contrast/synchronous-source-maps": "^1.1.3",
23
+ "@swc/core": "1.3.39",
26
24
  "multi-stage-sourcemap": "^0.3.1"
27
25
  }
28
- }
26
+ }