auditjs
Version:
Audit dependencies to identify known vulnerabilities and maintenance problems
383 lines • 21.5 kB
JavaScript
"use strict";
/*
* Copyright 2019-Present Sonatype Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Application = void 0;
const figlet = require("figlet");
const IqRequestService_1 = require("../Services/IqRequestService");
const NpmList_1 = require("../Munchers/NpmList");
const OssIndexRequestService_1 = require("../Services/OssIndexRequestService");
const GuideRequestService_1 = require("../Services/GuideRequestService");
const AuditIQServer_1 = require("../Audit/AuditIQServer");
const AuditOSSIndex_1 = require("../Audit/AuditOSSIndex");
const OssIndexServerResult_1 = require("../Types/OssIndexServerResult");
const ReportStatus_1 = require("../Types/ReportStatus");
const Bower_1 = require("../Munchers/Bower");
const Logger_1 = require("./Logger/Logger");
const Spinner_1 = require("./Spinner/Spinner");
const VulnerabilityExcluder_1 = require("../Whitelist/VulnerabilityExcluder");
const IqServerConfig_1 = require("../Config/IqServerConfig");
const OssIndexServerConfig_1 = require("../Config/OssIndexServerConfig");
const GuideServerConfig_1 = require("../Config/GuideServerConfig");
const VisualHelper_1 = require("../Visual/VisualHelper");
const pj = require('../../package.json');
class Application {
constructor(devDependency = false, silent = false, artie = false, allen = false, scanBower = false) {
this.devDependency = devDependency;
this.silent = silent;
this.artie = artie;
this.allen = allen;
this.scanBower = scanBower;
this.results = [];
this.sbom = '';
const npmList = new NpmList_1.NpmList(devDependency);
const bower = new Bower_1.Bower(devDependency);
this.printHeader();
this.spinner = new Spinner_1.Spinner(silent);
(0, Logger_1.createAppLogger)();
if (npmList.isValid() && !this.scanBower) {
(0, Logger_1.logMessage)('Setting Muncher to npm list', Logger_1.DEBUG);
this.muncher = npmList;
}
else if (bower.isValid()) {
(0, Logger_1.logMessage)('Setting Muncher to bower', Logger_1.DEBUG);
this.muncher = bower;
}
else {
(0, Logger_1.logMessage)('Failed project directory validation. Are you in a (built) node, yarn, or bower project directory?', 'error');
throw new Error('Could not instantiate muncher');
}
}
async startApplication(args) {
if (args._[0] == 'iq' || args._[0] == 'lifecycle') {
(0, Logger_1.logMessage)('Attempting to start application', Logger_1.DEBUG);
(0, Logger_1.logMessage)('Getting coordinates for Sonatype Lifecycle', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Getting coordinates for Sonatype Lifecycle');
await this.populateCoordinatesForIQ();
(0, Logger_1.logMessage)(`Coordinates obtained`, Logger_1.DEBUG, this.sbom);
this.spinner.maybeSucceed();
(0, Logger_1.logMessage)(`Auditing application`, Logger_1.DEBUG, args.application);
this.spinner.maybeCreateMessageForSpinner('Auditing your application with Sonatype Lifecycle');
await this.auditWithIQ(args);
}
else if (args._[0] == 'ossi') {
(0, Logger_1.logMessage)('Attempting to start application', Logger_1.DEBUG);
(0, Logger_1.logMessage)('Getting coordinates for Sonatype OSS Index', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Getting coordinates for Sonatype OSS Index');
await this.populateCoordinates();
(0, Logger_1.logMessage)(`Coordinates obtained`, Logger_1.DEBUG, this.results);
this.spinner.maybeSucceed();
(0, Logger_1.logMessage)('Auditing your application with Sonatype OSS Index', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Auditing your application with Sonatype OSS Index');
await this.auditWithOSSIndex(args);
}
else if (args._[0] == 'guide') {
(0, Logger_1.logMessage)('Attempting to start application', Logger_1.DEBUG);
(0, Logger_1.logMessage)('Getting coordinates for Sonatype Guide', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Getting coordinates for Sonatype Guide');
await this.populateCoordinates();
(0, Logger_1.logMessage)(`Coordinates obtained`, Logger_1.DEBUG, this.results);
this.spinner.maybeSucceed();
(0, Logger_1.logMessage)('Auditing your application with Sonatype Guide', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Auditing your application with Sonatype Guide');
await this.auditWithGuide(args);
}
else if (args._[0] == 'sbom') {
await this.populateCoordinatesForIQ();
console.log(this.sbom);
}
else {
(0, Logger_1.shutDownLoggerAndExit)(1);
}
}
printHeader() {
if (this.silent) {
return;
}
else if (this.artie) {
this.doPrintHeader('ArtieJS', 'Ghost');
}
else if (this.allen) {
this.doPrintHeader('AllenJS', 'Ghost');
}
else {
this.doPrintHeader();
}
}
doPrintHeader(title = 'AuditJS', font = '3D-ASCII') {
console.log(figlet.textSync(title, { font: font, horizontalLayout: 'fitted' }));
console.log(figlet.textSync('By Sonatype & Friends', { font: 'Pepper' }));
(0, VisualHelper_1.visuallySeperateText)(false, [`${title} version: ${pj.version}`]);
}
async populateCoordinates() {
try {
(0, Logger_1.logMessage)('Trying to get dependencies from Muncher', Logger_1.DEBUG);
this.results = await this.muncher.getDepList();
(0, Logger_1.logMessage)('Successfully got dependencies from Muncher', Logger_1.DEBUG);
}
catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
(0, Logger_1.logMessage)(`An error was encountered while gathering your dependencies.`, Logger_1.ERROR, {
title: err.message,
stack: err.stack,
});
(0, Logger_1.shutDownLoggerAndExit)(1);
}
}
async populateCoordinatesForIQ() {
try {
(0, Logger_1.logMessage)('Trying to get sbom from cyclonedx/bom', Logger_1.DEBUG);
this.sbom = await this.muncher.getSbomFromCommand();
(0, Logger_1.logMessage)('Successfully got sbom from cyclonedx/bom', Logger_1.DEBUG);
}
catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
(0, Logger_1.logMessage)(`An error was encountered while gathering your dependencies into an SBOM`, Logger_1.ERROR, {
title: err.message,
stack: err.stack,
});
(0, Logger_1.shutDownLoggerAndExit)(1);
}
}
async auditWithOSSIndex(args) {
(0, Logger_1.logMessage)('Instantiating OSS Index Request Service', Logger_1.DEBUG);
const requestService = this.getOssIndexRequestService(args);
this.spinner.maybeSucceed();
(0, Logger_1.logMessage)('Submitting coordinates to Sonatype OSS Index', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Submitting coordinates to Sonatype OSS Index');
const format = this.muncher instanceof Bower_1.Bower ? 'bower' : 'npm';
(0, Logger_1.logMessage)('Format to query OSS Index picked', Logger_1.DEBUG, { format: format });
try {
(0, Logger_1.logMessage)('Attempting to query OSS Index or use Cache', Logger_1.DEBUG);
const res = await requestService.callOSSIndexOrGetFromCache(this.results, format);
(0, Logger_1.logMessage)('Success from OSS Index', Logger_1.DEBUG, res);
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Reticulating splines');
(0, Logger_1.logMessage)('Turning response into Array<OssIndexServerResult>', Logger_1.DEBUG);
let ossIndexResults = res.map((y) => {
return new OssIndexServerResult_1.OssIndexServerResult(y);
});
(0, Logger_1.logMessage)('Response morphed into Array<OssIndexServerResult>', Logger_1.DEBUG, {
ossIndexServerResults: ossIndexResults,
});
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Removing whitelisted vulnerabilities');
(0, Logger_1.logMessage)('Response being ran against whitelist', Logger_1.DEBUG, { ossIndexServerResults: ossIndexResults });
ossIndexResults = await (0, VulnerabilityExcluder_1.filterVulnerabilities)(ossIndexResults, args.whitelist);
(0, Logger_1.logMessage)('Response has been whitelisted', Logger_1.DEBUG, { ossIndexServerResults: ossIndexResults });
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Auditing your results from Sonatype OSS Index');
(0, Logger_1.logMessage)('Instantiating OSS Index Request Service, with quiet option', Logger_1.DEBUG, { quiet: args.quiet });
const auditOSSIndex = new AuditOSSIndex_1.AuditOSSIndex(args.quiet ? true : false, args.json ? true : false, args.xml ? true : false);
this.spinner.maybeStop();
(0, Logger_1.logMessage)('Attempting to audit results', Logger_1.DEBUG);
const failed = auditOSSIndex.auditResults(ossIndexResults);
(0, Logger_1.logMessage)('Results audited', Logger_1.DEBUG, { failureCode: failed });
if (failed) {
(0, Logger_1.shutDownLoggerAndExit)(1);
}
else {
(0, Logger_1.shutDownLoggerAndExit)(0);
}
}
catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
this.spinner.maybeStop();
(0, Logger_1.logMessage)('There was an error auditing with Sonatype OSS Index', Logger_1.ERROR, {
title: err.message,
stack: err.stack,
});
(0, Logger_1.shutDownLoggerAndExit)(1);
}
}
async auditWithGuide(args) {
(0, Logger_1.logMessage)('Instantiating Guide Request Service', Logger_1.DEBUG);
const requestService = this.getGuideRequestService(args);
this.spinner.maybeSucceed();
(0, Logger_1.logMessage)('Submitting coordinates to Sonatype Guide', Logger_1.DEBUG);
this.spinner.maybeCreateMessageForSpinner('Submitting coordinates to Sonatype Guide');
const format = this.muncher instanceof Bower_1.Bower ? 'bower' : 'npm';
(0, Logger_1.logMessage)('Format to query Sonatype Guide picked', Logger_1.DEBUG, { format: format });
try {
(0, Logger_1.logMessage)('Attempting to query Sonatype Guide or use Cache', Logger_1.DEBUG);
const res = await requestService.callGuideOrGetFromCache(this.results, format);
(0, Logger_1.logMessage)('Success from Sonatype Guide', Logger_1.DEBUG, res);
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Reticulating splines');
(0, Logger_1.logMessage)('Turning response into Array<OssIndexServerResult>', Logger_1.DEBUG);
let guideResults = res.map((y) => {
return new OssIndexServerResult_1.OssIndexServerResult(y);
});
(0, Logger_1.logMessage)('Response morphed into Array<OssIndexServerResult>', Logger_1.DEBUG, {
ossIndexServerResults: guideResults,
});
this.spinner.maybeSucceed();
const allowlistPath = args.allowlist || args.whitelist;
this.spinner.maybeCreateMessageForSpinner('Removing allowlisted vulnerabilities');
(0, Logger_1.logMessage)('Response being ran against allowlist', Logger_1.DEBUG, { ossIndexServerResults: guideResults });
guideResults = await (0, VulnerabilityExcluder_1.filterVulnerabilities)(guideResults, allowlistPath);
(0, Logger_1.logMessage)('Response has been filtered', Logger_1.DEBUG, { ossIndexServerResults: guideResults });
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Auditing your results from Sonatype Guide');
(0, Logger_1.logMessage)('Instantiating AuditOSSIndex, with quiet option', Logger_1.DEBUG, { quiet: args.quiet });
const auditGuide = new AuditOSSIndex_1.AuditOSSIndex(args.quiet ? true : false, args.json ? true : false, args.xml ? true : false);
this.spinner.maybeStop();
(0, Logger_1.logMessage)('Attempting to audit results', Logger_1.DEBUG);
const failed = auditGuide.auditResults(guideResults);
(0, Logger_1.logMessage)('Results audited', Logger_1.DEBUG, { failureCode: failed });
if (args.recommend && !args.json && !args.xml) {
const vulnerablePurls = guideResults
.filter((r) => r.vulnerabilities && r.vulnerabilities.length > 0)
.map((r) => r.coordinates);
if (vulnerablePurls.length > 0) {
this.spinner.maybeCreateMessageForSpinner('Fetching Sonatype Guide recommendations');
(0, Logger_1.logMessage)('Fetching recommendations for vulnerable packages', Logger_1.DEBUG, { count: vulnerablePurls.length });
const recommendations = await requestService.getRecommendations(vulnerablePurls);
this.spinner.maybeStop();
this.printRecommendations(recommendations);
}
}
if (failed) {
(0, Logger_1.shutDownLoggerAndExit)(1);
}
else {
(0, Logger_1.shutDownLoggerAndExit)(0);
}
}
catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
this.spinner.maybeStop();
(0, Logger_1.logMessage)('There was an error auditing with Sonatype Guide', Logger_1.ERROR, {
title: err.message,
stack: err.stack,
});
(0, Logger_1.shutDownLoggerAndExit)(1);
}
}
printRecommendations(recommendations) {
if (recommendations.size === 0)
return;
const cols = Math.min(process.stdout.columns || 80, 80);
const line = '-'.repeat(cols);
console.log('\n' + line);
console.log('Sonatype Guide Recommendations:');
console.log(line);
for (const [purl, rec] of recommendations) {
const top = rec.toVersions?.[0];
if (!top?.version)
continue;
const displayPurl = purl.replace('%40', '@');
const fromDts = rec.fromVersion?.developerTrustScore?.toFixed(1) ?? 'N/A';
const toDts = top.developerTrustScore?.toFixed(1) ?? 'N/A';
const countVulns = (vulns) => vulns ? Object.values(vulns).reduce((s, n) => s + n, 0) : 0;
const fromVulnCount = countVulns(rec.fromVersion?.directVulnerabilities);
const toVulnCount = countVulns(top.directVulnerabilities);
console.log(`\n ${displayPurl}`);
console.log(` Current: ${rec.fromVersion?.version ?? '?'} (DTS: ${fromDts}, ${fromVulnCount} direct vuln(s))`);
console.log(` Upgrade to: ${top.version} (DTS: ${toDts}, ${toVulnCount} direct vuln(s))`);
if (top.breakingChangesCount && top.breakingChangesCount !== '0') {
console.log(` Note: ~${top.breakingChangesCount} breaking changes`);
}
}
console.log('\n' + line + '\n');
}
async auditWithIQ(args) {
try {
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Authenticating with Sonatype Lifecycle');
(0, Logger_1.logMessage)('Attempting to connect to Sonatype Lifecycle', Logger_1.DEBUG, args.application);
const requestService = this.getIqRequestService(args);
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Submitting your dependencies');
(0, Logger_1.logMessage)('Submitting SBOM to Sonatype Lifecycle', Logger_1.DEBUG, this.sbom);
const resultUrl = await requestService.submitToThirdPartyAPI(this.sbom);
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Checking for results (this could take a minute)');
(0, Logger_1.logMessage)('Polling Lifecycle for report results', Logger_1.DEBUG, resultUrl);
requestService.asyncPollForResults(`${resultUrl}`, (e) => {
this.spinner.maybeFail();
(0, Logger_1.logMessage)('There was an issue auditing your application!', Logger_1.ERROR, { title: e.message, stack: e.stack });
(0, Logger_1.shutDownLoggerAndExit)(1);
}, (x) => {
this.spinner.maybeSucceed();
this.spinner.maybeCreateMessageForSpinner('Auditing your results');
const results = Object.assign(new ReportStatus_1.ReportStatus(), x);
(0, Logger_1.logMessage)('Sonatype Lifecycle results obtained!', Logger_1.DEBUG, results);
results.reportHtmlUrl = new URL(results.reportHtmlUrl, requestService.host).href;
const auditResults = new AuditIQServer_1.AuditIQServer();
this.spinner.maybeStop();
(0, Logger_1.logMessage)('Auditing results', Logger_1.DEBUG, results);
const failure = auditResults.auditThirdPartyResults(results);
(0, Logger_1.logMessage)('Audit finished', Logger_1.DEBUG, { failure: failure });
if (failure) {
(0, Logger_1.shutDownLoggerAndExit)(1);
}
else {
(0, Logger_1.shutDownLoggerAndExit)(0);
}
});
}
catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
this.spinner.maybeFail();
(0, Logger_1.logMessage)('There was an issue auditing your application!', Logger_1.ERROR, {
title: err.message,
stack: err.stack,
});
(0, Logger_1.shutDownLoggerAndExit)(1);
}
}
getOssIndexRequestService(args) {
let config;
try {
config = new OssIndexServerConfig_1.OssIndexServerConfig();
config.getConfigFromFile();
}
catch {
// Ignore config load failure
}
return new OssIndexRequestService_1.OssIndexRequestService(args?.user || config?.getUsername(), args?.password || config?.getToken(), args?.cache || config?.getCacheLocation(), args?.server || config?.getServer());
}
getGuideRequestService(args) {
let config;
try {
config = new GuideServerConfig_1.GuideServerConfig();
if (config.exists())
config.getConfigFromFile();
}
catch {
// Ignore config load failure
}
const username = args?.user || process.env.AUDITJS_GUIDE_USERNAME || config?.getUsername();
// Bearer token: used when no username is present.
// Fall back to config Token for backward compat with configs saved before AccessToken field existed.
const accessToken = !username
? args?.token || process.env.AUDITJS_GUIDE_TOKEN || config?.getAccessToken() || config?.getToken()
: undefined;
return new GuideRequestService_1.GuideRequestService(username, args?.token || args?.password || process.env.AUDITJS_GUIDE_TOKEN || config?.getToken(), args?.cache, args?.server || config?.getServer?.(), accessToken);
}
getIqRequestService(args) {
const config = new IqServerConfig_1.IqServerConfig();
const hasCredentials = (args.user || process.env.AUDITJS_LIFECYCLE_USER) &&
(args.password || process.env.AUDITJS_LIFECYCLE_TOKEN) &&
(args.server || process.env.AUDITJS_LIFECYCLE_URL);
if (!config.exists() && !hasCredentials)
throw new Error('No config file is defined and you are missing one of the -h (host), -u (user), or -p (password) parameters.');
return new IqRequestService_1.IqRequestService(args.user ?? process.env.AUDITJS_LIFECYCLE_USER ?? config.getUsername(), args.password ?? process.env.AUDITJS_LIFECYCLE_TOKEN ?? config.getToken(), args.server ?? process.env.AUDITJS_LIFECYCLE_URL ?? config.getHost(), args.application, args.stage, args.timeout, args.insecure);
}
}
exports.Application = Application;
//# sourceMappingURL=Application.js.map