UNPKG

auditjs

Version:

Audit dependencies to identify known vulnerabilities and maintenance problems

383 lines 21.5 kB
"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