@contrast/rewriter 1.3.0 → 1.4.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 (2) hide show
  1. package/lib/index.js +66 -211
  2. package/package.json +5 -7
package/lib/index.js CHANGED
@@ -15,234 +15,89 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { transformSync } = require('@swc/core');
18
19
  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
20
 
21
+ const rewriterPath = require.resolve('@contrast/agent-swc-plugin');
22
+ const unwriterPath = require.resolve('@contrast/agent-swc-plugin-unwrite');
25
23
 
26
- /**
27
- * factory
28
- */
29
- module.exports = function(core) {
30
- const rewriter = new Rewriter(core);
31
- return core.rewriter = rewriter;
32
- };
33
-
34
-
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
- });
24
+ const prefix = Module.wrapper[0];
25
+ const suffix = Module.wrapper[1].replace(/;$/, '.apply(this, arguments);');
118
26
 
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
- );
27
+ /** @typedef {'assess' | 'protect'} Mode */
132
28
 
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
- self.injections.push(
147
- statement(
148
- 'const %%name%% = global.%%id%% || %%name%%;'
149
- )({ name: 'JSON', id: 'ContrastJSON' }),
150
- statement(
151
- 'const %%name%% = global.%%id%% || %%name%%;'
152
- )({ name: 'Object', id: 'ContrastObject' }),
153
- statement(
154
- 'const %%name%% = global.%%id%% || %%name%%;'
155
- )({ name: 'Number', id: 'ContrastNumber' })
156
- );
157
- Object.assign(self.rewriteTransforms, {
158
- AssignmentExpression(path) {
159
- if (path.node.operator !== '+=') return;
160
- path.replaceWith(
161
- t.assignmentExpression(
162
- '=',
163
- path.node.left,
164
- t.callExpression(expression('global.ContrastMethods.plus')(), [path.node.left, path.node.right])
165
- )
166
- );
167
- }
168
- });
169
- }
29
+ const rewriter = {
30
+ /** @type {Set<Mode>} */
31
+ modes: new Set(),
170
32
 
171
- self.tokens = Object.keys(self.methodLookups);
172
- };
173
- }
33
+ /**
34
+ * Sets the rewriter to 'assess' or 'protect' mode, enabling different
35
+ * transforms.
36
+ * @param {Mode} mode
37
+ */
38
+ install(mode) {
39
+ this.modes.add(mode);
40
+ },
174
41
 
175
42
  /**
176
43
  * @param {string} content the source code
177
44
  * @param {object} opts
178
45
  * @param {string} opts.filename e.g. 'index.js'
179
- * @param {boolean} opts.inject whether to inject contrast methods
180
- * @param {string} opts.sourceType script or module
181
- * @param {boolean} opts.wrap whether to wrap code in module wrap IIFE
182
- * @returns {object}
46
+ * @param {boolean} opts.isModule if true, file is parsed as an ES module instead of a CJS script
47
+ * @param {boolean} opts.inject if true, injects ContrastMethods on the global object
48
+ * @param {boolean} opts.wrap if true, wraps the content with a modified module wrapper IIFE
49
+ * @returns {import("@swc/core").Output}
183
50
  */
184
- rewrite(content, opts = {
185
- inject: false,
186
- wrap: false,
187
- sourceType: 'script',
188
- }) {
189
- opts.filename = opts.filename || 'no filename';
190
- opts.sourceType = opts.sourceType || 'script';
191
-
192
- const state = {
193
- orig: String(content),
194
- deps: [],
195
- filename: opts.filename,
196
- ...opts
197
- };
198
-
199
- if (this.tokens.every((token) => state.orig.indexOf(token) === -1)) {
200
- return { code: state.orig };
51
+ rewrite(content, opts = {}) {
52
+ if (opts.wrap) {
53
+ content = `${prefix}${content}${suffix}`;
201
54
  }
202
55
 
203
- const ast = parser.parse(state.orig, {
204
- plugins: [
205
- 'classPrivateMethods',
206
- 'classPrivateProperties',
207
- 'classProperties'
208
- ],
209
- ranges: true,
210
- sourceType: state.sourceType,
211
- sourceFilename: state.filename,
212
- tokens: true
213
- });
214
-
215
- traverse(ast, this.rewriteTransforms, null, state);
216
- // TODO: Look into how effective this is
217
- traverse.cache.clear();
218
-
219
- const result = generate(
220
- ast,
221
- {
222
- jsonCompatibleStrings: true,
223
- sourceMaps: true,
224
- sourceFileName: state.filename
56
+ return transformSync(content, {
57
+ filename: opts.filename,
58
+ isModule: opts.isModule,
59
+ jsc: {
60
+ target: 'es2019', // should work for node >14
61
+ experimental: {
62
+ plugins: [
63
+ [
64
+ rewriterPath,
65
+ {
66
+ assess: this.modes.has('assess'),
67
+ inject: this.modes.has('assess') && opts.inject,
68
+ },
69
+ ],
70
+ ],
71
+ },
225
72
  },
226
- state.orig
227
- );
228
-
229
- result.code = removeAddedSemicolons(content, result.code);
230
- result.deps = state.deps;
231
-
232
- return result;
233
- }
73
+ sourceMaps: true,
74
+ });
75
+ },
234
76
 
235
77
  /**
236
- * @param {string} code
237
- * @param {object} opts
78
+ * @param {string} content
238
79
  * @returns {string}
239
80
  */
240
- unwrite(code, opts) {
241
- const ast = parser.parse(code);
242
- traverse(ast, this.unwriteTransforms);
243
- const unwritten = generate(ast, { jsonCompatibleStrings: true }).code;
244
- return removeAddedSemicolons(code, unwritten);
245
- }
246
- }
81
+ unwrite(content) {
82
+ return transformSync(content, {
83
+ jsc: {
84
+ target: 'es2019', // should work for node >14
85
+ experimental: {
86
+ plugins: [[unwriterPath, {}]],
87
+ },
88
+ },
89
+ }).code;
90
+ },
91
+ };
247
92
 
248
- module.exports.Rewriter = Rewriter;
93
+ /** @typedef {{}} Core */
94
+ /** @typedef {typeof rewriter} Rewriter */
95
+
96
+ /**
97
+ * @param {Core} core
98
+ * @returns {Rewriter}
99
+ */
100
+ module.exports = function init(core) {
101
+ core.rewriter = rewriter;
102
+ return rewriter;
103
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/rewriter",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
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
+ }