From 8bbdc8414d4125b3d1356619b095aaff3441f0c0 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Thu, 28 Nov 2019 00:54:50 +0900 Subject: [PATCH 1/4] [trivy] Modify a method to validate trivy option --- dist/index.js | 44 ++++++++++++++++++++++++++++--------------- src/index.ts | 6 ++++-- src/trivy.ts | 52 ++++++++++++++++++++++++++++++++------------------- 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/dist/index.js b/dist/index.js index 257458f..8203997 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6588,14 +6588,15 @@ function run() { }; const downloader = new trivy_1.Downloader(); const trivyCmdPath = yield downloader.download(trivyVersion); - const result = trivy_1.Trivy.scan(trivyCmdPath, image, trivyOption); + const trivy = new trivy_1.Trivy(); + const result = trivy.scan(trivyCmdPath, image, trivyOption); if (!issueFlag) { core.info(`Not create a issue because issue parameter is false. Vulnerabilities: ${result}`); return; } - const issueContent = trivy_1.Trivy.parse(result); + const issueContent = trivy.parse(result); if (issueContent === '') { core.info('Vulnerabilities were not found.\nYour maintenance looks good 👍'); return; @@ -13315,8 +13316,8 @@ Downloader.trivyRepository = { repo: 'trivy', }; class Trivy { - static scan(trivyPath, image, option) { - Trivy.validateOption(option); + scan(trivyPath, image, option) { + this.validateOption(option); const args = [ '--severity', option.severity, @@ -13345,7 +13346,7 @@ class Trivy { erorr: ${result.error} `); } - static parse(vulnerabilities) { + parse(vulnerabilities) { let issueContent = ''; for (const vuln of vulnerabilities) { if (vuln.Vulnerabilities === null) @@ -13368,23 +13369,36 @@ class Trivy { } return issueContent; } - static validateOption(option) { + validateOption(option) { + this.validateSeverity(option.severity.split(',')); + this.validateVulnType(option.vulnType.split(',')); + } + validateSeverity(severities) { const allowedSeverities = /UNKNOWN|LOW|MEDIUM|HIGH|CRITICAL/; - const allowedVulnTypes = /os|library/; - for (const severity of option.severity.split(',')) { - if (!allowedSeverities.test(severity)) { - throw new Error(`severity option error: ${severity} is unknown severity`); - } + if (!validateArrayOption(allowedSeverities, severities)) { + throw new Error(`Trivy option error: ${severities.join(',')} is unknown severity. + Trivy supports UNKNOWN, LOW, MEDIUM, HIGH and CRITICAL.`); } - for (const vulnType of option.vulnType.split(',')) { - if (!allowedVulnTypes.test(vulnType)) { - throw new Error(`vuln-type option error: ${vulnType} is unknown vuln-type`); - } + return true; + } + validateVulnType(vulnTypes) { + const allowedVulnTypes = /os|library/; + if (!validateArrayOption(allowedVulnTypes, vulnTypes)) { + throw new Error(`Trivy option error: ${vulnTypes.join(',')} is unknown vuln-type. + Trivy supports os and library.`); } return true; } } exports.Trivy = Trivy; +function validateArrayOption(allowedValue, options) { + for (const option of options) { + if (!allowedValue.test(option)) { + return false; + } + } + return true; +} /***/ }), diff --git a/src/index.ts b/src/index.ts index 043af89..ed6dfaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,9 @@ async function run() { const downloader = new Downloader(); const trivyCmdPath: string = await downloader.download(trivyVersion); - const result: Vulnerability[] | string = Trivy.scan( + + const trivy = new Trivy(); + const result: Vulnerability[] | string = trivy.scan( trivyCmdPath, image, trivyOption @@ -45,7 +47,7 @@ async function run() { return; } - const issueContent: string = Trivy.parse(result as Vulnerability[]); + const issueContent: string = trivy.parse(result as Vulnerability[]); if (issueContent === '') { core.info( diff --git a/src/trivy.ts b/src/trivy.ts index c1c1b23..eae3f0e 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -6,6 +6,7 @@ import fetch, { Response } from 'node-fetch'; import { spawnSync, SpawnSyncReturns } from 'child_process'; import { TrivyOption, Vulnerability } from './interface'; +import { defaultCoreCipherList } from 'constants'; interface Repository { owner: string; @@ -119,12 +120,12 @@ export class Downloader { } export class Trivy { - static scan( + public scan( trivyPath: string, image: string, option: TrivyOption ): Vulnerability[] | string { - Trivy.validateOption(option); + this.validateOption(option); const args: string[] = [ '--severity', @@ -159,7 +160,7 @@ export class Trivy { `); } - static parse(vulnerabilities: Vulnerability[]): string { + public parse(vulnerabilities: Vulnerability[]): string { let issueContent: string = ''; for (const vuln of vulnerabilities) { @@ -187,26 +188,39 @@ export class Trivy { return issueContent; } - static validateOption(option: TrivyOption): boolean { - const allowedSeverities = /UNKNOWN|LOW|MEDIUM|HIGH|CRITICAL/; - const allowedVulnTypes = /os|library/; + private validateOption(option: TrivyOption): void { + this.validateSeverity(option.severity.split(',')); + this.validateVulnType(option.vulnType.split(',')); + } - for (const severity of option.severity.split(',')) { - if (!allowedSeverities.test(severity)) { - throw new Error( - `severity option error: ${severity} is unknown severity` - ); - } + private validateSeverity(severities: string[]): boolean { + const allowedSeverities = /UNKNOWN|LOW|MEDIUM|HIGH|CRITICAL/; + if (!validateArrayOption(allowedSeverities, severities)) { + throw new Error( + `Trivy option error: ${severities.join(',')} is unknown severity. + Trivy supports UNKNOWN, LOW, MEDIUM, HIGH and CRITICAL.` + ); } + return true; + } - for (const vulnType of option.vulnType.split(',')) { - if (!allowedVulnTypes.test(vulnType)) { - throw new Error( - `vuln-type option error: ${vulnType} is unknown vuln-type` - ); - } + private validateVulnType(vulnTypes: string[]): boolean { + const allowedVulnTypes = /os|library/; + if (!validateArrayOption(allowedVulnTypes, vulnTypes)) { + throw new Error( + `Trivy option error: ${vulnTypes.join(',')} is unknown vuln-type. + Trivy supports os and library.` + ); } - return true; } } + +function validateArrayOption(allowedValue: RegExp, options: string[]): boolean { + for (const option of options) { + if (!allowedValue.test(option)) { + return false; + } + } + return true; +} From 4705edc0ac27d875e6443012cb38b6fcd505a79f Mon Sep 17 00:00:00 2001 From: homoluctus Date: Thu, 28 Nov 2019 01:00:46 +0900 Subject: [PATCH 2/4] [tests] Add validation methods unit test --- __tests__/trivy.test.ts | 103 +++++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 17 deletions(-) diff --git a/__tests__/trivy.test.ts b/__tests__/trivy.test.ts index 447874a..760d82c 100644 --- a/__tests__/trivy.test.ts +++ b/__tests__/trivy.test.ts @@ -3,6 +3,7 @@ import { unlinkSync, writeFileSync } from 'fs'; import { Vulnerability, TrivyOption } from '../src/interface'; const downloader = new Downloader(); +const trivy = new Trivy(); function removeTrivyCmd(path: string) { path = path.replace(/\/trivy$/, ''); @@ -109,7 +110,7 @@ describe('Trivy command', () => { }); }); -describe('Scan', () => { +describe('Trivy scan', () => { let trivyPath: string; const image: string = 'alpine:3.10'; @@ -123,49 +124,49 @@ describe('Scan', () => { removeTrivyCmd(trivyPath); }); - test('with valid options', () => { - const options: TrivyOption = { + test('with valid option', () => { + const option: TrivyOption = { severity: 'HIGH,CRITICAL', vulnType: 'os,library', ignoreUnfixed: true, format: 'json', }; - const result: Vulnerability[] | string = Trivy.scan( + const result: Vulnerability[] | string = trivy.scan( trivyPath, image, - options + option ); expect(result.length).toBeGreaterThanOrEqual(1); expect(result).toBeInstanceOf(Object); }); test('without ignoreUnfixed', () => { - const options: TrivyOption = { + const option: TrivyOption = { severity: 'HIGH,CRITICAL', vulnType: 'os,library', ignoreUnfixed: false, format: 'json', }; - const result: Vulnerability[] | string = Trivy.scan( + const result: Vulnerability[] | string = trivy.scan( trivyPath, image, - options + option ); expect(result.length).toBeGreaterThanOrEqual(1); expect(result).toBeInstanceOf(Object); }); test('with table format', () => { - const options: TrivyOption = { + const option: TrivyOption = { severity: 'HIGH,CRITICAL', vulnType: 'os,library', ignoreUnfixed: false, format: 'table', }; - const result: Vulnerability[] | string = Trivy.scan( + const result: Vulnerability[] | string = trivy.scan( trivyPath, image, - options + option ); expect(result.length).toBeGreaterThanOrEqual(1); expect(result).toMatch(/alpine:3\.10/); @@ -179,8 +180,8 @@ describe('Scan', () => { format: 'json', }; expect(() => { - Trivy.scan(trivyPath, image, invalidOption); - }).toThrowError('severity option error: INVALID is unknown severity'); + trivy.scan(trivyPath, image, invalidOption); + }).toThrowError('Trivy option error: INVALID is unknown severity'); }); test('with invalid vulnType', () => { @@ -191,8 +192,8 @@ describe('Scan', () => { format: 'json', }; expect(() => { - Trivy.scan(trivyPath, image, invalidOption); - }).toThrowError('vuln-type option error: INVALID is unknown vuln-type'); + trivy.scan(trivyPath, image, invalidOption); + }).toThrowError('Trivy option error: INVALID is unknown vuln-type'); }); }); @@ -204,7 +205,7 @@ describe('Parse', () => { Vulnerabilities: null, }, ]; - const result = Trivy.parse(vulnerabilities); + const result = trivy.parse(vulnerabilities); expect(result).toBe(''); }); @@ -247,9 +248,77 @@ describe('Parse', () => { ], }, ]; - const result = Trivy.parse(vulnerabilities); + const result = trivy.parse(vulnerabilities); expect(result).toMatch( /\|Title\|Severity\|CVE\|Package Name\|Installed Version\|Fixed Version\|References\|/ ); }); }); + +describe('Validate trivy option', () => { + test('with a valid severity', () => { + const options: string[] = ['HIGH']; + const result = trivy['validateSeverity'](options); + expect(result).toBeTruthy(); + }); + + test('with two valid severities', () => { + const options: string[] = ['HIGH', 'CRITICAL']; + const result = trivy['validateSeverity'](options); + expect(result).toBeTruthy(); + }); + + test('with an invalid severity', () => { + const options: string[] = ['INVALID']; + expect(() => { + trivy['validateSeverity'](options); + }).toThrowError('Trivy option error: INVALID is unknown severity'); + }); + + test('with two invalid severities', () => { + const options: string[] = ['INVALID', 'ERROR']; + expect(() => { + trivy['validateSeverity'](options); + }).toThrowError('Trivy option error: INVALID,ERROR is unknown severity'); + }); + + test('with an invalid and a valid severities', () => { + const options: string[] = ['INVALID', 'HIGH']; + expect(() => { + trivy['validateSeverity'](options); + }).toThrowError('Trivy option error: INVALID,HIGH is unknown severity'); + }); + + test('with a valid vuln-type', () => { + const options: string[] = ['os']; + const result = trivy['validateVulnType'](options); + expect(result).toBeTruthy(); + }); + + test('with two valid vuln-types', () => { + const options: string[] = ['os', 'library']; + const result = trivy['validateVulnType'](options); + expect(result).toBeTruthy(); + }); + + test('with an invalid vuln-type', () => { + const options: string[] = ['INVALID']; + expect(() => { + trivy['validateVulnType'](options); + }).toThrowError('Trivy option error: INVALID is unknown vuln-type'); + }); + + test('with two invalid vuln-types', () => { + const options: string[] = ['INVALID', 'ERROR']; + expect(() => { + trivy['validateVulnType'](options); + }).toThrowError('Trivy option error: INVALID,ERROR is unknown vuln-type'); + }); + + test('with a valid and an invalid vuln-types', () => { + const options: string[] = ['INVALID', 'os']; + expect(() => { + trivy['validateVulnType'](options); + }).toThrowError('Trivy option error: INVALID,os is unknown vuln-type'); + }); +}); From 4a2381625213635a90451fc677a510c49aaf537f Mon Sep 17 00:00:00 2001 From: homoluctus Date: Sat, 30 Nov 2019 18:40:53 +0900 Subject: [PATCH 3/4] [trivy.ts] Refactoring --- __tests__/trivy.test.ts | 4 +-- dist/index.js | 61 ++++++++++++++-------------------- src/trivy.ts | 72 ++++++++++++++++++++++------------------- 3 files changed, 66 insertions(+), 71 deletions(-) diff --git a/__tests__/trivy.test.ts b/__tests__/trivy.test.ts index 760d82c..4fa60c9 100644 --- a/__tests__/trivy.test.ts +++ b/__tests__/trivy.test.ts @@ -55,7 +55,7 @@ describe('getDownloadUrl', () => { await expect( downloader['getDownloadUrl'](version, os) ).rejects.toThrowError( - 'The Trivy version that you specified does not exist.' + 'Cloud not be found a Trivy asset that you specified.' ); }); @@ -65,7 +65,7 @@ describe('getDownloadUrl', () => { await expect( downloader['getDownloadUrl'](version, os) ).rejects.toThrowError( - 'Cloud not be found Trivy asset that You specified.' + 'Cloud not be found a Trivy asset that you specified.' ); }); }); diff --git a/dist/index.js b/dist/index.js index 8203997..7c334c4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -13199,13 +13199,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; -var __asyncValues = (this && this.__asyncValues) || function (o) { - if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); - var m = o[Symbol.asyncIterator], i; - return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); - function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } - function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } -}; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -13244,53 +13237,49 @@ class Downloader { } } getDownloadUrl(version, os) { - var e_1, _a; return __awaiter(this, void 0, void 0, function* () { - let response; try { - if (version === 'latest') { - response = yield this.githubClient.repos.getLatestRelease(Object.assign({}, Downloader.trivyRepository)); - version = response.data.tag_name.replace(/v/, ''); - } - else { - response = yield this.githubClient.repos.getReleaseByTag(Object.assign(Object.assign({}, Downloader.trivyRepository), { tag: `v${version}` })); + const response = yield this.getAssets(version); + const filename = `trivy_${response.version}_${os}-64bit.tar.gz`; + for (const asset of response.assets) { + if (asset.name === filename) { + return asset.browser_download_url; + } } + throw new Error(); } catch (error) { - throw new Error(`The Trivy version that you specified does not exist. + const errorMsg = ` + Cloud not be found a Trivy asset that you specified. Version: ${version} - `); + OS: ${os} + `; + throw new Error(errorMsg); } - const filename = `trivy_${version}_${os}-64bit.tar.gz`; - try { - for (var _b = __asyncValues(response.data.assets), _c; _c = yield _b.next(), !_c.done;) { - const asset = _c.value; - if (asset.name === filename) { - return asset.browser_download_url; - } - } + }); + } + getAssets(version) { + return __awaiter(this, void 0, void 0, function* () { + let response; + if (version === 'latest') { + response = yield this.githubClient.repos.getLatestRelease(Object.assign({}, Downloader.trivyRepository)); + version = response.data.tag_name.replace(/v/, ''); } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b); - } - finally { if (e_1) throw e_1.error; } + else { + response = yield this.githubClient.repos.getReleaseByTag(Object.assign(Object.assign({}, Downloader.trivyRepository), { tag: `v${version}` })); } - const errorMsg = `Cloud not be found Trivy asset that You specified. - Version: ${version} - OS: ${os}`; - throw new Error(errorMsg); + return { assets: response.data.assets, version }; }); } downloadTrivyCmd(downloadUrl, savedPath = '.') { return __awaiter(this, void 0, void 0, function* () { const response = yield node_fetch_1.default(downloadUrl); return new Promise((resolve, reject) => { + const gunzip = zlib_1.default.createGunzip(); const extract = tar_1.default.extract({ C: savedPath }, ['trivy']); response.body .on('error', reject) - .pipe(zlib_1.default.createGunzip()) + .pipe(gunzip) .on('error', reject) .pipe(extract) .on('error', reject) diff --git a/src/trivy.ts b/src/trivy.ts index eae3f0e..0535db8 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -1,22 +1,19 @@ import fs from 'fs'; import zlib from 'zlib'; import tar from 'tar'; -import Octokit, { ReposGetLatestReleaseResponse } from '@octokit/rest'; +import Octokit, { + ReposGetLatestReleaseResponse, + ReposGetLatestReleaseResponseAssetsItem, +} from '@octokit/rest'; import fetch, { Response } from 'node-fetch'; import { spawnSync, SpawnSyncReturns } from 'child_process'; import { TrivyOption, Vulnerability } from './interface'; -import { defaultCoreCipherList } from 'constants'; - -interface Repository { - owner: string; - repo: string; -} export class Downloader { githubClient: Octokit; - static readonly trivyRepository: Repository = { + static readonly trivyRepository = { owner: 'aquasecurity', repo: 'trivy', }; @@ -55,37 +52,45 @@ export class Downloader { } private async getDownloadUrl(version: string, os: string): Promise { - let response: Octokit.Response; - try { - if (version === 'latest') { - response = await this.githubClient.repos.getLatestRelease({ - ...Downloader.trivyRepository, - }); - version = response.data.tag_name.replace(/v/, ''); - } else { - response = await this.githubClient.repos.getReleaseByTag({ - ...Downloader.trivyRepository, - tag: `v${version}`, - }); + const response = await this.getAssets(version); + const filename: string = `trivy_${response.version}_${os}-64bit.tar.gz`; + for (const asset of response.assets) { + if (asset.name === filename) { + return asset.browser_download_url; + } } + throw new Error(); } catch (error) { - throw new Error(`The Trivy version that you specified does not exist. + const errorMsg: string = ` + Cloud not be found a Trivy asset that you specified. Version: ${version} - `); + OS: ${os} + `; + throw new Error(errorMsg); } + } - const filename: string = `trivy_${version}_${os}-64bit.tar.gz`; - for await (const asset of response.data.assets) { - if (asset.name === filename) { - return asset.browser_download_url; - } - } + private async getAssets( + version: string + ): Promise<{ + assets: ReposGetLatestReleaseResponseAssetsItem[]; + version: string; + }> { + let response: Octokit.Response; - const errorMsg: string = `Cloud not be found Trivy asset that You specified. - Version: ${version} - OS: ${os}`; - throw new Error(errorMsg); + if (version === 'latest') { + response = await this.githubClient.repos.getLatestRelease({ + ...Downloader.trivyRepository, + }); + version = response.data.tag_name.replace(/v/, ''); + } else { + response = await this.githubClient.repos.getReleaseByTag({ + ...Downloader.trivyRepository, + tag: `v${version}`, + }); + } + return { assets: response.data.assets, version }; } private async downloadTrivyCmd( @@ -95,10 +100,11 @@ export class Downloader { const response: Response = await fetch(downloadUrl); return new Promise((resolve, reject) => { + const gunzip = zlib.createGunzip(); const extract = tar.extract({ C: savedPath }, ['trivy']); response.body .on('error', reject) - .pipe(zlib.createGunzip()) + .pipe(gunzip) .on('error', reject) .pipe(extract) .on('error', reject) From e8c23ae0cbee50d4d1fbf3313428937b2d4f4d12 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Sat, 7 Dec 2019 07:16:17 +0900 Subject: [PATCH 4/4] [readme] Fix a little --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3ab8b3..3601a94 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ name: Vulnerability Scan on: schedule: - - cron: '00 9 * * *' + - cron: '0 9 * * *' jobs: scan: @@ -51,7 +51,7 @@ jobs: - name: Pull docker image run: docker pull sample - - uses: homoluctus/gitrivy@v0.0.1 + - uses: homoluctus/gitrivy@v1.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} image: sample