@contrast/rewriter 1.0.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/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright: 2022 Contrast Security, Inc
2
+ Contact: support@contrastsecurity.com
3
+ License: Commercial
4
+
5
+ NOTICE: This Software and the patented inventions embodied within may only be
6
+ used as part of Contrast Security’s commercial offerings. Even though it is
7
+ made available through public repositories, use of this Software is subject to
8
+ the applicable End User Licensing Agreement found at
9
+ https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
10
+ between Contrast Security and the End User. The Software may not be reverse
11
+ engineered, modified, repackaged, sold, redistributed or otherwise used in a
12
+ way not consistent with the End User License Agreement.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ ## `@contrast/rewriter`
2
+
3
+ Provides a configured service for rewriting code.
4
+
5
+ Basic idea: Enabled features can register rewrite transforms to support their end goal.
6
+
7
+ Example: Assess
8
+
9
+ Assess will register transforms for `+` -> `contrast_add()` so that it can perform propagation
10
+ via instrumentation of `contrast_add()`.
11
+
12
+
13
+ #### Example Service Usage
14
+
15
+ ```typescript
16
+ const { Rewriter } = require('.');
17
+
18
+ const rewriter = new Rewriter({ config, logger });
19
+
20
+ rewriter.addTransforms({
21
+ BinaryExpression(path) {
22
+ const method = methodLookups[path.node.operator];
23
+ if (method) {
24
+ path.replaceWith(
25
+ t.callExpression(
26
+ expression('ContrastMethods.%%method%%')({ method }), [
27
+ path.node.left,
28
+ path.node.right
29
+ ]
30
+ )
31
+ );
32
+ }
33
+ }
34
+ });
35
+
36
+ const result = rewriter.rewriteFile({
37
+ content: 'function add(x, y) { return x + y; }'
38
+ });
39
+ ```
package/lib/index.js ADDED
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const Module = require('module');
4
+ const { default: traverse } = require('@babel/traverse');
5
+ const parser = require('@babel/parser');
6
+ const { default: generate } = require('@babel/generator');
7
+ const t = require('@babel/types');
8
+ const { expression, statement } = require('@babel/template');
9
+
10
+ /**
11
+ * factory
12
+ */
13
+ module.exports = function(core) {
14
+ const rewriter = new Rewriter(core);
15
+ core.rewriter = rewriter;
16
+ return rewriter;
17
+ };
18
+
19
+
20
+ /**
21
+ * Babel will add a trailing semicolon in some cases. This will remove it if it
22
+ * wasn't there to begin with.
23
+ * @param {string} rewritten rewritten content
24
+ * @param {string} orig original content before rewriting
25
+ * @returns {string}
26
+ */
27
+ function removeAddedSemicolons(orig, rewritten) {
28
+ if (
29
+ rewritten.charCodeAt(rewritten.length - 1) == 59 &&
30
+ orig.charCodeAt(orig.length - 1) != 59
31
+ ) {
32
+ rewritten = rewritten.substr(0, rewritten.length - 1);
33
+ }
34
+ return rewritten;
35
+ }
36
+
37
+ class Rewriter {
38
+ constructor(deps) {
39
+ const self = this;
40
+ this.logger = deps.logger;
41
+ this.visitors = [];
42
+ this.rewriteTransforms = {
43
+ enter(...args) {
44
+ for (const v of self.visitors) {
45
+ v(...args);
46
+ }
47
+ },
48
+ Program: function Program(path, state) {
49
+ if (state.wrap) {
50
+
51
+ let [prefix, suffix] = Module.wrapper;
52
+ prefix = prefix.trim();
53
+ suffix = suffix.trim().replace(/;$/, '.apply(this, arguments);');
54
+
55
+ path.node.body = [
56
+ statement(`${prefix} %%body%% ${suffix}`)({
57
+ body: path.node.body
58
+ })
59
+ ];
60
+ return;
61
+ }
62
+
63
+ if (state.inject) {
64
+ path.unshiftContainer('body', [
65
+ statement(
66
+ 'const ContrastMethods = global.ContrastMethods || (() => { throw new SyntaxError(\'ContrastMethods undefined during compilation\'); })();'
67
+ )(),
68
+ ]);
69
+ }
70
+ },
71
+ CallExpression(path, state) {
72
+ if (path.node.callee.name === 'eval') {
73
+ path.node.arguments = [
74
+ t.callExpression(expression('global.ContrastMethods.eval')(), path.node.arguments)
75
+ ];
76
+ }
77
+ },
78
+ };
79
+ this.unwriteTransforms = {
80
+ CallExpression(path) {
81
+ const obj = path.node.callee.object;
82
+ if (obj && obj.property && obj.property.name === 'ContrastMethods') {
83
+ path.replaceWith(path.node.arguments[0]);
84
+ }
85
+ }
86
+ };
87
+ }
88
+
89
+ /**
90
+ * @param {string} content the source code
91
+ * @param {object} opts
92
+ * @param {string} opts.filename e.g. 'index.js'
93
+ * @param {boolean} opts.inject whether to inject contrast methods
94
+ * @param {string} opts.sourceType script or module
95
+ * @param {boolean} opts.wrap whether to wrap code in module wrap IIFE
96
+ * @returns {object}
97
+ */
98
+ rewrite(content, opts = {
99
+ inject: false,
100
+ wrap: false,
101
+ sourceType: 'script',
102
+ }) {
103
+ opts.filename = opts.filename || 'no filename';
104
+ opts.sourceType = opts.sourceType || 'script';
105
+
106
+ const state = {
107
+ orig: String(content),
108
+ deps: [],
109
+ filename: opts.filename,
110
+ ...opts
111
+ };
112
+ const ast = parser.parse(state.orig, {
113
+ plugins: [
114
+ 'classPrivateMethods',
115
+ 'classPrivateProperties',
116
+ 'classProperties'
117
+ ],
118
+ ranges: true,
119
+ sourceType: state.sourceType,
120
+ sourceFilename: state.filename,
121
+ tokens: true
122
+ });
123
+
124
+ traverse(ast, this.rewriteTransforms, null, state);
125
+ // TODO: Look into how effective this is
126
+ traverse.cache.clear();
127
+
128
+ const result = generate(
129
+ ast,
130
+ {
131
+ jsonCompatibleStrings: true,
132
+ sourceMaps: true,
133
+ sourceFileName: state.filename
134
+ },
135
+ state.orig
136
+ );
137
+
138
+ result.code = removeAddedSemicolons(content, result.code);
139
+ result.deps = state.deps;
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * @param {string} code
146
+ * @param {object} opts
147
+ * @returns {string}
148
+ */
149
+ unwrite(code, opts) {
150
+ const ast = parser.parse(code);
151
+ traverse(ast, this.unwriteTransforms);
152
+ const unwritten = generate(ast, { jsonCompatibleStrings: true }).code;
153
+ return removeAddedSemicolons(code, unwritten);
154
+ }
155
+ }
156
+
157
+ module.exports.Rewriter = Rewriter;
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+
6
+ describe('rewriter', function() {
7
+ let core;
8
+ let visitorSpy;
9
+ let rewriter;
10
+
11
+ beforeEach(function() {
12
+ const mocks = require('../../test/mocks');
13
+ core = mocks.core();
14
+ core.config = mocks.config();
15
+ core.logger = mocks.logger();
16
+ visitorSpy = sinon.spy();
17
+
18
+ const factory = require('.');
19
+ rewriter = factory(core);
20
+ rewriter.visitors.push(visitorSpy);
21
+ });
22
+
23
+
24
+ // these baseline values don't have trailing semicolons
25
+ const content = 'eval(\'3\')';
26
+ const rewrittenContent = 'eval(global.ContrastMethods.eval(\'3\'))';
27
+
28
+ describe('rewrite()', function() {
29
+ [
30
+ {
31
+ content,
32
+ rewrittenContent,
33
+ desc: 'no spurious trailing semicolons are added',
34
+ },
35
+ {
36
+ content: `${content};`,
37
+ rewrittenContent: `${rewrittenContent};`,
38
+ desc: 'trailing semicolons are preserved',
39
+ },
40
+ {
41
+ content: 'foo',
42
+ rewrittenContent: 'foo',
43
+ desc: 'code is unaltered if no contrast methods need rewriting',
44
+ rawMappings: { length: 2 },
45
+ visitorsCall: 3
46
+ }
47
+ ].forEach(({ content, rewrittenContent, desc, rawMappings, visitorsCall }) => {
48
+ it(`rewrites successfully and ${desc}`, function() {
49
+ const result = rewriter.rewrite(content);
50
+ expect(result.code).eql(rewrittenContent);
51
+ expect(result.map).ok;
52
+ expect(result.rawMappings).lengthOf(rawMappings ? rawMappings.length : 5);
53
+ expect(visitorSpy.getCalls().length).eql(visitorsCall || 11);
54
+ });
55
+ });
56
+ });
57
+
58
+ describe('unwrite()', function() {
59
+ [
60
+ {
61
+ content,
62
+ rewrittenContent,
63
+ desc: 'no spurious trailing semicolons are added'
64
+ },
65
+ {
66
+ content: `${content};`,
67
+ rewrittenContent: `${rewrittenContent};`,
68
+ desc: 'trailing semicolons are preserved'
69
+ },
70
+ {
71
+ content,
72
+ rewrittenContent: content,
73
+ desc: 'code is unaltered if no contrast methods need unwriting',
74
+ },
75
+ ].forEach(({ content, rewrittenContent, desc }) => {
76
+ it(`unwrites successfully and ${desc}`, function() {
77
+ const ret = rewriter.unwrite(rewrittenContent);
78
+ expect(ret).eql(content);
79
+ });
80
+ });
81
+ });
82
+
83
+ describe('option: wrap', function() {
84
+ it('{ wrap: true } will add "Module.wrap" IIFE to', function () {
85
+ const result = rewriter.rewrite(content, { wrap: true });
86
+ expect(result.code).eql(`(function (exports, require, module, __filename, __dirname) {
87
+ ${rewrittenContent};
88
+ }).apply(this, arguments)`);
89
+ });
90
+ });
91
+
92
+ describe('option: inject', function() {
93
+ it('{ inject: true } will add contrast method declarations', function() {
94
+ const result = rewriter.rewrite(content, { inject: true });
95
+ expect(result.code).eql(`const ContrastMethods = global.ContrastMethods || (() => {
96
+ throw new SyntaxError('ContrastMethods undefined during compilation');
97
+ })();
98
+
99
+ ${rewrittenContent}`);
100
+ });
101
+ });
102
+ });
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { readFileSync, existsSync } = require('fs');
4
+ const { transfer } = require('multi-stage-sourcemap');
5
+ const { SourceMapConsumer } = require('@contrast/synchronous-source-maps');
6
+ const path = require('path');
7
+
8
+ module.exports = function(deps) {
9
+ const { isAgentPath } = deps;
10
+ const sourceMaps = deps.rewriter.sourceMaps = {};
11
+ const consumerCache = sourceMaps.consumerCache = {};
12
+
13
+ sourceMaps.cacheConsumerMap = function(filename, map) {
14
+ consumerCache[filename] = new SourceMapConsumer(map);
15
+ };
16
+
17
+ /**
18
+ */
19
+ sourceMaps.chain = function(filename, map) {
20
+ let ret;
21
+
22
+ if (existsSync(`${filename}.map`)) {
23
+ try {
24
+ const existingMap = JSON.parse(readFileSync(`${filename}.map`, 'utf8'));
25
+ ret = transfer({ fromSourceMap: map, toSourceMap: existingMap });
26
+ deps.logger.trace(`Merged sourcemap from ${filename}.map`);
27
+ } catch (e) {
28
+ deps.logger.debug(`Unable to read ${filename}.map.js`);
29
+ deps.logger.debug(`${e}`);
30
+ }
31
+ }
32
+
33
+ return ret;
34
+ };
35
+
36
+ return sourceMaps;
37
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@contrast/rewriter",
3
+ "version": "1.0.0",
4
+ "description": "A transpilation tool mainly used for instrumentation",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
7
+ "files": [
8
+ "lib/"
9
+ ],
10
+ "main": "lib/index.js",
11
+ "types": "lib/index.d.ts",
12
+ "engines": {
13
+ "npm": ">= 8.4.0",
14
+ "node": ">= 14.15.0"
15
+ },
16
+ "scripts": {
17
+ "test": "../scripts/test.sh"
18
+ },
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",
25
+ "@contrast/synchronous-source-maps": "^1.1.3",
26
+ "multi-stage-sourcemap": "^0.3.1"
27
+ }
28
+ }