@contrast/contrast 2.1.6 → 2.2.0

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.
@@ -343,6 +343,45 @@ const auditAdvancedOptionDefinitionsForHelp = [
343
343
  i18n.__('constantsMavenSettingsPath')
344
344
  }
345
345
  ];
346
+ const sarifAdvancedOptionDefinitionsForHelp = [
347
+ ...sharedConnectionOptionDefinitions,
348
+ ...sharedCertOptionDefinitions
349
+ ];
350
+ const sarifOptionDefinitions = [
351
+ ...sarifAdvancedOptionDefinitionsForHelp,
352
+ {
353
+ name: 'help',
354
+ alias: 'h',
355
+ type: Boolean
356
+ },
357
+ {
358
+ name: 'application-id',
359
+ description: '{bold ' +
360
+ i18n.__('constantsRequired') +
361
+ '}: ' +
362
+ i18n.__('constantsApplicationId')
363
+ },
364
+ {
365
+ name: 'metadata',
366
+ description: '{bold ' +
367
+ i18n.__('constantsOptional') +
368
+ '}: ' +
369
+ i18n.__('constantsMetadata')
370
+ },
371
+ {
372
+ name: 'severity',
373
+ type: severity => parseSeverity(severity),
374
+ description: '{bold ' +
375
+ i18n.__('constantsOptional') +
376
+ '}: ' +
377
+ i18n.__('constantsSarifSeverity')
378
+ },
379
+ {
380
+ name: 'debug',
381
+ alias: 'd',
382
+ type: Boolean
383
+ }
384
+ ];
346
385
  const auditOptionDefinitions = [
347
386
  ...auditAdvancedOptionDefinitionsForHelp,
348
387
  {
@@ -509,11 +548,13 @@ const mainUsageGuide = commandLineUsage([
509
548
  { name: i18n.__('configName'), summary: i18n.__('helpConfigSummary') },
510
549
  { name: i18n.__('versionName'), summary: i18n.__('helpVersionSummary') },
511
550
  { name: i18n.__('auditName'), summary: i18n.__('helpAuditSummary') },
551
+ { name: i18n.__('sarifName'), summary: i18n.__('helpSarifSummary') },
512
552
  { name: i18n.__('scanName'), summary: i18n.__('helpScanSummary') },
513
553
  { name: i18n.__('assessName'), summary: i18n.__('assessSummary') },
514
554
  { name: i18n.__('lambdaName'), summary: i18n.__('helpLambdaSummary') },
515
555
  { name: i18n.__('helpName'), summary: i18n.__('helpSummary') },
516
556
  { name: i18n.__('learnName'), summary: i18n.__('helpLearnSummary') },
557
+ { name: i18n.__('sarifName'), summary: i18n.__('sarifSummary') },
517
558
  {
518
559
  name: i18n.__('configGenerate'),
519
560
  summary: i18n.__('configGenerateSummary')
@@ -543,6 +584,8 @@ export const commandLineDefinitions = {
543
584
  auditOptionDefinitions,
544
585
  authOptionDefinitions,
545
586
  configOptionDefinitions,
587
+ sarifOptionDefinitions,
588
+ sarifAdvancedOptionDefinitionsForHelp,
546
589
  scanAdvancedOptionDefinitionsForHelp,
547
590
  auditAdvancedOptionDefinitionsForHelp,
548
591
  assessOptionDefinitions,
@@ -108,4 +108,5 @@ export var ErrorType;
108
108
  ErrorType["VULNERABILITIES"] = "VULNERABILITIES";
109
109
  ErrorType["SCAN"] = "SCAN";
110
110
  ErrorType["TELEMETRY"] = "TELEMETRY";
111
+ ErrorType["SARIF"] = "SARIF";
111
112
  })(ErrorType || (ErrorType = {}));
@@ -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.1.6';
20
+ const APP_VERSION = '2.2.0';
21
21
  export const TIMEOUT = 120000;
22
22
  export const CRITICAL_PRIORITY = 1;
23
23
  export const HIGH_PRIORITY = 2;
@@ -52,6 +52,7 @@ export const en_locales = () => {
52
52
  failThresholdOptionErrorMessage: 'More than 0 vulnerabilities found',
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
+ constantsSarifSeverity: 'Set the severity level to filter the vulnerabilities included in the SARIF output. Severity levels are critical, high, medium, low or note.',
55
56
  constantsHeader: `Contrast CLI @ v${getAppVersion()}`,
56
57
  configHeader2: 'Config options',
57
58
  clearHeader: '-c, --clear',
@@ -67,6 +68,8 @@ export const en_locales = () => {
67
68
  constantsConfigUsageContents: 'view / clear the configuration',
68
69
  constantsPrerequisitesContent: 'To scan a Java binary project you will need a .jar, .war or a zip of multiple .jar or .war files for analysis\n' +
69
70
  'To scan source code you will need a .zip file containing the code for analysis',
71
+ constantsSarifPreRequisitesContent: 'To generate a SARIF file you will need the application-id of the application you want to generate the SARIF file for.\n' +
72
+ 'To get the application-id, go to the Contrast UI, select the application you want to generate the SARIF file for, and copy the application-id from the URL.',
70
73
  constantsUsage: 'Usage',
71
74
  constantsUsageCommandExample: 'contrast [command] [options]',
72
75
  constantsUsageCommandInfo: 'The file argument is optional. If no file is given, Contrast will search for a .jar, .war, .exe or .zip file in the working directory.\n',
@@ -90,6 +93,7 @@ export const en_locales = () => {
90
93
  scanLabel: "adds a label to the scan - defaults to 'Started by CLI tool at current date'",
91
94
  constantsIgnoreDev: 'Excludes developer dependencies from the results. All dependencies are included by default.',
92
95
  constantsCommands: 'Commands',
96
+ constantsSarifOptions: 'SARIF Options',
93
97
  constantsScanOptions: 'Scan Options',
94
98
  constantsAssessOptions: 'Assess Options',
95
99
  generatorConfigOptions: 'Generate Config Options',
@@ -114,6 +118,7 @@ export const en_locales = () => {
114
118
  helpScanSummary: 'Searches for a .jar, .war, .js, or .zip file in the working directory, uploads files for analysis, and returns the results. \n[scan --help for options] Java, .NET, .NET Core, JavaScript are supported. ',
115
119
  assessSummary: 'Reports vulnerabilities found at run-time on a server or microservice using a Contrast agent. \n [assess --help for options] Java, .NET, Node, Ruby, Python, Go, PHP are supported.',
116
120
  helpLambdaSummary: 'Performs a static security scan on an AWS lambda function. [lambda --help for options] AWS Lambda - Java & Python are supported. ',
121
+ helpSarifSummary: 'Generates a SARIF file (contrast.sarif) for Contrast Assess and Contrast SCA results for the specified application-id.',
117
122
  helpVersionSummary: 'Displays version of Contrast CLI',
118
123
  helpConfigSummary: 'Displays stored credentials',
119
124
  helpSummary: 'Displays usage guide',
@@ -126,6 +131,7 @@ export const en_locales = () => {
126
131
  configName: 'config',
127
132
  helpName: 'help',
128
133
  learnName: 'learn',
134
+ sarifName: 'sarif',
129
135
  configGenerate: 'generate-config',
130
136
  helpLearnSummary: 'Launches Contrast’s Secure Code Learning Hub.',
131
137
  configGenerateSummary: 'Generates contrast configuration yaml file.',
@@ -170,6 +176,8 @@ export const en_locales = () => {
170
176
  foundDetailedVulnerabilities: chalk.bold('%s') + ' | ' + chalk.bold('%s') + ' | %s | %s | %s ',
171
177
  searchingScanFileDirectory: 'Searching for file to scan from %s...',
172
178
  searchingAuditFileDirectory: 'Searching for package manager files from %s...',
179
+ sarifHeader: 'Contrast SARIF CLI',
180
+ sarifHeaderMessage: "Use 'contrast sarif' to generate a SARIF file for the given application-id.",
173
181
  scanHeader: `Contrast Scan CLI`,
174
182
  assessHeader: `Contrast Assess CLI`,
175
183
  generateConfigHeader: `Contrast Generate Config`,
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { processLearn } from './commands/learn/processLearn.js';
14
14
  import { sendTelemetryConfigAsConfObj } from './telemetry/telemetry.js';
15
15
  import { findCommandOnError } from './common/errorHandling.js';
16
16
  import { processAssess } from './assess/index.js';
17
+ import { processSarif } from './sarif/generateSarif.js';
17
18
  import { logInfo } from './common/logging.js';
18
19
  import { generateYamlConfiguration } from './generateYaml/index.js';
19
20
  const config = localConfig(APP_NAME, getAppVersion());
@@ -70,15 +71,15 @@ const start = async () => {
70
71
  if (command === 'assess') {
71
72
  return await processAssess(config, argvMain);
72
73
  }
73
- if (command === 'assess') {
74
- return await processAssess(config, argvMain);
75
- }
76
74
  if (command === 'generate-config') {
77
75
  return await generateYamlConfiguration(config, argvMain);
78
76
  }
79
77
  if (command === 'fingerprint') {
80
78
  return await processFingerprint(config, argvMain);
81
79
  }
80
+ if (command === 'sarif') {
81
+ return await processSarif(config, argvMain);
82
+ }
82
83
  if (command === 'learn') {
83
84
  return processLearn();
84
85
  }
@@ -0,0 +1,27 @@
1
+ import { getSarifConfig } from './sarifConfig.js';
2
+ import { scaCVES } from './sarifScaClient.js';
3
+ import { checkApplicationAccess, iastData } from './sarifIastClient.js';
4
+ import { writeCombinedSarif } from './sarifWriter.js';
5
+ import { logInfo } from '../common/logging.js';
6
+ import { sarifUsageGuide } from './help.js';
7
+ // This filename could be set by a customer
8
+ // Defaulted to be ingested in a GH workflow
9
+ const outputFileName = 'contrast.sarif';
10
+ export const processSarif = async (contrastConf, argvMain) => {
11
+ if (argvMain.indexOf('--help') !== -1 || argvMain.indexOf('help') !== -1) {
12
+ printHelpMessage();
13
+ process.exit(0);
14
+ }
15
+ let config = await getSarifConfig(contrastConf, 'sarif', argvMain);
16
+ await checkApplicationAccess(config);
17
+ logInfo('Generating SARIF file');
18
+ const scaVulns = await scaCVES(config);
19
+ logInfo(`Found ${scaVulns.length} SCA vulnerabilities matching criteria`);
20
+ const iastVulns = await iastData(config);
21
+ logInfo(`Found ${iastVulns.length} IAST vulnerabilities matching criteria`);
22
+ writeCombinedSarif(iastVulns, scaVulns, outputFileName);
23
+ logInfo('contrast.sarif file generated successfully');
24
+ };
25
+ const printHelpMessage = () => {
26
+ logInfo(sarifUsageGuide);
27
+ };
@@ -0,0 +1,52 @@
1
+ import commandLineUsage from 'command-line-usage';
2
+ import i18n from 'i18n';
3
+ import { commandLineDefinitions } from '../cliConstants.js';
4
+ import { commonHelpLinks } from '../common/commonHelp.js';
5
+ const { __ } = i18n;
6
+ export const sarifUsageGuide = commandLineUsage([
7
+ {
8
+ header: __('constantsHeader')
9
+ },
10
+ {
11
+ header: __('sarifHeader'),
12
+ content: [__('sarifHeaderMessage')]
13
+ },
14
+ {
15
+ header: __('constantsPrerequisitesHeader'),
16
+ content: [__('constantsSarifPreRequisitesContent')]
17
+ },
18
+ {
19
+ header: __('constantsSarifOptions'),
20
+ optionList: commandLineDefinitions.sarifOptionDefinitions,
21
+ hide: [
22
+ 'organization-id',
23
+ 'api-key',
24
+ 'authorization',
25
+ 'host',
26
+ 'proxy',
27
+ 'cert',
28
+ 'cacert',
29
+ 'key',
30
+ 'help',
31
+ 'ff',
32
+ 'cert-self-signed',
33
+ 'debug',
34
+ 'experimental',
35
+ 'tags',
36
+ 'sub-project',
37
+ 'code',
38
+ 'maven-settings-path',
39
+ 'language',
40
+ 'app-groups',
41
+ 'branch',
42
+ 'repo'
43
+ ]
44
+ },
45
+ {
46
+ header: __('constantsAdvancedOptions'),
47
+ optionList: commandLineDefinitions.sarifAdvancedOptionDefinitionsForHelp
48
+ },
49
+ commonHelpLinks()[0],
50
+ commonHelpLinks()[1],
51
+ commonHelpLinks()[2]
52
+ ]);
@@ -0,0 +1,62 @@
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
+ }
@@ -0,0 +1,8 @@
1
+ import { getCommandLineArgsCustom } from '../utils/parsedCLIOptions.js';
2
+ import { commandLineDefinitions } from '../cliConstants.js';
3
+ import { getAuth } from '../utils/paramsUtil/paramHandler.js';
4
+ export const getSarifConfig = async (contrastConf, command, argv) => {
5
+ const sarifParameters = await getCommandLineArgsCustom(contrastConf, command, argv, commandLineDefinitions.sarifOptionDefinitions);
6
+ const paramsAuth = getAuth(sarifParameters);
7
+ return { ...paramsAuth, ...sarifParameters };
8
+ };
@@ -0,0 +1,121 @@
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
+ }
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1,38 @@
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
+ };
@@ -0,0 +1,196 @@
1
+ import { SarifBuilder, SarifRunBuilder, SarifResultBuilder, SarifRuleBuilder } from 'node-sarif-builder';
2
+ import fs from 'fs';
3
+ export const mapSeverity = contrastSeverity => {
4
+ switch (contrastSeverity.toLowerCase()) {
5
+ case 'critical':
6
+ return 'error';
7
+ case 'high':
8
+ return 'warning';
9
+ case 'medium':
10
+ return 'note';
11
+ case 'low':
12
+ return 'note';
13
+ case 'note':
14
+ return 'note';
15
+ default:
16
+ return 'note';
17
+ }
18
+ };
19
+ export const getFileFromTSDescription = description => {
20
+ const regex = /\(([^)]+):\d*\)/;
21
+ const match = regex.exec(description);
22
+ return match ? match[1] : 'NOT AVAILABLE';
23
+ };
24
+ export const getLineFromTSDescription = description => {
25
+ const regex = /\(.*:(\d*)\)/;
26
+ const match = regex.exec(description);
27
+ return match ? parseInt(match[1]) : 1;
28
+ };
29
+ export const getLogicalLocationFromTSDescription = description => {
30
+ const regex = /(.*)\([^]+:\d*\)/;
31
+ const match = regex.exec(description);
32
+ return match ? match[1] + '()' : 'NOT AVAILABLE';
33
+ };
34
+ export const generateIastSarifRun = traces => {
35
+ const sarifRunBuilderIast = new SarifRunBuilder().initSimple({
36
+ toolDriverName: 'contrast-assess',
37
+ toolDriverVersion: '1.0.0',
38
+ url: 'https://contrastsecurity.com' // Url of your analyzer tool
39
+ });
40
+ if (traces.length === 0) {
41
+ return sarifRunBuilderIast;
42
+ }
43
+ for (const trace of traces) {
44
+ let extractedString = null;
45
+ if (trace.trace.sink && trace.trace.sink.label) {
46
+ extractedString = getFileFromTSDescription(trace.trace.sink.label);
47
+ }
48
+ const sarifResultBuilder = new SarifResultBuilder().initSimple({
49
+ ruleId: trace.trace.rule_title,
50
+ messageText: trace.trace.title,
51
+ level: mapSeverity(trace.trace.severity)
52
+ });
53
+ const sarifRuleBuilder = new SarifRuleBuilder().initSimple({
54
+ ruleId: trace.trace.rule_title,
55
+ shortDescriptionText: trace.trace.title,
56
+ fullDescriptionText: trace.trace.title
57
+ });
58
+ sarifRunBuilderIast.addRule(sarifRuleBuilder);
59
+ if (extractedString) {
60
+ sarifResultBuilder.setLocationArtifactUri({ uri: extractedString });
61
+ }
62
+ if (trace.trace.request) {
63
+ sarifResultBuilder.result.webRequest = {
64
+ target: trace.trace.request.uri,
65
+ method: trace.trace.request.method,
66
+ protocol: trace.trace.request.protocol,
67
+ version: trace.trace.request.version
68
+ };
69
+ }
70
+ sarifResultBuilder.result.provenance = {
71
+ firstDetectionTimeUtc: new Date(trace.trace.discovered).toISOString(),
72
+ lastDetectionTimeUtc: new Date(trace.trace.last_time_seen).toISOString()
73
+ };
74
+ sarifResultBuilder.result.properties = {
75
+ vulnerability_id: trace.trace.uuid,
76
+ contrast_severity: trace.trace.severity,
77
+ contrast_status: trace.trace.status,
78
+ contrast_substatus: trace.trace.sub_status
79
+ };
80
+ // Add stack trace
81
+ sarifResultBuilder.result.codeFlows = [
82
+ {
83
+ threadFlows: trace.events.map(event => {
84
+ return {
85
+ locations: [
86
+ {
87
+ location: {
88
+ physicalLocation: {
89
+ artifactLocation: {
90
+ uri: getFileFromTSDescription(event.stacktraces[0].description)
91
+ },
92
+ region: {
93
+ startLine: getLineFromTSDescription(event.stacktraces[0].description)
94
+ }
95
+ },
96
+ logicalLocations: [
97
+ {
98
+ fullyQualifiedName: getLogicalLocationFromTSDescription(event.stacktraces[0].description)
99
+ }
100
+ ]
101
+ },
102
+ stack: {
103
+ frames: event.stacktraces
104
+ .filter(stacktrace => getFileFromTSDescription(stacktrace.description) !==
105
+ 'NOT AVAILABLE')
106
+ .map(stacktrace => {
107
+ return {
108
+ location: {
109
+ physicalLocation: {
110
+ artifactLocation: {
111
+ uri: getFileFromTSDescription(stacktrace.description)
112
+ },
113
+ region: {
114
+ startLine: getLineFromTSDescription(stacktrace.description)
115
+ }
116
+ },
117
+ logicalLocations: [
118
+ {
119
+ fullyQualifiedName: getLogicalLocationFromTSDescription(stacktrace.description)
120
+ }
121
+ ]
122
+ }
123
+ };
124
+ })
125
+ }
126
+ }
127
+ ]
128
+ };
129
+ }),
130
+ properties: {
131
+ routeSignature: trace.routes[0] ? trace.routes[0].signature : ''
132
+ }
133
+ }
134
+ ];
135
+ sarifResultBuilder.result.locations = [];
136
+ sarifResultBuilder.result.locations.push({
137
+ physicalLocation: {
138
+ artifactLocation: {
139
+ uri: getFileFromTSDescription(trace.trace.sink.label)
140
+ },
141
+ region: {
142
+ startLine: getLineFromTSDescription(trace.trace.sink.label)
143
+ }
144
+ },
145
+ logicalLocations: [
146
+ {
147
+ fullyQualifiedName: getLogicalLocationFromTSDescription(trace.trace.sink.label)
148
+ }
149
+ ]
150
+ });
151
+ sarifRunBuilderIast.addResult(sarifResultBuilder);
152
+ }
153
+ return sarifRunBuilderIast;
154
+ };
155
+ export const generateScaSarifRun = cveList => {
156
+ const sarifRunBuilderSca = new SarifRunBuilder().initSimple({
157
+ toolDriverName: 'contrast-sca',
158
+ toolDriverVersion: '1.0.0',
159
+ url: 'https://contrastsecurity.com' // Url of your analyzer tool
160
+ });
161
+ if (cveList.length === 0) {
162
+ return sarifRunBuilderSca;
163
+ }
164
+ for (const cve of cveList) {
165
+ const sarifResultBuilder = new SarifResultBuilder().initSimple({
166
+ ruleId: cve.name,
167
+ messageText: cve.description,
168
+ level: mapSeverity(cve.severityToUse)
169
+ });
170
+ sarifResultBuilder.setLocationArtifactUri({ uri: cve.library.file_name });
171
+ sarifResultBuilder.result.properties = {
172
+ contrast_severity: cve.severityToUse,
173
+ library_version: cve.library.version,
174
+ cvss_score: cve.cvss_3_severity_value,
175
+ vector: cve.cvss_3_vector
176
+ };
177
+ sarifRunBuilderSca.addResult(sarifResultBuilder);
178
+ }
179
+ return sarifRunBuilderSca;
180
+ };
181
+ export const writeCombinedSarif = (traces, cveList, output) => {
182
+ const sarifBuilder = new SarifBuilder();
183
+ const sarifRunBuilderIast = generateIastSarifRun(traces);
184
+ sarifBuilder.addRun(sarifRunBuilderIast);
185
+ const sarifRunBuilderSca = generateScaSarifRun(cveList);
186
+ sarifBuilder.addRun(sarifRunBuilderSca);
187
+ const sarifJsonString = sarifBuilder.buildSarifJsonString({
188
+ indent: true
189
+ }); // indent:true if you like
190
+ if (output) {
191
+ fs.writeFileSync(output, sarifJsonString);
192
+ }
193
+ else {
194
+ console.log(sarifJsonString);
195
+ }
196
+ };
@@ -78,6 +78,15 @@ export const noProjectUpload = async (analysis, config, reportSpinner) => {
78
78
  doPoll = false;
79
79
  const reportRes = await scaServiceReportNoProject(config, reportID);
80
80
  const reportBody = reportRes.body;
81
+ for (let x in reportBody) {
82
+ let dateTime = new Date();
83
+ if (reportBody[x].vulnerabilities.length === 0) {
84
+ logDebug(config, `${dateTime.toISOString()} Unable to find vulnerabilities for ${reportBody[x].groupName}:${reportBody[x].artifactName}, ${reportBody[x].version}, ${reportBody[x].hash}. Environment: ${config.host}, OrgId: ${config.organizationId}, AppId: ${config.applicationId}`);
85
+ }
86
+ if (reportBody[x].remediationAdvice === null) {
87
+ logDebug(config, `${dateTime.toISOString()} unable to find remediation advice for ${reportBody[x].groupName}:${reportBody[x].artifactName}, ${reportBody[x].version}, ${reportBody[x].hash}. Environment: ${config.host}, OrgId: ${config.organizationId}, AppId: ${config.applicationId}`);
88
+ }
89
+ }
81
90
  if (config.saveResults !== undefined) {
82
91
  fs.writeFileSync('audit-results.json', JSON.stringify(reportBody));
83
92
  }
@@ -7,8 +7,11 @@ import { logDebug } from '../../common/logging.js';
7
7
  import { GO, GRADLE, MAVEN, NODE, YARN } from '../../constants/constants.js';
8
8
  export const determineProjectTypeAndCwd = (files, packageManager, config) => {
9
9
  const projectData = {};
10
+ //clean up the path to be a folder not a file
11
+ projectData.cwd = config.file ? replacePathing(config) : config.file;
10
12
  if (isMaven(files, packageManager)) {
11
13
  projectData.projectType = MAVEN;
14
+ calculateMavenCommand(projectData);
12
15
  }
13
16
  else if (isGradle(files, packageManager)) {
14
17
  projectData.projectType = GRADLE;
@@ -46,7 +49,7 @@ const replacePathing = config => config.file
46
49
  .replace('build.gradle', '')
47
50
  .replace('build.gradle.kts', '');
48
51
  const buildMaven = (config, projectData, timeout) => {
49
- let command = 'mvn';
52
+ let command = projectData.mvnCommand;
50
53
  let args = ['dependency:tree', '-B', '-Dscope=runtime'];
51
54
  if (config.mavenSettingsPath) {
52
55
  args.push('-s');
@@ -61,6 +64,17 @@ const buildMaven = (config, projectData, timeout) => {
61
64
  checkForErrors(cmdDepTree, config);
62
65
  return cmdDepTree.stdout.toString();
63
66
  };
67
+ export const calculateMavenCommand = projectData => {
68
+ if (fs.existsSync(projectData.cwd + '/mvnw')) {
69
+ projectData.mvnCommand = projectData.cwd + '/mvnw';
70
+ if (process.platform === 'win32') {
71
+ projectData.mvnCommand += '.cmd';
72
+ }
73
+ }
74
+ else {
75
+ projectData.mvnCommand = 'mvn';
76
+ }
77
+ };
64
78
  export function checkForErrors(cmdDepTree, config) {
65
79
  checkMavenExists(cmdDepTree, config);
66
80
  if (cmdDepTree.error || cmdDepTree.status !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/contrast",
3
- "version": "2.1.6",
3
+ "version": "2.2.0",
4
4
  "description": "Contrast Security's command line tool",
5
5
  "exports": "./dist/index.js",
6
6
  "type": "module",
@@ -77,14 +77,15 @@
77
77
  "js-yaml": "4.1.0",
78
78
  "lodash-es": "4.17.21",
79
79
  "log-symbols": "4.1.0",
80
+ "node-sarif-builder": "^3.1.0",
80
81
  "open": "8.4.2",
81
82
  "ora": "6.3.1",
83
+ "pkginfo": "0.4.1",
82
84
  "semver": "7.5.4",
83
85
  "string-builder": "0.1.8",
84
86
  "string-multiple-replace": "1.0.5",
85
87
  "xml2js": "0.6.1",
86
- "yarn-lockfile": "1.1.1",
87
- "pkginfo": "0.4.1"
88
+ "yarn-lockfile": "1.1.1"
88
89
  },
89
90
  "devDependencies": {
90
91
  "@babel/core": "7.21.8",
@@ -115,7 +116,7 @@
115
116
  "ts-node": "^10.9.2",
116
117
  "typescript": "5.1.6",
117
118
  "uuid": "9.0.0",
118
- "vitest": "0.33.0"
119
+ "vitest": "1.4.0"
119
120
  },
120
121
  "resolutions": {
121
122
  "faker": "5.5.3",