@contrast/contrast 2.1.6 → 2.1.7

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.
@@ -514,6 +514,7 @@ const mainUsageGuide = commandLineUsage([
514
514
  { name: i18n.__('lambdaName'), summary: i18n.__('helpLambdaSummary') },
515
515
  { name: i18n.__('helpName'), summary: i18n.__('helpSummary') },
516
516
  { name: i18n.__('learnName'), summary: i18n.__('helpLearnSummary') },
517
+ // { name: i18n.__('sarifName'), summary: i18n.__('sarifSummary') },
517
518
  {
518
519
  name: i18n.__('configGenerate'),
519
520
  summary: i18n.__('configGenerateSummary')
@@ -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.1.7';
21
21
  export const TIMEOUT = 120000;
22
22
  export const CRITICAL_PRIORITY = 1;
23
23
  export const HIGH_PRIORITY = 2;
@@ -126,6 +126,7 @@ export const en_locales = () => {
126
126
  configName: 'config',
127
127
  helpName: 'help',
128
128
  learnName: 'learn',
129
+ sarifName: 'sarif',
129
130
  configGenerate: 'generate-config',
130
131
  helpLearnSummary: 'Launches Contrast’s Secure Code Learning Hub.',
131
132
  configGenerateSummary: 'Generates contrast configuration yaml file.',
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 processSarif(config, argvMain);
82
+ }
82
83
  if (command === 'learn') {
83
84
  return processLearn();
84
85
  }
@@ -0,0 +1,17 @@
1
+ import { getAuditConfig } from '../audit/auditConfig.js';
2
+ import { scaCVES } from './sarifScaClient.js';
3
+ import { iastData } from './sarifIastClient.js';
4
+ import { writeCombinedSarif } from './sarifWriter.js';
5
+ // This filename could be set by a customer
6
+ // Defaulted to be ingested in a GH workflow
7
+ const outputFileName = 'contrast.sarif.json';
8
+ export const processSarif = async (contrastConf, argvMain) => {
9
+ console.log('Generating SARIF export');
10
+ let config = await getAuditConfig(contrastConf, 'audit', argvMain);
11
+ const scaVulns = await scaCVES(config);
12
+ console.log(`Found ${scaVulns.length} SCA vulnerabilities`);
13
+ const iastVulns = await iastData(config);
14
+ console.log(`Found ${iastVulns.length} IAST vulnerabilities`);
15
+ writeCombinedSarif(iastVulns, scaVulns, outputFileName);
16
+ console.log('SARIF generated');
17
+ };
@@ -0,0 +1,39 @@
1
+ import { buildBaseRequestOptions, ErrorType } from '../common/baseRequest.js';
2
+ import { got } from 'got';
3
+ const getAssessTraceIdsUrl = (config, offset, resultsLimit) => {
4
+ return `${config.host}/Contrast/api/ng/${config.organizationId}/orgtraces/filter/?offset=${offset}&limit=${resultsLimit}`;
5
+ };
6
+ export function getAssessTraceIds(config, offset, resultsLimit) {
7
+ const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
8
+ options.url = getAssessTraceIdsUrl(config, offset, resultsLimit);
9
+ options.json = {
10
+ applicationID: config.applicationId,
11
+ // metadataFilters: sessionMetadata,
12
+ severities: [config.severity]
13
+ };
14
+ return got.post(options);
15
+ }
16
+ const getAssessTraceDetailsUrl = (config, traceId) => {
17
+ return `${config.host}/Contrast/api/ng/${config.organizationId}/traces/${config.applicationId}/filter/${traceId}?expand=events,request,sink,${traceId},skip_links`;
18
+ };
19
+ export function getAssessTraceDetails(config, traceId) {
20
+ const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
21
+ options.url = getAssessTraceDetailsUrl(config, traceId);
22
+ return got.get(options);
23
+ }
24
+ const getAssessTraceEventsUrl = (config, traceId, eventId) => {
25
+ return `${config.host}/Contrast/api/ng/${config.organizationId}/traces/${traceId}/events/${eventId}/details`;
26
+ };
27
+ export function getAssessTraceEvents(config, traceId, eventId) {
28
+ const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
29
+ options.url = getAssessTraceEventsUrl(config, traceId, eventId);
30
+ return got.get(options);
31
+ }
32
+ const getAssessTraceRoutesUrl = (config, traceId) => {
33
+ return `${config.host}/Contrast/api/ng/${config.organizationId}/traces/${config.applicationId}/trace/${traceId}/routes`;
34
+ };
35
+ export function getAssessTraceRoutes(config, traceId) {
36
+ const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
37
+ options.url = getAssessTraceRoutesUrl(config, traceId);
38
+ return got.get(options);
39
+ }
@@ -0,0 +1,65 @@
1
+ import { getAssessTraceDetails, getAssessTraceEvents, getAssessTraceIds, getAssessTraceRoutes } from './sarifAssessRequests.js';
2
+ const resultsLimit = 50;
3
+ let offset = 0;
4
+ export const iastData = async (config) => {
5
+ const filteredTraceIds = await getTraceIds(config, offset, resultsLimit);
6
+ console.log(`Found ${filteredTraceIds.length} traces`);
7
+ let completeTraces = [];
8
+ for (const traceId of filteredTraceIds) {
9
+ const traceDetails = await getTraceDetails(config, traceId);
10
+ const traceRoutes = await getTraceRoutes(config, traceId);
11
+ let eventsWithFrames = [];
12
+ if (traceDetails.events) {
13
+ for (const eventSummary of traceDetails.events) {
14
+ const eventDetails = await getTraceEvents(config, traceId, eventSummary.eventId);
15
+ eventsWithFrames.push(eventDetails);
16
+ }
17
+ }
18
+ completeTraces.push({
19
+ trace: traceDetails,
20
+ events: eventsWithFrames,
21
+ routes: traceRoutes
22
+ });
23
+ }
24
+ return completeTraces;
25
+ };
26
+ export async function getTraceIds(config, offset, resultsLimit) {
27
+ let hasResults = true;
28
+ let pivotedData = [];
29
+ while (hasResults) {
30
+ const response = getAssessTraceIds(config, offset, resultsLimit);
31
+ const responseBody = await response.json();
32
+ if (responseBody.traces == null) {
33
+ console.log('No libraries found');
34
+ return pivotedData;
35
+ }
36
+ const result = responseBody.traces;
37
+ const traceIds = result.map(trace => {
38
+ return trace.uuid;
39
+ });
40
+ pivotedData = pivotedData.concat(traceIds);
41
+ if (result.length < resultsLimit) {
42
+ hasResults = false;
43
+ }
44
+ offset += resultsLimit;
45
+ }
46
+ return pivotedData;
47
+ }
48
+ export async function getTraceDetails(config, traceId) {
49
+ console.log(`Getting trace details for ${traceId} of application ${config.applicationId}`);
50
+ const response = getAssessTraceDetails(config, traceId);
51
+ const responseBody = await response.json();
52
+ return responseBody.trace;
53
+ }
54
+ export async function getTraceEvents(config, traceId, eventId) {
55
+ const response = getAssessTraceEvents(config, traceId, eventId);
56
+ console.log(`Getting trace events for ${eventId} of trace ${traceId}`);
57
+ const responseBody = await response.json();
58
+ return responseBody.event;
59
+ }
60
+ export async function getTraceRoutes(config, traceId) {
61
+ console.log(`Getting trace route for ${traceId} of application ${config.applicationId}`);
62
+ const response = getAssessTraceRoutes(config, traceId);
63
+ const responseBody = await response.json();
64
+ return responseBody.routes;
65
+ }
@@ -0,0 +1,25 @@
1
+ import { buildBaseRequestOptions, ErrorType } from '../common/baseRequest.js';
2
+ import { got } from 'got';
3
+ const getSCAVulnsUrl = (config, offset, resultsLimit) => {
4
+ 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`;
5
+ };
6
+ export function getSCAVulns(config, offset, resultsLimit) {
7
+ const options = buildBaseRequestOptions(config, ErrorType.GENERIC);
8
+ options.url = getSCAVulnsUrl(config, offset, resultsLimit);
9
+ options.json = {
10
+ q: '',
11
+ quickFilter: 'VULNERABLE',
12
+ apps: [config.applicationId],
13
+ servers: [],
14
+ environments: [],
15
+ grades: [],
16
+ languages: [],
17
+ licenses: [],
18
+ status: [],
19
+ severities: [config.severity],
20
+ tags: [],
21
+ includeUnused: false,
22
+ includeUsed: false
23
+ };
24
+ return got.post(options);
25
+ }
@@ -0,0 +1,35 @@
1
+ import { getSCAVulns } from './sarifSCARequests.js';
2
+ const resultsLimit = 50;
3
+ let offset = 0;
4
+ export const scaCVES = async (config) => {
5
+ let hasResults = true;
6
+ let pivotedData = [];
7
+ while (hasResults) {
8
+ const response = getSCAVulns(config, offset, resultsLimit);
9
+ const responseBody = await response.json();
10
+ if (responseBody.libraries == null) {
11
+ console.log('No libraries found');
12
+ return pivotedData;
13
+ }
14
+ // Finds all libraries with a vulnerability of specified severity
15
+ const result = responseBody.libraries.flatMap(item => {
16
+ const { vulns } = item;
17
+ return vulns.flatMap(vuln => {
18
+ vuln.library = item;
19
+ return vuln;
20
+ });
21
+ });
22
+ // All vulns associated with the library will be returned,
23
+ // so we must filter by severity again
24
+ for (const vuln of result) {
25
+ if (vuln.severityToUse.toLowerCase() === config.severity) {
26
+ pivotedData.push(vuln);
27
+ }
28
+ }
29
+ if (result.length < resultsLimit) {
30
+ hasResults = false;
31
+ }
32
+ offset += resultsLimit;
33
+ }
34
+ return pivotedData;
35
+ };
@@ -0,0 +1,156 @@
1
+ import { SarifBuilder, SarifRunBuilder, SarifResultBuilder } 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]) : 0;
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.sink && trace.sink.label) {
46
+ extractedString = getFileFromTSDescription(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
+ if (extractedString) {
54
+ sarifResultBuilder.setLocationArtifactUri({ uri: extractedString });
55
+ }
56
+ if (trace.trace.request) {
57
+ sarifResultBuilder.result.webRequest = {
58
+ target: trace.trace.request.uri,
59
+ method: trace.trace.request.method,
60
+ protocol: trace.trace.request.protocol,
61
+ version: trace.trace.request.version
62
+ };
63
+ }
64
+ sarifResultBuilder.result.provenance = {
65
+ firstDetectionTimeUtc: new Date(trace.trace.discovered).toISOString(),
66
+ lastDetectionTimeUtc: new Date(trace.trace.last_time_seen).toISOString()
67
+ };
68
+ sarifResultBuilder.result.properties = {
69
+ vulnerability_id: trace.trace.uuid,
70
+ contrast_severity: trace.trace.severity,
71
+ contrast_status: trace.trace.status,
72
+ contrast_substatus: trace.trace.sub_status
73
+ };
74
+ // Add stack trace
75
+ sarifResultBuilder.result.codeFlows = [
76
+ {
77
+ threadFlows: trace.events.map(event => {
78
+ return {
79
+ locations: [
80
+ {
81
+ stack: {
82
+ frames: event.stacktraces.map(stacktrace => {
83
+ return {
84
+ location: {
85
+ physicalLocation: {
86
+ artifactLocation: {
87
+ uri: getFileFromTSDescription(stacktrace.description)
88
+ },
89
+ region: {
90
+ startLine: getLineFromTSDescription(stacktrace.description)
91
+ }
92
+ },
93
+ logicalLocations: [
94
+ {
95
+ fullyQualifiedName: getLogicalLocationFromTSDescription(stacktrace.description)
96
+ }
97
+ ]
98
+ }
99
+ };
100
+ })
101
+ }
102
+ }
103
+ ]
104
+ };
105
+ }),
106
+ properties: {
107
+ routeSignature: trace.routes[0] ? trace.routes[0].signature : ''
108
+ }
109
+ }
110
+ ];
111
+ sarifRunBuilderIast.addResult(sarifResultBuilder);
112
+ }
113
+ return sarifRunBuilderIast;
114
+ };
115
+ export const generateScaSarifRun = cveList => {
116
+ const sarifRunBuilderSca = new SarifRunBuilder().initSimple({
117
+ toolDriverName: 'contrast-sca',
118
+ toolDriverVersion: '1.0.0',
119
+ url: 'https://contrastsecurity.com' // Url of your analyzer tool
120
+ });
121
+ if (cveList.length === 0) {
122
+ return sarifRunBuilderSca;
123
+ }
124
+ for (const cve of cveList) {
125
+ const sarifResultBuilder = new SarifResultBuilder().initSimple({
126
+ ruleId: cve.name,
127
+ messageText: cve.description,
128
+ level: mapSeverity(cve.severityToUse)
129
+ });
130
+ sarifResultBuilder.setLocationArtifactUri({ uri: cve.library.file_name });
131
+ sarifResultBuilder.result.properties = {
132
+ contrast_severity: cve.severityToUse,
133
+ library_version: cve.library.version,
134
+ cvss_score: cve.cvss_3_severity_value,
135
+ vector: cve.cvss_3_vector
136
+ };
137
+ sarifRunBuilderSca.addResult(sarifResultBuilder);
138
+ }
139
+ return sarifRunBuilderSca;
140
+ };
141
+ export const writeCombinedSarif = (traces, cveList, output) => {
142
+ const sarifBuilder = new SarifBuilder();
143
+ const sarifRunBuilderIast = generateIastSarifRun(traces);
144
+ sarifBuilder.addRun(sarifRunBuilderIast);
145
+ const sarifRunBuilderSca = generateScaSarifRun(cveList);
146
+ sarifBuilder.addRun(sarifRunBuilderSca);
147
+ const sarifJsonString = sarifBuilder.buildSarifJsonString({
148
+ indent: true
149
+ }); // indent:true if you like
150
+ if (output) {
151
+ fs.writeFileSync(output, sarifJsonString);
152
+ }
153
+ else {
154
+ console.log(sarifJsonString);
155
+ }
156
+ };
@@ -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.1.7",
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",