2020-10-20 09:49:26 +02:00
package whitesource
import (
2021-03-08 17:01:18 +01:00
"bufio"
"bytes"
2021-02-03 14:52:48 +01:00
"encoding/json"
2020-10-20 09:49:26 +02:00
"fmt"
2021-03-08 17:01:18 +01:00
"io"
"os"
2021-02-03 14:52:48 +01:00
"path/filepath"
2021-03-08 17:01:18 +01:00
"regexp"
2021-07-13 20:36:36 +02:00
"strings"
2021-03-08 17:01:18 +01:00
"sync"
2024-06-25 19:08:27 +05:30
"time"
2021-02-03 14:52:48 +01:00
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
2020-10-20 09:49:26 +02:00
)
2021-02-03 14:52:48 +01:00
const jvmTarGz = "jvm.tar.gz"
const jvmDir = "./jvm"
2021-03-08 17:01:18 +01:00
const projectRegEx = ` Project name: ([^,]*), URL: (.*) `
2021-02-03 14:52:48 +01:00
2020-10-20 09:49:26 +02:00
// ExecuteUAScan executes a scan with the Whitesource Unified Agent.
func ( s * Scan ) ExecuteUAScan ( config * ScanOptions , utils Utils ) error {
2022-02-23 09:30:19 +01:00
s . AgentName = "WhiteSource Unified Agent"
2024-06-20 09:08:24 +03:00
switch config . BuildTool {
case "mta" :
return s . ExecuteUAScanForMTA ( config , utils )
case "npm" :
if config . DisableNpmSubmodulesAggregation {
return s . ExecuteUAScanForMultiModuleNPM ( config , utils )
} else {
return s . ExecuteUAScanInPath ( config , utils , config . ScanPath )
}
default :
2021-05-03 11:53:16 +03:00
return s . ExecuteUAScanInPath ( config , utils , config . ScanPath )
2021-02-03 14:52:48 +01:00
}
2024-06-20 09:08:24 +03:00
}
func ( s * Scan ) ExecuteUAScanForMTA ( config * ScanOptions , utils Utils ) error {
2021-02-03 14:52:48 +01:00
log . Entry ( ) . Infof ( "Executing WhiteSource UA scan for MTA project" )
2024-06-20 09:08:24 +03:00
log . Entry ( ) . Infof ( "Executing WhiteSource UA scan for Maven part" )
2021-02-03 14:52:48 +01:00
pomExists , _ := utils . FileExists ( "pom.xml" )
if pomExists {
mavenConfig := * config
mavenConfig . BuildTool = "maven"
2021-05-03 11:53:16 +03:00
if err := s . ExecuteUAScanInPath ( & mavenConfig , utils , config . ScanPath ) ; err != nil {
2021-02-03 14:52:48 +01:00
return errors . Wrap ( err , "failed to run scan for maven modules of mta" )
}
} else {
2021-03-04 10:38:57 +01:00
if pomFiles , _ := utils . Glob ( "**/pom.xml" ) ; len ( pomFiles ) > 0 {
2021-05-10 17:44:28 +02:00
log . SetErrorCategory ( log . ErrorCustom )
2021-03-04 10:38:57 +01:00
return fmt . Errorf ( "mta project with java modules does not contain an aggregator pom.xml in the root - this is mandatory" )
}
2021-02-03 14:52:48 +01:00
}
2024-06-20 09:08:24 +03:00
log . Entry ( ) . Infof ( "Executing WhiteSource UA scan for NPM part" )
return s . ExecuteUAScanForMultiModuleNPM ( config , utils )
}
func ( s * Scan ) ExecuteUAScanForMultiModuleNPM ( config * ScanOptions , utils Utils ) error {
log . Entry ( ) . Infof ( "Executing WhiteSource UA scan for multi-module NPM projects" )
2021-02-03 14:52:48 +01:00
packageJSONFiles , err := utils . FindPackageJSONFiles ( config )
if err != nil {
return errors . Wrap ( err , "failed to find package.json files" )
}
if len ( packageJSONFiles ) > 0 {
npmConfig := * config
npmConfig . BuildTool = "npm"
for _ , packageJSONFile := range packageJSONFiles {
// we only need the path here
modulePath , _ := filepath . Split ( packageJSONFile )
projectName , err := getProjectNameFromPackageJSON ( packageJSONFile , utils )
if err != nil {
return errors . Wrapf ( err , "failed retrieve project name" )
}
npmConfig . ProjectName = projectName
// ToDo: likely needs to be refactored, AggregateProjectName should only be available if we want to force aggregation?
s . AggregateProjectName = projectName
if err := s . ExecuteUAScanInPath ( & npmConfig , utils , modulePath ) ; err != nil {
return errors . Wrapf ( err , "failed to run scan for npm module %v" , modulePath )
}
}
}
_ = removeJre ( filepath . Join ( jvmDir , "bin" , "java" ) , utils )
return nil
}
// ExecuteUAScanInPath executes a scan with the Whitesource Unified Agent in a dedicated scanPath.
func ( s * Scan ) ExecuteUAScanInPath ( config * ScanOptions , utils Utils , scanPath string ) error {
2020-10-20 09:49:26 +02:00
// Download the unified agent jar file if one does not exist
2021-02-03 14:52:48 +01:00
err := downloadAgent ( config , utils )
if err != nil {
2020-10-20 09:49:26 +02:00
return err
}
2021-02-03 14:52:48 +01:00
// Download JRE in case none is available
javaPath , err := downloadJre ( config , utils )
if err != nil {
2020-10-20 09:49:26 +02:00
return err
}
2022-02-23 09:30:19 +01:00
// Fetch version of UA
versionBuffer := bytes . Buffer { }
utils . Stdout ( & versionBuffer )
err = utils . RunExecutable ( javaPath , "-jar" , config . AgentFileName , "-v" )
if err != nil {
return errors . Wrap ( err , "Failed to determine UA version" )
}
s . AgentVersion = strings . TrimSpace ( versionBuffer . String ( ) )
log . Entry ( ) . Debugf ( "Read UA version %v from Stdout" , s . AgentVersion )
utils . Stdout ( log . Writer ( ) )
2021-02-03 14:52:48 +01:00
// ToDo: Check if Download of Docker/container image should be done here instead of in cmd/whitesourceExecuteScan.go
// ToDo: check if this is required
2024-01-05 18:23:55 +03:00
if ! config . SkipParentProjectResolution {
if err := s . AppendScannedProject ( s . AggregateProjectName ) ; err != nil {
return err
}
2020-11-10 09:09:51 +01:00
}
2024-10-18 17:06:41 +03:00
if config . UseGlobalConfiguration {
config . ConfigFilePath , err = filepath . Abs ( config . ConfigFilePath )
if err != nil {
return err
}
}
2024-07-10 19:02:14 +05:00
configPath , err := config . RewriteUAConfigurationFile ( utils , s . AggregateProjectName , config . Verbose )
2021-02-03 14:52:48 +01:00
if err != nil {
return err
}
if len ( scanPath ) == 0 {
scanPath = "."
}
2021-03-08 17:01:18 +01:00
// log parsing in order to identify the projects WhiteSource really scanned
// we may refactor this in case there is a safer way to identify the projects e.g. via REST API
//ToDO: we only need stdOut or stdErr, let's see where UA writes to ...
prOut , stdOut := io . Pipe ( )
trOut := io . TeeReader ( prOut , os . Stderr )
utils . Stdout ( stdOut )
prErr , stdErr := io . Pipe ( )
trErr := io . TeeReader ( prErr , os . Stderr )
utils . Stdout ( stdErr )
var wg sync . WaitGroup
wg . Add ( 2 )
go func ( ) {
defer wg . Done ( )
scanLog ( trOut , s )
} ( )
go func ( ) {
defer wg . Done ( )
scanLog ( trErr , s )
} ( )
2021-02-25 13:16:48 +01:00
err = utils . RunExecutable ( javaPath , "-jar" , config . AgentFileName , "-d" , scanPath , "-c" , configPath , "-wss.url" , config . AgentURL )
2021-02-03 14:52:48 +01:00
if err := removeJre ( javaPath , utils ) ; err != nil {
log . Entry ( ) . Warning ( err )
}
if err != nil {
if err := removeJre ( javaPath , utils ) ; err != nil {
log . Entry ( ) . Warning ( err )
}
exitCode := utils . GetExitCode ( )
log . Entry ( ) . Infof ( "WhiteSource scan failed with exit code %v" , exitCode )
evaluateExitCode ( exitCode )
return errors . Wrapf ( err , "failed to execute WhiteSource scan with exit code %v" , exitCode )
}
return nil
}
func evaluateExitCode ( exitCode int ) {
switch exitCode {
case 255 :
log . Entry ( ) . Info ( "General error has occurred." )
log . SetErrorCategory ( log . ErrorUndefined )
case 254 :
log . Entry ( ) . Info ( "Whitesource found one or multiple policy violations." )
log . SetErrorCategory ( log . ErrorCompliance )
case 253 :
log . Entry ( ) . Info ( "The local scan client failed to execute the scan." )
log . SetErrorCategory ( log . ErrorUndefined )
case 252 :
log . Entry ( ) . Info ( "There was a failure in the connection to the WhiteSource servers." )
log . SetErrorCategory ( log . ErrorInfrastructure )
case 251 :
log . Entry ( ) . Info ( "The server failed to analyze the scan." )
log . SetErrorCategory ( log . ErrorService )
case 250 :
log . Entry ( ) . Info ( "One of the package manager's prerequisite steps (e.g. npm install) failed." )
log . SetErrorCategory ( log . ErrorCustom )
default :
log . Entry ( ) . Info ( "Whitesource scan failed with unknown error code" )
log . SetErrorCategory ( log . ErrorUndefined )
}
2020-10-20 09:49:26 +02:00
}
// downloadAgent downloads the unified agent jar file if one does not exist
func downloadAgent ( config * ScanOptions , utils Utils ) error {
agentFile := config . AgentFileName
exists , err := utils . FileExists ( agentFile )
if err != nil {
2021-02-03 14:52:48 +01:00
return errors . Wrapf ( err , "failed to check if file '%s' exists" , agentFile )
2020-10-20 09:49:26 +02:00
}
if ! exists {
err := utils . DownloadFile ( config . AgentDownloadURL , agentFile , nil , nil )
2021-07-13 20:36:36 +02:00
if err != nil {
2021-11-02 15:10:04 +01:00
// we check if the copy and the unauthorized error occurs and retry the download
2021-07-13 20:36:36 +02:00
// if the copy error did not happen, we rerun the whole download mechanism once
2021-11-02 15:10:04 +01:00
if strings . Contains ( err . Error ( ) , "unable to copy content from url to file" ) || strings . Contains ( err . Error ( ) , "returned with response 404 Not Found" ) || strings . Contains ( err . Error ( ) , "returned with response 403 Forbidden" ) {
2021-07-13 20:36:36 +02:00
// retry the download once again
2021-08-19 14:49:24 +02:00
log . Entry ( ) . Warnf ( "[Retry] Previous download failed due to %v" , err )
2021-07-13 20:36:36 +02:00
err = nil // reset error to nil
err = utils . DownloadFile ( config . AgentDownloadURL , agentFile , nil , nil )
}
}
2020-10-20 09:49:26 +02:00
if err != nil {
2021-02-03 14:52:48 +01:00
return errors . Wrapf ( err , "failed to download unified agent from URL '%s' to file '%s'" , config . AgentDownloadURL , agentFile )
2020-10-20 09:49:26 +02:00
}
}
return nil
}
2021-02-03 14:52:48 +01:00
// downloadJre downloads the a JRE in case no java command can be executed
func downloadJre ( config * ScanOptions , utils Utils ) ( string , error ) {
// cater for multiple executions
if exists , _ := utils . FileExists ( filepath . Join ( jvmDir , "bin" , "java" ) ) ; exists {
return filepath . Join ( jvmDir , "bin" , "java" ) , nil
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
err := utils . RunExecutable ( "java" , "-version" )
javaPath := "java"
if err != nil {
log . Entry ( ) . Infof ( "No Java installation found, downloading JVM from %v" , config . JreDownloadURL )
2024-06-25 19:08:27 +05:30
const maxRetries = 3
retries := 0
for retries < maxRetries {
err = utils . DownloadFile ( config . JreDownloadURL , jvmTarGz , nil , nil )
if err == nil {
break
}
log . Entry ( ) . Warnf ( "Attempt %d: Download failed due to %v" , retries + 1 , err )
retries ++
if retries >= maxRetries {
log . Entry ( ) . Errorf ( "Download failed after %d attempts" , retries )
return "" , errors . Wrapf ( err , "failed to download jre from URL '%s'" , config . JreDownloadURL )
2021-07-13 20:36:36 +02:00
}
2024-06-25 19:08:27 +05:30
time . Sleep ( 1 * time . Second )
2021-07-13 20:36:36 +02:00
}
2021-02-03 14:52:48 +01:00
if err != nil {
2024-06-25 19:08:27 +05:30
return "" , errors . Wrapf ( err , "Even after retry failed to download jre from URL '%s'" , config . JreDownloadURL )
2021-02-03 14:52:48 +01:00
}
2020-10-20 09:49:26 +02:00
2021-02-03 14:52:48 +01:00
// ToDo: replace tar call with go library call
err = utils . MkdirAll ( jvmDir , 0755 )
err = utils . RunExecutable ( "tar" , fmt . Sprintf ( "--directory=%v" , jvmDir ) , "--strip-components=1" , "-xzf" , jvmTarGz )
if err != nil {
return "" , errors . Wrapf ( err , "failed to extract %v" , jvmTarGz )
}
log . Entry ( ) . Info ( "Java successfully installed" )
javaPath = filepath . Join ( jvmDir , "bin" , "java" )
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
return javaPath , nil
}
2020-10-20 09:49:26 +02:00
2021-02-03 14:52:48 +01:00
func removeJre ( javaPath string , utils Utils ) error {
if javaPath == "java" {
return nil
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
if err := utils . RemoveAll ( jvmDir ) ; err != nil {
return fmt . Errorf ( "failed to remove downloaded and extracted jvm from %v" , jvmDir )
}
log . Entry ( ) . Debugf ( "Java successfully removed from %v" , jvmDir )
if err := utils . FileRemove ( jvmTarGz ) ; err != nil {
return fmt . Errorf ( "failed to remove downloaded %v" , jvmTarGz )
}
log . Entry ( ) . Debugf ( "%v successfully removed" , jvmTarGz )
return nil
}
2020-10-20 09:49:26 +02:00
2021-02-03 14:52:48 +01:00
func getProjectNameFromPackageJSON ( packageJSONPath string , utils Utils ) ( string , error ) {
fileContents , err := utils . FileRead ( packageJSONPath )
if err != nil {
return "" , errors . Wrapf ( err , "failed to read file %v" , packageJSONPath )
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
var packageJSON = make ( map [ string ] interface { } )
if err := json . Unmarshal ( fileContents , & packageJSON ) ; err != nil {
return "" , errors . Wrapf ( err , "failed to read file content of %v" , packageJSONPath )
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
projectNameEntry , exists := packageJSON [ "name" ]
if ! exists {
return "" , fmt . Errorf ( "the file '%s' must configure a name" , packageJSONPath )
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
projectName , isString := projectNameEntry . ( string )
if ! isString {
return "" , fmt . Errorf ( "the file '%s' must configure a name as string" , packageJSONPath )
2020-10-20 09:49:26 +02:00
}
2021-02-03 14:52:48 +01:00
return projectName , nil
2020-10-20 09:49:26 +02:00
}
2021-03-08 17:01:18 +01:00
func scanLog ( in io . Reader , scan * Scan ) {
scanner := bufio . NewScanner ( in )
scanner . Split ( scanShortLines )
for scanner . Scan ( ) {
line := scanner . Text ( )
parseForProjects ( line , scan )
}
if err := scanner . Err ( ) ; err != nil {
log . Entry ( ) . WithError ( err ) . Info ( "failed to scan log file" )
}
}
func parseForProjects ( logLine string , scan * Scan ) {
compile := regexp . MustCompile ( projectRegEx )
values := compile . FindStringSubmatch ( logLine )
if len ( values ) > 0 && scan . scannedProjects != nil && len ( scan . scannedProjects [ values [ 1 ] ] . Name ) == 0 {
scan . scannedProjects [ values [ 1 ] ] = Project { Name : values [ 1 ] }
}
}
func scanShortLines ( data [ ] byte , atEOF bool ) ( advance int , token [ ] byte , err error ) {
lenData := len ( data )
if atEOF && lenData == 0 {
return 0 , nil , nil
}
if lenData > 32767 && ! bytes . Contains ( data [ 0 : lenData ] , [ ] byte ( "\n" ) ) {
// we will neglect long output
// no use cases known where this would be relevant
return lenData , nil , nil
}
if i := bytes . IndexByte ( data , '\n' ) ; i >= 0 && i < 32767 {
// We have a full newline-terminated line with a size limit
// Size limit is required since otherwise scanner would stall
return i + 1 , data [ 0 : i ] , nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len ( data ) , data , nil
}
// Request more data.
return 0 , nil , nil
}