better-npm-audit
Advanced tools
Comparing version 3.2.1 to 3.3.0-rc.1
54
index.js
@@ -11,4 +11,6 @@ #!/usr/bin/env node | ||
var handleInput_1 = __importDefault(require("./src/handlers/handleInput")); | ||
var handleFinish_1 = __importDefault(require("./src/handlers/handleFinish")); | ||
var handleDisplay_1 = __importDefault(require("./src/handlers/handleDisplay")); | ||
var package_json_1 = __importDefault(require("./package.json")); | ||
var handleAudit_1 = __importDefault(require("./src/handlers/handleAudit")); | ||
var handleScanV7_1 = __importDefault(require("./src/handlers/handleScanV7")); | ||
var MAX_BUFFER_SIZE = 1024 * 1000 * 50; // 50 MB | ||
@@ -18,7 +20,8 @@ var program = new commander_1.Command(); | ||
* Run audit | ||
* @param {String} auditCommand The NPM audit command to use (with flags) | ||
* @param {String} auditLevel The level of vulnerabilities we care about | ||
* @param {Array} exceptionIds List of vulnerability IDs to exclude | ||
* @param {String} auditCommand The NPM audit command to use (with flags) | ||
* @param {Array} exceptionIds List of vulnerability IDs to exclude | ||
* @param {Object} options Parsed command options | ||
* @param {Boolean} shouldScanModules Flag if we should scan the node_modules | ||
*/ | ||
function callback(auditCommand, auditLevel, exceptionIds) { | ||
function callback(auditCommand, exceptionIds, options) { | ||
// Increase the default max buffer size (1 MB) | ||
@@ -33,3 +36,40 @@ var audit = child_process_1.exec(auditCommand + " --json", { maxBuffer: MAX_BUFFER_SIZE }); | ||
if (audit.stderr) { | ||
audit.stderr.on('close', function () { return handleFinish_1.default(jsonBuffer, auditLevel, exceptionIds); }); | ||
audit.stderr.on('close', function () { | ||
// Analyze the npm audit response | ||
var _a = handleAudit_1.default(jsonBuffer, exceptionIds, options), unhandledIds = _a.unhandledIds, vulnerabilityIds = _a.vulnerabilityIds, report = _a.report, scanModules = _a.scanModules, npmVersion = _a.npmVersion; | ||
// Grab any un-filtered vulnerabilities at the appropriate level | ||
var unusedExceptionIds = exceptionIds.filter(function (id) { return !vulnerabilityIds.includes(id); }); | ||
// Make additional round of scanning the internal modules. | ||
// The reason to do this outside of the above was to keep it cleaner to manage the code | ||
// as npm v7 audit does not provide information of the installed version, we have to | ||
// retrieve that information from the `package.json` file, so we could check the | ||
// dependent modules via `npm ls {module}@{version}` properly. | ||
if (!options.scanModules) { | ||
// Display report | ||
handleDisplay_1.default(report, [], unhandledIds, unusedExceptionIds); | ||
return; | ||
} | ||
// If unable to determine the npm version | ||
if (npmVersion !== 6 && npmVersion !== 7) { | ||
console.error('Unable to determine NPM version from the audit response'); | ||
// Exit failed | ||
process.exit(1); | ||
} | ||
if (npmVersion === 6) { | ||
// TODO: Add auto exclusion scanning | ||
handleDisplay_1.default(report, [], unhandledIds, unusedExceptionIds); | ||
} | ||
if (npmVersion === 7) { | ||
// Scanning dependent modules | ||
handleScanV7_1.default(scanModules, report, unhandledIds, options, function (error, response) { | ||
if (error) { | ||
console.error(error); | ||
// Exit failed | ||
process.exit(1); | ||
} | ||
// Display report | ||
handleDisplay_1.default(response.securityReport, response.scanReport, response.unhandledIds, unusedExceptionIds); | ||
}); | ||
} | ||
}); | ||
// stderr | ||
@@ -48,3 +88,5 @@ audit.stderr.on('data', console.error); | ||
.option('-r, --registry <url>', 'The npm registry url to use.') | ||
.option('-s, --scan-modules [boolean]', 'Scan through reported modules for .nsprc file.', true) | ||
.option('-d, --debug', 'Enable debug mode.') | ||
.action(function (options) { return handleInput_1.default(options, callback); }); | ||
program.parse(process.argv); |
{ | ||
"name": "better-npm-audit", | ||
"version": "3.2.1", | ||
"version": "3.3.0-rc.1", | ||
"author": "Jee Mok <jee.ict@hotmail.com>", | ||
"description": "Reshape into a better npm audit for the community and encourage more people to include security audit into their process.", | ||
"description": "Reshape a better npm audit for the community and encourage more people to include security audits into their process", | ||
"license": "MIT", | ||
@@ -19,3 +19,4 @@ "main": "lib/index.js", | ||
"scripts": { | ||
"audit": "npm run build && node . audit", | ||
"audit:only": "node . audit", | ||
"audit": "npm run build && npm run audit:only", | ||
"test": "mocha -r ts-node/register test/**/*.test.ts", | ||
@@ -28,4 +29,4 @@ "lint": "eslint .", | ||
"postbuild": "cp README.md lib", | ||
"publish:live": "npm run build && npm publish lib --tag latest", | ||
"publish:next": "npm run build && npm publish lib --tag next" | ||
"publish:live": "npm run build && npm publish ./lib --tag latest", | ||
"publish:next": "npm run build && npm publish ./lib --tag next" | ||
}, | ||
@@ -32,0 +33,0 @@ "dependencies": { |
@@ -74,8 +74,10 @@ # Better NPM Audit | ||
| Flag | Short | Description | | ||
| -------------- | ----- | ------------------------------------------------------------------------------ | | ||
| `--exclude` | `-x` | Exceptions or the vulnerabilities ID(s) to exclude | | ||
| `--level` | `-l` | The minimum audit level to validate; Same as the original `--audit-level` flag | | ||
| `--production` | `-p` | Skip checking the `devDependencies` | | ||
| `--registry` | `-r` | The npm registry url to use | | ||
| Flag | Short | Default | Description | | ||
| ---------------- | ----- | ------- | ------------------------------------------------------------------------------------------------- | | ||
| `--exclude` | `-x` | | Exceptions or the vulnerabilities ID(s) to exclude | | ||
| `--level` | `-l` | | The minimum audit level to validate; Same as the original `--audit-level` flag | | ||
| `--production` | `-p` | | Skip checking the `devDependencies` | | ||
| `--registry` | `-r` | | The npm registry url to use | | ||
| `--scan-modules` | `-s` | `true` | Scan through reported modules for `.nsprc` file. Note: this feature currently only support NPM v7 | | ||
| `--debug` | `-d` | | Debug mode | | ||
@@ -130,2 +132,42 @@ <br /> | ||
## Auto trust security model | ||
If we trust a package author enough to install their package, then we also trust them to create an `.nsprc` file that covers all the (transitive) dependencies of that package, in the context of that package. | ||
So if we are working on a project `A`, and we install a package `B` as a dependency, then we trust the author of `B` to decide whether `B` is affected by a vulnerability in its dependency `C`. I also trust the author of `B` to make decisions about the author of package `C`, so if `C` contains an `.nsprc` file with an exception about a vulnerability in its dependency, `D`, then we trust that exception because the author of `B` trusts it, and we trust him. | ||
More generally, we can imagine a chain like this: | ||
`A` -> `B` -> `C` -> `D` -> `E` -> `F` | ||
where npm audit reports a vulnerability in `F`, but we are trusting the authors of `B`, `C`, `D`, and `E` to say whether that vulnerability is relevant in the context of their packages. | ||
Extending the example above, then, if we have a tree like this: | ||
``` | ||
A -> B -> C -> D -> E -> F | ||
| | ||
-> X -> Y -> Z -> F | ||
``` | ||
then the author of package `A` (us), still needs to worry about a vulnerability in `F` due to the way it may be used by `X`, `Y`, and `Z`. Again, though, any of the authors of `X`, `Y`, or `Z` can include an `.nsprc` exception for the vulnerability in `F`, and we will trust their judgement (because we are installing `X`'s package, and he trusts `Y`'s code, etc.) | ||
The auto excepted vulnerabilities will be labeled as "auto" in the report table: | ||
<img src="./.README/auto_exclusion.png" alt="Demo of excluding vulnerabilities flagged by the module maintainers" /> | ||
You can turn this feature off by using the flag `--scan-modules=false` | ||
Special shout out to [@EdwinTaylor](https://github.com/alertme-edwin) for his effort in making this possible. | ||
> Note: This feature currently only support npm v7 | ||
### Debug mode | ||
To inspect the module `.nsprc` file paths and details, use `--debug` flag to turn on debug mode: | ||
<img src="./.README/debug_mode.png" alt="Debug mode showing all scan paths" /> | ||
<br /> | ||
## Changelog | ||
@@ -132,0 +174,0 @@ |
"use strict"; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -11,3 +22,3 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
/** | ||
* Handle user's input | ||
* Process and clean user's input | ||
* @param {Object} options User's command options or flags | ||
@@ -29,2 +40,3 @@ * @param {Function} fn The function to handle the inputs | ||
var auditLevel = lodash_get_1.default(options, 'level', envVar) || 'info'; | ||
var parsedOptions = __assign(__assign({}, options), { level: auditLevel, scanModules: options.scanModules !== 'false' }); | ||
// Get the exceptions | ||
@@ -34,4 +46,4 @@ var nsprc = file_1.readFile('.nsprc'); | ||
var exceptionIds = vulnerability_1.getExceptionsIds(nsprc, cmdExceptions); | ||
fn(auditCommand, auditLevel, exceptionIds); | ||
fn(auditCommand, exceptionIds, parsedOptions); | ||
} | ||
exports.default = handleInput; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.trimArray = exports.isJsonString = exports.isWholeNumber = void 0; | ||
exports.cleanContent = exports.trimArray = exports.isJsonString = exports.isWholeNumber = void 0; | ||
/** | ||
@@ -49,1 +49,17 @@ * @param {String | Number | Null | Boolean} value The input number | ||
exports.trimArray = trimArray; | ||
// TODO: Add unit tests | ||
/** | ||
* Clean text from color formatting | ||
* @param {String} string Input | ||
* @return {String} | ||
*/ | ||
function cleanContent(string) { | ||
var content = JSON.stringify(string); | ||
// Remove the color codes | ||
content = content.replace(/\\x1b\[\d{1,2}m/g, ''); | ||
content = content.replace(/\\u001b\[\d{1,2}m/g, ''); | ||
// Remove additional stringified " | ||
content = content.replace(/"/g, ''); | ||
return content; | ||
} | ||
exports.cleanContent = cleanContent; |
@@ -11,7 +11,9 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.printExceptionReport = exports.printSecurityReport = exports.getColumnWidth = void 0; | ||
exports.printScanReport = exports.printExceptionReport = exports.printSecurityReport = exports.getColumnWidth = void 0; | ||
var lodash_get_1 = __importDefault(require("lodash.get")); | ||
var table_1 = require("table"); | ||
var common_1 = require("./common"); | ||
var SECURITY_REPORT_HEADER = ['ID', 'Module', 'Title', 'Paths', 'Sev.', 'URL', 'Ex.']; | ||
var EXCEPTION_REPORT_HEADER = ['ID', 'Status', 'Expiry', 'Notes']; | ||
var SCAN_REPORT_HEADER = ['ID', 'Version', 'Node', 'Status', 'Expiry', 'Notes', '.nsprc']; | ||
// TODO: Add unit tests | ||
@@ -31,7 +33,3 @@ /** | ||
var contentLength = tableData.reduce(function (max, cur) { | ||
var content = JSON.stringify(lodash_get_1.default(cur, columnIndex, '')); | ||
// Remove the color codes | ||
content = content.replace(/\\x1b\[\d{1,2}m/g, ''); | ||
content = content.replace(/\\u001b\[\d{1,2}m/g, ''); | ||
content = content.replace(/"/g, ''); | ||
var content = common_1.cleanContent(lodash_get_1.default(cur, columnIndex, '')); | ||
// Keep whichever number that is bigger | ||
@@ -47,7 +45,19 @@ return content.length > max ? content.length : max; | ||
/** | ||
* Print the security report in a table format | ||
* @param {Array} data Array of arrays | ||
* @return {undefined} Returns void | ||
* Print the security report | ||
* @param {Array} data Array of arrays | ||
* @return {undefined} Returns void | ||
*/ | ||
function printSecurityReport(data) { | ||
var columns = { | ||
// "Title" column index | ||
2: { | ||
width: getColumnWidth(data, 2), | ||
wrapWord: true, | ||
}, | ||
// "Paths" column index | ||
3: { | ||
width: getColumnWidth(data, 3), | ||
wrapWord: true, | ||
}, | ||
}; | ||
var configs = { | ||
@@ -59,14 +69,3 @@ singleLine: true, | ||
}, | ||
columns: { | ||
// "Title" column index | ||
2: { | ||
width: getColumnWidth(data, 2), | ||
wrapWord: true, | ||
}, | ||
// "Paths" column index | ||
3: { | ||
width: getColumnWidth(data, 3), | ||
wrapWord: true, | ||
}, | ||
}, | ||
columns: columns, | ||
}; | ||
@@ -77,3 +76,3 @@ console.info(table_1.table(__spreadArray([SECURITY_REPORT_HEADER], data), configs)); | ||
/** | ||
* Print the exception report in a table format | ||
* Print the exception report | ||
* @param {Array} data Array of arrays | ||
@@ -93,1 +92,35 @@ * @return {undefined} Returns void | ||
exports.printExceptionReport = printExceptionReport; | ||
/** | ||
* Print the scan report | ||
* @param {Array} data Array of arrays | ||
* @return {undefined} Returns void | ||
*/ | ||
function printScanReport(data) { | ||
var columns = { | ||
// "Node" column index | ||
2: { | ||
width: getColumnWidth(data, 2, 40), | ||
wrapWord: true, | ||
}, | ||
// "Notes" column index | ||
5: { | ||
width: getColumnWidth(data, 5), | ||
wrapWord: true, | ||
}, | ||
// ".nsprc" column index | ||
6: { | ||
width: getColumnWidth(data, 6), | ||
wrapWord: true, | ||
}, | ||
}; | ||
var configs = { | ||
singleLine: true, | ||
header: { | ||
alignment: 'center', | ||
content: '=== auto exclusion scan report ===\n', | ||
}, | ||
columns: columns, | ||
}; | ||
console.info(table_1.table(__spreadArray([SCAN_REPORT_HEADER], data), configs)); | ||
} | ||
exports.printScanReport = printScanReport; |
@@ -11,3 +11,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.processExceptions = exports.getExceptionsIds = exports.processAuditJson = exports.mapLevelToNumber = void 0; | ||
exports.processExceptions = exports.getExceptionsIds = exports.processAuditJson = exports.mapLevelToNumber = exports.mapModuleDependencies = void 0; | ||
var lodash_get_1 = __importDefault(require("lodash.get")); | ||
@@ -20,2 +20,25 @@ var common_1 = require("./common"); | ||
/** | ||
* Mao out all dependencies path | ||
* @param {Object} json JSON output of `npm ls --json` command | ||
* @param {Array} excludeModules Modules to be excluded | ||
* @return {Array} List of dependencies paths | ||
*/ | ||
function mapModuleDependencies(json, excludeModules) { | ||
if (!json.dependencies) { | ||
return []; | ||
} | ||
return Object.values(json.dependencies).reduce(function (a, c) { | ||
// Replace the full path to relative path, so we get path that starts with 'node_modules/...' | ||
var relativePath = c.path.replace(/(.*)better-npm-audit\//, ''); | ||
// Exclude some modules | ||
var shouldExclude = Array.isArray(excludeModules) && excludeModules.some(function (module) { return relativePath.endsWith(module); }); | ||
if (!shouldExclude) { | ||
a.push(relativePath); | ||
} | ||
// Recursively mapping the inner dependencies | ||
return a.concat(mapModuleDependencies(c, excludeModules)); | ||
}, []); | ||
} | ||
exports.mapModuleDependencies = mapModuleDependencies; | ||
/** | ||
* Converts an audit level to a numeric value | ||
@@ -42,13 +65,35 @@ * @param {String} auditLevel Audit level | ||
exports.mapLevelToNumber = mapLevelToNumber; | ||
var constructV6TableRow = function (vul, isExcepted) { | ||
// Record this vulnerability into the report, and highlight it using yellow color if it's new | ||
return [ | ||
color_1.color(vul.id, isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.module_name, isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.title, isExcepted ? '' : 'yellow'), | ||
color_1.color(common_1.trimArray(vul.findings.reduce(function (a, c) { return __spreadArray(__spreadArray([], a), c.paths); }, []), MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.severity, isExcepted ? '' : 'yellow', color_1.getSeverityBgColor(vul.severity)), | ||
color_1.color(vul.url, isExcepted ? '' : 'yellow'), | ||
isExcepted ? 'y' : color_1.color('n', 'yellow'), | ||
]; | ||
}; | ||
var constructV7TableRow = function (vul, isExcepted, nodes) { | ||
var id = lodash_get_1.default(vul, 'source', ''); | ||
// Record this vulnerability into the report, and highlight it using yellow color if it's new | ||
return [ | ||
color_1.color(String(id), isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.name, isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.title, isExcepted ? '' : 'yellow'), | ||
color_1.color(common_1.trimArray(nodes, MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.severity, isExcepted ? '' : 'yellow', color_1.getSeverityBgColor(vul.severity)), | ||
color_1.color(vul.url, isExcepted ? '' : 'yellow'), | ||
isExcepted ? 'y' : color_1.color('n', 'yellow'), | ||
]; | ||
}; | ||
/** | ||
* Analyze the JSON string buffer | ||
* @param {String} jsonBuffer NPM Audit JSON string buffer | ||
* @param {String} auditLevel User's target audit level | ||
* @param {Array} exceptionIds User's exception IDs | ||
* @return {Object} Processed vulnerabilities details | ||
* Analyze the audit JSON string | ||
* @param {String} jsonBuffer NPM Audit JSON string buffer | ||
* @param {Array} exceptionIds User's exception IDs | ||
* @param {Object} options Parsed command options | ||
* @return {Object} Processed vulnerabilities details | ||
*/ | ||
function processAuditJson(jsonBuffer, auditLevel, exceptionIds) { | ||
if (jsonBuffer === void 0) { jsonBuffer = ''; } | ||
if (auditLevel === void 0) { auditLevel = 'info'; } | ||
if (exceptionIds === void 0) { exceptionIds = []; } | ||
function processAuditJson(jsonBuffer, exceptionIds, options) { | ||
if (!common_1.isJsonString(jsonBuffer)) { | ||
@@ -59,7 +104,9 @@ return { | ||
report: [], | ||
scanModules: [], | ||
failed: true, | ||
}; | ||
} | ||
// NPM v6 uses `advisories` | ||
// NPM v7 uses `vulnerabilities` | ||
// There is a difference in the audit JSON structure for NPM v6 and v7 | ||
// - NPM v6 uses `advisories` | ||
// - NPM v7 uses `vulnerabilities` | ||
// Refer to the `test/__mocks__` folder for some sample mockups | ||
@@ -70,14 +117,6 @@ var _a = JSON.parse(jsonBuffer), advisories = _a.advisories, vulnerabilities = _a.vulnerabilities; | ||
return Object.values(advisories).reduce(function (acc, cur) { | ||
var shouldAudit = mapLevelToNumber(cur.severity) >= mapLevelToNumber(auditLevel); | ||
var shouldAudit = mapLevelToNumber(cur.severity) >= mapLevelToNumber(options.level); | ||
var isExcepted = exceptionIds.includes(Number(cur.id)); | ||
// Record this vulnerability into the report, and highlight it using yellow color if it's new | ||
acc.report.push([ | ||
color_1.color(cur.id, isExcepted ? '' : 'yellow'), | ||
color_1.color(cur.module_name, isExcepted ? '' : 'yellow'), | ||
color_1.color(cur.title, isExcepted ? '' : 'yellow'), | ||
color_1.color(common_1.trimArray(cur.findings.reduce(function (a, c) { return __spreadArray(__spreadArray([], a), c.paths); }, []), MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'), | ||
color_1.color(cur.severity, isExcepted ? '' : 'yellow', color_1.getSeverityBgColor(cur.severity)), | ||
color_1.color(cur.url, isExcepted ? '' : 'yellow'), | ||
isExcepted ? 'y' : color_1.color('n', 'yellow'), | ||
]); | ||
acc.report.push(constructV6TableRow(cur, isExcepted)); | ||
acc.vulnerabilityIds.push(Number(cur.id)); | ||
@@ -87,2 +126,7 @@ // Found unhandled vulnerabilities | ||
acc.unhandledIds.push(Number(cur.id)); | ||
// TODO: | ||
// Prepare later for scanning usage (only for unhandled vulnerabilities) | ||
// if (options.scanModules) { | ||
// acc.scanModules.push({ id: Number(cur.id) }); | ||
// } | ||
} | ||
@@ -94,2 +138,4 @@ return acc; | ||
report: [], | ||
scanModules: [], | ||
npmVersion: 6, | ||
}); | ||
@@ -108,14 +154,6 @@ } | ||
} | ||
var shouldAudit = mapLevelToNumber(vul.severity) >= mapLevelToNumber(auditLevel); | ||
// Checks if this reported vulnerability is within the target range | ||
var shouldAudit = mapLevelToNumber(vul.severity) >= mapLevelToNumber(options.level); | ||
var isExcepted = exceptionIds.includes(id); | ||
// Record this vulnerability into the report, and highlight it using yellow color if it's new | ||
acc.report.push([ | ||
color_1.color(String(id), isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.name, isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.title, isExcepted ? '' : 'yellow'), | ||
color_1.color(common_1.trimArray(lodash_get_1.default(cur, 'nodes', []), MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'), | ||
color_1.color(vul.severity, isExcepted ? '' : 'yellow', color_1.getSeverityBgColor(vul.severity)), | ||
color_1.color(vul.url, isExcepted ? '' : 'yellow'), | ||
isExcepted ? 'y' : color_1.color('n', 'yellow'), | ||
]); | ||
acc.report.push(constructV7TableRow(vul, isExcepted, lodash_get_1.default(cur, 'nodes', []))); | ||
acc.vulnerabilityIds.push(id); | ||
@@ -125,2 +163,6 @@ // Found unhandled vulnerabilities | ||
acc.unhandledIds.push(id); | ||
// Prepare later for scanning usage (only for unhandled vulnerabilities) | ||
if (options.scanModules) { | ||
acc.scanModules.push({ id: id, name: cur.name, nodes: cur.nodes }); | ||
} | ||
} | ||
@@ -133,2 +175,4 @@ }); | ||
report: [], | ||
scanModules: [], | ||
npmVersion: 7, | ||
}); | ||
@@ -140,2 +184,3 @@ } | ||
report: [], | ||
scanModules: [], | ||
failed: true, | ||
@@ -142,0 +187,0 @@ }; |
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
50865
14
938
192
2
2