@contrast/core 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,98 @@
1
+ # `@contrast/core`
2
+
3
+ Discovers Contrast configuration data (yaml, env vars, etc) and preconfigures a common set of APIs to be used for agent and tooling development.
4
+
5
+ ## Basic Usage
6
+
7
+ The module exports a factory function.
8
+
9
+ ```typescript
10
+ const core = require('@contrast/core')();
11
+
12
+ ```
13
+ ### What You Get
14
+
15
+ - Logging
16
+
17
+ ```typescript
18
+ core.logger.info('...');
19
+ ```
20
+
21
+ See more about the `@contrast/logger` service [here](../logger/README.md).
22
+
23
+ - Monkey-patching
24
+
25
+ ```typescript
26
+ core.patcher.patch(res, 'end', {
27
+ name: 'http.ServerResponse.end',
28
+ patchType: 'http-things',
29
+ pre(data) {
30
+ // ...
31
+ }
32
+ });
33
+ ```
34
+
35
+ See more about the `@contrast/patcher` service [here](../patcher/README.md).
36
+
37
+
38
+ - Code rewriting
39
+
40
+ ```typescript
41
+ core.rewriter.addTransforms({
42
+ CallExpression(path, state) {
43
+ // ...
44
+ };
45
+ });
46
+ core.rewriter.rewrite('function() { ...');
47
+ ```
48
+
49
+ See more about the `@contrast/rewriter` service [here](../rewriter/README.md).
50
+
51
+
52
+ - Dependency hooks
53
+
54
+ ```typescript
55
+ core.depHooks.resolve({ name: 'http' }, http => {
56
+ // implemention details
57
+ });
58
+ ```
59
+
60
+ See more about the `@contrast/dep-hooks` service [here](../dep-hooks/README.md).
61
+
62
+
63
+ - Models and factories
64
+
65
+ The construction of model data _can_ rely on configuration and therefore can be stateful. So, we provide the models and their factories as services that can be used by consumers as if static.
66
+
67
+ ```typescript
68
+ // stackframe filtration is configurable, thus stateful
69
+ const snap = core.models.StacktraceFactory.createSnapshot();
70
+ const frames = snap();
71
+ ```
72
+
73
+ See more about the `@contrast/models` service [here](../models/README.md).
74
+
75
+ - Report messages
76
+
77
+ ```typescript
78
+ // configuration will tell which reporters become active
79
+ core.reporters.install();
80
+ core.messages.emit('ProtectInputTracingEvent', { ... });
81
+ ```
82
+
83
+ See more about the `@contrast/reporter` service [here](../reporter/README.md).
84
+
85
+ - Other stuff
86
+
87
+ There are some utility-type functions that rely on configuration state.
88
+
89
+ ```typescript
90
+ // This uses core.config.stack_trace_filters (new to v5)
91
+ core.isAgentPath('/foo');
92
+ ```
93
+
94
+ ## Related
95
+
96
+ - `@contrast/agentify`: Integrate core services and instrumentation into an application. See more [here](../agentify/README.md).
97
+
98
+ <br><br>
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const process = require('process');
7
+
8
+ const MAX_ATTEMPTS = 5;
9
+
10
+ module.exports = function (deps) {
11
+ const { logger, config } = deps;
12
+
13
+ const appInfo = (deps.appInfo = {
14
+ os: {
15
+ type: os.type(),
16
+ platform: os.platform(),
17
+ architecture: os.arch(),
18
+ release: os.release(),
19
+ },
20
+ hostname: os.hostname(),
21
+ });
22
+
23
+ let cmd, _path, pkg;
24
+
25
+ try {
26
+ const entrypoint = config.script || config.entrypoint;
27
+ if (entrypoint) {
28
+ cmd = appInfo.cmd = path.resolve(entrypoint);
29
+ } else {
30
+ appInfo.cmd = undefined;
31
+ }
32
+
33
+ _path = appInfo.path = resolveAppPath(
34
+ config.agent.node.app_root,
35
+ cmd ? path.dirname(cmd) : undefined
36
+ );
37
+ pkg = require(_path);
38
+ appInfo.pkg = pkg;
39
+ appInfo.name = config.application.name || pkg.name;
40
+ appInfo.app_dir = path.dirname(appInfo.path);
41
+ } catch (e) {
42
+ throw new Error(`Unable to find application's package.json: ${_path}`);
43
+ }
44
+
45
+ appInfo.serverVersion = config.server.version;
46
+ appInfo.node_version = process.version;
47
+
48
+ appInfo.appPath = config.application.path || appInfo.app_dir;
49
+ appInfo.indexFile = cmd;
50
+ appInfo.serverName = config.server.name;
51
+ appInfo.serverEnvironment = config.server.environment;
52
+
53
+ return appInfo;
54
+
55
+ function resolveAppPath(appRoot, scriptPath) {
56
+ let packageLocation;
57
+
58
+ if (appRoot) {
59
+ packageLocation = findFile({ directory: appRoot, file: 'package.json' });
60
+ }
61
+
62
+ if (!packageLocation) {
63
+ packageLocation = findFile({
64
+ directory: scriptPath,
65
+ file: 'package.json',
66
+ });
67
+ }
68
+
69
+ if (packageLocation) {
70
+ deps.logger.info('using package.json at %s', packageLocation);
71
+ }
72
+
73
+ return packageLocation;
74
+ }
75
+
76
+ /**
77
+ * Gets contents of a directory
78
+ *
79
+ * @param {String} directory
80
+ * @param {Array} contents of a directory
81
+ */
82
+ function getDirectoryEntries(directory) {
83
+ try {
84
+ return fs.readdirSync(directory);
85
+ } catch (err) {
86
+ logger.error({ err }, 'error reading directory %s', directory);
87
+ return [];
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Filters files with name matching the file param
93
+ *
94
+ * @param {String} directory path to check for file
95
+ * @param {Array} files list of entries in a directory
96
+ * @param {String} file file to check
97
+ *
98
+ * @return {*} path to file file or false
99
+ */
100
+ function filterFiles(directory, files, file) {
101
+ const hit = files.filter((entry) => entry === file)[0];
102
+ return hit ? path.resolve(directory, hit) : false;
103
+ }
104
+
105
+ /**
106
+ * Checks each direct sibling folder for file
107
+ *
108
+ * @param {Object} params
109
+ * @param {String} params.file file to check
110
+ * @param {String} params.directory path to check for file
111
+ * @param {Array} params.foundFiles array to hold found files
112
+ * @param {Array} params.entries list of directory contents
113
+ */
114
+ function checkNestedFolders({ file, directory, entries, foundFiles }) {
115
+ entries.reduce((foundFiles, entry) => {
116
+ const resolvedEntry = path.resolve(directory, entry);
117
+ try {
118
+ if (!fs.statSync(resolvedEntry).isFile()) {
119
+ const path = filterFiles(
120
+ resolvedEntry,
121
+ getDirectoryEntries(resolvedEntry),
122
+ file
123
+ );
124
+ if (path) {
125
+ foundFiles.push(path);
126
+ }
127
+ }
128
+ } catch (err) {
129
+ // swallow this error, we don't care
130
+ }
131
+ return foundFiles;
132
+ }, foundFiles);
133
+ }
134
+
135
+ /**
136
+ * Tries to find a file in entryPoint, 1 folder below or up to 5 directories
137
+ * above initial path
138
+ *
139
+ * @param {Object} params
140
+ * @param {String} params.directory path to check for file
141
+ * @param {String} params.file file to check
142
+ * @param {Int} [params.maxAttempts=5] max attempts to check above params.directory
143
+ * @param {Int} params.attempts current attempt to find file in path
144
+ * @param {Boolean} params.checkNested flag to check in nested directories
145
+ *
146
+ * @return {String} path to file
147
+ */
148
+ function findFile({
149
+ directory,
150
+ file,
151
+ maxAttempts = MAX_ATTEMPTS,
152
+ attempts = 0,
153
+ checkNested = true,
154
+ }) {
155
+ if (attempts >= maxAttempts) {
156
+ return;
157
+ }
158
+
159
+ attempts++;
160
+ directory = path.resolve(directory);
161
+
162
+ const entries = getDirectoryEntries(directory);
163
+ const location = filterFiles(directory, entries, file);
164
+ if (location) {
165
+ return location;
166
+ }
167
+
168
+ // we only want to check files for each directory in the intial call to this function
169
+ if (checkNested) {
170
+ const foundFiles = [];
171
+ checkNestedFolders({ directory, entries, file, foundFiles });
172
+ // we found a file in the child folders return path to it
173
+ if (foundFiles.length > 0) {
174
+ return foundFiles[0];
175
+ }
176
+ }
177
+
178
+ // we only want to look at dirs in the intial directory
179
+ // assign parent directory to directory
180
+ directory = path.dirname(directory);
181
+ return findFile({
182
+ directory,
183
+ file,
184
+ maxAttempts,
185
+ attempts,
186
+ checkNested: false,
187
+ });
188
+ }
189
+ };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+ const proxyquire = require('proxyquire');
6
+ const nodePath = require('path');
7
+ const mockPackageJson = require('../../test/mocks/package.json');
8
+
9
+ describe('core app-info', function() {
10
+ let os;
11
+ let path;
12
+ let fs;
13
+ let core;
14
+ let appInfo;
15
+ let pathToPackageJson;
16
+
17
+ beforeEach(function() {
18
+ const mocks = require('../../test/mocks');
19
+ core = mocks.core();
20
+ core.config = mocks.config();
21
+ core.logger = mocks.logger();
22
+ core.config.server.name = 'zoing';
23
+
24
+ os = {
25
+ type: sinon.stub().returns('Linux'),
26
+ platform: sinon.stub().returns('linux'),
27
+ arch: sinon.stub().returns('x64'),
28
+ release: sinon.stub().returns('1.23.45'),
29
+ hostname: sinon.stub().returns('hostname'),
30
+ };
31
+ path = {
32
+ resolve: sinon.stub().callsFake(() => {
33
+ pathToPackageJson = nodePath.resolve('./test/mocks/package.json');
34
+ return pathToPackageJson;
35
+ }),
36
+ dirname: sinon.stub().callsFake(() => '/path/to'),
37
+ };
38
+ fs = {
39
+ readdirSync: sinon.stub().callsFake(() => ['package.json']),
40
+ statSync: sinon.stub(),
41
+ };
42
+ appInfo = proxyquire(
43
+ './app-info',
44
+ {
45
+ fs,
46
+ os,
47
+ path,
48
+ process: { ...process, version: 'v14.17.5' }
49
+ }
50
+ )(core);
51
+ });
52
+
53
+ it('builds out app data from os and process information', function() {
54
+ expect(appInfo).eql({
55
+ os: {
56
+ type: 'Linux',
57
+ platform: 'linux',
58
+ architecture: 'x64',
59
+ release: '1.23.45'
60
+ },
61
+ app_dir: '/path/to',
62
+ hostname: 'hostname',
63
+ cmd: undefined,
64
+ name: 'project-name',
65
+ pkg: mockPackageJson,
66
+ serverVersion: undefined,
67
+ node_version: 'v14.17.5',
68
+ appPath: '/',
69
+ path: pathToPackageJson,
70
+ indexFile: undefined,
71
+ serverName: 'zoing',
72
+ serverEnvironment: undefined
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ const process = require('process');
4
+ const EVAL_ORIGIN_REGEX = /\((.*?):(\d+):\d+\)/;
5
+
6
+ module.exports = function(core) {
7
+ const { config, isAgentPath } = core;
8
+
9
+ const stacktraceFactory = new StacktraceFactory({
10
+ stackTraceLimit: config.agent.stack_trace_limit,
11
+ isAgentPath
12
+ });
13
+
14
+ core.captureStacktrace = function(...args) {
15
+ return stacktraceFactory.captureStacktrace(...args);
16
+ };
17
+
18
+ return core.captureStacktrace;
19
+ };
20
+
21
+ /**
22
+ * The factory will set stacktrace limit for stackframe lists created by it.
23
+ * @param {number} stackTraceLimit set the stack trace limit
24
+ * @param {function} isAgentPath function indicating if path is agent code
25
+ */
26
+ class StacktraceFactory {
27
+ constructor({ stackTraceLimit, isAgentPath }) {
28
+ this.stackTraceLimit = stackTraceLimit;
29
+ this.isAgentPath = isAgentPath;
30
+ }
31
+
32
+ captureStacktrace(obj, opts, key = 'stack') {
33
+ let stack;
34
+ const snapshot = this.createSnapshot(opts);
35
+ Object.defineProperty(obj, key, {
36
+ enumerable: true,
37
+ configurable: true,
38
+ get() {
39
+ if (!stack) {
40
+ Object.defineProperty(obj, key, {
41
+ configurable: true,
42
+ writable: true,
43
+ enumerable: true,
44
+ value: snapshot()
45
+ });
46
+ }
47
+ return obj[key];
48
+ }
49
+ });
50
+ return obj;
51
+ }
52
+
53
+ /**
54
+ * Creates a function that will build a stacktrace when invoked. It will keep
55
+ * an error in a closure whose stack will be generated and processed when the
56
+ * returned function executes. The result will be cached.
57
+ * @param {object} params
58
+ * @param {function} params.limitFn The constructorOpt param used when creating stack
59
+ * @returns {function}
60
+ */
61
+ createSnapshot({ constructorOpt, prependFrames } = {}) {
62
+ const { isAgentPath } = this;
63
+ const target = {};
64
+
65
+ this.appendStackGetter(target, constructorOpt);
66
+
67
+ let ret;
68
+
69
+ /**
70
+ * Generates array of frames from `target`'s `stack` getter
71
+ * @returns {array}
72
+ */
73
+ function snapshot() {
74
+ if (!ret) {
75
+ const callsites = StacktraceFactory.generateCallsites(target);
76
+ /* eslint-disable complexity */
77
+ ret = (callsites || []).reduce((acc, callsite) => {
78
+ if (StacktraceFactory.isCallsiteValid(callsite)) {
79
+ const frame = StacktraceFactory.makeFrame(callsite);
80
+
81
+ if (
82
+ frame.file &&
83
+ !`${frame.type}${frame.method}`.includes('ContrastMethods')
84
+ ) {
85
+ if (!isAgentPath(frame.file)) {
86
+ acc.push(frame);
87
+ }
88
+ }
89
+ }
90
+
91
+ return acc;
92
+ }, []);
93
+ }
94
+
95
+ return prependFrames ? [...prependFrames, ...ret] : ret;
96
+ }
97
+
98
+ return snapshot;
99
+ }
100
+
101
+ /**
102
+ * Based on stacktrace limit and constructor opt, will append a stack getter
103
+ * @param {} error
104
+ * @param {} limitFn
105
+ */
106
+ appendStackGetter(error, constructorOpt) {
107
+ const { stackTraceLimit } = Error;
108
+ Error.stackTraceLimit = this.stackTraceLimit;
109
+ Error.captureStackTrace(error, constructorOpt);
110
+ Error.stackTraceLimit = stackTraceLimit;
111
+ }
112
+
113
+ static isCallsiteValid(callsite) {
114
+ return callsite instanceof Callsite;
115
+ }
116
+
117
+ /**
118
+ * Creates an array frame objects from an array of Callsite instances
119
+ * @param {} callsite
120
+ * @returns {}
121
+ */
122
+ static makeFrame(callsite) {
123
+ let evalOrigin;
124
+ let file;
125
+ let lineNumber;
126
+
127
+ if (callsite.isEval()) {
128
+ evalOrigin = StacktraceFactory.formatFileName(callsite.getEvalOrigin());
129
+ [, file, lineNumber] = evalOrigin.match(EVAL_ORIGIN_REGEX) || evalOrigin.endsWith('.ejs');
130
+ }
131
+
132
+ file = file || callsite.getFileName();
133
+ lineNumber = lineNumber || callsite.getLineNumber();
134
+
135
+ return {
136
+ eval: evalOrigin,
137
+ file,
138
+ lineNumber,
139
+ method: callsite.getFunctionName(),
140
+ type: callsite.getTypeName()
141
+ };
142
+ }
143
+
144
+ static formatFileName(fileName = '') {
145
+ const cwd = `${process.cwd()}/`;
146
+ const idx = fileName.indexOf(cwd);
147
+
148
+ if (idx > -1) {
149
+ return fileName.replace(cwd, ''); // + 1 to remove /
150
+ }
151
+ return fileName;
152
+ }
153
+
154
+ /**
155
+ * Will access `stack` getter propery on the provided error to generate
156
+ * a stacktrace. We capture the callsite instances passed to `prepareStacktrace`
157
+ * using an ephemeral monkey patch.
158
+ * @param {object} error object with a `stack` getter property
159
+ * @returns {}
160
+ */
161
+ static generateCallsites(error) {
162
+ let callsites;
163
+
164
+ const { prepareStackTrace } = Error;
165
+
166
+ Error.prepareStackTrace = function(_, _callsites) {
167
+ callsites = _callsites;
168
+ return _callsites;
169
+ };
170
+
171
+ // accessing the getter will call `Error.prepareStacktrace`
172
+ error.stack;
173
+
174
+ // restore original method
175
+ Error.prepareStackTrace = prepareStackTrace;
176
+
177
+ return callsites;
178
+ }
179
+ }
180
+
181
+ module.exports.StacktraceFactory = StacktraceFactory;
182
+ module.exports.generateCallsites = StacktraceFactory.generateCallsites;
183
+
184
+ const Callsite = (module.exports.Callsite = StacktraceFactory.generateCallsites(
185
+ new Error()
186
+ )[0].constructor);
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+
5
+ describe('core capture-stactrace', function g() {
6
+ let core;
7
+
8
+ beforeEach(function() {
9
+ const mocks = require('../../test/mocks');
10
+ core = mocks.core();
11
+ core.config = mocks.config();
12
+ require('./capture-stacktrace')(core);
13
+ });
14
+
15
+ it('appends a stack getter property', function() {
16
+ const obj = { foo: 'bar' };
17
+ core.captureStacktrace(obj);
18
+ const desc = Object.getOwnPropertyDescriptor(obj, 'stack');
19
+ expect(typeof desc.get).to.eql('function');
20
+ });
21
+
22
+ it('generates a stacktrace from given options', function() {
23
+ (function outer() {
24
+ (function inner() {
25
+ const obj = {};
26
+ core.captureStacktrace(obj, { constructorOpt: inner });
27
+ expect(obj.stack[0]).to.have.property('method', 'outer');
28
+ expect(obj.stack).to.have.lengthOf(10);
29
+ })();
30
+ })();
31
+ });
32
+ });
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ const Module = require('module');
4
+ const process = require('process');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const util = require('util');
8
+ const builtins = require('builtin-modules');
9
+ const readFile = util.promisify(fs.readFile);
10
+
11
+ module.exports = function(core) {
12
+ const dependencyRewriter = new RecursiveDependencyRewriter(core);
13
+ core.dependencyRewriter = dependencyRewriter;
14
+ return dependencyRewriter;
15
+ };
16
+
17
+ // Ported from node agent lib/cli-rewriter/index.js
18
+ class RecursiveDependencyRewriter {
19
+ /**
20
+ * Receives entrypoint used to initialize agent.
21
+ * Sets up config, logging, agent, and rewriter state.
22
+ * @param {string} entrypoint application's entrypoint script
23
+ */
24
+ constructor(core) {
25
+ this.deps = core;
26
+ this.logger = core.logger;
27
+ this.rewriter = core.rewriter;
28
+ this.rewriter.visitors.push(RecursiveDependencyRewriter.requireDetector);
29
+ // keep track of files rewritten - don't rewrite if file is required more than once
30
+ this.visited = new Set();
31
+ this.entrypoint = core.config.entrypoint;
32
+ this.filename = path.resolve(process.cwd(), this.entrypoint);
33
+ try {
34
+ fs.statSync(this.filename);
35
+ } catch (err) {
36
+ this.logger.error('entrypoint file not found: %s', this.filename);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Starting at the provided entrypoint will rewrite it and any dependencies
43
+ * detected. Recursively continues until all files are rewritten.
44
+ * @param {string} entrypoint provided application entrypoint script
45
+ */
46
+ async rewrite() {
47
+
48
+ const parent = RecursiveDependencyRewriter.getModuleData(this.filename);
49
+ const start = Date.now();
50
+
51
+ await this.traverse(this.filename, parent);
52
+
53
+ this.logger.info('rewriting complete [%ss]', (Date.now() - start) / 1000);
54
+ }
55
+
56
+ /**
57
+ * Visits the filename in order to rewrite and cache. Visitor returns found
58
+ * dependencies in the file. Recursively traverses the absolute file paths of
59
+ * dependencies found by the require detector.
60
+ * @param {string} filename file to rewrite
61
+ * @param {object} parent parent module data
62
+ */
63
+ async traverse(filename, parent) {
64
+ const fileDependencies = await this.visitDependency(filename);
65
+
66
+ return Promise.all(
67
+ fileDependencies.map((request) => {
68
+ try {
69
+ const _filename = Module._resolveFilename(request, parent);
70
+ if (_filename.endsWith('.js')) {
71
+ const _parent = RecursiveDependencyRewriter.getModuleData(_filename);
72
+ return this.traverse(_filename, _parent);
73
+ }
74
+ } catch (err) {
75
+ if (err.code === 'MODULE_NOT_FOUND') {
76
+ this.logger.debug(
77
+ `module not found. skipping ${request} required by ${filename}`
78
+ );
79
+ } else {
80
+ this.logger.error('error resolving filename: %o', err);
81
+ }
82
+ }
83
+ })
84
+ );
85
+ }
86
+
87
+ /**
88
+ * For each file dependency rewrite and cache it. Returns array of found
89
+ * dependencies.
90
+ * @param {string} filename name of file to rewrite / cache
91
+ * @returns {array[string]} list of the file's dependencies
92
+ */
93
+ async visitDependency(filename) {
94
+ if (this.visited.has(filename)) {
95
+ return [];
96
+ }
97
+
98
+ this.visited.add(filename);
99
+
100
+ try {
101
+ const content = await readFile(filename, 'utf8');
102
+ const rewriteData = this.rewriter.rewriteFile({
103
+ content,
104
+ filename,
105
+ opts: {
106
+ inject: true,
107
+ sourceType: 'script',
108
+ wrap: true,
109
+ deps: []
110
+ }
111
+ });
112
+
113
+ if (!rewriteData.deps) {
114
+ return [];
115
+ }
116
+
117
+ return rewriteData.deps.filter((dep) => !builtins.includes(dep));
118
+ } catch (e) {
119
+ console.log('visit error\n', e);
120
+ return;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Gets module data in order to resolve abs paths of deps.
126
+ * @param {string} filename absolute path of file being rewritten
127
+ * @returns {object}
128
+ */
129
+ static getModuleData(filename) {
130
+ return {
131
+ id: filename,
132
+ filename,
133
+ paths: Module._nodeModulePaths(path.dirname(filename))
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Added to agent's static visitors. Runs during rewriting to detect require
139
+ * calls to discover dependencies.
140
+ * @param {object} node AST node being visited during rewrite
141
+ * @param {string} filename file being rewritten
142
+ * @param {object} state rewriter state
143
+ */
144
+ static requireDetector(path, state) {
145
+ const { node } = path;
146
+ if (isRequire(node)) {
147
+ if (isLiteralRequire(node) || isStaticTemplateRequire(node)) {
148
+ const request = getRequireArg(node);
149
+ if (request) {
150
+ state.deps.push(request);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Whether AST node looks like a `require([request])` call.
159
+ * @param {object} node AST node
160
+ * @returns {boolean}
161
+ */
162
+ function isRequire(node) {
163
+ return (
164
+ node.type === 'CallExpression' &&
165
+ node.callee &&
166
+ node.callee.type === 'Identifier' &&
167
+ node.callee.name === 'require' &&
168
+ node.arguments &&
169
+ node.arguments.length === 1
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Whether node is static template require
175
+ * @param {object} node AST node
176
+ * @returns {boolean}
177
+ */
178
+ function isStaticTemplateRequire(node) {
179
+ return (
180
+ node.arguments[0].type === 'TemplateLiteral' &&
181
+ node.arguments[0].expressions.length === 0
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Whether node is literal require
187
+ * @param {object} node AST node
188
+ * @returns {boolean}
189
+ */
190
+ function isLiteralRequire(node) {
191
+ return (
192
+ node.arguments[0].type === 'Literal' ||
193
+ node.arguments[0].type === 'StringLiteral'
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Gets value of argument to `require([request])`
199
+ * @param {object} node AST node
200
+ * @returns {string}
201
+ */
202
+ function getRequireArg(node) {
203
+ return node.arguments[0].type === 'TemplateLiteral'
204
+ ? node.arguments[0].quasis[0].value.raw
205
+ : node.arguments[0].value;
206
+ }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ module.exports = function(deps) {
4
+ const cliRewriter = deps.cliRewriter = async function (preHook) {
5
+ // fix this - config doesn't resolve this properly for this cli use case
6
+ deps.config.entrypoint = process.argv[2];
7
+
8
+ require('./dependency-rewriter')(deps);
9
+
10
+ /*
11
+ * Callers just need to set up their rewriter policy e.g.
12
+ * deps.depHooks.rewriting.install()
13
+ * deps.assess.rewriting.install()
14
+ */
15
+ preHook(deps);
16
+
17
+ // once clients configure their stuff
18
+ await deps.dependencyRewriter.rewrite();
19
+
20
+ deps.logger.info('done');
21
+ };
22
+
23
+ return cliRewriter;
24
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { Agentify } from '@contrast/agentify';
2
+ import { Config } from '@contrast/config';
3
+ import { Logger } from '@contrast/logger';
4
+ import { Messages } from '@contrast/common';
5
+ import { Patcher } from '@contrast/patcher';
6
+ import { ReporterBus } from '@contrast/reporter';
7
+ import { Rewriter } from '@contrast/rewriter';
8
+ import RequireHook from '@contrast/require-hook';
9
+ import { Scopes } from '@contrast/scopes';
10
+
11
+ export interface AppInfo {
12
+ os: {
13
+ type: string;
14
+ platform: string;
15
+ architecture: string;
16
+ release: string;
17
+ },
18
+ hostname: string;
19
+ version: string;
20
+ name: string;
21
+ pkg: object; // package.json
22
+ agentVersion: string;
23
+ app_dir: string;
24
+ serverVersion: string;
25
+ node_version: string;
26
+ appPath: string;
27
+ indexFile: string;
28
+ serverName: string;
29
+ serverEnvironment: string;
30
+ }
31
+
32
+ export interface Core {
33
+ agentify: Agentify<Core>;
34
+ config: Config;
35
+ depHooks: RequireHook;
36
+ appInfo: AppInfo;
37
+ logger: Logger;
38
+ messages: Messages;
39
+ patcher: Patcher;
40
+ reporter: ReporterBus;
41
+ protect: Protect;
42
+ rewriter: Rewriter;
43
+ scopes: Scopes;
44
+ }
45
+
46
+ declare function init(): Core;
47
+
48
+ export = init;
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+
5
+ module.exports = function init(core = {}) {
6
+ core.messages = new EventEmitter();
7
+
8
+ require('@contrast/config')(core);
9
+ require('@contrast/logger').default(core);
10
+ require('./app-info')(core);
11
+ require('./is-agent-path')(core);
12
+ require('./capture-stacktrace')(core);
13
+ require('./cli-rewriter')(core);
14
+ require('@contrast/patcher')(core);
15
+ require('@contrast/rewriter')(core);
16
+ require('@contrast/dep-hooks')(core);
17
+ require('@contrast/scopes')(core);
18
+ require('@contrast/reporter').default(core);
19
+ require('@contrast/agentify')(core);
20
+
21
+ return core;
22
+ };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ describe.skip('core', function () {});
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ */
5
+ module.exports = function(core) {
6
+ const { config } = core;
7
+
8
+ /**
9
+ */
10
+ function isAgentPath(path) {
11
+ for (const seg of config.agent.stack_trace_filters) {
12
+ if (path.indexOf(seg) > -1) {
13
+ return true;
14
+ }
15
+ }
16
+ return false;
17
+ }
18
+
19
+ core.isAgentPath = isAgentPath;
20
+
21
+ return isAgentPath;
22
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@contrast/core",
3
+ "version": "1.0.0",
4
+ "description": "Preconfigured Contrast agent core services and models",
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
+ "@contrast/agentify": "1.0.0",
21
+ "@contrast/config": "1.0.0",
22
+ "@contrast/dep-hooks": "1.0.0",
23
+ "@contrast/logger": "1.0.0",
24
+ "@contrast/patcher": "1.0.0",
25
+ "@contrast/reporter": "1.0.0",
26
+ "@contrast/rewriter": "1.0.0",
27
+ "@contrast/scopes": "1.0.0",
28
+ "builtin-modules": "^3.2.0",
29
+ "semver": "^7.3.7"
30
+ }
31
+ }