@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 +12 -0
- package/README.md +98 -0
- package/lib/app-info.js +189 -0
- package/lib/app-info.test.js +75 -0
- package/lib/capture-stacktrace.js +186 -0
- package/lib/capture-stacktrace.test.js +32 -0
- package/lib/cli-rewriter/dependency-rewriter.js +206 -0
- package/lib/cli-rewriter/index.js +24 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.js +22 -0
- package/lib/index.test.js +3 -0
- package/lib/is-agent-path.js +22 -0
- package/package.json +31 -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,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>
|
package/lib/app-info.js
ADDED
|
@@ -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,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
|
+
}
|