@contrast/rewriter
Advanced tools
Comparing version 1.4.2 to 1.5.0
232
lib/index.js
/* | ||
* Copyright: 2022 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -16,10 +16,26 @@ * License: Commercial | ||
// @ts-check | ||
'use strict'; | ||
const { transformSync } = require('@swc/core'); | ||
const Module = require('module'); | ||
const { transform, transformSync } = require('@swc/core'); | ||
const Module = require('node:module'); | ||
const { Cache } = require('./cache'); | ||
const rewriterPath = require.resolve('@contrast/agent-swc-plugin'); | ||
const unwriterPath = require.resolve('@contrast/agent-swc-plugin-unwrite'); | ||
/** | ||
* @typedef {Object} Core | ||
* @prop {import('@contrast/common').AppInfo} appInfo | ||
* @prop {string} agentVersion | ||
* @prop {import('@contrast/config').Config} config | ||
* @prop {import('@contrast/logger').Logger} logger | ||
*/ | ||
/** | ||
* @typedef {'assess' | 'protect'} Mode | ||
*/ | ||
/** | ||
* @typedef {Object} RewriteOpts | ||
* @prop {string=} filename e.g. 'index.js' | ||
* @prop {boolean=} isModule if true, file is parsed as an ES module instead of a CJS script | ||
* @prop {boolean=} inject if true, injects ContrastMethods on the global object | ||
* @prop {boolean=} wrap if true, wraps the content with a modified module wrapper IIFE | ||
* @prop {boolean=} trim if true, removes added characters from the end of the generated code | ||
*/ | ||
@@ -31,9 +47,68 @@ // @ts-expect-error `wrapper` is missing from @types/node | ||
/** @typedef {'assess' | 'protect'} Mode */ | ||
const rewriterPath = require.resolve('@contrast/agent-swc-plugin'); | ||
const unwriterPath = require.resolve('@contrast/agent-swc-plugin-unwrite'); | ||
const rewriter = { | ||
/** @type {Set<Mode>} */ | ||
modes: new Set(), | ||
/** | ||
* Wraps the source content as necessary to support rewriting. | ||
* Wrapping must occur before rewriting since the underlying rewriter cannot | ||
* parse certain valid statements such as `return` statements in a CJS script. | ||
* @param {string} content | ||
* @returns {string} | ||
*/ | ||
const wrap = (content) => { | ||
let shebang = ''; | ||
// The shebang will be commented out since it cannot be present in a | ||
// function body. swc doesn't include the commented shebang in the generated | ||
// code despite including comments otherwise. | ||
if (content.charAt(0) === '#') { | ||
shebang = content.substring(0, content.indexOf('\n') + 1); | ||
content = `//${content}`; | ||
} | ||
content = `${shebang}${prefix}${content}${suffix}`; | ||
return content; | ||
}; | ||
/** | ||
* Trims extraneous characters that may have been added by the rewriter. | ||
* Handles newline or semicolon insertion, removing the added characters if they | ||
* were not present in the original source content. | ||
* @param {string} content | ||
* @param {import('@swc/core').Output} result | ||
* @returns {import('@swc/core').Output} | ||
*/ | ||
const trim = (content, result) => { | ||
let carriageReturn = 0; | ||
// swc always adds a newline, so we only need to check the input | ||
if (!content.endsWith('\n')) { | ||
result.code = result.code.substring(0, result.code.length - 1); | ||
} else if (content.endsWith('\r\n')) { | ||
// if EOL is \r\n, then we need to account for that when we check the | ||
// negative index of the last semicolon below | ||
carriageReturn = 1; | ||
} | ||
const resultSemicolonIdx = result.code.lastIndexOf(';'); | ||
const contentSemicolonIdx = content.lastIndexOf(';'); | ||
if (contentSemicolonIdx === -1 || resultSemicolonIdx - result.code.length !== contentSemicolonIdx - content.length + carriageReturn) { | ||
result.code = result.code.substring(0, resultSemicolonIdx) + result.code.substring(resultSemicolonIdx + 1, result.code.length); | ||
} | ||
return result; | ||
}; | ||
class Rewriter { | ||
/** | ||
* @param {Core} core | ||
*/ | ||
constructor(core) { | ||
this.core = core; | ||
this.logger = core.logger.child({ name: 'contrast:rewriter' }); | ||
/** @type {Set<Mode>} */ | ||
this.modes = new Set(); | ||
this.cache = new Cache(core); | ||
} | ||
/** | ||
* Sets the rewriter to 'assess' or 'protect' mode, enabling different | ||
@@ -44,75 +119,93 @@ * transforms. | ||
install(mode) { | ||
this.logger.trace('installing rewriter mode: %s', mode); | ||
this.modes.add(mode); | ||
}, | ||
this.cache.install(mode); | ||
} | ||
/** | ||
* @param {string} content the source code | ||
* @param {object} opts | ||
* @param {string=} opts.filename e.g. 'index.js' | ||
* @param {boolean=} opts.isModule if true, file is parsed as an ES module instead of a CJS script | ||
* @param {boolean=} opts.inject if true, injects ContrastMethods on the global object | ||
* @param {boolean=} opts.wrap if true, wraps the content with a modified module wrapper IIFE | ||
* @returns {import("@swc/core").Output} | ||
* @param {RewriteOpts} opts | ||
* @returns {import('@swc/core').Options} | ||
*/ | ||
rewrite(content, opts = {}) { | ||
let shebang = ''; | ||
if (content.charAt(0) === '#') { | ||
shebang = content.substring(0, content.indexOf('\n') + 1); | ||
// see the test output: swc doesn't include the commented shebang in the generated code despite including comments otherwise | ||
content = `//${content}`; | ||
} | ||
if (opts.wrap) { | ||
content = `${shebang}${prefix}${content}${suffix}`; | ||
} | ||
const result = transformSync(content, { | ||
rewriteConfig(opts) { | ||
return { | ||
filename: opts.filename, | ||
isModule: opts.isModule, | ||
env: { | ||
targets: { | ||
node: process.versions.node | ||
} | ||
}, | ||
jsc: { | ||
target: 'es2019', // should work for node >14 | ||
experimental: { | ||
plugins: [ | ||
[ | ||
rewriterPath, | ||
{ | ||
assess: this.modes.has('assess'), | ||
inject: opts.inject, | ||
}, | ||
], | ||
], | ||
plugins: [[rewriterPath, { | ||
assess: this.modes.has('assess'), | ||
inject: opts.inject, | ||
}]], | ||
}, | ||
}, | ||
sourceMaps: true, | ||
}); | ||
// if we're trimming the output we're not rewriting an entire file, which | ||
// means source maps are not relevant. | ||
sourceMaps: !opts.trim && this.core.config.agent.node.source_maps.enable, | ||
}; | ||
} | ||
if (!opts.wrap) { | ||
let carriageReturn = 0; | ||
// swc always adds a newline, so we only need to check the input | ||
if (!content.endsWith('\n')) { | ||
result.code = result.code.substring(0, result.code.length - 1); | ||
} else if (content.endsWith('\r\n')) { | ||
// if EOL is \r\n, then we need to account for that when we check the | ||
// negative index of the last semicolon below | ||
carriageReturn = 1; | ||
} | ||
const resultSemicolonIdx = result.code.lastIndexOf(';'); | ||
const contentSemicolonIdx = content.lastIndexOf(';'); | ||
if (contentSemicolonIdx === -1 || resultSemicolonIdx - result.code.length !== contentSemicolonIdx - content.length + carriageReturn) { | ||
result.code = result.code.substring(0, resultSemicolonIdx) + result.code.substring(resultSemicolonIdx + 1, result.code.length); | ||
} | ||
/** | ||
* Rewrites the provided source code string asynchronously to be consumed by | ||
* ESM hooks. | ||
* @param {string} content | ||
* @param {RewriteOpts=} opts | ||
* @returns {Promise<import('@swc/core').Output>} | ||
*/ | ||
async rewrite(content, opts = {}) { | ||
this.logger.trace({ opts }, 'rewriting %s', opts.filename); | ||
if (opts.wrap) { | ||
content = wrap(content); | ||
} | ||
let result = await transform(content, this.rewriteConfig(opts)); | ||
if (opts.trim) { | ||
result = trim(content, result); | ||
} | ||
return result; | ||
}, | ||
} | ||
/** | ||
* Rewrites the provided source code string synchronously to be consumed by | ||
* CJS hooks. | ||
* @param {string} content | ||
* @param {RewriteOpts=} opts | ||
* @returns {import('@swc/core').Output} | ||
*/ | ||
rewriteSync(content, opts = {}) { | ||
this.logger.trace({ opts }, 'rewriting %s', opts.filename); | ||
if (opts.wrap) { | ||
content = wrap(content); | ||
} | ||
let result = transformSync(content, this.rewriteConfig(opts)); | ||
if (opts.trim) { | ||
result = trim(content, result); | ||
} | ||
return result; | ||
} | ||
/** | ||
* Removes contrast-related rewritten code from provided source code string. | ||
* @param {string} content | ||
* @returns {string} | ||
*/ | ||
unwrite(content) { | ||
unwriteSync(content) { | ||
return transformSync(content, { | ||
env: { | ||
targets: { | ||
node: process.versions.node | ||
} | ||
}, | ||
jsc: { | ||
target: 'es2019', // should work for node >14 | ||
experimental: { | ||
@@ -122,15 +215,16 @@ plugins: [[unwriterPath, {}]], | ||
}, | ||
sourceMaps: false, | ||
}).code; | ||
}, | ||
}; | ||
} | ||
} | ||
/** @typedef {typeof rewriter} Rewriter */ | ||
/** | ||
* @param {{ rewriter: Rewriter }} core | ||
* @param {Core & { rewriter?: Rewriter; }} core | ||
* @returns {Rewriter} | ||
*/ | ||
module.exports = function init(core) { | ||
core.rewriter = rewriter; | ||
return rewriter; | ||
core.rewriter = new Rewriter(core); | ||
return core.rewriter; | ||
}; | ||
module.exports.Rewriter = Rewriter; |
/* | ||
* Copyright: 2022 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -22,7 +22,7 @@ * License: Commercial | ||
module.exports = function(deps) { | ||
module.exports = function (deps) { | ||
const sourceMaps = deps.rewriter.sourceMaps = {}; | ||
const consumerCache = sourceMaps.consumerCache = {}; | ||
sourceMaps.cacheConsumerMap = function(filename, map) { | ||
sourceMaps.cacheConsumerMap = function (filename, map) { | ||
consumerCache[filename] = new SourceMapConsumer(map); | ||
@@ -33,3 +33,3 @@ }; | ||
*/ | ||
sourceMaps.chain = function(filename, map) { | ||
sourceMaps.chain = function (filename, map) { | ||
let ret; | ||
@@ -41,6 +41,5 @@ | ||
ret = transfer({ fromSourceMap: map, toSourceMap: existingMap }); | ||
deps.logger.trace(`Merged sourcemap from ${filename}.map`); | ||
} catch (e) { | ||
deps.logger.debug(`Unable to read ${filename}.map.js`); | ||
deps.logger.debug(`${e}`); | ||
deps.logger.trace('Merged sourcemap from %s.map', filename); | ||
} catch (err) { | ||
deps.logger.debug({ err }, 'Unable to read %s.map.js', filename); | ||
} | ||
@@ -47,0 +46,0 @@ } |
{ | ||
"name": "@contrast/rewriter", | ||
"version": "1.4.2", | ||
"version": "1.5.0", | ||
"description": "A transpilation tool mainly used for instrumentation", | ||
"license": "SEE LICENSE IN LICENSE", | ||
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)", | ||
"files": [ | ||
"lib/" | ||
], | ||
"main": "lib/index.js", | ||
"types": "lib/index.d.ts", | ||
"types": "types/index.d.ts", | ||
"engines": { | ||
@@ -20,4 +17,5 @@ "npm": ">=6.13.7 <7 || >= 8.3.1", | ||
"dependencies": { | ||
"@contrast/agent-swc-plugin": "^1.2.0", | ||
"@contrast/agent-swc-plugin-unwrite": "^1.2.0", | ||
"@contrast/agent-swc-plugin": "^1.3.0", | ||
"@contrast/agent-swc-plugin-unwrite": "^1.3.0", | ||
"@contrast/common": "^1.16.0", | ||
"@contrast/synchronous-source-maps": "^1.1.3", | ||
@@ -27,2 +25,2 @@ "@swc/core": "1.3.39", | ||
} | ||
} | ||
} |
@@ -5,30 +5,3 @@ ## `@contrast/rewriter` | ||
For example, Assess will register transforms for `+` -> `contrast_add()` so that it can perform propagation | ||
via instrumentation of `contrast_add()`. | ||
#### Example Service Usage | ||
```typescript | ||
const { Rewriter } = require('.'); | ||
const rewriter = new Rewriter({ logger }); | ||
rewriter.addTransforms({ | ||
BinaryExpression(path) { | ||
const method = methodLookups[path.node.operator]; | ||
if (method) { | ||
path.replaceWith( | ||
t.callExpression( | ||
expression('ContrastMethods.%%method%%')({ method }), [ | ||
path.node.left, | ||
path.node.right | ||
] | ||
) | ||
); | ||
} | ||
} | ||
}); | ||
const result = rewriter.rewrite('function add(x, y) { return x + y; }'); | ||
``` | ||
For example, Assess will register transforms for `+` -> `ContrastMethods.add()` | ||
so that it can perform propagation via instrumentation of `ContrastMethods.add()`. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
17378
6
473
2
6
7
1
+ Added@contrast/common@^1.16.0
+ Added@contrast/common@1.26.0(transitive)