@contrast/library-analysis
Advanced tools
Comparing version 1.15.0 to 1.15.1
/* | ||
* Copyright: 2023 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -4,0 +4,0 @@ * License: Commercial |
/* | ||
* Copyright: 2023 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -142,3 +142,3 @@ * License: Commercial | ||
if (!nodeModulesPath) { | ||
logger.warn('Unable to determine the location of node_modules, aborting library analysis. Ensure `agent.node.app_root` is set to the directory containing your node_modules folder.'); | ||
logger.warn('Unable to determine the location of the `node_modules` directory; aborting library analysis. Ensure the `agent.node.app_root` configuration variable is set to the directory containing your `node_modules` folder.'); | ||
return; | ||
@@ -149,2 +149,3 @@ } | ||
const npmData = await listInstalled( | ||
config.agent.node.npm_path, | ||
nodeModulesPath, | ||
@@ -151,0 +152,0 @@ logger, |
/* | ||
* Copyright: 2023 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -15,2 +15,3 @@ * License: Commercial | ||
*/ | ||
// @ts-check | ||
'use strict'; | ||
@@ -36,2 +37,3 @@ | ||
/** | ||
* @param {string} npm the path to the npm executable | ||
* @param {string} cwd directory in which we want to execute `npm ls` | ||
@@ -42,3 +44,3 @@ * @param {import('@contrast/logger').Logger} logger | ||
*/ | ||
module.exports = async function listInstalled(cwd, logger, npmVersionRange) { | ||
module.exports = async function listInstalled(npm, cwd, logger, npmVersionRange) { | ||
const execFileOpts = { | ||
@@ -50,18 +52,17 @@ cwd, | ||
}; | ||
let stdout; | ||
let version, location, cause; | ||
try { | ||
const result = await execFile('npm', ['help'], execFileOpts); | ||
stdout = result.stdout; | ||
const result = await execFile(npm, ['help'], execFileOpts); | ||
[, version, location] = result.stdout.match(VERSION_REGEX) || []; | ||
} catch (err) { | ||
logger.trace({ err }, '`npm help` returned an error'); | ||
// If npm encounters any errors whatsoever it will return with a non-zero | ||
// exit code but still output the relevant information to stdout. | ||
// If an even worse error occurs, we may not be able to parse stdout. | ||
stdout = err.stdout ?? '{}'; | ||
cause = err; | ||
} | ||
const [, version, location] = stdout.match(VERSION_REGEX) || []; | ||
if (!version) { | ||
throw new Error("Unable to locate `npm`. `npm` is required for your application's libraries to be reported to Contrast for analysis. Please enable debug or trace level logs for more information."); | ||
throw new Error( | ||
"Unable to locate an `npm` executable. `npm` is required for your application's libraries to be reported to Contrast for analysis. You can use the `agent.node.npm_path` configuration variable to specify the path to the npm executable if it is not correctly detected by the agent.", | ||
// @ts-expect-error error.cause added in 16.9.0 | ||
{ cause } | ||
); | ||
} | ||
@@ -80,19 +81,29 @@ | ||
const lsArgs = ['ls', '--json', '--long']; | ||
// This will be needs to be updated once node 14 is no longer LTS | ||
if (semver.gte(version, '8.0.0')) lsArgs.push('--all'); | ||
if (semver.gte(version, '7.0.0')) lsArgs.push('--all'); | ||
let list; | ||
try { | ||
const result = await execFile('npm', lsArgs, execFileOpts); | ||
stdout = result.stdout; | ||
const result = await execFile(npm, lsArgs, execFileOpts); | ||
list = result.stdout; | ||
} catch (err) { | ||
logger.trace({ err }, '`npm ls` returned an error'); | ||
stdout = err.stdout ?? '{}'; | ||
// If npm encounters any errors whatsoever it will return with a non-zero | ||
// exit code but still output the relevant information to stdout. | ||
// If an even worse error occurs, we may not be able to parse stdout. | ||
list = err.stdout ?? '{}'; | ||
// Since stdout includes the entire JSON object describing npm dependencies, | ||
// it makes the log entry enormous. We omit that content and log the rest of | ||
// the error here. | ||
delete err.stdout; | ||
logger.debug({ err }, 'error occured when executing `npm ls`'); | ||
} | ||
try { | ||
return JSON.parse(stdout); | ||
return JSON.parse(list); | ||
} catch (err) { | ||
logger.trace({ err }, 'parsing the output of `npm ls` failed'); | ||
throw new Error('`npm ls` failed to provide a list of installed dependencies. Please enable trace level logs for more information.'); | ||
throw new Error( | ||
'`npm ls` failed to provide a valid list of installed dependencies.', | ||
// @ts-expect-error error.cause added in 16.9.0 | ||
{ cause: err }, | ||
); | ||
} | ||
}; |
/* | ||
* Copyright: 2023 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -18,5 +18,4 @@ * License: Commercial | ||
const path = require('path'); | ||
const semver = require('semver'); | ||
const { Event, substring, substr, replace, split } = require('@contrast/common'); | ||
const { setCodeEventListener } = require('@contrast/fn-inspect'); | ||
const { setCodeEventListener } = require('@contrast/code-events'); | ||
const { buildLibraryHash } = require('../../util'); | ||
@@ -29,3 +28,2 @@ | ||
const libInfoMap = libraryUsage.libInfoMap = new Map(); | ||
const type = semver.gte(process.version, '20.0.0') ? 'Function' : 'LazyCompile'; | ||
@@ -72,4 +70,3 @@ libraryUsage.readPackageFile = function readPackageFile(installPath) { | ||
if ( | ||
codeEvent.type !== type || | ||
codeEvent.script?.indexOf(`node_modules${path.sep}`) === -1 || | ||
!codeEvent.script?.includes(`node_modules${path.sep}`) || | ||
!codeEvent.func | ||
@@ -102,5 +99,7 @@ ) return; | ||
libraryUsage.install = function () { | ||
const evalInterval = config.agent.node.library_usage.reporting.interval_ms; | ||
const interval = config.agent.node.library_usage.reporting.interval_ms; | ||
setCodeEventListener(libraryUsage.codeEventListener, evalInterval); | ||
// by default it will not report node: internal scripts or non-Function/LazyCompile | ||
// types. | ||
setCodeEventListener(libraryUsage.codeEventListener, { interval }); | ||
setInterval(libraryUsage.report, 2000).unref(); | ||
@@ -107,0 +106,0 @@ }; |
107
lib/util.js
/* | ||
* Copyright: 2023 Contrast Security, Inc | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
@@ -15,9 +15,23 @@ * License: Commercial | ||
*/ | ||
// @ts-check | ||
'use strict'; | ||
const fs = require('fs').promises; | ||
const pathModule = require('path'); | ||
const os = require('os'); | ||
const fs = require('fs/promises'); | ||
const { EOL } = require('os'); | ||
const { join } = require('path'); | ||
const APPLICABLE_FILE_REGEX = /\.([cm]?js|node)$/; | ||
function buildLibraryHash(data) { | ||
if (data._shasum) { | ||
return data._shasum; | ||
} | ||
if (data.dist) { | ||
return data.dist.shasum; | ||
} | ||
return `${data.name}:${data.name}:${data.version}`; | ||
} | ||
function createLibData(data, tags) { | ||
@@ -39,30 +53,19 @@ return { | ||
function buildLibraryHash(data) { | ||
if (data._shasum) { | ||
return data._shasum; | ||
} | ||
if (data.dist) { | ||
return data.dist.shasum; | ||
} | ||
return `${data.name}:${data.name}:${data.version}`; | ||
} | ||
function serializeLibrary(library) { | ||
let manifest = `${library.name}${os.EOL}${os.EOL}`; | ||
let manifest = `${library.name}${EOL}${EOL}`; | ||
const { description, license, dependencies, requiredBy } = library; | ||
if (description) { | ||
manifest += `${description}${os.EOL}`; | ||
manifest += `${description}${EOL}`; | ||
} | ||
if (license) { | ||
manifest += `license: ${license}${os.EOL}`; | ||
manifest += `license: ${license}${EOL}`; | ||
} | ||
if (dependencies) { | ||
manifest += `dependencies:${os.EOL}`; | ||
manifest += `dependencies:${EOL}`; | ||
Object.entries(dependencies).forEach(([name, version]) => { | ||
manifest += ` ${name}: ${version}${os.EOL}`; | ||
manifest += ` ${name}: ${version}${EOL}`; | ||
}); | ||
@@ -72,5 +75,5 @@ } | ||
if (requiredBy && requiredBy.length > 0) { | ||
manifest += `dependants:${os.EOL}`; | ||
manifest += `dependants:${EOL}`; | ||
requiredBy.forEach((d) => { | ||
manifest += ` ${d}${os.EOL}`; | ||
manifest += ` ${d}${EOL}`; | ||
}); | ||
@@ -92,51 +95,49 @@ } | ||
function applicableFile(file) { | ||
return ( | ||
['.js', '.node', '.cjs', '.mjs'].filter((validExtension) => | ||
file.endsWith(validExtension) | ||
).length > 0 | ||
); | ||
/** | ||
* @param {string} path | ||
* @returns {boolean} | ||
*/ | ||
function applicableFile(path) { | ||
return APPLICABLE_FILE_REGEX.test(path); | ||
} | ||
async function getFileCount(path) { | ||
const files = await readdir(path); | ||
return files.filter((file) => applicableFile(file) | ||
).length; | ||
} | ||
/** | ||
* recursive fs.readdir, since the recursive option is only added in node 18. | ||
* @param {string} path | ||
* @returns {Promise<string[]>} | ||
*/ | ||
async function readdir(path) { | ||
let list = []; | ||
const files = await fs.readdir(path); | ||
let pending = files.length; | ||
const list = []; | ||
if (!pending) { | ||
return list; | ||
} | ||
for (const file of files) { | ||
const filePath = pathModule.join(path, file); | ||
const stats = await fs.stat(filePath); | ||
const filePath = join(path, file); | ||
const fileStat = await fs.stat(filePath); | ||
if (stats.isDirectory() && !filePath.endsWith(`${pathModule.sep}node_modules`)) { | ||
if (fileStat.isDirectory() && file !== 'node_modules') { | ||
const subList = await readdir(filePath); | ||
list = list.concat(subList); | ||
pending -= 1; | ||
list.push(...subList); | ||
} else { | ||
list.push(filePath); | ||
pending -= 1; | ||
} | ||
} | ||
if (!pending) { | ||
return list; | ||
} | ||
} | ||
return list; | ||
} | ||
/** | ||
* @param {string} path | ||
* @returns {Promise<number>} | ||
*/ | ||
async function getFileCount(path) { | ||
const files = await readdir(path); | ||
return files.filter(applicableFile).length; | ||
} | ||
module.exports = { | ||
buildLibraryHash, | ||
createLibData, | ||
serializeLibrary, | ||
getFileCount, | ||
createLibData | ||
}; | ||
{ | ||
"name": "@contrast/library-analysis", | ||
"version": "1.15.0", | ||
"version": "1.15.1", | ||
"description": "Handles library reporting and library usage analysis", | ||
@@ -20,3 +20,3 @@ "license": "SEE LICENSE IN LICENSE", | ||
"dependencies": { | ||
"@contrast/fn-inspect": "^3.4.0", | ||
"@contrast/code-events": "^2.0.0", | ||
"@contrast/common": "1.16.0", | ||
@@ -23,0 +23,0 @@ "semver": "^7.3.8" |
Sorry, the diff of this file is not supported yet
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
18541
23451
477
3
+ Added@contrast/code-events@^2.0.0
+ Added@contrast/code-events@2.1.0(transitive)
- Removed@contrast/fn-inspect@^3.4.0
- Removed@contrast/fn-inspect@3.4.0(transitive)
- Removednan@2.20.0(transitive)