Skip to content

Commit

Permalink
Merge pull request #1384 from snyk/feat/sarif-support
Browse files Browse the repository at this point in the history
feat: SARIF format support for IaC and containers
  • Loading branch information
RotemS committed Sep 15, 2020
2 parents 481ea44 + 02e9bf8 commit c3212af
Show file tree
Hide file tree
Showing 16 changed files with 870 additions and 61 deletions.
11 changes: 10 additions & 1 deletion help/container.txt
Expand Up @@ -15,10 +15,19 @@ Options:
--exclude-base-image-vulns .............. Exclude from display base image vulnerabilities.
--file=<string> ......................... Include the path to the image's Dockerfile for more detailed advice.
-h, --help
--json
--platform=<string> ..................... For multi-architecture images, specify the platform to test. Options are:
[linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x,
linux/386, linux/arm/v7 orlinux/arm/v6]
--json .................................. Return results in JSON format.
--json-file-output=<string>
(test command only)
Save test output in JSON format directly to the specified file, regardless of whether or not you use the `--json` option.
This is especially useful if you want to display the human-readable test output via stdout and at the same time save the JSON format output to a file.
--sarif ................................. Return results in SARIF format.
--sarif-file-output=<string>
(test command only)
Save test output in SARIF format directly to the specified file, regardless of whether or not you use the `--sarif` option.
This is especially useful if you want to display the human-readable test output via stdout and at the same time save the SARIF format output to a file.
--print-deps ............................ Print the dependency tree before sending it for analysis.
--project-name=<string> ................. Specify a custom Snyk project name.
--policy-path=<path> .................... Manually pass a path to a snyk policy file.
Expand Down
9 changes: 9 additions & 0 deletions help/iac.txt
Expand Up @@ -12,6 +12,15 @@ Options:

-h, --help
--json .................................. Return results in JSON format.
--json-file-output=<string>
(test command only)
Save test output in JSON format directly to the specified file, regardless of whether or not you use the `--json` option.
This is especially useful if you want to display the human-readable test output via stdout and at the same time save the JSON format output to a file.
--sarif ................................. Return results in SARIF format.
--sarif-file-output=<string>
(test command only)
Save test output in SARIF format directly to the specified file, regardless of whether or not you use the `--sarif` option.
This is especially useful if you want to display the human-readable test output via stdout and at the same time save the SARIF format output to a file.
--severity-threshold=<low|medium|high>... Only report issues of provided level or higher.

Examples:
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -107,6 +107,7 @@
"@types/needle": "^2.0.4",
"@types/node": "8.10.59",
"@types/restify": "^8.4.2",
"@types/sarif": "^2.1.2",
"@types/sinon": "^7.5.0",
"@types/update-notifier": "^4.1.0",
"@typescript-eslint/eslint-plugin": "2.18.0",
Expand Down
93 changes: 93 additions & 0 deletions src/cli/commands/test/iac-output.ts
Expand Up @@ -7,6 +7,9 @@ import {
import { getSeverityValue } from './formatters';
import { printPath } from './formatters/remediation-based-format-issues';
import { titleCaseText } from './formatters/legacy-format-issue';
import * as sarif from 'sarif';
import { SEVERITY } from '../../../lib/snyk-test/legacy';
import upperFirst = require('lodash/upperFirst');
const debug = Debug('iac-output');

function formatIacIssue(
Expand Down Expand Up @@ -122,3 +125,93 @@ export function capitalizePackageManager(type) {
}
}
}

export function createSarifOutputForIac(
iacTestResponses: IacTestResponse[],
): sarif.Log {
const sarifRes: sarif.Log = {
version: '2.1.0',
runs: [],
};

iacTestResponses
.filter((iacTestResponse) => iacTestResponse.result?.cloudConfigResults)
.forEach((iacTestResponse) => {
sarifRes.runs.push({
tool: mapIacTestResponseToSarifTool(iacTestResponse),
results: mapIacTestResponseToSarifResults(iacTestResponse),
});
});

return sarifRes;
}

function getIssueLevel(severity: SEVERITY): sarif.ReportingConfiguration.level {
return severity === SEVERITY.HIGH ? 'error' : 'warning';
}

export function mapIacTestResponseToSarifTool(
iacTestResponse: IacTestResponse,
): sarif.Tool {
const tool: sarif.Tool = {
driver: {
name: 'Snyk Infrastructure as Code',
rules: [],
},
};

const pushedIds = {};
iacTestResponse.result.cloudConfigResults.forEach(
(iacIssue: AnnotatedIacIssue) => {
if (pushedIds[iacIssue.id]) {
return;
}
tool.driver.rules?.push({
id: iacIssue.id,
shortDescription: {
text: `${upperFirst(iacIssue.severity)} - ${iacIssue.title}`,
},
fullDescription: {
text: `Kubernetes ${iacIssue.subType}`,
},
help: {
text: '',
markdown: iacIssue.description,
},
defaultConfiguration: {
level: getIssueLevel(iacIssue.severity),
},
properties: {
tags: ['security', `kubernetes/${iacIssue.subType}`],
},
});
pushedIds[iacIssue.id] = true;
},
);
return tool;
}

export function mapIacTestResponseToSarifResults(
iacTestResponse: IacTestResponse,
): sarif.Result[] {
return iacTestResponse.result.cloudConfigResults.map(
(iacIssue: AnnotatedIacIssue) => ({
ruleId: iacIssue.id,
message: {
text: `This line contains a potential ${iacIssue.severity} severity misconfiguration affacting the Kubernetes ${iacIssue.subType}`,
},
locations: [
{
physicalLocation: {
artifactLocation: {
uri: iacTestResponse.targetFile,
},
region: {
startLine: iacIssue.lineNumber,
},
},
},
],
}),
);
}
56 changes: 48 additions & 8 deletions src/cli/commands/test/index.ts
Expand Up @@ -13,6 +13,7 @@ import {
ShowVulnPaths,
SupportedProjectTypes,
TestOptions,
OutputDataTypes,
} from '../../../lib/types';
import { isLocalFolder } from '../../../lib/detect';
import { MethodArgs } from '../../args';
Expand Down Expand Up @@ -47,10 +48,11 @@ import {
summariseVulnerableResults,
} from './formatters';
import * as utils from './utils';
import { getIacDisplayedOutput } from './iac-output';
import { getIacDisplayedOutput, createSarifOutputForIac } from './iac-output';
import { getEcosystem, testEcosystem } from '../../../lib/ecosystems';
import { TestLimitReachedError } from '../../../lib/errors';
import { isMultiProjectScan } from '../../../lib/is-multi-project-scan';
import { createSarifOutputForContainers } from './sarif-output';

const debug = Debug('snyk-test');
const SEPARATOR = '\n-------------------------------------------------------\n';
Expand Down Expand Up @@ -206,13 +208,19 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
: results.map(mapIacTestResult);

// backwards compat - strip array IFF only one result
const dataToSend =
const jsonData =
errorMappedResults.length === 1
? errorMappedResults[0]
: errorMappedResults;
const stringifiedData = JSON.stringify(dataToSend, null, 2);

if (options.json) {
const {
stdout: dataToSend,
stringifiedData,
stringifiedJsonData,
stringifiedSarifData,
} = extractDataToSendFromResults(results, jsonData, options);

if (options.json || options.sarif) {
// if all results are ok (.ok == true) then return the json
if (errorMappedResults.every((res) => res.ok)) {
return TestCommandResult.createJsonTestCommandResult(stringifiedData);
Expand All @@ -235,7 +243,8 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
}

err.json = stringifiedData;
err.jsonStringifiedResults = stringifiedData;
err.jsonStringifiedResults = stringifiedJsonData;
err.sarifStringifiedResults = stringifiedSarifData;
throw err;
}

Expand Down Expand Up @@ -300,7 +309,8 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
response += chalk.bold.green(summaryMessage);
return TestCommandResult.createHumanReadableTestCommandResult(
response,
stringifiedData,
stringifiedJsonData,
stringifiedSarifData,
);
}
}
Expand All @@ -313,14 +323,16 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
// first one
error.code = vulnerableResults[0].code || 'VULNS';
error.userMessage = vulnerableResults[0].userMessage;
error.jsonStringifiedResults = stringifiedData;
error.jsonStringifiedResults = stringifiedJsonData;
error.sarifStringifiedResults = stringifiedSarifData;
throw error;
}

response += chalk.bold.green(summaryMessage);
return TestCommandResult.createHumanReadableTestCommandResult(
response,
stringifiedData,
stringifiedJsonData,
stringifiedSarifData,
);
}

Expand Down Expand Up @@ -744,3 +756,31 @@ function dockerUserCTA(options) {
}
return '';
}

function extractDataToSendFromResults(
results,
jsonData,
options: Options,
): OutputDataTypes {
let sarifData = {};
if (options.sarif || options['sarif-file-output']) {
sarifData = !options.iac
? createSarifOutputForContainers(results)
: createSarifOutputForIac(results);
}

const stringifiedJsonData = JSON.stringify(jsonData, null, 2);
const stringifiedSarifData = JSON.stringify(sarifData, null, 2);

const dataToSend = options.sarif ? sarifData : jsonData;
const stringifiedData = options.sarif
? stringifiedSarifData
: stringifiedJsonData;

return {
stdout: dataToSend,
stringifiedData,
stringifiedJsonData,
stringifiedSarifData,
};
}
93 changes: 93 additions & 0 deletions src/cli/commands/test/sarif-output.ts
@@ -0,0 +1,93 @@
import * as sarif from 'sarif';

export function createSarifOutputForContainers(testResult): sarif.Log {
const sarifRes: sarif.Log = {
version: '2.1.0',
runs: [],
};

testResult.forEach((testResult) => {
sarifRes.runs.push({
tool: getTool(testResult),
results: getResults(testResult),
});
});

return sarifRes;
}

export function getTool(testResult): sarif.Tool {
const tool: sarif.Tool = {
driver: {
name: 'Snyk Container',
rules: [],
},
};

if (!testResult.vulnerabilities) {
return tool;
}

const pushedIds = {};
tool.driver.rules = testResult.vulnerabilities
.map((vuln) => {
if (pushedIds[vuln.id]) {
return;
}
const level = vuln.severity === 'high' ? 'error' : 'warning';
const cve = vuln['identifiers']['CVE'][0];
pushedIds[vuln.id] = true;
return {
id: vuln.id,
shortDescription: {
text: `${vuln.severity} severity ${vuln.title} vulnerability in ${vuln.packageName}`,
},
fullDescription: {
text: cve
? `(${cve}) ${vuln.name}@${vuln.version}`
: `${vuln.name}@${vuln.version}`,
},
help: {
text: '',
markdown: vuln.description,
},
defaultConfiguration: {
level: level,
},
properties: {
tags: ['security', ...vuln.identifiers.CWE],
},
};
})
.filter(Boolean);
return tool;
}

export function getResults(testResult): sarif.Result[] {
const results: sarif.Result[] = [];

if (!testResult.vulnerabilities) {
return results;
}
testResult.vulnerabilities.forEach((vuln) => {
results.push({
ruleId: vuln.id,
message: {
text: `This file introduces a vulnerable ${vuln.packageName} package with a ${vuln.severity} severity vulnerability.`,
},
locations: [
{
physicalLocation: {
artifactLocation: {
uri: testResult.displayTargetFile,
},
region: {
startLine: vuln.lineNumber || 1,
},
},
},
],
});
});
return results;
}

0 comments on commit c3212af

Please sign in to comment.