vuln-regex-detector
Advanced tools
Comparing version 1.0.1 to 1.1.0
# v1 | ||
## v1.1 | ||
### v1.1.0 | ||
*Additions* | ||
- Add a synchronous API for use in an eslint plugin. | ||
- Add an in-memory cache in case the same regex is repeated during a Node process's lifetime. | ||
Contributors: | ||
- [Jamie Davis](davisjam@vt.edu) | ||
## v1.0 | ||
### v1.0.1 | ||
*Additions* | ||
- Misc. bugfixes | ||
- Enhance test suites | ||
Contributors: | ||
- [Jamie Davis](davisjam@vt.edu) | ||
### v1.0.0 | ||
@@ -6,0 +28,0 @@ |
{ | ||
"name": "vuln-regex-detector", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "Detect vulnerable regexes by querying a service hosted at Virginia Tech.", | ||
@@ -10,5 +10,6 @@ "main": "vuln-regex-detector-client.js", | ||
"dependencies": { | ||
"sync-request": "^6.0.0" | ||
}, | ||
"scripts": { | ||
"test": "mocha", | ||
"test": "npm run lint && mocha", | ||
"lint": "eslint vuln-regex-detector-client.js test/*" | ||
@@ -15,0 +16,0 @@ }, |
@@ -29,3 +29,3 @@ # Summary | ||
The module exports: | ||
- a function `test` | ||
- functions `test` and `testSync` | ||
- a set of responses `responses` | ||
@@ -41,3 +41,3 @@ | ||
* | ||
* returns a Promise fulfilled with a response or rejected with RESPONSE_INVALID or an error. | ||
* returns a Promise fulfilled with a vulnerable/safe/unknown or rejected with invalid. | ||
*/ | ||
@@ -47,5 +47,20 @@ vulnRegexDetector.test (regex, config) | ||
## testSync | ||
```javascript | ||
/** | ||
* @regex: RegExp or string (e.g. /re/ or 're') | ||
* @config: object with fields: hostname port | ||
* default: 'toybox.cs.vt.edu', '8000' | ||
* | ||
* returns with vulnerable/safe/unknown/invalid. | ||
*/ | ||
vulnRegexDetector.testSync (regex, config) | ||
``` | ||
NB: This API makes synchronous HTTP queries, which can be slow. You should probably not use it. | ||
## responses | ||
If fulfilled, the returned Promise takes on one of the following values: | ||
If fulfilled, the returned Promise gets one of the following values: | ||
- `responses.vulnerable` | ||
@@ -55,3 +70,3 @@ - `responses.safe` | ||
If rejected, the returned Promise might be: | ||
If rejected, the returned Promise gets the value: | ||
- `responses.invalid` | ||
@@ -64,5 +79,7 @@ | ||
If the regex has not been seen before, the server will respond "UNKNOWN" and test it in the background. | ||
If the regex has not been seen before, the server will respond "unknown" and test it in the background. | ||
The server cannot test synchronously because testing is expensive (potentially minutes) and there might be a long line. | ||
If the server has not seen the regex before, it should have an answer if you query it again in a few minutes. | ||
## Privacy | ||
@@ -69,0 +86,0 @@ |
176
test/test.js
@@ -10,3 +10,2 @@ /* eslint-env node, mocha */ | ||
vulnRegexDetector.responses.unknown; | ||
console.log(`outcome: ${outcome} isOK ${isOK}`); | ||
assert.ok(isOK, `outcome ${outcome} should be safe|unknown`); | ||
@@ -19,3 +18,2 @@ } | ||
outcome === vulnRegexDetector.responses.unknown; | ||
console.log(`outcome: ${outcome} isOK ${isOK}`); | ||
assert.ok(isOK, `outcome ${outcome} should be vulnerable|unknown`); | ||
@@ -29,3 +27,2 @@ } | ||
outcome === vulnRegexDetector.responses.unknown; | ||
console.log(`outcome: ${outcome} isOK ${isOK}`); | ||
assert.ok(isOK, `outcome ${outcome} should be vulnerable|safe|unknown`); | ||
@@ -37,3 +34,2 @@ } | ||
const isOK = outcome === vulnRegexDetector.responses.invalid; | ||
console.log(`outcome: ${outcome} isOK ${isOK}`); | ||
assert.ok(isOK, `outcome ${outcome} should be invalid`); | ||
@@ -43,55 +39,161 @@ } | ||
describe('vulnRegexDetector', () => { | ||
describe('input format', () => { | ||
it('should accept regexes as strings', () => { | ||
return vulnRegexDetector.test('abc') | ||
.then(assertIsOK, assertIsOK); | ||
describe('checkRegex', () => { | ||
describe('input format', () => { | ||
it('should accept regexes as strings', () => { | ||
return vulnRegexDetector.test('abc') | ||
.then(assertIsOK, assertIsOK); | ||
}); | ||
it('should accept regexes as RegExps', () => { | ||
return vulnRegexDetector.test(/abc/) | ||
.then(assertIsOK, assertIsOK); | ||
}); | ||
it('should reject an undefined regex', () => { | ||
return vulnRegexDetector.test(undefined) | ||
.then(assertIsInvalid, assertIsInvalid); | ||
}); | ||
it('should reject a random object', () => { | ||
return vulnRegexDetector.test({foo: 1}) | ||
.then(assertIsInvalid, assertIsInvalid); | ||
}); | ||
it('should accept config', () => { | ||
return vulnRegexDetector.test('abc', { hostname: 'toybox.cs.vt.edu', port: 8000 }) | ||
.then(assertIsOK, assertIsOK); | ||
}); | ||
}); | ||
it('should accept regexes as RegExps', () => { | ||
return vulnRegexDetector.test(/abc/) | ||
.then(assertIsOK, assertIsOK); | ||
describe('outcome validity', () => { | ||
it('should label safe as such: simple', () => { | ||
return vulnRegexDetector.test('abc') | ||
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown); | ||
}); | ||
it('should label safe as such: non-vulnerable star height', () => { | ||
return vulnRegexDetector.test('(ab+)+$') | ||
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown); | ||
}); | ||
it('should label vulnerable as such: star height', () => { | ||
return vulnRegexDetector.test('(a+)+$') | ||
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown); | ||
}); | ||
it('should label vulnerable as such: QOD', () => { | ||
return vulnRegexDetector.test(/(\d|\w)+$/) | ||
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown); | ||
}); | ||
it('should label vulnerable as such: QOA', () => { | ||
return vulnRegexDetector.test(/.*a.*a.*a.*a$/) | ||
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown); | ||
}); | ||
}); | ||
it('should reject an undefined regex', () => { | ||
return vulnRegexDetector.test(undefined) | ||
.then(assertIsInvalid, assertIsInvalid); | ||
describe('invalid config', () => { | ||
it('should reject an invalid host', () => { | ||
return vulnRegexDetector.test('abcde', { hostname: 'no such host', port: 8000 }) | ||
.then((response) => { | ||
assert.ok(false, `Invalid config should not have resolved (with ${response})`); | ||
}, (err) => { | ||
assert.ok(err === vulnRegexDetector.responses.invalid, `Invalid config rejected, but with ${err}`); | ||
}); | ||
}); | ||
it('should reject an invalid port', () => { | ||
return vulnRegexDetector.test('abcde', { hostname: 'toybox.cs.vt.edu', port: 22 }) | ||
.then((response) => { | ||
assert.ok(false, `Invalid config should not have resolved (with ${response})`); | ||
}, (err) => { | ||
assert.ok(err === vulnRegexDetector.responses.invalid, `Invalid config rejected, but with ${err}`); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('should reject a random object', () => { | ||
return vulnRegexDetector.test({foo: 1}) | ||
.then(assertIsInvalid, assertIsInvalid); | ||
describe('checkRegexSync', () => { | ||
describe('input format', () => { | ||
it('should accept regexes as strings', () => { | ||
assertIsOK(vulnRegexDetector.testSync('abc')); | ||
}); | ||
it('should accept regexes as RegExps', () => { | ||
assertIsOK(vulnRegexDetector.testSync(/abc/)); | ||
}); | ||
it('should reject an undefined regex', () => { | ||
assertIsInvalid(vulnRegexDetector.testSync(undefined)); | ||
}); | ||
it('should reject a random object', () => { | ||
assertIsInvalid(vulnRegexDetector.testSync({foo: 1})); | ||
}); | ||
it('should accept config', () => { | ||
assertIsOK(vulnRegexDetector.testSync('abc', { hostname: 'toybox.cs.vt.edu', port: 8000 })); | ||
}); | ||
}); | ||
it('should accept config', () => { | ||
return vulnRegexDetector.test('abc', { hostname: 'toybox.cs.vt.edu', port: 8000 }) | ||
.then(assertIsOK, assertIsOK); | ||
describe('outcome validity', () => { | ||
it('should label safe as such: simple', () => { | ||
assertIsSafeOrUnknown(vulnRegexDetector.testSync('abc')); | ||
}); | ||
it('should label safe as such: non-vulnerable star height', () => { | ||
assertIsSafeOrUnknown(vulnRegexDetector.testSync('(ab+)+$')); | ||
}); | ||
it('should label vulnerable as such: star height', () => { | ||
assertIsVulnerableOrUnknown(vulnRegexDetector.testSync('(a+)+$')); | ||
}); | ||
it('should label vulnerable as such: QOD', () => { | ||
assertIsVulnerableOrUnknown(vulnRegexDetector.testSync(/(\d|\w)+$/)); | ||
}); | ||
it('should label vulnerable as such: QOA', () => { | ||
assertIsVulnerableOrUnknown(vulnRegexDetector.testSync(/.*a.*a.*a.*a$/)); | ||
}); | ||
}); | ||
}); | ||
describe('outcome validity', () => { | ||
it('should label safe as such: simple', () => { | ||
return vulnRegexDetector.test('abc') | ||
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown); | ||
describe('invalid config', () => { | ||
it('should reject an invalid host', () => { | ||
const response = vulnRegexDetector.testSync('abcde', { hostname: 'no such host', port: 8000 }); | ||
assert.ok(response === vulnRegexDetector.responses.invalid, `Invalid config returned ${response}`); | ||
}); | ||
it('should reject an invalid port', () => { | ||
const response = vulnRegexDetector.testSync('abcde', { hostname: 'toybox.cs.vt.edu', port: 22 }); | ||
assert.ok(response === vulnRegexDetector.responses.invalid, `Invalid config returned ${response}`); | ||
}); | ||
}); | ||
it('should label safe as such: non-vulnerable star height', () => { | ||
return vulnRegexDetector.test('(ab+)+$') | ||
.then(assertIsSafeOrUnknown, assertIsSafeOrUnknown); | ||
describe('cache', () => { | ||
it('should hit cache on successive duplicate queries', () => { | ||
for (let i = 0; i < 10; i++) { | ||
assertIsSafeOrUnknown(vulnRegexDetector.testSync('abc')); | ||
} | ||
}); | ||
}); | ||
}); | ||
it('should label vulnerable as such: star height', () => { | ||
return vulnRegexDetector.test('(a+)+$') | ||
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown); | ||
describe('responses', () => { | ||
it('has vulnerable', () => { | ||
return assert.ok(vulnRegexDetector.responses.vulnerable, 'Missing vulnerable'); | ||
}); | ||
it('should label vulnerable as such: QOD', () => { | ||
return vulnRegexDetector.test(/(\d|\w)+$/) | ||
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown); | ||
it('has safe', () => { | ||
return assert.ok(vulnRegexDetector.responses.safe, 'Missing safe'); | ||
}); | ||
it('should label vulnerable as such: QOA', () => { | ||
return vulnRegexDetector.test(/.*a.*a.*a.*a$/) | ||
.then(assertIsVulnerableOrUnknown, assertIsVulnerableOrUnknown); | ||
it('has unknown', () => { | ||
return assert.ok(vulnRegexDetector.responses.unknown, 'Missing unknown'); | ||
}); | ||
it('has invalid', () => { | ||
return assert.ok(vulnRegexDetector.responses.invalid, 'Missing invalid'); | ||
}); | ||
}); | ||
}); |
@@ -5,2 +5,3 @@ 'use strict'; | ||
const https = require('https'); | ||
const syncRequest = require('sync-request'); | ||
@@ -20,10 +21,15 @@ /* Globals. */ | ||
const LOGGING = true; | ||
const LOGGING = false; | ||
const USE_CACHE = true; | ||
/* Map pattern to RESPONSE_VULNERABLE or RESPONSE_SAFE in case of duplicate queries. | ||
* We do not cache RESPONSE_UNKNOWN or RESPONSE_INVALID responses since these might change. */ | ||
let patternCache = {}; | ||
/** | ||
* @regex: RegExp or string (e.g. /re/ or 're') | ||
* @config: object with fields: hostname port | ||
* @param regex: RegExp or string (e.g. /re/ or 're') | ||
* @param config: object with fields: hostname port | ||
* default: 'toybox.cs.vt.edu', '8000' | ||
* | ||
* returns a Promise fulfilled with a response or rejected with RESPONSE_INVALID or an error. | ||
* returns a Promise fulfilled with a response or rejected with RESPONSE_INVALID. | ||
*/ | ||
@@ -34,4 +40,147 @@ function checkRegex (regex, config) { | ||
/* Validate args. */ | ||
// regex | ||
/* Handle args. */ | ||
try { | ||
[_pattern, _config] = handleArgs(regex, config); | ||
} catch (e) { | ||
return Promise.reject(RESPONSE_INVALID); | ||
} | ||
log(`Input OK. _pattern /${_pattern}/ _config ${JSON.stringify(_config)}`); | ||
let postObject = generatePostObject(_pattern); | ||
let postBuffer = JSON.stringify(postObject); | ||
let postHeaders = generatePostHeaders(_config, Buffer.byteLength(postBuffer)); | ||
// Wrapper so we can return a Promise. | ||
function promiseResult (options, data) { | ||
log(`promiseResult: data ${data}`); | ||
return new Promise((resolve, reject) => { | ||
if (USE_CACHE) { | ||
/* Check cache to avoid I/O. */ | ||
const cacheHit = checkCache(_pattern); | ||
if (cacheHit !== RESPONSE_UNKNOWN) { | ||
log(`Cache hit: ${cacheHit}`); | ||
return resolve(cacheHit); | ||
} | ||
} | ||
const req = https.request(options, (res) => { | ||
res.setEncoding('utf8'); | ||
let response = ''; | ||
res.on('data', (chunk) => { | ||
log(`Got data`); | ||
response += chunk; | ||
}); | ||
res.on('end', () => { | ||
log(`end: I got ${JSON.stringify(response)}`); | ||
const result = serverResponseToRESPONSE(response); | ||
log(`end: result ${result}`); | ||
if (USE_CACHE) { | ||
updateCache(postObject.pattern, result); | ||
} | ||
if (result === RESPONSE_INVALID) { | ||
return reject(result); | ||
} else { | ||
return resolve(result); | ||
} | ||
}); | ||
}); | ||
req.on('error', (e) => { | ||
log(`Error: ${e}`); | ||
return reject(RESPONSE_INVALID); | ||
}); | ||
// Write data to request body. | ||
log(`Writing to req:\n${data}`); | ||
req.write(data); | ||
req.end(); | ||
}); | ||
} | ||
return promiseResult(postHeaders, postBuffer); | ||
} | ||
/** | ||
* @param regex: RegExp or string (e.g. /re/ or 're') | ||
* @param config: object with fields: hostname port | ||
* default: 'toybox.cs.vt.edu', '8000' | ||
* | ||
* returns synchronous result: RESPONSE_X | ||
* | ||
* Since this makes a synchronous HTTP query it will be slow. | ||
*/ | ||
function checkRegexSync (regex, config) { | ||
let _pattern; | ||
let _config; | ||
/* Handle args. */ | ||
try { | ||
[_pattern, _config] = handleArgs(regex, config); | ||
} catch (e) { | ||
return RESPONSE_INVALID; | ||
} | ||
log(`Input OK. _pattern /${_pattern}/ _config ${JSON.stringify(_config)}`); | ||
if (USE_CACHE) { | ||
/* Check cache to avoid I/O. */ | ||
const cacheHit = checkCache(_pattern); | ||
if (cacheHit !== RESPONSE_UNKNOWN) { | ||
log(`Cache hit: ${cacheHit}`); | ||
return cacheHit; | ||
} | ||
} | ||
let postObject = generatePostObject(_pattern); | ||
let postBuffer = JSON.stringify(postObject); | ||
let postHeaders = generatePostHeaders(_config, Buffer.byteLength(postBuffer)); | ||
let url = `https://${postHeaders.hostname}:${postHeaders.port}${postHeaders.path}`; | ||
try { | ||
log(`sending syncRequest: method ${postHeaders.method} url ${url} headers ${JSON.stringify(postHeaders.headers)} body ${postBuffer}`); | ||
/* Send request. */ | ||
const response = syncRequest(postHeaders.method, url, { | ||
headers: postHeaders.headers, | ||
body: postBuffer | ||
}); | ||
/* Extract body as JSON. */ | ||
let responseBody; | ||
try { | ||
responseBody = response.getBody('utf8'); | ||
} catch (e) { | ||
log(`checkRegexSync: Unparseable response ${JSON.stringify(response)}`); | ||
return RESPONSE_INVALID; | ||
} | ||
log(`checkRegexSync: I got ${responseBody}`); | ||
/* Convert to a RESPONSE_X value. */ | ||
const result = serverResponseToRESPONSE(responseBody); | ||
if (USE_CACHE) { | ||
updateCache(postObject.pattern, result); | ||
} | ||
return result; | ||
} catch (e) { | ||
log(`syncRequest threw: ${JSON.stringify(e)}`); | ||
return RESPONSE_INVALID; | ||
} | ||
} | ||
/********** | ||
* Helpers. | ||
**********/ | ||
/** | ||
* @param regex: Input to checkRegex, etc. | ||
* @param config: Input to checkRegex, etc. | ||
* | ||
* Returns: [pattern, config] or throws exception | ||
*/ | ||
function handleArgs (regex, config) { | ||
let _pattern; | ||
if (regex) { | ||
@@ -52,6 +201,8 @@ if (typeof regex === 'string') { | ||
if (!_pattern) { | ||
return Promise.reject(RESPONSE_INVALID); | ||
let errObj = { msg: 'Invalid args' }; | ||
throw errObj; | ||
} | ||
// config | ||
let _config; | ||
if (config && config.hasOwnProperty('hostname') && config.hasOwnProperty('port')) { | ||
@@ -63,15 +214,21 @@ _config = config; | ||
log(`Input OK. _pattern /${_pattern}/ _config ${JSON.stringify(_config)}`); | ||
return [_pattern, _config]; | ||
} | ||
// Prep POST request. | ||
const postObj = { | ||
pattern: _pattern, | ||
/* Return object to be sent over the wire as JSON. */ | ||
function generatePostObject (pattern) { | ||
const postObject = { | ||
pattern: pattern, | ||
language: 'javascript', | ||
requestType: REQUEST_LOOKUP_ONLY | ||
}; | ||
const postData = JSON.stringify(postObj); | ||
const postOptions = { | ||
hostname: _config.hostname, | ||
port: _config.port, | ||
return postObject; | ||
} | ||
/* Return headers for the POST request. */ | ||
function generatePostHeaders (config, payloadSize) { | ||
const postHeaders = { | ||
hostname: config.hostname, | ||
port: config.port, | ||
path: '/api/lookup', | ||
@@ -81,53 +238,61 @@ method: 'POST', | ||
'Content-Type': 'application/json', | ||
'Content-Length': Buffer.byteLength(postData) | ||
'Content-Length': payloadSize | ||
} | ||
}; | ||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // TODO. | ||
return postHeaders; | ||
} | ||
// Wrapper so we can return a Promise. | ||
function promiseResult (options, data) { | ||
log(`promiseResult: data ${JSON.stringify(data)}`); | ||
return new Promise(function (resolve, reject) { | ||
const req = https.request(options, (res) => { | ||
log(`Hello in res`); | ||
res.setEncoding('utf8'); | ||
/* response: raw response from server */ | ||
function serverResponseToRESPONSE (response) { | ||
try { | ||
const obj = JSON.parse(response); | ||
if (obj.result === RESPONSE_UNKNOWN) { | ||
return RESPONSE_UNKNOWN; | ||
} else { | ||
return obj.result.result; | ||
} | ||
} catch (e) { | ||
return RESPONSE_INVALID; | ||
} | ||
} | ||
let response = ''; | ||
res.on('data', (chunk) => { | ||
log(`Got data`); | ||
response += chunk; | ||
}); | ||
/********** | ||
* Cache. | ||
**********/ | ||
res.on('end', () => { | ||
const fullResponse = JSON.parse(response); | ||
log(`end: I got ${JSON.stringify(fullResponse)}`); | ||
function updateCache (pattern, response) { | ||
if (!USE_CACHE) { | ||
return; | ||
} | ||
let resolveWith; | ||
if (fullResponse.result === RESPONSE_UNKNOWN) { | ||
resolveWith = RESPONSE_UNKNOWN; | ||
} else { | ||
resolveWith = fullResponse.result.result; | ||
} | ||
log(`end: resolving with ${resolveWith}`); | ||
resolve(resolveWith); | ||
}); | ||
}); | ||
/* Only cache VULNERABLE|SAFE responses. */ | ||
if (response !== RESPONSE_VULNERABLE && response !== RESPONSE_SAFE) { | ||
return; | ||
} | ||
req.on('error', (e) => { | ||
log(`Error: ${e}`); | ||
reject(e); | ||
}); | ||
if (!patternCache.hasOwnProperty(pattern)) { | ||
patternCache[pattern] = response; | ||
} | ||
} | ||
// Write data to request body. | ||
log(`Writing to req:\n${data}`); | ||
req.write(data); | ||
req.end(); | ||
}); | ||
/* Returns RESPONSE_{VULNERABLE|SAFE} on hit, else RESPONSE_UNKNOWN. */ | ||
function checkCache (pattern) { | ||
if (!USE_CACHE) { | ||
return RESPONSE_UNKNOWN; | ||
} | ||
return promiseResult(postOptions, postData); | ||
const hit = patternCache[pattern]; | ||
if (hit) { | ||
log(`checkCache: pattern ${pattern}: hit in patternCache\n ${JSON.stringify(patternCache)}`); | ||
return hit; | ||
} else { | ||
return RESPONSE_UNKNOWN; | ||
} | ||
} | ||
/* Helpers. */ | ||
/********** | ||
* Utilities. | ||
**********/ | ||
function log (msg) { | ||
@@ -139,6 +304,9 @@ if (LOGGING) { | ||
/* Public. */ | ||
/********** | ||
* Exports. | ||
**********/ | ||
module.exports = { | ||
test: checkRegex, | ||
testSync: checkRegexSync, | ||
responses: { | ||
@@ -145,0 +313,0 @@ vulnerable: RESPONSE_VULNERABLE, |
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
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
20012
437
107
0
1
2
+ Addedsync-request@^6.0.0
+ Added@types/concat-stream@1.6.1(transitive)
+ Added@types/form-data@0.0.33(transitive)
+ Added@types/node@10.17.608.10.66(transitive)
+ Added@types/qs@6.9.16(transitive)
+ Addedasap@2.0.6(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedbuffer-from@1.1.2(transitive)
+ Addedcall-bind@1.0.7(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedconcat-stream@1.6.2(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addeddefine-data-property@1.1.4(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedes-define-property@1.0.0(transitive)
+ Addedes-errors@1.3.0(transitive)
+ Addedform-data@2.5.2(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedget-intrinsic@1.2.4(transitive)
+ Addedget-port@3.2.0(transitive)
+ Addedgopd@1.0.1(transitive)
+ Addedhas-property-descriptors@1.0.2(transitive)
+ Addedhas-proto@1.0.3(transitive)
+ Addedhas-symbols@1.0.3(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedhttp-basic@8.1.3(transitive)
+ Addedhttp-response-object@3.0.2(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@1.0.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedobject-inspect@1.13.2(transitive)
+ Addedparse-cache-control@1.0.1(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedpromise@8.3.0(transitive)
+ Addedqs@6.13.0(transitive)
+ Addedreadable-stream@2.3.8(transitive)
+ Addedsafe-buffer@5.1.25.2.1(transitive)
+ Addedset-function-length@1.2.2(transitive)
+ Addedside-channel@1.0.6(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedsync-request@6.1.0(transitive)
+ Addedsync-rpc@1.3.6(transitive)
+ Addedthen-request@6.0.2(transitive)
+ Addedtypedarray@0.0.6(transitive)
+ Addedutil-deprecate@1.0.2(transitive)