@contrast/contrast 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/report/models/reportListModel.js +8 -0
- package/dist/audit/report/models/reportSeverityModel.js +8 -0
- package/dist/cliConstants.js +7 -0
- package/dist/constants/constants.js +1 -1
- package/dist/constants/locales.js +1 -0
- package/dist/sarif/generateSarif.js +8 -9
- package/dist/sarif/sarifClient.js +33 -0
- package/dist/sarif/sarifRequests.js +17 -0
- package/dist/sarif/sarifWriter.js +7 -4
- package/dist/scaAnalysis/common/commonReportingFunctionsSca.js +14 -6
- package/dist/scaAnalysis/common/utils/reportUtilsSca.js +11 -4
- package/dist/scaAnalysis/javascript/analysis.js +9 -2
- package/dist/scaAnalysis/javascript/index.js +1 -6
- package/dist/scaAnalysis/javascript/v3LockFileParser.js +107 -0
- package/dist/scaAnalysis/scaAnalysis.js +1 -0
- package/package.json +11 -11
- package/dist/sarif/sarifAssessRequests.js +0 -62
- package/dist/sarif/sarifIastClient.js +0 -121
- package/dist/sarif/sarifSCARequests.js +0 -27
- package/dist/sarif/sarifScaClient.js +0 -38
|
@@ -17,4 +17,12 @@ export class ReportCompositeKey {
|
|
|
17
17
|
this.highestSeverity = highestSeverity;
|
|
18
18
|
this.numberOfSeverities = numberOfSeverities;
|
|
19
19
|
}
|
|
20
|
+
toString() {
|
|
21
|
+
return `ReportCompositeKey {
|
|
22
|
+
libraryName: ${this.libraryName},
|
|
23
|
+
libraryVersion: ${this.libraryVersion},
|
|
24
|
+
highestSeverity: ${this.highestSeverity},
|
|
25
|
+
numberOfSeverities: ${this.numberOfSeverities}
|
|
26
|
+
}`;
|
|
27
|
+
}
|
|
20
28
|
}
|
package/dist/cliConstants.js
CHANGED
|
@@ -376,6 +376,13 @@ const sarifOptionDefinitions = [
|
|
|
376
376
|
'}: ' +
|
|
377
377
|
i18n.__('constantsSarifSeverity')
|
|
378
378
|
},
|
|
379
|
+
{
|
|
380
|
+
name: 'tool-type',
|
|
381
|
+
description: '{bold ' +
|
|
382
|
+
i18n.__('constantsOptional') +
|
|
383
|
+
'}: ' +
|
|
384
|
+
i18n.__('constantsToolType')
|
|
385
|
+
},
|
|
379
386
|
{
|
|
380
387
|
name: 'debug',
|
|
381
388
|
alias: 'd',
|
|
@@ -17,7 +17,7 @@ export const HIGH = 'HIGH';
|
|
|
17
17
|
export const CRITICAL = 'CRITICAL';
|
|
18
18
|
// App
|
|
19
19
|
export const APP_NAME = 'contrast';
|
|
20
|
-
const APP_VERSION = '2.
|
|
20
|
+
const APP_VERSION = '2.3.1';
|
|
21
21
|
export const TIMEOUT = 120000;
|
|
22
22
|
export const CRITICAL_PRIORITY = 1;
|
|
23
23
|
export const HIGH_PRIORITY = 2;
|
|
@@ -53,6 +53,7 @@ export const en_locales = () => {
|
|
|
53
53
|
failSeverityOptionErrorMessage: ' FAIL - Results detected vulnerabilities over accepted severity level',
|
|
54
54
|
constantsSeverity: 'Use with "contrast scan --fail --severity high" or "contrast audit --fail --severity high". Set the severity level to detect vulnerabilities or dependencies. Severity levels are critical, high, medium, low or note.',
|
|
55
55
|
constantsSarifSeverity: 'Set the severity level to filter the vulnerabilities included in the SARIF output. Severity levels are critical, high, medium, low or note.',
|
|
56
|
+
constantsToolType: 'The tools that are included in the generated SARIF file. Valid options are SCA or ASSESS. The default value is both.',
|
|
56
57
|
constantsHeader: `Contrast CLI @ v${getAppVersion()}`,
|
|
57
58
|
configHeader2: 'Config options',
|
|
58
59
|
clearHeader: '-c, --clear',
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { getSarifConfig } from './sarifConfig.js';
|
|
2
|
-
import {
|
|
3
|
-
import { checkApplicationAccess, iastData } from './sarifIastClient.js';
|
|
4
|
-
import { writeCombinedSarif } from './sarifWriter.js';
|
|
2
|
+
import { writeSarif } from './sarifWriter.js';
|
|
5
3
|
import { logInfo } from '../common/logging.js';
|
|
6
4
|
import { sarifUsageGuide } from './help.js';
|
|
5
|
+
import { sarifVulns } from './sarifClient.js';
|
|
7
6
|
// This filename could be set by a customer
|
|
8
7
|
// Defaulted to be ingested in a GH workflow
|
|
9
8
|
const outputFileName = 'contrast.sarif';
|
|
@@ -13,13 +12,13 @@ export const processSarif = async (contrastConf, argvMain) => {
|
|
|
13
12
|
process.exit(0);
|
|
14
13
|
}
|
|
15
14
|
let config = await getSarifConfig(contrastConf, 'sarif', argvMain);
|
|
16
|
-
await checkApplicationAccess(config);
|
|
17
15
|
logInfo('Generating SARIF file');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const sarifData = await sarifVulns(config);
|
|
17
|
+
if (sarifData === '') {
|
|
18
|
+
logInfo('No SARIF data found');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
writeSarif(outputFileName, sarifData);
|
|
23
22
|
logInfo('contrast.sarif file generated successfully');
|
|
24
23
|
};
|
|
25
24
|
const printHelpMessage = () => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSarifVulns } from './sarifRequests.js';
|
|
2
|
+
export const sarifVulns = async (config) => {
|
|
3
|
+
let severities = [];
|
|
4
|
+
if (config.severity) {
|
|
5
|
+
severities = convertSeverityInput(config.severity);
|
|
6
|
+
}
|
|
7
|
+
const response = await getSarifVulns(config, severities);
|
|
8
|
+
if (response.statusCode === 200) {
|
|
9
|
+
console.log('Retrieved SARIF data');
|
|
10
|
+
return JSON.stringify(response.body, null, 2);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
console.log(`Error retrieving SARIF data`);
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export function convertSeverityInput(severity) {
|
|
18
|
+
const severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'NOTE'];
|
|
19
|
+
switch (severity.toUpperCase()) {
|
|
20
|
+
case 'CRITICAL':
|
|
21
|
+
return severities.slice(0, 1);
|
|
22
|
+
case 'HIGH':
|
|
23
|
+
return severities.slice(0, 2);
|
|
24
|
+
case 'MEDIUM':
|
|
25
|
+
return severities.slice(0, 3);
|
|
26
|
+
case 'LOW':
|
|
27
|
+
return severities.slice(0, 4);
|
|
28
|
+
case 'NOTE':
|
|
29
|
+
return severities;
|
|
30
|
+
default:
|
|
31
|
+
return severities;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { buildBaseRequestOptions, ErrorType } from '../common/baseRequest.js';
|
|
2
|
+
import { got } from 'got';
|
|
3
|
+
const getSarifUrl = (config) => {
|
|
4
|
+
return `${config.host}/Contrast/api/ng/organizations/${config.organizationId}/applications/${config.applicationId}/sarif`;
|
|
5
|
+
};
|
|
6
|
+
export function getSarifVulns(config, severities) {
|
|
7
|
+
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
8
|
+
options.url = getSarifUrl(config);
|
|
9
|
+
options.json = {
|
|
10
|
+
metadataFilters: config.metadata ? config.metadata : [],
|
|
11
|
+
// User can specify minimum severity, if none will include all
|
|
12
|
+
severities: severities,
|
|
13
|
+
// Allow user to specify toolTypes and default as both
|
|
14
|
+
toolTypes: config.toolType ? [config.toolType] : ['SCA', 'ASSESS']
|
|
15
|
+
};
|
|
16
|
+
return got.post(options);
|
|
17
|
+
}
|
|
@@ -178,7 +178,7 @@ export const generateScaSarifRun = cveList => {
|
|
|
178
178
|
}
|
|
179
179
|
return sarifRunBuilderSca;
|
|
180
180
|
};
|
|
181
|
-
export const writeCombinedSarif = (traces, cveList,
|
|
181
|
+
export const writeCombinedSarif = (traces, cveList, outputFileName) => {
|
|
182
182
|
const sarifBuilder = new SarifBuilder();
|
|
183
183
|
const sarifRunBuilderIast = generateIastSarifRun(traces);
|
|
184
184
|
sarifBuilder.addRun(sarifRunBuilderIast);
|
|
@@ -187,10 +187,13 @@ export const writeCombinedSarif = (traces, cveList, output) => {
|
|
|
187
187
|
const sarifJsonString = sarifBuilder.buildSarifJsonString({
|
|
188
188
|
indent: true
|
|
189
189
|
}); // indent:true if you like
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
writeSarif(outputFileName, sarifJsonString);
|
|
191
|
+
};
|
|
192
|
+
export const writeSarif = (outputFileName, sarifData) => {
|
|
193
|
+
if (outputFileName) {
|
|
194
|
+
fs.writeFileSync(outputFileName, sarifData);
|
|
192
195
|
}
|
|
193
196
|
else {
|
|
194
|
-
console.log(
|
|
197
|
+
console.log(sarifData);
|
|
195
198
|
}
|
|
196
199
|
};
|
|
@@ -3,12 +3,12 @@ import { countVulnerableLibrariesBySeverity } from '../../audit/report/utils/rep
|
|
|
3
3
|
import { SeverityCountModel } from '../../audit/report/models/severityCountModel.js';
|
|
4
4
|
import { orderBy } from 'lodash-es';
|
|
5
5
|
import { ReportOutputModel, ReportOutputHeaderModel, ReportOutputBodyModel } from '../../audit/report/models/reportOutputModel.js';
|
|
6
|
-
import { CE_URL, CRITICAL_COLOUR, HIGH_COLOUR, MEDIUM_COLOUR, LOW_COLOUR, NOTE_COLOUR } from '../../constants/constants.js';
|
|
6
|
+
import { CE_URL, CRITICAL_COLOUR, HIGH_COLOUR, MEDIUM_COLOUR, LOW_COLOUR, NOTE_COLOUR, NOTE_PRIORITY } from '../../constants/constants.js';
|
|
7
7
|
import Table from 'cli-table3';
|
|
8
8
|
import { findHighestSeverityCVESca, severityCountAllCVEsSca, findCVESeveritySca, orderByHighestPrioritySca } from './utils/reportUtilsSca.js';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { buildFormattedHeaderNum } from '../../audit/report/commonReportingFunctions.js';
|
|
11
|
-
import { logInfo } from '../../common/logging.js';
|
|
11
|
+
import { logDebug, logInfo } from '../../common/logging.js';
|
|
12
12
|
export const createSummaryMessageTop = (numberOfVulnerableLibraries, numberOfCves) => {
|
|
13
13
|
numberOfVulnerableLibraries === 1
|
|
14
14
|
? logInfo(`\n\nFound 1 vulnerable library containing ${numberOfCves} CVE`)
|
|
@@ -25,11 +25,18 @@ export const printFormattedOutputSca = (config, reportModelList, numberOfVulnera
|
|
|
25
25
|
const report = new ReportList();
|
|
26
26
|
for (const library of reportModelList) {
|
|
27
27
|
const { artifactName, version, vulnerabilities, remediationAdvice } = library;
|
|
28
|
-
const
|
|
28
|
+
const highestSeverity = findHighestSeverityCVESca(vulnerabilities, config);
|
|
29
|
+
const severityCount = severityCountAllCVEsSca(vulnerabilities, new SeverityCountModel()).getTotal;
|
|
30
|
+
if (highestSeverity.priority === undefined) {
|
|
31
|
+
highestSeverity.priority = NOTE_PRIORITY;
|
|
32
|
+
logDebug(config, `Unknown severity for vulnerability ${artifactName}`);
|
|
33
|
+
}
|
|
34
|
+
const newOutputModel = new ReportModelStructure(new ReportCompositeKey(artifactName, version, highestSeverity, severityCount), vulnerabilities, remediationAdvice);
|
|
29
35
|
report.reportOutputList.push(newOutputModel);
|
|
30
36
|
}
|
|
31
37
|
const outputOrderedByLowestSeverityAndLowestNumOfCvesFirst = orderBy(report.reportOutputList, [
|
|
32
38
|
reportListItem => {
|
|
39
|
+
logDebug(config, reportListItem.compositeKey);
|
|
33
40
|
return reportListItem.compositeKey.highestSeverity.priority;
|
|
34
41
|
},
|
|
35
42
|
reportListItem => {
|
|
@@ -44,7 +51,7 @@ export const printFormattedOutputSca = (config, reportModelList, numberOfVulnera
|
|
|
44
51
|
const numOfCVEs = reportModel.cveArray.length;
|
|
45
52
|
const table = getReportTable();
|
|
46
53
|
const header = buildHeader(highestSeverity, contrastHeaderNumCounter, libraryName, libraryVersion, numOfCVEs);
|
|
47
|
-
const body = buildBody(cveArray, remediationAdvice);
|
|
54
|
+
const body = buildBody(cveArray, remediationAdvice, config);
|
|
48
55
|
const reportOutputModel = new ReportOutputModel(header, body);
|
|
49
56
|
table.push(reportOutputModel.body.issueMessage, reportOutputModel.body.adviceMessage);
|
|
50
57
|
logInfo(`${reportOutputModel.header.vulnMessage} ${reportOutputModel.header.introducesMessage}`);
|
|
@@ -98,8 +105,9 @@ export function buildHeader(highestSeverity, contrastHeaderNum, libraryName, ver
|
|
|
98
105
|
const introducesMessage = `introduces ${numOfCVEs} ${vulnerabilityPluralised}`;
|
|
99
106
|
return new ReportOutputHeaderModel(vulnMessage, introducesMessage);
|
|
100
107
|
}
|
|
101
|
-
export function buildBody(cveArray, advice) {
|
|
102
|
-
|
|
108
|
+
export function buildBody(cveArray, advice, config) {
|
|
109
|
+
logDebug(config, `buildBody 204: ${JSON.stringify(cveArray)}`);
|
|
110
|
+
const orderedCvesWithSeverityAssigned = orderByHighestPrioritySca(cveArray.map(cve => findCVESeveritySca(cve, config)));
|
|
103
111
|
const issueMessage = getIssueRow(orderedCvesWithSeverityAssigned);
|
|
104
112
|
const adviceMessage = getAdviceRow(advice);
|
|
105
113
|
return new ReportOutputBodyModel(issueMessage, adviceMessage);
|
|
@@ -2,16 +2,19 @@ import { orderBy } from 'lodash-es';
|
|
|
2
2
|
import { CRITICAL_COLOUR, CRITICAL_PRIORITY, HIGH_COLOUR, HIGH_PRIORITY, LOW_COLOUR, LOW_PRIORITY, MEDIUM_COLOUR, MEDIUM_PRIORITY, NOTE_COLOUR, NOTE_PRIORITY } from '../../../constants/constants.js';
|
|
3
3
|
import { ReportSeverityModel } from '../../../audit/report/models/reportSeverityModel.js';
|
|
4
4
|
import { ScaReportModel } from '../models/ScaReportModel.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { logDebug } from '../../../common/logging.js';
|
|
6
|
+
export function findHighestSeverityCVESca(cveArray, config) {
|
|
7
|
+
logDebug(config, `\n findHighestSeverityCVESca 25: ${JSON.stringify(cveArray)} \n`);
|
|
8
|
+
const mappedToReportSeverityModels = cveArray.map(cve => findCVESeveritySca(cve, config));
|
|
9
|
+
// order and get first
|
|
8
10
|
return orderBy(mappedToReportSeverityModels, cve => cve?.priority)[0];
|
|
9
11
|
}
|
|
10
12
|
export function orderByHighestPrioritySca(reportSeverityModel) {
|
|
11
13
|
return orderBy(reportSeverityModel, ['priority'], ['asc']);
|
|
12
14
|
}
|
|
13
|
-
export function findCVESeveritySca(vulnerabilityModel) {
|
|
15
|
+
export function findCVESeveritySca(vulnerabilityModel, config) {
|
|
14
16
|
const { name } = vulnerabilityModel;
|
|
17
|
+
logDebug(config, `\n findCVESeveritySca 44: ${JSON.stringify(vulnerabilityModel)} \n`);
|
|
15
18
|
if (vulnerabilityModel.cvss3Severity === 'CRITICAL' ||
|
|
16
19
|
vulnerabilityModel.severity === 'CRITICAL') {
|
|
17
20
|
return new ReportSeverityModel('CRITICAL', CRITICAL_PRIORITY, CRITICAL_COLOUR, name);
|
|
@@ -32,6 +35,10 @@ export function findCVESeveritySca(vulnerabilityModel) {
|
|
|
32
35
|
vulnerabilityModel.severity === 'NOTE') {
|
|
33
36
|
return new ReportSeverityModel('NOTE', NOTE_PRIORITY, NOTE_COLOUR, name);
|
|
34
37
|
}
|
|
38
|
+
else {
|
|
39
|
+
logDebug(config, `Unknown severity for vulnerability ${name} with cvss3severity of ${vulnerabilityModel.cvss3Severity} and and cvss severity of: ${vulnerabilityModel.severity}`);
|
|
40
|
+
return new ReportSeverityModel('NOTE', NOTE_PRIORITY, NOTE_COLOUR, name);
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
43
|
export function convertGenericToTypedReportModelSca(reportArray) {
|
|
37
44
|
return reportArray.map((library) => {
|
|
@@ -3,6 +3,7 @@ import { load } from 'js-yaml';
|
|
|
3
3
|
import i18n from 'i18n';
|
|
4
4
|
import { formatKey } from '../../audit/nodeAnalysisEngine/parseYarn2LockFileContents.js';
|
|
5
5
|
import yarnpkg from '@yarnpkg/lockfile';
|
|
6
|
+
import { buildDependencyTreeForV3LockFile } from './v3LockFileParser.js';
|
|
6
7
|
export const readFile = async (config, languageFiles, nameOfFile) => {
|
|
7
8
|
const index = languageFiles.findIndex(v => v.includes(nameOfFile));
|
|
8
9
|
if (config.file) {
|
|
@@ -31,7 +32,7 @@ export const readYarn = async (config, languageFiles, nameOfFile) => {
|
|
|
31
32
|
throw new Error(i18n.__('nodeReadYarnLockFileError') + `${err.message}`);
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
|
-
export const parseNpmLockFile = async (npmLockFile) => {
|
|
35
|
+
export const parseNpmLockFile = async (npmLockFile, lockFileVersion) => {
|
|
35
36
|
try {
|
|
36
37
|
if (!npmLockFile.parsedPackages) {
|
|
37
38
|
npmLockFile.parsedPackages = {};
|
|
@@ -42,9 +43,15 @@ export const parseNpmLockFile = async (npmLockFile) => {
|
|
|
42
43
|
//e.g: node_modules/@aws-amplify/datastore/node_modules/uuid --> @aws-amplify/datastore/uuid
|
|
43
44
|
packageKey = packageKey.replace(/(node_modules\/)+/g, '');
|
|
44
45
|
}
|
|
46
|
+
if (lockFileVersion === 3) {
|
|
47
|
+
delete packageValue.license;
|
|
48
|
+
}
|
|
45
49
|
npmLockFile.parsedPackages[packageKey] = packageValue;
|
|
46
50
|
});
|
|
47
|
-
|
|
51
|
+
if (lockFileVersion === 3) {
|
|
52
|
+
buildDependencyTreeForV3LockFile(npmLockFile);
|
|
53
|
+
}
|
|
54
|
+
// remove base project package - unneeded
|
|
48
55
|
delete npmLockFile.parsedPackages[''];
|
|
49
56
|
return npmLockFile;
|
|
50
57
|
}
|
|
@@ -47,12 +47,7 @@ const parseFiles = async (config, files, js) => {
|
|
|
47
47
|
logDebug(config, message);
|
|
48
48
|
throw new Error(message);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
const message = `NPM lockfileVersion 3 is not support with --legacy`;
|
|
52
|
-
logDebug(config, message);
|
|
53
|
-
throw new Error(message);
|
|
54
|
-
}
|
|
55
|
-
js.npmLockFile = await parseNpmLockFile(npmLockFile);
|
|
50
|
+
js.npmLockFile = await parseNpmLockFile(npmLockFile, currentLockFileVersion);
|
|
56
51
|
}
|
|
57
52
|
if (files.includes('yarn.lock')) {
|
|
58
53
|
js = await parseYarnLockFile(js);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const buildDependencyTreeForV3LockFile = npmLockFile => {
|
|
2
|
+
let npmLockFileWithDepsObject = createDepTreeObject(npmLockFile);
|
|
3
|
+
const distinguishedDeps = distinguishParentAndTransitiveDependencies(npmLockFileWithDepsObject.dependencies);
|
|
4
|
+
assignChildrenToParents(distinguishedDeps.transitive, distinguishedDeps.parents, npmLockFile);
|
|
5
|
+
};
|
|
6
|
+
export const createDepTreeObject = npmLockFile => {
|
|
7
|
+
const parsedPackages = npmLockFile.parsedPackages;
|
|
8
|
+
let dependencies = {};
|
|
9
|
+
for (let dep in parsedPackages) {
|
|
10
|
+
dependencies[dep] = {
|
|
11
|
+
version: parsedPackages[dep].version,
|
|
12
|
+
resolved: parsedPackages[dep].resolved,
|
|
13
|
+
integrity: parsedPackages[dep].integrity,
|
|
14
|
+
...(parsedPackages[dep].peer != null
|
|
15
|
+
? { peer: parsedPackages[dep].peer }
|
|
16
|
+
: {}),
|
|
17
|
+
...(parsedPackages[dep].optional != null
|
|
18
|
+
? { optional: parsedPackages[dep].optional }
|
|
19
|
+
: {}),
|
|
20
|
+
...(parsedPackages[dep].dev != null
|
|
21
|
+
? { dev: parsedPackages[dep].dev }
|
|
22
|
+
: {}),
|
|
23
|
+
...(parsedPackages[dep].dependencies != null
|
|
24
|
+
? { requires: parsedPackages[dep].dependencies }
|
|
25
|
+
: {})
|
|
26
|
+
};
|
|
27
|
+
npmLockFile.dependencies = dependencies;
|
|
28
|
+
delete npmLockFile.dependencies[''];
|
|
29
|
+
}
|
|
30
|
+
return npmLockFile;
|
|
31
|
+
};
|
|
32
|
+
export const distinguishParentAndTransitiveDependencies = dependencies => {
|
|
33
|
+
let listOfParentObjects = {};
|
|
34
|
+
let listOfChildObjects = {};
|
|
35
|
+
for (let library in dependencies) {
|
|
36
|
+
let library_name = filterDependency(library.split('/'));
|
|
37
|
+
if (library_name.length === 1) {
|
|
38
|
+
Object.assign(listOfParentObjects, {
|
|
39
|
+
[library_name]: dependencies[library]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (library_name.join('/').includes('@') && library_name.length === 2) {
|
|
44
|
+
Object.assign(listOfParentObjects, {
|
|
45
|
+
[library_name.join('/')]: dependencies[library]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
Object.assign(listOfChildObjects, {
|
|
50
|
+
[library_name.join('/')]: dependencies[library]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { parents: listOfParentObjects, transitive: listOfChildObjects };
|
|
56
|
+
};
|
|
57
|
+
export const assignChildrenToParents = (transitiveDeps, parentDeps, npmLockFile) => {
|
|
58
|
+
for (let child in transitiveDeps) {
|
|
59
|
+
let group = child.split('/');
|
|
60
|
+
let name = group.pop();
|
|
61
|
+
if (parentDeps[group.join('/')] === undefined) {
|
|
62
|
+
let additionalName = group.pop();
|
|
63
|
+
if (parentDeps[group.join('/')] !== undefined) {
|
|
64
|
+
let newName = additionalName + '/' + name;
|
|
65
|
+
addDependencyToNode(parentDeps[group.join('/')], transitiveDeps[child], newName);
|
|
66
|
+
delete transitiveDeps[child];
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log('WARNING: Unable to find parent dependency for ', child);
|
|
70
|
+
console.log('Adding as a parent dependency');
|
|
71
|
+
Object.assign(parentDeps, { [child]: transitiveDeps[child] });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
addDependencyToNode(parentDeps[group.join('/')], transitiveDeps[child], name);
|
|
76
|
+
delete transitiveDeps[child];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
delete npmLockFile.dependencies;
|
|
80
|
+
npmLockFile.dependencies = parentDeps;
|
|
81
|
+
return npmLockFile;
|
|
82
|
+
};
|
|
83
|
+
export const filterDependency = inputString => {
|
|
84
|
+
let foundFirstInstance = false;
|
|
85
|
+
// Filter the parts to remove any item containing '@' except for the first instance
|
|
86
|
+
const filteredParts = inputString.filter(part => {
|
|
87
|
+
if (part.includes('@')) {
|
|
88
|
+
if (!foundFirstInstance) {
|
|
89
|
+
foundFirstInstance = true;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
return filteredParts;
|
|
97
|
+
};
|
|
98
|
+
export function addDependencyToNode(baseObject, newDependency, potentialChildDeps) {
|
|
99
|
+
// Ensure the dependencies node exists
|
|
100
|
+
if (!baseObject.dependencies) {
|
|
101
|
+
baseObject.dependencies = {};
|
|
102
|
+
}
|
|
103
|
+
// Add the new dependency
|
|
104
|
+
Object.assign(baseObject.dependencies, {
|
|
105
|
+
[potentialChildDeps]: newDependency
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -21,6 +21,7 @@ import { auditUsageGuide } from '../audit/help.js';
|
|
|
21
21
|
import chalk from 'chalk';
|
|
22
22
|
import { logDebug, logInfo } from '../common/logging.js';
|
|
23
23
|
export const processSca = async (config) => {
|
|
24
|
+
logDebug(config, `audit started at ${new Date().toISOString()}`);
|
|
24
25
|
let filesFound;
|
|
25
26
|
if (config.help) {
|
|
26
27
|
logInfo(auditUsageGuide);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/contrast",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Contrast Security's command line tool",
|
|
5
5
|
"exports": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
"build-package": "yarn build && yarn build-binary && yarn package-binary",
|
|
31
31
|
"test": "export VITEST_MAX_THREADS=4 && export VITEST_MIN_THREADS=1 && vitest --dir ./tests/unit-tests/",
|
|
32
32
|
"test-debug": "export VITEST_MAX_THREADS=4 && export VITEST_MIN_THREADS=1 && vitest --dir ./tests/unit-tests/ --inspect-brk",
|
|
33
|
-
"test-int": "vitest --dir ./tests/integration-tests/ --
|
|
34
|
-
"test-int-scan": "vitest --dir ./tests/integration-tests/scan --
|
|
35
|
-
"test-int-audit": "vitest --dir ./tests/integration-tests/audit --
|
|
36
|
-
"test-int-scan-errors": "vitest ./tests/integration-tests/scan/scanLocalErrors.spec.js --
|
|
37
|
-
"test-int-scan-reports": "vitest ./tests/integration-tests/scan/scanReport.spec.js --
|
|
38
|
-
"test-int-audit-features": "vitest --dir ./tests/integration-tests/audit/auditFeatures/ --
|
|
39
|
-
"test-int-audit-projects": "vitest ./tests/integration-tests/audit/audit-projects.spec.js --
|
|
40
|
-
"test-int-fingerprint": "vitest ./tests/integration-tests/fingerprint/fingerprint.spec.js --
|
|
41
|
-
"test-int-repository": "vitest ./tests/integration-tests/fingerprint/repository.spec.js --
|
|
33
|
+
"test-int": "vitest --dir ./tests/integration-tests/ --pool=forks",
|
|
34
|
+
"test-int-scan": "vitest --dir ./tests/integration-tests/scan --pool=forks",
|
|
35
|
+
"test-int-audit": "vitest --dir ./tests/integration-tests/audit --pool=forks",
|
|
36
|
+
"test-int-scan-errors": "vitest ./tests/integration-tests/scan/scanLocalErrors.spec.js --pool=forks",
|
|
37
|
+
"test-int-scan-reports": "vitest ./tests/integration-tests/scan/scanReport.spec.js --pool=forks",
|
|
38
|
+
"test-int-audit-features": "vitest --dir ./tests/integration-tests/audit/auditFeatures/ --pool=forks",
|
|
39
|
+
"test-int-audit-projects": "vitest ./tests/integration-tests/audit/audit-projects.spec.js --pool=forks",
|
|
40
|
+
"test-int-fingerprint": "vitest ./tests/integration-tests/fingerprint/fingerprint.spec.js --pool=forks",
|
|
41
|
+
"test-int-repository": "vitest ./tests/integration-tests/fingerprint/repository.spec.js --pool=forks",
|
|
42
42
|
"format": "prettier --write \"**/*.{ts,tsx,js,json,md,yml}\" .eslintrc.*",
|
|
43
43
|
"check-format": "prettier --check \"**/*.{ts,tsx,js,json,md,yml,mjs,cjs}\" .eslintrc.*",
|
|
44
44
|
"coverage-local": "c8 --reporter=text mocha './tests/unit-tests/**/*.spec.js'",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"extract-licenses": "node scripts/extract-licenses",
|
|
48
48
|
"lambda-dev": "npx ts-node src/index.ts lambda",
|
|
49
49
|
"dev": "npx ts-node src/index.ts",
|
|
50
|
-
"proxy-tests": "vitest ./tests/integration-tests/proxy/proxy-coverage.spec.js --
|
|
50
|
+
"proxy-tests": "vitest ./tests/integration-tests/proxy/proxy-coverage.spec.js --pool=forks"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=18.16.0"
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { buildBaseRequestOptions, ErrorType } from '../common/baseRequest.js';
|
|
2
|
-
import { got } from 'got';
|
|
3
|
-
import { logDebug } from '../common/logging.js';
|
|
4
|
-
export function getApplicationAccess(config) {
|
|
5
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
6
|
-
options.url = getApplicationUrl(config, config.applicationId, config.organizationId);
|
|
7
|
-
logDebug(config, 'url: ' + options.url);
|
|
8
|
-
return got.get(options);
|
|
9
|
-
}
|
|
10
|
-
const getApplicationUrl = (config, applicationId, organizationId) => {
|
|
11
|
-
return `${config.host}/Contrast/api/ng/${organizationId}/applications/${applicationId}?expand=license`;
|
|
12
|
-
};
|
|
13
|
-
const getAssessTraceIdsUrl = (config, offset, resultsLimit) => {
|
|
14
|
-
return `${config.host}/Contrast/api/ng/${config.organizationId}/orgtraces/filter/?offset=${offset}&limit=${resultsLimit}`;
|
|
15
|
-
};
|
|
16
|
-
export function getAssessTraceIds(config, offset, resultsLimit) {
|
|
17
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
18
|
-
options.url = getAssessTraceIdsUrl(config, offset, resultsLimit);
|
|
19
|
-
options.json = {
|
|
20
|
-
applicationID: config.applicationId,
|
|
21
|
-
metadataFilters: config.metadata ? config.metadata : [],
|
|
22
|
-
severities: config.severity ? [config.severity] : []
|
|
23
|
-
};
|
|
24
|
-
logDebug(config, 'url: ' + options.url);
|
|
25
|
-
return got.post(options);
|
|
26
|
-
}
|
|
27
|
-
const getAssessTraceDetailsUrl = (config, traceId) => {
|
|
28
|
-
return `${config.host}/Contrast/api/ng/${config.organizationId}/traces/${config.applicationId}/filter/${traceId}?expand=events,request,sink,${traceId},skip_links`;
|
|
29
|
-
};
|
|
30
|
-
export function getAssessTraceDetails(config, traceId) {
|
|
31
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
32
|
-
options.url = getAssessTraceDetailsUrl(config, traceId);
|
|
33
|
-
logDebug(config, 'url: ' + options.url);
|
|
34
|
-
return got.get(options);
|
|
35
|
-
}
|
|
36
|
-
const getAssessTraceEventsUrl = (config, traceId, eventId) => {
|
|
37
|
-
return `${config.host}/Contrast/api/ng/${config.organizationId}/traces/${traceId}/events/${eventId}/details`;
|
|
38
|
-
};
|
|
39
|
-
export function getAssessTraceEvents(config, traceId, eventId) {
|
|
40
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
41
|
-
options.url = getAssessTraceEventsUrl(config, traceId, eventId);
|
|
42
|
-
logDebug(config, 'url: ' + options.url);
|
|
43
|
-
return got.get(options);
|
|
44
|
-
}
|
|
45
|
-
const getAssessTraceRoutesUrl = (config, traceId) => {
|
|
46
|
-
return `${config.host}/Contrast/api/ng/${config.organizationId}/traces/${config.applicationId}/trace/${traceId}/routes`;
|
|
47
|
-
};
|
|
48
|
-
export function getAssessTraceRoutes(config, traceId) {
|
|
49
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
50
|
-
options.url = getAssessTraceRoutesUrl(config, traceId);
|
|
51
|
-
logDebug(config, 'url: ' + options.url);
|
|
52
|
-
return got.get(options);
|
|
53
|
-
}
|
|
54
|
-
const getAssessAgentSessionFiltersUrl = (config) => {
|
|
55
|
-
return `${config.host}/Contrast/api/ng/${config.organizationId}/metadata/session/${config.applicationId}/filters`;
|
|
56
|
-
};
|
|
57
|
-
export function getAssessAgentSessionFilters(config) {
|
|
58
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
59
|
-
options.url = getAssessAgentSessionFiltersUrl(config);
|
|
60
|
-
logDebug(config, 'url: ' + options.url);
|
|
61
|
-
return got.get(options);
|
|
62
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { getAssessAgentSessionFilters, getAssessTraceDetails, getAssessTraceEvents, getAssessTraceIds, getAssessTraceRoutes, getApplicationAccess } from './sarifAssessRequests.js';
|
|
2
|
-
import { logInfo, logDebug } from '../common/logging.js';
|
|
3
|
-
import { get } from 'http';
|
|
4
|
-
const resultsLimit = 50;
|
|
5
|
-
let offset = 0;
|
|
6
|
-
export const iastData = async (config) => {
|
|
7
|
-
if (config.metadata) {
|
|
8
|
-
const filters = await getAgentSessionFilters(config);
|
|
9
|
-
logInfo(`Found ${filters.length} available filters`);
|
|
10
|
-
if (filters.length !== 0) {
|
|
11
|
-
config = convertMetadataInput(config, filters);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
const filteredTraceIds = await getTraceIds(config, offset, resultsLimit);
|
|
15
|
-
logInfo(`Found ${filteredTraceIds.length} traces`);
|
|
16
|
-
let completeTraces = [];
|
|
17
|
-
for (const traceId of filteredTraceIds) {
|
|
18
|
-
const traceDetails = await getTraceDetails(config, traceId);
|
|
19
|
-
const traceRoutes = await getTraceRoutes(config, traceId);
|
|
20
|
-
let eventsWithFrames = [];
|
|
21
|
-
if (traceDetails.events) {
|
|
22
|
-
logInfo(`Found ${traceDetails.events.length} events for trace ${traceId} getting further details`);
|
|
23
|
-
for (const eventSummary of traceDetails.events) {
|
|
24
|
-
const eventDetails = await getTraceEvents(config, traceId, eventSummary.eventId);
|
|
25
|
-
eventsWithFrames.push(eventDetails);
|
|
26
|
-
}
|
|
27
|
-
// Only push when we have actual events , is this right?
|
|
28
|
-
completeTraces.push({
|
|
29
|
-
trace: traceDetails,
|
|
30
|
-
events: eventsWithFrames,
|
|
31
|
-
routes: traceRoutes
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return completeTraces;
|
|
36
|
-
};
|
|
37
|
-
export async function checkApplicationAccess(config) {
|
|
38
|
-
logInfo('Checking application access');
|
|
39
|
-
if (config.applicationId === undefined) {
|
|
40
|
-
logInfo('Application ID not specified - this is required for IAST vulnerability retrieval');
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
const response = getApplicationAccess(config);
|
|
44
|
-
const json = await response.json();
|
|
45
|
-
if (json.application.license.level === 'Unlicensed') {
|
|
46
|
-
logInfo('Application is not licensed for Assess, please contact your administrator to enable Assess for this application.');
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
logInfo('Error accessing application, ensure you have access to application:' +
|
|
52
|
-
config.applicationId);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export async function getTraceIds(config, offset, resultsLimit) {
|
|
57
|
-
let hasResults = true;
|
|
58
|
-
let pivotedData = [];
|
|
59
|
-
// Will return all traces if no severity is specified
|
|
60
|
-
while (hasResults) {
|
|
61
|
-
const response = getAssessTraceIds(config, offset, resultsLimit);
|
|
62
|
-
const responseBody = await response.json();
|
|
63
|
-
if (responseBody.traces == null) {
|
|
64
|
-
logInfo('No libraries found');
|
|
65
|
-
return pivotedData;
|
|
66
|
-
}
|
|
67
|
-
const result = responseBody.traces;
|
|
68
|
-
const traceIds = result.map(trace => {
|
|
69
|
-
return trace.uuid;
|
|
70
|
-
});
|
|
71
|
-
pivotedData = pivotedData.concat(traceIds);
|
|
72
|
-
if (result.length < resultsLimit) {
|
|
73
|
-
hasResults = false;
|
|
74
|
-
}
|
|
75
|
-
offset += resultsLimit;
|
|
76
|
-
}
|
|
77
|
-
return pivotedData;
|
|
78
|
-
}
|
|
79
|
-
export async function getTraceDetails(config, traceId) {
|
|
80
|
-
logInfo(`Getting trace details for ${traceId} of application ${config.applicationId}`);
|
|
81
|
-
const response = getAssessTraceDetails(config, traceId);
|
|
82
|
-
const responseBody = await response.json();
|
|
83
|
-
return responseBody.trace;
|
|
84
|
-
}
|
|
85
|
-
export async function getTraceEvents(config, traceId, eventId) {
|
|
86
|
-
const response = getAssessTraceEvents(config, traceId, eventId);
|
|
87
|
-
logDebug(`Getting trace events for ${eventId} of trace ${traceId}`);
|
|
88
|
-
const responseBody = await response.json();
|
|
89
|
-
return responseBody.event;
|
|
90
|
-
}
|
|
91
|
-
export async function getTraceRoutes(config, traceId) {
|
|
92
|
-
logInfo(`Getting trace routes for ${traceId} of application ${config.applicationId}`);
|
|
93
|
-
const response = getAssessTraceRoutes(config, traceId);
|
|
94
|
-
const responseBody = await response.json();
|
|
95
|
-
return responseBody.routes;
|
|
96
|
-
}
|
|
97
|
-
export async function getAgentSessionFilters(config) {
|
|
98
|
-
const response = getAssessAgentSessionFilters(config);
|
|
99
|
-
const responseBody = await response.json();
|
|
100
|
-
return responseBody.filters;
|
|
101
|
-
}
|
|
102
|
-
export function convertMetadataInput(config, filterLabels) {
|
|
103
|
-
// Agent session filters response returns a list of filters each containing an id and label
|
|
104
|
-
// The user input metadata will contain a label, which must be replaced by the id
|
|
105
|
-
// to then be used in the traces endpoint
|
|
106
|
-
let updatedMetadataInput = [];
|
|
107
|
-
const inputList = config.metadata.split(',');
|
|
108
|
-
const metadataInput = inputList.map(item => {
|
|
109
|
-
return { label: item.split('=')[0], values: [item.split('=')[1]] };
|
|
110
|
-
});
|
|
111
|
-
metadataInput.forEach(item => {
|
|
112
|
-
const filterLabel = filterLabels.find(f => f.label.toLowerCase() === item.label.toLowerCase());
|
|
113
|
-
if (filterLabel) {
|
|
114
|
-
item.fieldId = filterLabel.id;
|
|
115
|
-
}
|
|
116
|
-
const newValue = { fieldID: item.fieldId, values: item.values };
|
|
117
|
-
updatedMetadataInput.push(newValue);
|
|
118
|
-
});
|
|
119
|
-
config.metadata = updatedMetadataInput;
|
|
120
|
-
return config;
|
|
121
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { buildBaseRequestOptions, ErrorType } from '../common/baseRequest.js';
|
|
2
|
-
import { got } from 'got';
|
|
3
|
-
import { logDebug } from '../common/logging.js';
|
|
4
|
-
const getSCAVulnsUrl = (config, offset, resultsLimit) => {
|
|
5
|
-
return `${config.host}/Contrast/api/ng/${config.organizationId}/libraries/filter?expand=skip_links,apps,quickFilters,vulns,status,usage_counts&offset=${offset}&limit=${resultsLimit}&sort=score`;
|
|
6
|
-
};
|
|
7
|
-
export function getSCAVulns(config, offset, resultsLimit) {
|
|
8
|
-
const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
|
|
9
|
-
options.url = getSCAVulnsUrl(config, offset, resultsLimit);
|
|
10
|
-
options.json = {
|
|
11
|
-
q: '',
|
|
12
|
-
quickFilter: 'VULNERABLE',
|
|
13
|
-
apps: [config.applicationId],
|
|
14
|
-
servers: [],
|
|
15
|
-
environments: [],
|
|
16
|
-
grades: [],
|
|
17
|
-
languages: [],
|
|
18
|
-
licenses: [],
|
|
19
|
-
status: [],
|
|
20
|
-
severities: config.severity ? [config.severity] : [],
|
|
21
|
-
tags: [],
|
|
22
|
-
includeUnused: false,
|
|
23
|
-
includeUsed: false
|
|
24
|
-
};
|
|
25
|
-
logDebug(config, 'url: ' + options.url);
|
|
26
|
-
return got.post(options);
|
|
27
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { logInfo } from '../common/logging.js';
|
|
2
|
-
import { getSCAVulns } from './sarifSCARequests.js';
|
|
3
|
-
const resultsLimit = 50;
|
|
4
|
-
let offset = 0;
|
|
5
|
-
export const scaCVES = async (config) => {
|
|
6
|
-
let hasResults = true;
|
|
7
|
-
let pivotedData = [];
|
|
8
|
-
// Will return all CVEs if no severity is specified
|
|
9
|
-
while (hasResults) {
|
|
10
|
-
const response = getSCAVulns(config, offset, resultsLimit);
|
|
11
|
-
const responseBody = await response.json();
|
|
12
|
-
if (responseBody.libraries == null) {
|
|
13
|
-
logInfo('No libraries found');
|
|
14
|
-
return pivotedData;
|
|
15
|
-
}
|
|
16
|
-
// Finds all libraries with a vulnerability of specified severity
|
|
17
|
-
const result = responseBody.libraries.flatMap(item => {
|
|
18
|
-
const { vulns } = item;
|
|
19
|
-
return vulns.flatMap(vuln => {
|
|
20
|
-
vuln.library = item;
|
|
21
|
-
return vuln;
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
// All vulns associated with the library will be returned,
|
|
25
|
-
// so we must filter by severity again
|
|
26
|
-
for (const vuln of result) {
|
|
27
|
-
if (config.severity == null ||
|
|
28
|
-
vuln.severityToUse.toLowerCase() === config.severity) {
|
|
29
|
-
pivotedData.push(vuln);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
if (result.length < resultsLimit) {
|
|
33
|
-
hasResults = false;
|
|
34
|
-
}
|
|
35
|
-
offset += resultsLimit;
|
|
36
|
-
}
|
|
37
|
-
return pivotedData;
|
|
38
|
-
};
|