@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 +12 -0
- package/README.md +39 -0
- package/lib/index.js +157 -0
- package/lib/index.test.js +102 -0
- package/lib/source-maps.js +37 -0
- package/package.json +28 -0
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
|
+
}
|