From 3f0be909b21a01c75800c6e4a29328e2bbcb2670 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Mon, 25 Nov 2019 13:04:14 +0900 Subject: [PATCH 01/15] [action.yml] Chage action name --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 7863315..d963deb 100644 --- a/action.yml +++ b/action.yml @@ -1,4 +1,4 @@ -name: 'Gitrivy' +name: 'Trivy Action' description: 'Scan docker image vulnerability using Trivy and create GitHub Issue' author: 'homoluctus' inputs: From b6889bcff13acc7ef392ba478e4b5f1c3f20365c Mon Sep 17 00:00:00 2001 From: homoluctus Date: Tue, 26 Nov 2019 02:19:14 +0900 Subject: [PATCH 02/15] [jest] Add coverage options --- jest.config.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 563d4cc..5216703 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,10 +2,15 @@ module.exports = { clearMocks: true, moduleFileExtensions: ['js', 'ts'], testEnvironment: 'node', - testMatch: ['**/*.test.ts'], + testMatch: ['**/__tests__/*.test.ts'], testRunner: 'jest-circus/runner', transform: { '^.+\\.ts$': 'ts-jest' }, - verbose: true + verbose: true, + collectCoverage: true, + collectCoverageFrom: [ + '**/src/*.ts', + '!**/node_modules/**' + ] } \ No newline at end of file From 652070ed7ae182be4a1b28ebeef9ca827dbc8e7a Mon Sep 17 00:00:00 2001 From: homoluctus Date: Tue, 26 Nov 2019 02:19:52 +0900 Subject: [PATCH 03/15] [trivy] Extract trivy binary only --- dist/index.js | 1256 +++++++++++++++++++++++++------------------------ src/index.ts | 2 +- src/trivy.ts | 82 ++-- 3 files changed, 674 insertions(+), 666 deletions(-) diff --git a/dist/index.js b/dist/index.js index 5cea0c1..d361e11 100644 --- a/dist/index.js +++ b/dist/index.js @@ -299,6 +299,28 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 13: +/***/ (function(module) { + +module.exports = getPageLinks + +function getPageLinks (link) { + link = link.link || link.headers.link || '' + + const links = {} + + // link format: + // '; rel="next", ; rel="last"' + link.replace(/<([^>]*)>;\s*rel="([\w]*)"/g, (m, uri, type) => { + links[type] = uri + }) + + return links +} + + /***/ }), /***/ 18: @@ -653,7 +675,7 @@ const Parser = __webpack_require__(203) const fs = __webpack_require__(747) const fsm = __webpack_require__(827) const path = __webpack_require__(622) -const mkdir = __webpack_require__(491) +const mkdir = __webpack_require__(577) const mkdirSync = mkdir.sync const wc = __webpack_require__(478) const pathReservations = __webpack_require__(182) @@ -2287,7 +2309,7 @@ module.exports = require("child_process"); const assert = __webpack_require__(357) -const Buffer = __webpack_require__(407).Buffer +const Buffer = __webpack_require__(293).Buffer const realZlib = __webpack_require__(761) const constants = exports.constants = __webpack_require__(60) @@ -2662,7 +2684,7 @@ function withAuthorizationPrefix(authorization) { "use strict"; const pump = __webpack_require__(453); -const bufferStream = __webpack_require__(966); +const bufferStream = __webpack_require__(158); class MaxBufferError extends Error { constructor() { @@ -2728,6 +2750,65 @@ function paginatePlugin(octokit) { } +/***/ }), + +/***/ 158: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + +const {PassThrough} = __webpack_require__(413); + +module.exports = options => { + options = Object.assign({}, options); + + const {array} = options; + let {encoding} = options; + const buffer = encoding === 'buffer'; + let objectMode = false; + + if (array) { + objectMode = !(encoding || buffer); + } else { + encoding = encoding || 'utf8'; + } + + if (buffer) { + encoding = null; + } + + let len = 0; + const ret = []; + const stream = new PassThrough({objectMode}); + + if (encoding) { + stream.setEncoding(encoding); + } + + stream.on('data', chunk => { + ret.push(chunk); + + if (objectMode) { + len = ret.length; + } else { + len += chunk.length; + } + }); + + stream.getBufferedValue = () => { + if (array) { + return ret; + } + + return buffer ? Buffer.concat(ret, len) : ret.join(''); + }; + + stream.getBufferedLength = () => len; + + return stream; +}; + + /***/ }), /***/ 159: @@ -3069,7 +3150,7 @@ module.exports = () => { module.exports = authenticationPlugin; const beforeRequest = __webpack_require__(863); -const requestError = __webpack_require__(293); +const requestError = __webpack_require__(991); const validate = __webpack_require__(954); function authenticationPlugin(octokit, options) { @@ -4199,7 +4280,7 @@ exports.Context = Context; module.exports = getPage const deprecate = __webpack_require__(370) -const getPageLinks = __webpack_require__(577) +const getPageLinks = __webpack_require__(13) const HttpError = __webpack_require__(297) function getPage (octokit, link, which, headers) { @@ -5726,178 +5807,12 @@ function coerce (version) { } -/***/ }), - -/***/ 289: -/***/ (function(module, __unusedexports, __webpack_require__) { - -var path = __webpack_require__(622); -var fs = __webpack_require__(747); -var _0777 = parseInt('0777', 8); - -module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; - -function mkdirP (p, opts, f, made) { - if (typeof opts === 'function') { - f = opts; - opts = {}; - } - else if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 & (~process.umask()); - } - if (!made) made = null; - - var cb = f || function () {}; - p = path.resolve(p); - - xfs.mkdir(p, mode, function (er) { - if (!er) { - made = made || p; - return cb(null, made); - } - switch (er.code) { - case 'ENOENT': - mkdirP(path.dirname(p), opts, function (er, made) { - if (er) cb(er, made); - else mkdirP(p, opts, cb, made); - }); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - xfs.stat(p, function (er2, stat) { - // if the stat fails, then that's super weird. - // let the original error be the failure reason. - if (er2 || !stat.isDirectory()) cb(er, made) - else cb(null, made); - }); - break; - } - }); -} - -mkdirP.sync = function sync (p, opts, made) { - if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 & (~process.umask()); - } - if (!made) made = null; - - p = path.resolve(p); - - try { - xfs.mkdirSync(p, mode); - made = made || p; - } - catch (err0) { - switch (err0.code) { - case 'ENOENT' : - made = sync(path.dirname(p), opts, made); - sync(p, opts, made); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - var stat; - try { - stat = xfs.statSync(p); - } - catch (err1) { - throw err0; - } - if (!stat.isDirectory()) throw err0; - break; - } - } - - return made; -}; - - /***/ }), /***/ 293: -/***/ (function(module, __unusedexports, __webpack_require__) { - -module.exports = authenticationRequestError; - -const { RequestError } = __webpack_require__(463); - -function authenticationRequestError(state, error, options) { - if (!error.headers) throw error; - - const otpRequired = /required/.test(error.headers["x-github-otp"] || ""); - // handle "2FA required" error only - if (error.status !== 401 || !otpRequired) { - throw error; - } - - if ( - error.status === 401 && - otpRequired && - error.request && - error.request.headers["x-github-otp"] - ) { - if (state.otp) { - delete state.otp; // no longer valid, request again - } else { - throw new RequestError( - "Invalid one-time password for two-factor authentication", - 401, - { - headers: error.headers, - request: options - } - ); - } - } - - if (typeof state.auth.on2fa !== "function") { - throw new RequestError( - "2FA required, but options.on2fa is not a function. See https://github.com/octokit/rest.js#authentication", - 401, - { - headers: error.headers, - request: options - } - ); - } - - return Promise.resolve() - .then(() => { - return state.auth.on2fa(); - }) - .then(oneTimePassword => { - const newOptions = Object.assign(options, { - headers: Object.assign(options.headers, { - "x-github-otp": oneTimePassword - }) - }); - return state.octokit.request(newOptions).then(response => { - // If OTP still valid, then persist it for following requests - state.otp = oneTimePassword; - return response; - }); - }); -} +/***/ (function(module) { +module.exports = require("buffer"); /***/ }), @@ -6670,7 +6585,7 @@ function run() { .toLowerCase() === 'true' ? true : false }; - const downloader = new trivy_1.Downloader(token); + const downloader = new trivy_1.Downloader(); const trivyCmdPath = yield downloader.download(trivyVersion); const result = trivy_1.Trivy.scan(trivyCmdPath, image, trivyOptions); const issueContent = trivy_1.Trivy.parse(result); @@ -6705,7 +6620,7 @@ run(); module.exports = hasLastPage const deprecate = __webpack_require__(370) -const getPageLinks = __webpack_require__(577) +const getPageLinks = __webpack_require__(13) function hasLastPage (link) { deprecate(`octokit.hasLastPage() – You can use octokit.paginate or async iterators instead: https://github.com/octokit/rest.js#pagination.`) @@ -7188,7 +7103,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } -var isPlainObject = _interopDefault(__webpack_require__(626)); +var isPlainObject = _interopDefault(__webpack_require__(442)); var universalUserAgent = __webpack_require__(562); function lowercaseKeys(object) { @@ -7655,13 +7570,6 @@ function Octokit(plugins, options) { } -/***/ }), - -/***/ 407: -/***/ (function(module) { - -module.exports = require("buffer"); - /***/ }), /***/ 413: @@ -7812,8 +7720,64 @@ function escape(s) { /***/ }), -/***/ 453: -/***/ (function(module, __unusedexports, __webpack_require__) { +/***/ 442: +/***/ (function(module) { + +"use strict"; + + +/*! + * isobject + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false; +} + +/*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + +function isObjectObject(o) { + return isObject(o) === true + && Object.prototype.toString.call(o) === '[object Object]'; +} + +function isPlainObject(o) { + var ctor,prot; + + if (isObjectObject(o) === false) return false; + + // If has modified constructor + ctor = o.constructor; + if (typeof ctor !== 'function') return false; + + // If has modified prototype + prot = ctor.prototype; + if (isObjectObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; +} + +module.exports = isPlainObject; + + +/***/ }), + +/***/ 453: +/***/ (function(module, __unusedexports, __webpack_require__) { var once = __webpack_require__(969) var eos = __webpack_require__(9) @@ -10041,301 +10005,87 @@ module.exports = resolveCommand; /***/ }), -/***/ 491: +/***/ 500: /***/ (function(module, __unusedexports, __webpack_require__) { -"use strict"; - -// wrapper around mkdirp for tar's needs. - -// TODO: This should probably be a class, not functionally -// passing around state in a gazillion args. - -const mkdirp = __webpack_require__(289) -const fs = __webpack_require__(747) -const path = __webpack_require__(622) -const chownr = __webpack_require__(941) +module.exports = graphql -class SymlinkError extends Error { - constructor (symlink, path) { - super('Cannot extract through symbolic link') - this.path = path - this.symlink = symlink - } +const GraphqlError = __webpack_require__(862) - get name () { - return 'SylinkError' - } -} +const NON_VARIABLE_OPTIONS = ['method', 'baseUrl', 'url', 'headers', 'request', 'query'] -class CwdError extends Error { - constructor (path, code) { - super(code + ': Cannot cd into \'' + path + '\'') - this.path = path - this.code = code +function graphql (request, query, options) { + if (typeof query === 'string') { + options = Object.assign({ query }, options) + } else { + options = query } - get name () { - return 'CwdError' - } -} + const requestOptions = Object.keys(options).reduce((result, key) => { + if (NON_VARIABLE_OPTIONS.includes(key)) { + result[key] = options[key] + return result + } -const mkdir = module.exports = (dir, opt, cb) => { - // if there's any overlap between mask and mode, - // then we'll need an explicit chmod - const umask = opt.umask - const mode = opt.mode | 0o0700 - const needChmod = (mode & umask) !== 0 + if (!result.variables) { + result.variables = {} + } - const uid = opt.uid - const gid = opt.gid - const doChown = typeof uid === 'number' && - typeof gid === 'number' && - ( uid !== opt.processUid || gid !== opt.processGid ) + result.variables[key] = options[key] + return result + }, {}) - const preserve = opt.preserve - const unlink = opt.unlink - const cache = opt.cache - const cwd = opt.cwd + return request(requestOptions) + .then(response => { + if (response.data.errors) { + throw new GraphqlError(requestOptions, response) + } - const done = (er, created) => { - if (er) - cb(er) - else { - cache.set(dir, true) - if (created && doChown) - chownr(created, uid, gid, er => done(er)) - else if (needChmod) - fs.chmod(dir, mode, cb) - else - cb() - } - } + return response.data.data + }) +} - if (cache && cache.get(dir) === true) - return done() - if (dir === cwd) - return fs.stat(dir, (er, st) => { - if (er || !st.isDirectory()) - er = new CwdError(dir, er && er.code || 'ENOTDIR') - done(er) - }) +/***/ }), - if (preserve) - return mkdirp(dir, mode, done) +/***/ 503: +/***/ (function(module, __unusedexports, __webpack_require__) { - const sub = path.relative(cwd, dir) - const parts = sub.split(/\/|\\/) - mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done) -} +const { request } = __webpack_require__(753) +const getUserAgent = __webpack_require__(46) -const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => { - if (!parts.length) - return cb(null, created) - const p = parts.shift() - const part = base + '/' + p - if (cache.get(part)) - return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) - fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb)) -} +const version = __webpack_require__(314).version +const userAgent = `octokit-graphql.js/${version} ${getUserAgent()}` -const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => { - if (er) { - if (er.path && path.dirname(er.path) === cwd && - (er.code === 'ENOTDIR' || er.code === 'ENOENT')) - return cb(new CwdError(cwd, er.code)) +const withDefaults = __webpack_require__(958) - fs.lstat(part, (statEr, st) => { - if (statEr) - cb(statEr) - else if (st.isDirectory()) - mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) - else if (unlink) - fs.unlink(part, er => { - if (er) - return cb(er) - fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb)) - }) - else if (st.isSymbolicLink()) - return cb(new SymlinkError(part, part + '/' + parts.join('/'))) - else - cb(er) - }) - } else { - created = created || part - mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) +module.exports = withDefaults(request, { + method: 'POST', + url: '/graphql', + headers: { + 'user-agent': userAgent } -} +}) -const mkdirSync = module.exports.sync = (dir, opt) => { - // if there's any overlap between mask and mode, - // then we'll need an explicit chmod - const umask = opt.umask - const mode = opt.mode | 0o0700 - const needChmod = (mode & umask) !== 0 - const uid = opt.uid - const gid = opt.gid - const doChown = typeof uid === 'number' && - typeof gid === 'number' && - ( uid !== opt.processUid || gid !== opt.processGid ) +/***/ }), - const preserve = opt.preserve - const unlink = opt.unlink - const cache = opt.cache - const cwd = opt.cwd +/***/ 510: +/***/ (function(module) { - const done = (created) => { - cache.set(dir, true) - if (created && doChown) - chownr.sync(created, uid, gid) - if (needChmod) - fs.chmodSync(dir, mode) - } +module.exports = addHook - if (cache && cache.get(dir) === true) - return done() +function addHook (state, kind, name, hook) { + var orig = hook + if (!state.registry[name]) { + state.registry[name] = [] + } - if (dir === cwd) { - let ok = false - let code = 'ENOTDIR' - try { - ok = fs.statSync(dir).isDirectory() - } catch (er) { - code = er.code - } finally { - if (!ok) - throw new CwdError(dir, code) - } - done() - return - } - - if (preserve) - return done(mkdirp.sync(dir, mode)) - - const sub = path.relative(cwd, dir) - const parts = sub.split(/\/|\\/) - let created = null - for (let p = parts.shift(), part = cwd; - p && (part += '/' + p); - p = parts.shift()) { - - if (cache.get(part)) - continue - - try { - fs.mkdirSync(part, mode) - created = created || part - cache.set(part, true) - } catch (er) { - if (er.path && path.dirname(er.path) === cwd && - (er.code === 'ENOTDIR' || er.code === 'ENOENT')) - return new CwdError(cwd, er.code) - - const st = fs.lstatSync(part) - if (st.isDirectory()) { - cache.set(part, true) - continue - } else if (unlink) { - fs.unlinkSync(part) - fs.mkdirSync(part, mode) - created = created || part - cache.set(part, true) - continue - } else if (st.isSymbolicLink()) - return new SymlinkError(part, part + '/' + parts.join('/')) - } - } - - return done(created) -} - - -/***/ }), - -/***/ 500: -/***/ (function(module, __unusedexports, __webpack_require__) { - -module.exports = graphql - -const GraphqlError = __webpack_require__(862) - -const NON_VARIABLE_OPTIONS = ['method', 'baseUrl', 'url', 'headers', 'request', 'query'] - -function graphql (request, query, options) { - if (typeof query === 'string') { - options = Object.assign({ query }, options) - } else { - options = query - } - - const requestOptions = Object.keys(options).reduce((result, key) => { - if (NON_VARIABLE_OPTIONS.includes(key)) { - result[key] = options[key] - return result - } - - if (!result.variables) { - result.variables = {} - } - - result.variables[key] = options[key] - return result - }, {}) - - return request(requestOptions) - .then(response => { - if (response.data.errors) { - throw new GraphqlError(requestOptions, response) - } - - return response.data.data - }) -} - - -/***/ }), - -/***/ 503: -/***/ (function(module, __unusedexports, __webpack_require__) { - -const { request } = __webpack_require__(753) -const getUserAgent = __webpack_require__(46) - -const version = __webpack_require__(314).version -const userAgent = `octokit-graphql.js/${version} ${getUserAgent()}` - -const withDefaults = __webpack_require__(958) - -module.exports = withDefaults(request, { - method: 'POST', - url: '/graphql', - headers: { - 'user-agent': userAgent - } -}) - - -/***/ }), - -/***/ 510: -/***/ (function(module) { - -module.exports = addHook - -function addHook (state, kind, name, hook) { - var orig = hook - if (!state.registry[name]) { - state.registry[name] = [] - } - - if (kind === 'before') { - hook = function (method, options) { - return Promise.resolve() - .then(orig.bind(null, options)) - .then(method.bind(null, options)) + if (kind === 'before') { + hook = function (method, options) { + return Promise.resolve() + .then(orig.bind(null, options)) + .then(method.bind(null, options)) } } @@ -10453,7 +10203,7 @@ module.exports = factory(); module.exports = hasFirstPage const deprecate = __webpack_require__(370) -const getPageLinks = __webpack_require__(577) +const getPageLinks = __webpack_require__(13) function hasFirstPage (link) { deprecate(`octokit.hasFirstPage() – You can use octokit.paginate or async iterators instead: https://github.com/octokit/rest.js#pagination.`) @@ -10591,7 +10341,7 @@ exports.code = new Map(Array.from(exports.name).map(kv => [kv[1], kv[0]])) module.exports = hasPreviousPage const deprecate = __webpack_require__(370) -const getPageLinks = __webpack_require__(577) +const getPageLinks = __webpack_require__(13) function hasPreviousPage (link) { deprecate(`octokit.hasPreviousPage() – You can use octokit.paginate or async iterators instead: https://github.com/octokit/rest.js#pagination.`) @@ -10779,22 +10529,214 @@ module.exports = parse; /***/ }), /***/ 577: -/***/ (function(module) { +/***/ (function(module, __unusedexports, __webpack_require__) { -module.exports = getPageLinks +"use strict"; -function getPageLinks (link) { - link = link.link || link.headers.link || '' +// wrapper around mkdirp for tar's needs. - const links = {} +// TODO: This should probably be a class, not functionally +// passing around state in a gazillion args. - // link format: - // '; rel="next", ; rel="last"' - link.replace(/<([^>]*)>;\s*rel="([\w]*)"/g, (m, uri, type) => { - links[type] = uri - }) +const mkdirp = __webpack_require__(626) +const fs = __webpack_require__(747) +const path = __webpack_require__(622) +const chownr = __webpack_require__(941) - return links +class SymlinkError extends Error { + constructor (symlink, path) { + super('Cannot extract through symbolic link') + this.path = path + this.symlink = symlink + } + + get name () { + return 'SylinkError' + } +} + +class CwdError extends Error { + constructor (path, code) { + super(code + ': Cannot cd into \'' + path + '\'') + this.path = path + this.code = code + } + + get name () { + return 'CwdError' + } +} + +const mkdir = module.exports = (dir, opt, cb) => { + // if there's any overlap between mask and mode, + // then we'll need an explicit chmod + const umask = opt.umask + const mode = opt.mode | 0o0700 + const needChmod = (mode & umask) !== 0 + + const uid = opt.uid + const gid = opt.gid + const doChown = typeof uid === 'number' && + typeof gid === 'number' && + ( uid !== opt.processUid || gid !== opt.processGid ) + + const preserve = opt.preserve + const unlink = opt.unlink + const cache = opt.cache + const cwd = opt.cwd + + const done = (er, created) => { + if (er) + cb(er) + else { + cache.set(dir, true) + if (created && doChown) + chownr(created, uid, gid, er => done(er)) + else if (needChmod) + fs.chmod(dir, mode, cb) + else + cb() + } + } + + if (cache && cache.get(dir) === true) + return done() + + if (dir === cwd) + return fs.stat(dir, (er, st) => { + if (er || !st.isDirectory()) + er = new CwdError(dir, er && er.code || 'ENOTDIR') + done(er) + }) + + if (preserve) + return mkdirp(dir, mode, done) + + const sub = path.relative(cwd, dir) + const parts = sub.split(/\/|\\/) + mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done) +} + +const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => { + if (!parts.length) + return cb(null, created) + const p = parts.shift() + const part = base + '/' + p + if (cache.get(part)) + return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) + fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb)) +} + +const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => { + if (er) { + if (er.path && path.dirname(er.path) === cwd && + (er.code === 'ENOTDIR' || er.code === 'ENOENT')) + return cb(new CwdError(cwd, er.code)) + + fs.lstat(part, (statEr, st) => { + if (statEr) + cb(statEr) + else if (st.isDirectory()) + mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) + else if (unlink) + fs.unlink(part, er => { + if (er) + return cb(er) + fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb)) + }) + else if (st.isSymbolicLink()) + return cb(new SymlinkError(part, part + '/' + parts.join('/'))) + else + cb(er) + }) + } else { + created = created || part + mkdir_(part, parts, mode, cache, unlink, cwd, created, cb) + } +} + +const mkdirSync = module.exports.sync = (dir, opt) => { + // if there's any overlap between mask and mode, + // then we'll need an explicit chmod + const umask = opt.umask + const mode = opt.mode | 0o0700 + const needChmod = (mode & umask) !== 0 + + const uid = opt.uid + const gid = opt.gid + const doChown = typeof uid === 'number' && + typeof gid === 'number' && + ( uid !== opt.processUid || gid !== opt.processGid ) + + const preserve = opt.preserve + const unlink = opt.unlink + const cache = opt.cache + const cwd = opt.cwd + + const done = (created) => { + cache.set(dir, true) + if (created && doChown) + chownr.sync(created, uid, gid) + if (needChmod) + fs.chmodSync(dir, mode) + } + + if (cache && cache.get(dir) === true) + return done() + + if (dir === cwd) { + let ok = false + let code = 'ENOTDIR' + try { + ok = fs.statSync(dir).isDirectory() + } catch (er) { + code = er.code + } finally { + if (!ok) + throw new CwdError(dir, code) + } + done() + return + } + + if (preserve) + return done(mkdirp.sync(dir, mode)) + + const sub = path.relative(cwd, dir) + const parts = sub.split(/\/|\\/) + let created = null + for (let p = parts.shift(), part = cwd; + p && (part += '/' + p); + p = parts.shift()) { + + if (cache.get(part)) + continue + + try { + fs.mkdirSync(part, mode) + created = created || part + cache.set(part, true) + } catch (er) { + if (er.path && path.dirname(er.path) === cwd && + (er.code === 'ENOTDIR' || er.code === 'ENOENT')) + return new CwdError(cwd, er.code) + + const st = fs.lstatSync(part) + if (st.isDirectory()) { + cache.set(part, true) + continue + } else if (unlink) { + fs.unlinkSync(part) + fs.mkdirSync(part, mode) + created = created || part + cache.set(part, true) + continue + } else if (st.isSymbolicLink()) + return new SymlinkError(part, part + '/' + parts.join('/')) + } + } + + return done(created) } @@ -11506,57 +11448,106 @@ module.exports = require("path"); /***/ }), /***/ 626: -/***/ (function(module) { - -"use strict"; - - -/*! - * isobject - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. - */ - -function isObject(val) { - return val != null && typeof val === 'object' && Array.isArray(val) === false; -} +/***/ (function(module, __unusedexports, __webpack_require__) { -/*! - * is-plain-object - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. - */ +var path = __webpack_require__(622); +var fs = __webpack_require__(747); +var _0777 = parseInt('0777', 8); -function isObjectObject(o) { - return isObject(o) === true - && Object.prototype.toString.call(o) === '[object Object]'; -} +module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; -function isPlainObject(o) { - var ctor,prot; +function mkdirP (p, opts, f, made) { + if (typeof opts === 'function') { + f = opts; + opts = {}; + } + else if (!opts || typeof opts !== 'object') { + opts = { mode: opts }; + } + + var mode = opts.mode; + var xfs = opts.fs || fs; + + if (mode === undefined) { + mode = _0777 & (~process.umask()); + } + if (!made) made = null; + + var cb = f || function () {}; + p = path.resolve(p); + + xfs.mkdir(p, mode, function (er) { + if (!er) { + made = made || p; + return cb(null, made); + } + switch (er.code) { + case 'ENOENT': + mkdirP(path.dirname(p), opts, function (er, made) { + if (er) cb(er, made); + else mkdirP(p, opts, cb, made); + }); + break; - if (isObjectObject(o) === false) return false; + // In the case of any other error, just see if there's a dir + // there already. If so, then hooray! If not, then something + // is borked. + default: + xfs.stat(p, function (er2, stat) { + // if the stat fails, then that's super weird. + // let the original error be the failure reason. + if (er2 || !stat.isDirectory()) cb(er, made) + else cb(null, made); + }); + break; + } + }); +} - // If has modified constructor - ctor = o.constructor; - if (typeof ctor !== 'function') return false; +mkdirP.sync = function sync (p, opts, made) { + if (!opts || typeof opts !== 'object') { + opts = { mode: opts }; + } + + var mode = opts.mode; + var xfs = opts.fs || fs; + + if (mode === undefined) { + mode = _0777 & (~process.umask()); + } + if (!made) made = null; - // If has modified prototype - prot = ctor.prototype; - if (isObjectObject(prot) === false) return false; + p = path.resolve(p); - // If constructor does not have an Object-specific method - if (prot.hasOwnProperty('isPrototypeOf') === false) { - return false; - } + try { + xfs.mkdirSync(p, mode); + made = made || p; + } + catch (err0) { + switch (err0.code) { + case 'ENOENT' : + made = sync(path.dirname(p), opts, made); + sync(p, opts, made); + break; - // Most likely a plain Object - return true; -} + // In the case of any other error, just see if there's a dir + // there already. If so, then hooray! If not, then something + // is borked. + default: + var stat; + try { + stat = xfs.statSync(p); + } + catch (err1) { + throw err0; + } + if (!stat.isDirectory()) throw err0; + break; + } + } -module.exports = isPlainObject; + return made; +}; /***/ }), @@ -13204,24 +13195,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const rest_1 = __importDefault(__webpack_require__(0)); -const child_process_1 = __webpack_require__(129); const fs_1 = __importDefault(__webpack_require__(747)); -const node_fetch_1 = __importDefault(__webpack_require__(454)); const zlib_1 = __importDefault(__webpack_require__(761)); const tar_1 = __importDefault(__webpack_require__(885)); +const rest_1 = __importDefault(__webpack_require__(0)); +const node_fetch_1 = __importDefault(__webpack_require__(454)); +const child_process_1 = __webpack_require__(129); class Downloader { - constructor(token) { - this.githubClient = new rest_1.default({ auth: `token ${token}` }); + constructor() { + this.githubClient = new rest_1.default(); } download(version) { return __awaiter(this, void 0, void 0, function* () { const os = this.checkPlatform(process.platform); const downloadUrl = yield this.getDownloadUrl(version, os); console.debug(`Download URL: ${downloadUrl}`); - const response = yield node_fetch_1.default(downloadUrl); const trivyCmdBaseDir = process.env.GITHUB_WORKSPACE || '.'; - const trivyCmdPath = yield this.saveTrivyCmd(response, trivyCmdBaseDir); + const trivyCmdPath = yield this.downloadTrivyCmd(downloadUrl, trivyCmdBaseDir); console.debug(`Trivy Command Path: ${trivyCmdPath}`); return trivyCmdPath; }); @@ -13233,9 +13223,9 @@ class Downloader { case 'darwin': return 'macOS'; default: - throw new Error(`Sorry, ${platform} is not supported. - Trivy support Linux, MacOS, FreeBSD and OpenBSD. - `); + const errorMsg = `Sorry, ${platform} is not supported. + Trivy support Linux, MacOS, FreeBSD and OpenBSD.`; + throw new Error(errorMsg); } } getDownloadUrl(version, os) { @@ -13252,9 +13242,8 @@ class Downloader { } } catch (error) { - throw new Error(` - The Trivy version that you specified does not exist. - Version: ${version} + throw new Error(`The Trivy version that you specified does not exist. + Version: ${version} `); } const filename = `trivy_${version}_${os}-64bit.tar.gz`; @@ -13273,26 +13262,34 @@ class Downloader { } finally { if (e_1) throw e_1.error; } } - throw new Error(`Cloud not be found Trivy asset that You specified. + const errorMsg = `Cloud not be found Trivy asset that You specified. Version: ${version} - OS: ${os} - `); + OS: ${os}`; + throw new Error(errorMsg); }); } - saveTrivyCmd(response, savedPath = '.') { - return new Promise((resolve, reject) => { - const extract = tar_1.default.extract({ path: savedPath }); - response.body.pipe(zlib_1.default.createGunzip()).pipe(extract); - extract.on('finish', () => { - if (!this.trivyExists(savedPath)) { - reject('Failed to extract Trivy command file.'); - } - resolve(`${savedPath}/trivy`); + 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 extract = tar_1.default.extract({ C: savedPath }, ['trivy']); + response.body + .on('error', reject) + .pipe(zlib_1.default.createGunzip()) + .on('error', reject) + .pipe(extract) + .on('error', reject) + .on('finish', () => { + if (!this.trivyExists(savedPath)) { + reject('Failed to extract Trivy command file.'); + } + resolve(`${savedPath}/trivy`); + }); }); }); } - trivyExists(baseDir) { - const trivyCmdPaths = fs_1.default.readdirSync(baseDir).filter(f => f === 'trivy'); + trivyExists(targetDir) { + const trivyCmdPaths = fs_1.default.readdirSync(targetDir).filter(f => f === 'trivy'); return trivyCmdPaths.length === 1; } } @@ -13319,9 +13316,9 @@ class Trivy { return JSON.parse(result.stdout); } throw new Error(`Failed vulnerability scan using Trivy. - stdout: ${result.stdout} - stderr: ${result.stderr} - erorr: ${result.error} + stdout: ${result.stdout} + stderr: ${result.stderr} + erorr: ${result.error} `); } static parse(vulnerabilities) { @@ -16601,7 +16598,7 @@ module.exports = set; exports.c = exports.create = __webpack_require__(159) exports.r = exports.replace = __webpack_require__(630) exports.t = exports.list = __webpack_require__(381) -exports.u = exports.update = __webpack_require__(931) +exports.u = exports.update = __webpack_require__(966) exports.x = exports.extract = __webpack_require__(656) // classes @@ -16797,7 +16794,7 @@ module.exports = (mode, isDir, portable) => { module.exports = hasNextPage const deprecate = __webpack_require__(370) -const getPageLinks = __webpack_require__(577) +const getPageLinks = __webpack_require__(13) function hasNextPage (link) { deprecate(`octokit.hasNextPage() – You can use octokit.paginate or async iterators instead: https://github.com/octokit/rest.js#pagination.`) @@ -16805,50 +16802,6 @@ function hasNextPage (link) { } -/***/ }), - -/***/ 931: -/***/ (function(module, __unusedexports, __webpack_require__) { - -"use strict"; - - -// tar -u - -const hlo = __webpack_require__(891) -const r = __webpack_require__(630) -// just call tar.r with the filter and mtimeCache - -const u = module.exports = (opt_, files, cb) => { - const opt = hlo(opt_) - - if (!opt.file) - throw new TypeError('file is required') - - if (opt.gzip) - throw new TypeError('cannot append to compressed archives') - - if (!files || !Array.isArray(files) || !files.length) - throw new TypeError('no files or directories specified') - - files = Array.from(files) - - mtimeFilter(opt) - return r(opt, files, cb) -} - -const mtimeFilter = opt => { - const filter = opt.filter - - if (!opt.mtimeCache) - opt.mtimeCache = new Map() - - opt.filter = filter ? (path, stat) => - filter(path, stat) && !(opt.mtimeCache.get(path) > stat.mtime) - : (path, stat) => !(opt.mtimeCache.get(path) > stat.mtime) -} - - /***/ }), /***/ 941: @@ -17452,56 +17405,41 @@ function withDefaults (request, newDefaults) { "use strict"; -const {PassThrough} = __webpack_require__(413); - -module.exports = options => { - options = Object.assign({}, options); - const {array} = options; - let {encoding} = options; - const buffer = encoding === 'buffer'; - let objectMode = false; +// tar -u - if (array) { - objectMode = !(encoding || buffer); - } else { - encoding = encoding || 'utf8'; - } +const hlo = __webpack_require__(891) +const r = __webpack_require__(630) +// just call tar.r with the filter and mtimeCache - if (buffer) { - encoding = null; - } +const u = module.exports = (opt_, files, cb) => { + const opt = hlo(opt_) - let len = 0; - const ret = []; - const stream = new PassThrough({objectMode}); + if (!opt.file) + throw new TypeError('file is required') - if (encoding) { - stream.setEncoding(encoding); - } + if (opt.gzip) + throw new TypeError('cannot append to compressed archives') - stream.on('data', chunk => { - ret.push(chunk); + if (!files || !Array.isArray(files) || !files.length) + throw new TypeError('no files or directories specified') - if (objectMode) { - len = ret.length; - } else { - len += chunk.length; - } - }); + files = Array.from(files) - stream.getBufferedValue = () => { - if (array) { - return ret; - } + mtimeFilter(opt) + return r(opt, files, cb) +} - return buffer ? Buffer.concat(ret, len) : ret.join(''); - }; +const mtimeFilter = opt => { + const filter = opt.filter - stream.getBufferedLength = () => len; + if (!opt.mtimeCache) + opt.mtimeCache = new Map() - return stream; -}; + opt.filter = filter ? (path, stat) => + filter(path, stat) && !(opt.mtimeCache.get(path) > stat.mtime) + : (path, stat) => !(opt.mtimeCache.get(path) > stat.mtime) +} /***/ }), @@ -17553,6 +17491,74 @@ function onceStrict (fn) { } +/***/ }), + +/***/ 991: +/***/ (function(module, __unusedexports, __webpack_require__) { + +module.exports = authenticationRequestError; + +const { RequestError } = __webpack_require__(463); + +function authenticationRequestError(state, error, options) { + if (!error.headers) throw error; + + const otpRequired = /required/.test(error.headers["x-github-otp"] || ""); + // handle "2FA required" error only + if (error.status !== 401 || !otpRequired) { + throw error; + } + + if ( + error.status === 401 && + otpRequired && + error.request && + error.request.headers["x-github-otp"] + ) { + if (state.otp) { + delete state.otp; // no longer valid, request again + } else { + throw new RequestError( + "Invalid one-time password for two-factor authentication", + 401, + { + headers: error.headers, + request: options + } + ); + } + } + + if (typeof state.auth.on2fa !== "function") { + throw new RequestError( + "2FA required, but options.on2fa is not a function. See https://github.com/octokit/rest.js#authentication", + 401, + { + headers: error.headers, + request: options + } + ); + } + + return Promise.resolve() + .then(() => { + return state.auth.on2fa(); + }) + .then(oneTimePassword => { + const newOptions = Object.assign(options, { + headers: Object.assign(options.headers, { + "x-github-otp": oneTimePassword + }) + }); + return state.octokit.request(newOptions).then(response => { + // If OTP still valid, then persist it for following requests + state.otp = oneTimePassword; + return response; + }); + }); +} + + /***/ }) /******/ }); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f9cebab..16b85b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ async function run() { ? true : false } - const downloader = new Downloader(token) + const downloader = new Downloader() const trivyCmdPath: string = await downloader.download(trivyVersion) const result: Vulnerability[] = Trivy.scan(trivyCmdPath, image, trivyOptions) const issueContent: string = Trivy.parse(result) diff --git a/src/trivy.ts b/src/trivy.ts index 60fdaeb..9e939a7 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -1,11 +1,11 @@ +import fs from 'fs' +import zlib from 'zlib' +import tar from 'tar' import Octokit, { ReposGetLatestReleaseResponse } from '@octokit/rest' -import { spawnSync, SpawnSyncReturns } from 'child_process' -import fs from 'fs' import fetch, { Response } from 'node-fetch' -import zlib from 'zlib' -import tar from 'tar' +import { spawnSync, SpawnSyncReturns } from 'child_process' import { TrivyOption, Vulnerability } from './interface' @@ -22,17 +22,16 @@ export class Downloader { repo: 'trivy' } - constructor(token: string) { - this.githubClient = new Octokit({ auth: `token ${token}` }) + constructor() { + this.githubClient = new Octokit(); } public async download(version: string): Promise { const os: string = this.checkPlatform(process.platform) const downloadUrl: string = await this.getDownloadUrl(version, os) console.debug(`Download URL: ${downloadUrl}`) - const response: Response = await fetch(downloadUrl) const trivyCmdBaseDir: string = process.env.GITHUB_WORKSPACE || '.' - const trivyCmdPath: string = await this.saveTrivyCmd(response, trivyCmdBaseDir) + const trivyCmdPath: string = await this.downloadTrivyCmd(downloadUrl, trivyCmdBaseDir) console.debug(`Trivy Command Path: ${trivyCmdPath}`) return trivyCmdPath } @@ -40,13 +39,13 @@ export class Downloader { private checkPlatform(platform: string): string { switch (platform) { case 'linux': - return 'Linux' + return 'Linux'; case 'darwin': - return 'macOS' + return 'macOS'; default: - throw new Error(`Sorry, ${platform} is not supported. - Trivy support Linux, MacOS, FreeBSD and OpenBSD. - `) + const errorMsg: string = `Sorry, ${platform} is not supported. + Trivy support Linux, MacOS, FreeBSD and OpenBSD.`; + throw new Error(errorMsg); } } @@ -66,43 +65,46 @@ export class Downloader { }) } } catch (error) { - throw new Error(` - The Trivy version that you specified does not exist. - Version: ${version} - `) + throw new Error(`The Trivy version that you specified does not exist. + Version: ${version} + `); } - const filename: string = `trivy_${version}_${os}-64bit.tar.gz` - + 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 + return asset.browser_download_url; } } - throw new Error(`Cloud not be found Trivy asset that You specified. + const errorMsg: string = `Cloud not be found Trivy asset that You specified. Version: ${version} - OS: ${os} - `) + OS: ${os}`; + throw new Error(errorMsg); } - private saveTrivyCmd(response: Response, savedPath: string = '.'): Promise { - return new Promise((resolve, reject) => { - const extract = tar.extract({ path: savedPath }) - response.body.pipe(zlib.createGunzip()).pipe(extract) - - extract.on('finish', () => { - if (!this.trivyExists(savedPath)) { - reject('Failed to extract Trivy command file.') - } - resolve(`${savedPath}/trivy`) - }) - }) + private async downloadTrivyCmd(downloadUrl: string, savedPath: string = '.'): Promise { + const response: Response = await fetch(downloadUrl); + return new Promise((resolve, reject) => { + const extract = tar.extract({ C: savedPath }, ['trivy']) + response.body + .on('error', reject) + .pipe(zlib.createGunzip()) + .on('error', reject) + .pipe(extract) + .on('error', reject) + .on('finish', () => { + if (!this.trivyExists(savedPath)) { + reject('Failed to extract Trivy command file.'); + } + resolve(`${savedPath}/trivy`); + }); + }); } - public trivyExists(baseDir: string): boolean { - const trivyCmdPaths: string[] = fs.readdirSync(baseDir).filter(f => f === 'trivy') + public trivyExists(targetDir: string): boolean { + const trivyCmdPaths: string[] = fs.readdirSync(targetDir).filter(f => f === 'trivy') return trivyCmdPaths.length === 1 } } @@ -129,9 +131,9 @@ export class Trivy { } throw new Error(`Failed vulnerability scan using Trivy. - stdout: ${result.stdout} - stderr: ${result.stderr} - erorr: ${result.error} + stdout: ${result.stdout} + stderr: ${result.stderr} + erorr: ${result.error} `) } From cd138bc928454aff353c5b6a0a9122279654c829 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 05:42:42 +0900 Subject: [PATCH 04/15] [gitignore] Add .vscode dir --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ffdd18c..fa6a12f 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,5 @@ typings/ # DynamoDB Local files .dynamodb/ + +.vscode/ \ No newline at end of file From 8d73bde1e0664a0122c8957d798b5280bdcaf3ad Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 05:43:54 +0900 Subject: [PATCH 05/15] [package] Install prettier to format code --- package-lock.json | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index dd185b8..e195784 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4063,6 +4063,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, "pretty-format": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", diff --git a/package.json b/package.json index 722972d..bec9fe9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@zeit/ncc": "^0.20.5", "jest": "^24.8.0", "jest-circus": "^24.7.1", + "prettier": "^1.19.1", "ts-jest": "^24.0.2", "typescript": "^3.5.1" } From aac3a2463c3c3ec2c87aeb3d972f0f902034bec4 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 05:45:48 +0900 Subject: [PATCH 06/15] [prettier] Add prettier config --- .prettierrc.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .prettierrc.yml diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..fe702cb --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,5 @@ +semi: true +singleQuote: true +trailingComma: all +parser": typescript +bracketSpacing: true \ No newline at end of file From 08857ee4be462900e6b94c027fe0fa5e893591ce Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 05:45:51 +0900 Subject: [PATCH 07/15] [src] Format code by prettier --- src/index.ts | 74 ++++++++++++++++----------- src/interface.ts | 40 +++++++-------- src/issue.ts | 28 ++++++----- src/trivy.ts | 128 ++++++++++++++++++++++++++--------------------- 4 files changed, 153 insertions(+), 117 deletions(-) diff --git a/src/index.ts b/src/index.ts index 16b85b4..889a6af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,52 +1,68 @@ -import * as core from '@actions/core' -import { Trivy, Downloader } from './trivy' -import { createIssue } from './issue' +import * as core from '@actions/core'; +import { Trivy, Downloader } from './trivy'; +import { createIssue } from './issue'; import { - TrivyOption, IssueOption, IssueResponse, Vulnerability -} from './interface' + TrivyOption, + IssueOption, + IssueResponse, + Vulnerability, +} from './interface'; async function run() { try { - const token: string = core.getInput('token', { required: true }) - const trivyVersion: string = core.getInput('trivy_version').replace(/^v/, '') - const image: string | undefined = core.getInput('image') || process.env.IMAGE_NAME + const token: string = core.getInput('token', { required: true }); + const trivyVersion: string = core + .getInput('trivy_version') + .replace(/^v/, ''); + const image: string | undefined = + core.getInput('image') || process.env.IMAGE_NAME; if (image === undefined || image === '') { - throw new Error('Please specify scan target image name') + throw new Error('Please specify scan target image name'); } const trivyOptions: TrivyOption = { severity: core.getInput('severity').replace(/\s+/g, ''), vulnType: core.getInput('vuln_type').replace(/\s+/g, ''), - ignoreUnfixed: core.getInput('ignore_unfixed') - .toLowerCase() === 'true' - ? true : false - } + ignoreUnfixed: + core.getInput('ignore_unfixed').toLowerCase() === 'true' ? true : false, + }; - const downloader = new Downloader() - const trivyCmdPath: string = await downloader.download(trivyVersion) - const result: Vulnerability[] = Trivy.scan(trivyCmdPath, image, trivyOptions) - const issueContent: string = Trivy.parse(result) + const downloader = new Downloader(); + const trivyCmdPath: string = await downloader.download(trivyVersion); + const result: Vulnerability[] = Trivy.scan( + trivyCmdPath, + image, + trivyOptions, + ); + const issueContent: string = Trivy.parse(result); if (issueContent === '') { - core.info('Vulnerabilities were not found.\nYour maintenance looks good 👍') - return + core.info( + 'Vulnerabilities were not found.\nYour maintenance looks good 👍', + ); + return; } const issueOptions: IssueOption = { title: core.getInput('issue_title'), body: issueContent, - labels: core.getInput('issue_label').replace(/\s+/g, '').split(','), - assignees: core.getInput('issue_assignee').replace(/\s+/g, '').split(','), - } - const output: IssueResponse = await createIssue(token, issueOptions) - core.setOutput('html_url', output.htmlUrl) - core.setOutput('issue_number', output.issueNumber.toString()) - + labels: core + .getInput('issue_label') + .replace(/\s+/g, '') + .split(','), + assignees: core + .getInput('issue_assignee') + .replace(/\s+/g, '') + .split(','), + }; + const output: IssueResponse = await createIssue(token, issueOptions); + core.setOutput('html_url', output.htmlUrl); + core.setOutput('issue_number', output.issueNumber.toString()); } catch (error) { - core.error(error.stack) - core.setFailed(error.message) + core.error(error.stack); + core.setFailed(error.message); } } -run() +run(); diff --git a/src/interface.ts b/src/interface.ts index ace1d95..0eafec0 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,33 +1,33 @@ export interface IssueOption { - title: string, - body: string, - labels?: string[], - assignees?: string[], + title: string; + body: string; + labels?: string[]; + assignees?: string[]; } export interface IssueResponse { - issueNumber: number, - htmlUrl: string + issueNumber: number; + htmlUrl: string; } export interface TrivyOption { - severity: string, - vulnType: string, - ignoreUnfixed: boolean + severity: string; + vulnType: string; + ignoreUnfixed: boolean; } export interface Vulnerability { - Target: string, - Vulnerabilities: CVE[] | null + Target: string; + Vulnerabilities: CVE[] | null; } interface CVE { - VulnerabilityID: string, - PkgName: string, - InstalledVersion: string, - FixedVersion: string, - Title: string, - Description: string, - Severity: string, - References: string[] -} \ No newline at end of file + VulnerabilityID: string; + PkgName: string; + InstalledVersion: string; + FixedVersion: string; + Title: string; + Description: string; + Severity: string; + References: string[]; +} diff --git a/src/issue.ts b/src/issue.ts index 2923c8c..c9f8a68 100644 --- a/src/issue.ts +++ b/src/issue.ts @@ -1,17 +1,21 @@ -import Octokit, { IssuesCreateResponse } from '@octokit/rest' -import * as github from '@actions/github' -import { IssueOption, IssueResponse } from './interface' +import Octokit, { IssuesCreateResponse } from '@octokit/rest'; +import * as github from '@actions/github'; +import { IssueOption, IssueResponse } from './interface'; - -export async function createIssue(token: string, options: IssueOption): Promise { - const client: Octokit = new github.GitHub(token) - const { data: issue }: Octokit.Response = await client.issues.create({ +export async function createIssue( + token: string, + options: IssueOption, +): Promise { + const client: Octokit = new github.GitHub(token); + const { + data: issue, + }: Octokit.Response = await client.issues.create({ ...github.context.repo, ...options, - }) + }); const result: IssueResponse = { issueNumber: issue.number, - htmlUrl: issue.html_url - } - return result -} \ No newline at end of file + htmlUrl: issue.html_url, + }; + return result; +} diff --git a/src/trivy.ts b/src/trivy.ts index 9e939a7..ea3de78 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -1,39 +1,40 @@ -import fs from 'fs' -import zlib from 'zlib' -import tar from 'tar' -import Octokit, { - ReposGetLatestReleaseResponse -} from '@octokit/rest' -import fetch, { Response } from 'node-fetch' -import { spawnSync, SpawnSyncReturns } from 'child_process' +import fs from 'fs'; +import zlib from 'zlib'; +import tar from 'tar'; +import Octokit, { ReposGetLatestReleaseResponse } from '@octokit/rest'; +import fetch, { Response } from 'node-fetch'; +import { spawnSync, SpawnSyncReturns } from 'child_process'; -import { TrivyOption, Vulnerability } from './interface' +import { TrivyOption, Vulnerability } from './interface'; interface Repository { - owner: string, - repo: string + owner: string; + repo: string; } export class Downloader { - githubClient: Octokit + githubClient: Octokit; static readonly trivyRepository: Repository = { owner: 'aquasecurity', - repo: 'trivy' - } + repo: 'trivy', + }; constructor() { this.githubClient = new Octokit(); } public async download(version: string): Promise { - const os: string = this.checkPlatform(process.platform) - const downloadUrl: string = await this.getDownloadUrl(version, os) - console.debug(`Download URL: ${downloadUrl}`) - const trivyCmdBaseDir: string = process.env.GITHUB_WORKSPACE || '.' - const trivyCmdPath: string = await this.downloadTrivyCmd(downloadUrl, trivyCmdBaseDir) - console.debug(`Trivy Command Path: ${trivyCmdPath}`) - return trivyCmdPath + const os: string = this.checkPlatform(process.platform); + const downloadUrl: string = await this.getDownloadUrl(version, os); + console.debug(`Download URL: ${downloadUrl}`); + const trivyCmdBaseDir: string = process.env.GITHUB_WORKSPACE || '.'; + const trivyCmdPath: string = await this.downloadTrivyCmd( + downloadUrl, + trivyCmdBaseDir, + ); + console.debug(`Trivy Command Path: ${trivyCmdPath}`); + return trivyCmdPath; } private checkPlatform(platform: string): string { @@ -50,19 +51,19 @@ export class Downloader { } private async getDownloadUrl(version: string, os: string): Promise { - let response: Octokit.Response + let response: Octokit.Response; try { if (version === 'latest') { response = await this.githubClient.repos.getLatestRelease({ - ...Downloader.trivyRepository - }) - version = response.data.tag_name.replace(/v/, '') + ...Downloader.trivyRepository, + }); + version = response.data.tag_name.replace(/v/, ''); } else { response = await this.githubClient.repos.getReleaseByTag({ ...Downloader.trivyRepository, - tag: `v${version}` - }) + tag: `v${version}`, + }); } } catch (error) { throw new Error(`The Trivy version that you specified does not exist. @@ -83,11 +84,14 @@ export class Downloader { throw new Error(errorMsg); } - private async downloadTrivyCmd(downloadUrl: string, savedPath: string = '.'): Promise { + private async downloadTrivyCmd( + downloadUrl: string, + savedPath: string = '.', + ): Promise { const response: Response = await fetch(downloadUrl); return new Promise((resolve, reject) => { - const extract = tar.extract({ C: savedPath }, ['trivy']) + const extract = tar.extract({ C: savedPath }, ['trivy']); response.body .on('error', reject) .pipe(zlib.createGunzip()) @@ -104,64 +108,76 @@ export class Downloader { } public trivyExists(targetDir: string): boolean { - const trivyCmdPaths: string[] = fs.readdirSync(targetDir).filter(f => f === 'trivy') - return trivyCmdPaths.length === 1 + const trivyCmdPaths: string[] = fs + .readdirSync(targetDir) + .filter(f => f === 'trivy'); + return trivyCmdPaths.length === 1; } } export class Trivy { - static scan(trivyPath: string, image: string, options: TrivyOption): Vulnerability[] { + static scan( + trivyPath: string, + image: string, + options: TrivyOption, + ): Vulnerability[] { const args: string[] = [ - '--severity', options.severity, - '--vuln-type', options.vulnType, - '--format', 'json', + '--severity', + options.severity, + '--vuln-type', + options.vulnType, + '--format', + 'json', '--quiet', '--no-progress', - ] + ]; if (options.ignoreUnfixed) { - args.push('--ignore-unfixed') + args.push('--ignore-unfixed'); } - args.push(image) - const result: SpawnSyncReturns = spawnSync(trivyPath, args, { encoding: 'utf-8' }) + args.push(image); + const result: SpawnSyncReturns = spawnSync(trivyPath, args, { + encoding: 'utf-8', + }); if (result.stdout && result.stdout.length > 0) { - return JSON.parse(result.stdout) + return JSON.parse(result.stdout); } throw new Error(`Failed vulnerability scan using Trivy. stdout: ${result.stdout} stderr: ${result.stderr} erorr: ${result.error} - `) + `); } static parse(vulnerabilities: Vulnerability[]): string { - let issueContent: string = '' + let issueContent: string = ''; for (const vuln of vulnerabilities) { - if (vuln.Vulnerabilities === null) continue + if (vuln.Vulnerabilities === null) continue; - issueContent += `## ${vuln.Target}\n` - let vulnTable: string = '|Title|Severity|CVE|Package Name|' - vulnTable += 'Installed Version|Fixed Version|References|\n' - vulnTable += '|:--:|:--:|:--:|:--:|:--:|:--:|:--|\n' + issueContent += `## ${vuln.Target}\n`; + let vulnTable: string = '|Title|Severity|CVE|Package Name|'; + vulnTable += 'Installed Version|Fixed Version|References|\n'; + vulnTable += '|:--:|:--:|:--:|:--:|:--:|:--:|:--|\n'; for (const cve of vuln.Vulnerabilities) { - vulnTable += `|${cve.Title || 'N/A'}|${cve.Severity || 'N/A'}` - vulnTable += `|${cve.VulnerabilityID || 'N/A'}|${cve.PkgName || 'N/A'}` - vulnTable += `|${cve.InstalledVersion || 'N/A'}|${cve.FixedVersion || 'N/A'}|` + vulnTable += `|${cve.Title || 'N/A'}|${cve.Severity || 'N/A'}`; + vulnTable += `|${cve.VulnerabilityID || 'N/A'}|${cve.PkgName || 'N/A'}`; + vulnTable += `|${cve.InstalledVersion || 'N/A'}|${cve.FixedVersion || + 'N/A'}|`; for (const reference of cve.References) { - vulnTable += `${reference || 'N/A'}
` + vulnTable += `${reference || 'N/A'}
`; } - vulnTable.replace(/
$/, '|\n') + vulnTable.replace(/
$/, '|\n'); } - issueContent += `${vulnTable}\n\n` + issueContent += `${vulnTable}\n\n`; } - console.debug(issueContent) - return issueContent + console.debug(issueContent); + return issueContent; } -} \ No newline at end of file +} From 59a482fd0f1ca2ec651a50ea4e90c016b17e4857 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 05:52:18 +0900 Subject: [PATCH 08/15] [trivy] Add default value to download arguments --- dist/index.js | 48 +++++++++++++++++++++++++++++++----------------- src/trivy.ts | 7 +++++-- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/dist/index.js b/dist/index.js index d361e11..2595ff2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2950,10 +2950,10 @@ const github = __importStar(__webpack_require__(469)); function createIssue(token, options) { return __awaiter(this, void 0, void 0, function* () { const client = new github.GitHub(token); - const { data: issue } = yield client.issues.create(Object.assign(Object.assign({}, github.context.repo), options)); + const { data: issue, } = yield client.issues.create(Object.assign(Object.assign({}, github.context.repo), options)); const result = { issueNumber: issue.number, - htmlUrl: issue.html_url + htmlUrl: issue.html_url, }; return result; }); @@ -6573,7 +6573,9 @@ function run() { return __awaiter(this, void 0, void 0, function* () { try { const token = core.getInput('token', { required: true }); - const trivyVersion = core.getInput('trivy_version').replace(/^v/, ''); + const trivyVersion = core + .getInput('trivy_version') + .replace(/^v/, ''); const image = core.getInput('image') || process.env.IMAGE_NAME; if (image === undefined || image === '') { throw new Error('Please specify scan target image name'); @@ -6581,9 +6583,7 @@ function run() { const trivyOptions = { severity: core.getInput('severity').replace(/\s+/g, ''), vulnType: core.getInput('vuln_type').replace(/\s+/g, ''), - ignoreUnfixed: core.getInput('ignore_unfixed') - .toLowerCase() === 'true' - ? true : false + ignoreUnfixed: core.getInput('ignore_unfixed').toLowerCase() === 'true' ? true : false, }; const downloader = new trivy_1.Downloader(); const trivyCmdPath = yield downloader.download(trivyVersion); @@ -6596,8 +6596,14 @@ function run() { const issueOptions = { title: core.getInput('issue_title'), body: issueContent, - labels: core.getInput('issue_label').replace(/\s+/g, '').split(','), - assignees: core.getInput('issue_assignee').replace(/\s+/g, '').split(','), + labels: core + .getInput('issue_label') + .replace(/\s+/g, '') + .split(','), + assignees: core + .getInput('issue_assignee') + .replace(/\s+/g, '') + .split(','), }; const output = yield issue_1.createIssue(token, issueOptions); core.setOutput('html_url', output.htmlUrl); @@ -13205,12 +13211,12 @@ class Downloader { constructor() { this.githubClient = new rest_1.default(); } - download(version) { + download(version = 'latest', trivyCmdDir = __dirname) { return __awaiter(this, void 0, void 0, function* () { const os = this.checkPlatform(process.platform); const downloadUrl = yield this.getDownloadUrl(version, os); console.debug(`Download URL: ${downloadUrl}`); - const trivyCmdBaseDir = process.env.GITHUB_WORKSPACE || '.'; + const trivyCmdBaseDir = process.env.GITHUB_WORKSPACE || trivyCmdDir; const trivyCmdPath = yield this.downloadTrivyCmd(downloadUrl, trivyCmdBaseDir); console.debug(`Trivy Command Path: ${trivyCmdPath}`); return trivyCmdPath; @@ -13289,21 +13295,26 @@ class Downloader { }); } trivyExists(targetDir) { - const trivyCmdPaths = fs_1.default.readdirSync(targetDir).filter(f => f === 'trivy'); + const trivyCmdPaths = fs_1.default + .readdirSync(targetDir) + .filter(f => f === 'trivy'); return trivyCmdPaths.length === 1; } } exports.Downloader = Downloader; Downloader.trivyRepository = { owner: 'aquasecurity', - repo: 'trivy' + repo: 'trivy', }; class Trivy { static scan(trivyPath, image, options) { const args = [ - '--severity', options.severity, - '--vuln-type', options.vulnType, - '--format', 'json', + '--severity', + options.severity, + '--vuln-type', + options.vulnType, + '--format', + 'json', '--quiet', '--no-progress', ]; @@ -13311,7 +13322,9 @@ class Trivy { args.push('--ignore-unfixed'); } args.push(image); - const result = child_process_1.spawnSync(trivyPath, args, { encoding: 'utf-8' }); + const result = child_process_1.spawnSync(trivyPath, args, { + encoding: 'utf-8', + }); if (result.stdout && result.stdout.length > 0) { return JSON.parse(result.stdout); } @@ -13333,7 +13346,8 @@ class Trivy { for (const cve of vuln.Vulnerabilities) { vulnTable += `|${cve.Title || 'N/A'}|${cve.Severity || 'N/A'}`; vulnTable += `|${cve.VulnerabilityID || 'N/A'}|${cve.PkgName || 'N/A'}`; - vulnTable += `|${cve.InstalledVersion || 'N/A'}|${cve.FixedVersion || 'N/A'}|`; + vulnTable += `|${cve.InstalledVersion || 'N/A'}|${cve.FixedVersion || + 'N/A'}|`; for (const reference of cve.References) { vulnTable += `${reference || 'N/A'}
`; } diff --git a/src/trivy.ts b/src/trivy.ts index ea3de78..ccb50f7 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -24,11 +24,14 @@ export class Downloader { this.githubClient = new Octokit(); } - public async download(version: string): Promise { + public async download( + version: string = 'latest', + trivyCmdDir: string = __dirname, + ): Promise { const os: string = this.checkPlatform(process.platform); const downloadUrl: string = await this.getDownloadUrl(version, os); console.debug(`Download URL: ${downloadUrl}`); - const trivyCmdBaseDir: string = process.env.GITHUB_WORKSPACE || '.'; + const trivyCmdBaseDir: string = process.env.GITHUB_WORKSPACE || trivyCmdDir; const trivyCmdPath: string = await this.downloadTrivyCmd( downloadUrl, trivyCmdBaseDir, From a1970b554e3a48cdf5417b8c9ba6535db0bc1024 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 06:05:25 +0900 Subject: [PATCH 09/15] [index] Not use conditional operator to substitute into ignoreUnfixed flag --- dist/index.js | 2 +- src/index.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dist/index.js b/dist/index.js index 2595ff2..9f1450e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6583,7 +6583,7 @@ function run() { const trivyOptions = { severity: core.getInput('severity').replace(/\s+/g, ''), vulnType: core.getInput('vuln_type').replace(/\s+/g, ''), - ignoreUnfixed: core.getInput('ignore_unfixed').toLowerCase() === 'true' ? true : false, + ignoreUnfixed: core.getInput('ignore_unfixed').toLowerCase() === 'true', }; const downloader = new trivy_1.Downloader(); const trivyCmdPath = yield downloader.download(trivyVersion); diff --git a/src/index.ts b/src/index.ts index 889a6af..64e2d94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,8 +24,7 @@ async function run() { const trivyOptions: TrivyOption = { severity: core.getInput('severity').replace(/\s+/g, ''), vulnType: core.getInput('vuln_type').replace(/\s+/g, ''), - ignoreUnfixed: - core.getInput('ignore_unfixed').toLowerCase() === 'true' ? true : false, + ignoreUnfixed: core.getInput('ignore_unfixed').toLowerCase() === 'true', }; const downloader = new Downloader(); From 5048a05bde712f748845cc6a77721a43c5aa9db3 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 06:19:39 +0900 Subject: [PATCH 10/15] [interface] Modify Vulnerability Title to optional --- src/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.ts b/src/interface.ts index 0eafec0..89498d0 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -26,7 +26,7 @@ interface CVE { PkgName: string; InstalledVersion: string; FixedVersion: string; - Title: string; + Title?: string; Description: string; Severity: string; References: string[]; From f3e9097ae12ebe1b472c4fe7221ed12ef72bc890 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 06:32:20 +0900 Subject: [PATCH 11/15] [trivy] Remove default value from version arg of download method --- dist/index.js | 2 +- src/trivy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 9f1450e..d301b5c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -13211,7 +13211,7 @@ class Downloader { constructor() { this.githubClient = new rest_1.default(); } - download(version = 'latest', trivyCmdDir = __dirname) { + download(version, trivyCmdDir = __dirname) { return __awaiter(this, void 0, void 0, function* () { const os = this.checkPlatform(process.platform); const downloadUrl = yield this.getDownloadUrl(version, os); diff --git a/src/trivy.ts b/src/trivy.ts index ccb50f7..a5d7a7a 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -25,7 +25,7 @@ export class Downloader { } public async download( - version: string = 'latest', + version: string, trivyCmdDir: string = __dirname, ): Promise { const os: string = this.checkPlatform(process.platform); From e79ba887fcdb3f2ce347d21f59b8d2159fb82424 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 07:07:57 +0900 Subject: [PATCH 12/15] [trivy] Add a method to validate trivy option --- src/trivy.ts | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/trivy.ts b/src/trivy.ts index a5d7a7a..8cd7c31 100644 --- a/src/trivy.ts +++ b/src/trivy.ts @@ -26,7 +26,7 @@ export class Downloader { public async download( version: string, - trivyCmdDir: string = __dirname, + trivyCmdDir: string = __dirname ): Promise { const os: string = this.checkPlatform(process.platform); const downloadUrl: string = await this.getDownloadUrl(version, os); @@ -34,7 +34,7 @@ export class Downloader { const trivyCmdBaseDir: string = process.env.GITHUB_WORKSPACE || trivyCmdDir; const trivyCmdPath: string = await this.downloadTrivyCmd( downloadUrl, - trivyCmdBaseDir, + trivyCmdBaseDir ); console.debug(`Trivy Command Path: ${trivyCmdPath}`); return trivyCmdPath; @@ -89,7 +89,7 @@ export class Downloader { private async downloadTrivyCmd( downloadUrl: string, - savedPath: string = '.', + savedPath: string = '.' ): Promise { const response: Response = await fetch(downloadUrl); @@ -122,20 +122,22 @@ export class Trivy { static scan( trivyPath: string, image: string, - options: TrivyOption, + option: TrivyOption ): Vulnerability[] { + Trivy.validateOption(option); + const args: string[] = [ '--severity', - options.severity, + option.severity, '--vuln-type', - options.vulnType, + option.vulnType, '--format', 'json', '--quiet', '--no-progress', ]; - if (options.ignoreUnfixed) { + if (option.ignoreUnfixed) { args.push('--ignore-unfixed'); } @@ -145,7 +147,10 @@ export class Trivy { }); if (result.stdout && result.stdout.length > 0) { - return JSON.parse(result.stdout); + const vulnerabilities: Vulnerability[] = JSON.parse(result.stdout); + if (vulnerabilities.length > 0) { + return vulnerabilities; + } } throw new Error(`Failed vulnerability scan using Trivy. @@ -183,4 +188,27 @@ export class Trivy { console.debug(issueContent); return issueContent; } + + static validateOption(option: TrivyOption): boolean { + 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` + ); + } + } + + 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; + } } From c71a52df3f26024708ca6a4e6e716e7e25e1cc6b Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 07:08:18 +0900 Subject: [PATCH 13/15] [tests] Add a test for trivy.ts --- __tests__/trivy.test.ts | 225 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 __tests__/trivy.test.ts diff --git a/__tests__/trivy.test.ts b/__tests__/trivy.test.ts new file mode 100644 index 0000000..79f4423 --- /dev/null +++ b/__tests__/trivy.test.ts @@ -0,0 +1,225 @@ +import { Downloader, Trivy } from '../src/trivy'; +import { unlinkSync, writeFileSync } from 'fs'; +import { Vulnerability, TrivyOption } from '../src/interface'; + +const downloader = new Downloader(); + +function removeTrivyCmd(path: string) { + path = path.replace(/\/trivy$/, ''); + if (downloader.trivyExists(path)) { + unlinkSync(`${path}/trivy`); + } +} + +describe('Platform', () => { + test('is Liniux', () => { + const result = downloader['checkPlatform']('linux'); + expect(result).toBe('Linux'); + }); + + test('is Darwin', () => { + const result = downloader['checkPlatform']('darwin'); + expect(result).toBe('macOS'); + }); + + test('is not linux and darwin', () => { + expect(() => { + downloader['checkPlatform']('other'); + }).toThrowError('Sorry, other is not supported.'); + }); +}); + +describe('getDownloadUrl', () => { + test('with latest version and linux', async () => { + const version = 'latest'; + const os = 'Linux'; + const result = await downloader['getDownloadUrl'](version, os); + expect(result).toMatch( + /releases\/download\/v[0-9]\.[0-9]\.[0-9]\/trivy_[0-9]\.[0-9]\.[0-9]_Linux-64bit\.tar\.gz$/ + ); + }); + + test('with 0.2.0 and macOS', async () => { + const version = '0.2.0'; + const os = 'macOS'; + const result = await downloader['getDownloadUrl'](version, os); + expect(result).toMatch( + /releases\/download\/v0\.2\.0\/trivy_0\.2\.0_macOS-64bit\.tar\.gz$/ + ); + }); + + test('with non-supported version', async () => { + const version = 'none'; + const os = 'Linux'; + await expect( + downloader['getDownloadUrl'](version, os) + ).rejects.toThrowError( + 'The Trivy version that you specified does not exist.' + ); + }); + + test('with non-supported os', async () => { + const version = 'latest'; + const os = 'none'; + await expect( + downloader['getDownloadUrl'](version, os) + ).rejects.toThrowError( + 'Cloud not be found Trivy asset that You specified.' + ); + }); +}); + +describe('Download trivy command', () => { + afterAll(() => { + removeTrivyCmd('__tests__'); + }); + + test('with valid download URL and save in __tests__', async () => { + let downloadUrl = 'https://github.com/aquasecurity/trivy'; + downloadUrl += '/releases/download/v0.2.1/trivy_0.2.1_Linux-64bit.tar.gz'; + const savePath = './__tests__'; + await expect( + downloader['downloadTrivyCmd'](downloadUrl, savePath) + ).resolves.toEqual(`${savePath}/trivy`); + }, 300000); + + test('with invalid download URL', async () => { + const downloadUrl = 'https://github.com/this_is_invalid'; + await expect(downloader['downloadTrivyCmd'](downloadUrl)).rejects.toThrow(); + }); +}); + +describe('Trivy command', () => { + beforeAll(() => { + writeFileSync('./trivy', ''); + }); + + afterAll(() => { + removeTrivyCmd('.'); + }); + + test('exists', () => { + const result = downloader.trivyExists('.'); + expect(result).toBeTruthy(); + }); + + test('does not exist', () => { + const result = downloader.trivyExists('src'); + expect(result).toBeFalsy(); + }); +}); + +describe('Scan', () => { + let trivyPath: string; + const image: string = 'alpine:3.10'; + + beforeAll(async () => { + trivyPath = !downloader.trivyExists('./__tests__') + ? await downloader.download('latest', './__tests__') + : './__tests__/trivy'; + }, 300000); + + afterAll(() => { + removeTrivyCmd(trivyPath); + }); + + test('with valid options', () => { + const options: TrivyOption = { + severity: 'HIGH,CRITICAL', + vulnType: 'os,library', + ignoreUnfixed: true, + }; + const result: Vulnerability[] = Trivy.scan(trivyPath, image, options); + expect(result.length).toBeGreaterThanOrEqual(1); + }); + + test('without ignoreUnfixed', () => { + const options: TrivyOption = { + severity: 'HIGH,CRITICAL', + vulnType: 'os,library', + ignoreUnfixed: false, + }; + const result: Vulnerability[] = Trivy.scan(trivyPath, image, options); + expect(result.length).toBeGreaterThanOrEqual(1); + }); + + test('with invalid severity', () => { + const invalidOption: TrivyOption = { + severity: 'INVALID', + vulnType: 'os,library', + ignoreUnfixed: true, + }; + expect(() => { + Trivy.scan(trivyPath, image, invalidOption); + }).toThrowError('severity option error: INVALID is unknown severity'); + }); + + test('with invalid vulnType', () => { + const invalidOption: TrivyOption = { + severity: 'HIGH', + vulnType: 'INVALID', + ignoreUnfixed: true, + }; + expect(() => { + Trivy.scan(trivyPath, image, invalidOption); + }).toThrowError('vuln-type option error: INVALID is unknown vuln-type'); + }); +}); + +describe('Parse', () => { + test('the result without vulnerabilities', () => { + const vulnerabilities: Vulnerability[] = [ + { + Target: 'alpine:3.10 (alpine 3.10.3)', + Vulnerabilities: null, + }, + ]; + const result = Trivy.parse(vulnerabilities); + expect(result).toBe(''); + }); + + test('the result including vulnerabilities', () => { + const vulnerabilities: Vulnerability[] = [ + { + Target: 'alpine:3.9 (alpine 3.9.4)', + Vulnerabilities: [ + { + VulnerabilityID: 'CVE-2019-14697', + PkgName: 'musl', + InstalledVersion: '1.1.20-r4', + FixedVersion: '1.1.20-r5', + Description: + "musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.", + Severity: 'HIGH', + References: [ + 'http://www.openwall.com/lists/oss-security/2019/08/06/4', + 'https://www.openwall.com/lists/musl/2019/08/06/1', + ], + }, + { + VulnerabilityID: 'CVE-2019-1549', + PkgName: 'openssl', + InstalledVersion: '1.1.1b-r1', + FixedVersion: '1.1.1d-r0', + Title: 'openssl: information disclosure in fork()', + Description: + 'OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).', + Severity: 'MEDIUM', + References: [ + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549', + 'https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be', + 'https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/', + 'https://security.netapp.com/advisory/ntap-20190919-0002/', + 'https://support.f5.com/csp/article/K44070243', + 'https://www.openssl.org/news/secadv/20190910.txt', + ], + }, + ], + }, + ]; + const result = Trivy.parse(vulnerabilities); + expect(result).toMatch( + /\|Title\|Severity\|CVE\|Package Name\|Installed Version\|Fixed Version\|References\|/ + ); + }); +}); From 7e2907dfcb42a6adc65600d1c857c678d7719d9e Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 07:09:05 +0900 Subject: [PATCH 14/15] [prettier] Modify trailling comma to es5 --- .prettierrc.yml | 2 +- dist/index.js | 29 ++++++++++++++++++++++++----- src/index.ts | 4 ++-- src/issue.ts | 2 +- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.prettierrc.yml b/.prettierrc.yml index fe702cb..cd617f9 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -1,5 +1,5 @@ semi: true singleQuote: true -trailingComma: all +trailingComma: es5 parser": typescript bracketSpacing: true \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index d301b5c..567cef9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -13307,18 +13307,19 @@ Downloader.trivyRepository = { repo: 'trivy', }; class Trivy { - static scan(trivyPath, image, options) { + static scan(trivyPath, image, option) { + Trivy.validateOption(option); const args = [ '--severity', - options.severity, + option.severity, '--vuln-type', - options.vulnType, + option.vulnType, '--format', 'json', '--quiet', '--no-progress', ]; - if (options.ignoreUnfixed) { + if (option.ignoreUnfixed) { args.push('--ignore-unfixed'); } args.push(image); @@ -13326,7 +13327,10 @@ class Trivy { encoding: 'utf-8', }); if (result.stdout && result.stdout.length > 0) { - return JSON.parse(result.stdout); + const vulnerabilities = JSON.parse(result.stdout); + if (vulnerabilities.length > 0) { + return vulnerabilities; + } } throw new Error(`Failed vulnerability scan using Trivy. stdout: ${result.stdout} @@ -13358,6 +13362,21 @@ class Trivy { console.debug(issueContent); return issueContent; } + static validateOption(option) { + 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`); + } + } + 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; + } } exports.Trivy = Trivy; diff --git a/src/index.ts b/src/index.ts index 64e2d94..9d67632 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,13 +32,13 @@ async function run() { const result: Vulnerability[] = Trivy.scan( trivyCmdPath, image, - trivyOptions, + trivyOptions ); const issueContent: string = Trivy.parse(result); if (issueContent === '') { core.info( - 'Vulnerabilities were not found.\nYour maintenance looks good 👍', + 'Vulnerabilities were not found.\nYour maintenance looks good 👍' ); return; } diff --git a/src/issue.ts b/src/issue.ts index c9f8a68..7c96306 100644 --- a/src/issue.ts +++ b/src/issue.ts @@ -4,7 +4,7 @@ import { IssueOption, IssueResponse } from './interface'; export async function createIssue( token: string, - options: IssueOption, + options: IssueOption ): Promise { const client: Octokit = new github.GitHub(token); const { From 0f27c0d49701ccc3c14f4cb9321ecbb9c5266b01 Mon Sep 17 00:00:00 2001 From: homoluctus Date: Wed, 27 Nov 2019 07:12:50 +0900 Subject: [PATCH 15/15] [ci] Add jest test job --- .github/workflows/test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1e72f0..23fb4d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,22 @@ env: IMAGE_NAME: alpine:3.10.1 jobs: + jest: + name: Test with jest + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-node@v1 + with: + node-version: '12.x' + + - name: Install dependencies + run: npm install + + - name: Jest + run: npm run test + test1: name: Test for with parameter runs-on: ubuntu-18.04