-
Notifications
You must be signed in to change notification settings - Fork 552
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1622 from snyk/feat/open-source-sarif
feat: add SARIF support for snyk open-source
- Loading branch information
Showing
8 changed files
with
3,958 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import * as sarif from 'sarif'; | ||
import * as _ from 'lodash'; | ||
import { upperFirst } from 'lodash'; | ||
import { | ||
TestResult, | ||
SEVERITY, | ||
AnnotatedIssue, | ||
} from '../../../lib/snyk-test/legacy'; | ||
|
||
const LOCK_FILES_TO_MANIFEST_MAP = { | ||
'Gemfile.lock': 'Gemfile', | ||
'package-lock.json': 'package.json', | ||
'yarn.lock': 'package.json', | ||
'Gopkg.lock': 'Gopkg.toml', | ||
'go.sum': 'go.mod', | ||
'composer.lock': 'composer.json', | ||
'Podfile.lock': 'Podfile', | ||
'poetry.lock': 'pyproject.toml', | ||
}; | ||
|
||
export function createSarifOutputForOpenSource( | ||
testResults: TestResult[], | ||
): sarif.Log { | ||
return { | ||
$schema: | ||
'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json', | ||
version: '2.1.0', | ||
runs: testResults.map(replaceLockfileWithManifest).map((testResult) => ({ | ||
tool: { | ||
driver: { | ||
name: 'Snyk Open Source', | ||
rules: getRules(testResult), | ||
}, | ||
}, | ||
results: getResults(testResult), | ||
})), | ||
}; | ||
} | ||
|
||
function replaceLockfileWithManifest(testResult: TestResult): TestResult { | ||
let targetFile = testResult.displayTargetFile || ''; | ||
for (const [key, replacer] of Object.entries(LOCK_FILES_TO_MANIFEST_MAP)) { | ||
targetFile = targetFile.replace(new RegExp(key, 'g'), replacer); | ||
} | ||
return { | ||
...testResult, | ||
vulnerabilities: testResult.vulnerabilities || [], | ||
displayTargetFile: targetFile, | ||
}; | ||
} | ||
|
||
export function getRules(testResult: TestResult): sarif.ReportingDescriptor[] { | ||
return _.chain(testResult.vulnerabilities) | ||
.groupBy('id') | ||
.values() | ||
.map( | ||
([vuln, ...moreVulns]): sarif.ReportingDescriptor => { | ||
const cves = vuln.identifiers?.CVE?.join(); | ||
return { | ||
id: vuln.id, | ||
shortDescription: { | ||
text: `${upperFirst(vuln.severity)} severity - ${ | ||
vuln.title | ||
} vulnerability in ${vuln.packageName}`, | ||
}, | ||
fullDescription: { | ||
text: cves | ||
? `(${cves}) ${vuln.name}@${vuln.version}` | ||
: `${vuln.name}@${vuln.version}`, | ||
}, | ||
help: { | ||
text: '', | ||
markdown: `* Package Manager: ${testResult.packageManager} | ||
* ${vuln.type === 'license' ? 'Module' : 'Vulnerable module'}: ${vuln.name} | ||
* Introduced through: ${getIntroducedThrough(vuln)} | ||
#### Detailed paths | ||
${[vuln, ...moreVulns] | ||
.map((item) => `* _Introduced through_: ${item.from.join(' › ')}`) | ||
.join('\n')} | ||
${vuln.description}`.replace(/##\s/g, '# '), | ||
}, | ||
properties: { | ||
tags: [ | ||
'security', | ||
...(vuln.identifiers?.CWE || []), | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
testResult.packageManager!, | ||
], | ||
}, | ||
}; | ||
}, | ||
) | ||
.value(); | ||
} | ||
|
||
export function getResults(testResult): sarif.Result[] { | ||
return testResult.vulnerabilities.map((vuln) => ({ | ||
ruleId: vuln.id, | ||
level: getLevel(vuln), | ||
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, | ||
}, | ||
}, | ||
}, | ||
], | ||
})); | ||
} | ||
|
||
export function getLevel(vuln: AnnotatedIssue) { | ||
switch (vuln.severity) { | ||
case SEVERITY.HIGH: | ||
return 'error'; | ||
case SEVERITY.MEDIUM: | ||
return 'warning'; | ||
case SEVERITY.LOW: | ||
default: | ||
return 'note'; | ||
} | ||
} | ||
|
||
function getIntroducedThrough(vuln: AnnotatedIssue) { | ||
const [firstFrom, secondFrom] = vuln.from || []; | ||
|
||
return vuln.from.length > 2 | ||
? `${firstFrom}, ${secondFrom} and others` | ||
: vuln.from.length === 2 | ||
? `${firstFrom} and ${secondFrom}` | ||
: firstFrom; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file Gemfile.lock 1`] = `"Gemfile"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file Gopkg.lock 1`] = `"Gopkg.toml"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file Podfile.lock 1`] = `"Podfile"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file composer.lock 1`] = `"composer.json"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file go.sum 1`] = `"go.mod"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file package-lock.json 1`] = `"package.json"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file poetry.lock 1`] = `"pyproject.toml"`; | ||
|
||
exports[`createSarifOutputForOpenSource replace lock-file to manifest-file yarn.lock 1`] = `"package.json"`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { createSarifOutputForOpenSource } from '../src/cli/commands/test/open-source-sarif-output'; | ||
import { SEVERITY, TestResult } from '../src/lib/snyk-test/legacy'; | ||
|
||
describe('createSarifOutputForOpenSource', () => { | ||
it('general', () => { | ||
const testFile = getTestResult(); | ||
const sarif = createSarifOutputForOpenSource([testFile]); | ||
expect(sarif.runs).toHaveLength(1); | ||
|
||
const [run] = sarif.runs; | ||
expect(run.tool.driver.name).toEqual('Snyk Open Source'); | ||
expect(run.tool.driver.rules).toHaveLength(1); | ||
expect(run.results).toHaveLength(1); | ||
}); | ||
|
||
describe('replace lock-file to manifest-file', () => { | ||
const lockFiles = [ | ||
'Gemfile.lock', | ||
'package-lock.json', | ||
'yarn.lock', | ||
'Gopkg.lock', | ||
'go.sum', | ||
'composer.lock', | ||
'Podfile.lock', | ||
'poetry.lock', | ||
]; | ||
|
||
lockFiles.forEach((lockFileName) => | ||
it(lockFileName, () => { | ||
const time = Date.now(); | ||
const testFile = getTestResult({ | ||
displayTargetFile: `${time}/${lockFileName}`, | ||
}); | ||
const sarif = createSarifOutputForOpenSource([testFile]); | ||
const uri = sarif.runs?.[0]?.results?.[0].locations?.[0]?.physicalLocation?.artifactLocation?.uri?.replace( | ||
`${time}/`, | ||
'', | ||
); | ||
expect(uri).toMatchSnapshot(); | ||
}), | ||
); | ||
}); | ||
}); | ||
|
||
function getTestResult(testResultOverride = {}, vulnOverride = {}): TestResult { | ||
return { | ||
vulnerabilities: [ | ||
{ | ||
below: '', | ||
credit: ['Unknown'], | ||
description: '## Overview\n', | ||
fixedIn: ['6.12.3'], | ||
id: 'SNYK-JS-AJV-584908', | ||
identifiers: { | ||
CVE: ['CVE-2020-15366'], | ||
CWE: ['CWE-400'], | ||
}, | ||
moduleName: 'ajv', | ||
packageManager: 'npm', | ||
packageName: 'ajv', | ||
patches: [], | ||
publicationTime: '2020-07-16T13:58:04Z', | ||
semver: { | ||
vulnerable: ['<6.12.3'], | ||
}, | ||
severity: SEVERITY.HIGH, | ||
title: 'Prototype Pollution', | ||
from: [ | ||
'PROJECT_NAME@1.0.0', | ||
'jimp@0.2.28', | ||
'request@2.88.2', | ||
'har-validator@5.1.3', | ||
'ajv@6.12.2', | ||
], | ||
upgradePath: [ | ||
false, | ||
'jimp@0.2.28', | ||
'request@2.88.2', | ||
'har-validator@5.1.3', | ||
'ajv@6.12.3', | ||
], | ||
isUpgradable: true, | ||
isPatchable: false, | ||
name: 'ajv', | ||
version: '6.12.2', | ||
__filename: 'node_modules/ajv/package.json', | ||
parentDepType: 'prod', | ||
isNew: false, | ||
...vulnOverride, | ||
}, | ||
], | ||
ok: false, | ||
dependencyCount: 969, | ||
org: 'ORG', | ||
policy: '', | ||
isPrivate: true, | ||
licensesPolicy: { | ||
severities: {}, | ||
orgLicenseRules: {}, | ||
}, | ||
packageManager: 'npm', | ||
ignoreSettings: null, | ||
summary: '165 vulnerable dependency paths', | ||
filesystemPolicy: false, | ||
uniqueCount: 42, | ||
projectName: 'PROJECT_NAME', | ||
foundProjectCount: 22, | ||
displayTargetFile: 'package.json', | ||
...testResultOverride, | ||
}; | ||
} |
Oops, something went wrong.