import Docker = require ( "dockerode" ) ; import * as path from "path" ; import yaml from "js-yaml" ; import stream from "stream" ; import fs from "fs" ; import { IArgs , parseInputParameters } from "./cli-args" ; import { ChildProcess , exec , ExecException } from "child_process" ; const SNYK_CLI_DOCKER_IMAGE_NAME = "snyk/snyk:docker" ; let isDebugSet = false ; export interface SnykResult { imageName : string ; result : string ; } function logDebug ( msg ) { if ( isDebugSet ) { console . error ( msg ) ; } } export interface IExecCommandResult { exitCode : number ; stdout : string ; stderr : string ; } export async function runCommand ( fullCommand ) { return new Promise < IExecCommandResult > ( ( resolve , reject ) => { const res = exec ( fullCommand , ( err , stdout , stderr ) => { if ( err ) { const retValue = { exitCode : err . code , stdout : stdout , stderr : stderr } as IExecCommandResult ; reject ( retValue ) ; } else { const retValue = { exitCode : 0 , stdout : stdout , stderr : stderr } as IExecCommandResult ; resolve ( retValue ) ; } } ) ; } ) ; } export async function pullImage ( imageName ) { logDebug ( `pulling image: ${ imageName }` ) ; const docker = new Docker ( ) ; return new Promise < string > ( ( resolve , reject ) => { docker . pull ( imageName , ( err , stream ) => { if ( err ) { console . error ( "failed pulling image" ) ; console . error ( `err.statusCode: ${ err . statusCode }` ) ; console . error ( `err.message: ${ err . message }` ) ; reject ( err ) ; } else { let message = "" ; stream . on ( "data" , data => { message += data ; } ) ; stream . on ( "end" , ( ) => { logDebug ( message ) ; resolve ( message ) ; } ) ; stream . on ( "error" , err => { reject ( err ) ; } ) ; } } ) ; } ) ; } async function runSnykTestWithDocker ( snykToken , snykCLIImageName , imageToTest , options ) { const docker = new Docker ( ) ; const myStdOutCaptureStream = new stream . Writable ( ) ; let stdoutString = "" ; myStdOutCaptureStream . _write = function ( chunk , encoding , done ) { stdoutString += chunk . toString ( ) ; done ( ) ; } ; const myStdErrCaptureStream = new stream . Writable ( ) ; let stderrString = "" ; myStdErrCaptureStream . _write = function ( chunk , encoding , done ) { stderrString += chunk . toString ( ) ; done ( ) ; } ; const createOptions = { env : [ `SNYK_TOKEN=${ snykToken }` ] , Binds : [ "/var/run/docker.sock:/var/run/docker.sock" ] , Tty : false } ; const startOptions = { } ; const command = assembleSnykCommand ( imageToTest , options ) ; return new Promise ( ( resolve , reject ) => { docker . run ( snykCLIImageName , [ command ] , [ myStdOutCaptureStream , myStdErrCaptureStream ] , createOptions , startOptions , ( err , data , container ) => { if ( err ) { reject ( err ) ; } else { logDebug ( `runSnykTestWithDocker(${ imageToTest }): data.StatusCode: ${ data . StatusCode }` ) ; resolve ( stdoutString ) ; } } ) ; } ) ; } export const assembleSnykCommand = ( imageName , options ) => { let command = `snyk test --docker ${ imageName }` ; if ( options . json ) command = command + " --json" ; if ( options . debug ) console . debug ( command ) ; if ( options . debug ) console . debug ( options ) ; return command ; } ; export function isValidImageName ( imageName ) { const newRegexString = "^(?:(?=[^:\\/]{1,253})(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(?:\\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*(?::[0-9]{1,5})?/)?((?![._-])(?:[a-z0-9._-]*)(?<![._-])(?:/(?![._-])[a-z0-9._-]*(?<![._-]))*)(?::(?![.-])[a-zA-Z0-9_.-]{1,128})?$" ; const regex = RegExp ( newRegexString ) ; const isMatch = regex . test ( imageName ) ; return isMatch ; } export function flatImageSearch ( allYamlStr ) { const setImages = new Set ( ) ; const allLines = allYamlStr . split ( "\n" ) ; for ( const nextLine of allLines ) { const trimmedLine = nextLine . trim ( ) ; if ( trimmedLine . startsWith ( "image:" ) ) { const splited = trimmedLine . split ( /: (.+)?/ , 2 ) ; if ( splited . length == 2 ) { let imageName = splited [ 1 ] . trim ( ) ; if ( ( imageName . startsWith ( '"' ) && imageName . endsWith ( '"' ) ) || ( imageName . startsWith ( "'" ) && imageName . endsWith ( "'" ) ) ) { imageName = imageName . substr ( 1 , imageName . length - 2 ) ; } if ( isValidImageName ( imageName ) ) { setImages . add ( imageName ) ; } else { exported . logDebug ( `warning: image name thrown out because it didn't pass validation: ${ imageName }` ) ; } } } } return Array . from ( setImages ) as string [ ] ; } function getHelmChartLabelForOutput ( helmChartDirectory ) { try { const fullPath = path . join ( helmChartDirectory , "Chart.yaml" ) ; const chartfileContentsStr = fs . readFileSync ( fullPath , "utf8" ) ; const parsedObj = yaml . safeLoad ( chartfileContentsStr ) ; const name = parsedObj . name ; const version = parsedObj . version ; return `${ name }@${ version }` ; } catch ( err ) { console . error ( "error while trying to read Chart.yaml file" ) ; console . error ( err ) ; throw err ; } } export async function mainWithParams ( args , snykToken ) { let renderedTemplates ; let helmCommand ; try { helmCommand = assembleHelmTemplateCommand ( args ) ; if ( args . debug ) console . debug ( "helm template command: " , helmCommand ) ; const helmCommandResObj = await runCommand ( helmCommand ) ; renderedTemplates = helmCommandResObj . stdout ; } catch ( err ) { console . error ( err . stderr ) ; console . info ( `try to run '${ helmCommand }' first!` ) ; return ; } const allImages = flatImageSearch ( renderedTemplates ) ; logDebug ( "found all the images:" ) ; allImages . forEach ( ( i ) => logDebug ( `  - ${ i }` ) ) ; const doTest = ! args . notest ; if ( doTest ) { await pullImage ( SNYK_CLI_DOCKER_IMAGE_NAME ) ; } const helmChartLabel = getHelmChartLabelForOutput ( args . inputDirectory ) ; const allOutputData = [ ] ; for ( const imageName of allImages ) { try { const response = { imageName : imageName , result : "" } ; if ( doTest ) { await pullImage ( imageName ) ; const rawResult = await runSnykTestWithDocker ( snykToken , SNYK_CLI_DOCKER_IMAGE_NAME , imageName , args ) ; response . result = args . json ? JSON . parse ( rawResult ) : rawResult ; allOutputData . push ( response ) ; if ( args . debug ) console . debug ( response ) ; } } catch ( err ) { console . error ( "Error caught: " + err . message ) ; } } handleOutput ( handleResult ( helmChartLabel , allOutputData , args ) , args ) ; return allOutputData ; } export const assembleHelmTemplateCommand = ( options ) => { let cmd = `helm template ${ options . inputDirectory }` ; if ( options . helmTemplateOptions ) cmd = `${ cmd } ${ options . helmTemplateOptions }` ; return cmd ; } ; export const handleResult = ( helmChartLabel , results , options ) => { let output ; if ( options . json ) { output = { helmChart : helmChartLabel , images : results } ; } else { output = "" ; for ( const result of results ) { output += `Image: ${ result . imageName }\n` ; output += `${ chopTheAdditionalSuggestions ( result ) }\n` ; } } return output ; } ; const chopTheAdditionalSuggestions = ( result ) => { const preOutput = result . result . split ( "\n" ) ; const NUMBER_OF_LINES_TO_BE_CUT = 7 ; const NUMBER_OF_LINES_TO_BE_KEEP = preOutput . length - NUMBER_OF_LINES_TO_BE_CUT ; const trimmedOutput = preOutput . slice ( 0 , NUMBER_OF_LINES_TO_BE_KEEP ) ; return trimmedOutput . join ( "\n" ) ; } ; export const handleOutput = ( output , options ) => { if ( options . json ) output = JSON . stringify ( output , null , 2 ) ; if ( options . output ) { fs . writeFileSync ( options . output , output ) ; } else { console . log ( output ) ; } } ; async function main ( ) { const snykToken = process . env . SNYK_TOKEN ? process . env . SNYK_TOKEN : "" ; if ( ! snykToken ) { console . error ( "SNYK_TOKEN environment variable is not set" ) ; process . exit ( 2 ) ; } const inputArgs = process . argv . slice ( 2 ) ; const args = parseInputParameters ( inputArgs ) ; isDebugSet = args . debug ; if ( ! args . inputDirectory || ( args . inputDirectory && args . inputDirectory === "." ) ) { args . inputDirectory = process . cwd ( ) ; } await mainWithParams ( args , snykToken ) ; } if ( require . main === module ) { main ( ) ; } const exported = { logDebug , flatImageSearch , isValidImageName } ; export default exported ;