package cmd import ( "crypto/sha256" "encoding/json" "fmt" "github.com/SAP/jenkins-library/pkg/command" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/pkg/errors" "io" "io/ioutil" "net/http" "os" "time" ) var open = _open var getSHA256 = _getSHA256 func _open(path string) (io.ReadCloser, error) { return os.Open(path) } type malwareExecuteScanResponse struct { MalwareDetected bool EncryptedContentDetected bool ScanSize int MimeType string SHA256 string } func malwareExecuteScan(config malwareExecuteScanOptions, telemetryData *telemetry.CustomData) { // for command execution use Command c := command.Command{} // reroute command output to logging framework c.Stdout(log.Writer()) c.Stderr(log.Writer()) // for http calls import piperhttp "github.com/SAP/jenkins-library/pkg/http" // and use a &piperhttp.Client{} in a custom system // Example: step checkmarxExecuteScan.go httpClient := &piperhttp.Client{} // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end err := runMalwareScan(&config, telemetryData, &c, httpClient) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } func runMalwareScan(config *malwareExecuteScanOptions, telemetryData *telemetry.CustomData, command execRunner, httpClient piperhttp.Sender) error { log.Entry().Infof("Scanning file \"%s\" for malware using service \"%s\"", config.File, config.Host) candidate, err := open(config.File) if err != nil { return err } defer candidate.Close() timeout, err := time.ParseDuration(fmt.Sprintf("%ss", config.Timeout)) if err != nil { return errors.Wrapf(err, "Invalid timeout: %v", config.Timeout) } opts := piperhttp.ClientOptions{ Username: config.Username, Password: config.Password, MaxRequestDuration: timeout, TransportTimeout: timeout, } httpClient.SetOptions(opts) var scanResponse *malwareExecuteScanResponse scanResponse, err = sendMalwareScanRequest(httpClient, "POST", config.Host+"/scan", candidate) if err != nil { return err } log.Entry().Debugf( "File '%s' has been scanned. MalwareDetected: %t, EncryptedContentDetected: %t, ScanSize: %d, MimeType: '%s', SHA256: '%s'", config.File, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected, scanResponse.ScanSize, scanResponse.MimeType, scanResponse.SHA256) err = validateHash(scanResponse.SHA256, config.File) if err != nil { return err } if scanResponse.MalwareDetected || scanResponse.EncryptedContentDetected { return fmt.Errorf("Malware scan failed for file '%s'. Malware detected: %t, encrypted content detected: %t", config.File, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected) } log.Entry().Infof("Malware scan succeeded for file '%s'. Malware detected: %t, encrypted content detected: %t", config.File, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected) return nil } func sendMalwareScanRequest(client piperhttp.Sender, method, url string, candidate io.Reader) (*malwareExecuteScanResponse, error) { // piper http utils mashall some http response codes into errors. We wan't to check the status code // ourselvs hence we wait with returning that error (maybe also related to errors others than http status codes) // sendRequest results in any combination of nil and non-nil response and error. // a response body could even be already closed. response, err := client.SendRequest(method, url, candidate, prepareHeaders(), nil) if response != nil && response.Body != nil { defer response.Body.Close() } return validateResponse(response, err) } func validateResponse(response *http.Response, err error) (*malwareExecuteScanResponse, error) { var body []byte var errRead error if response != nil && response.Body != nil { body, errRead = ioutil.ReadAll(response.Body) } if err != nil { return nil, fmt.Errorf("HTTP request failed with error: %v. Details: \"%s\"", err, body) } if response == nil { return nil, fmt.Errorf("No response available") } if response.StatusCode != 200 { return nil, fmt.Errorf("Unexpected response code (%d). %d expected. Details: \"%s\"", response.StatusCode, 200, body) } if errRead != nil { return nil, errRead } return marshalResponse(body) } func marshalResponse(body []byte) (*malwareExecuteScanResponse, error) { var scanResponse malwareExecuteScanResponse err := json.Unmarshal(body, &scanResponse) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("Unmarshalling of response body failed. Body: '%s'", body)) } return &scanResponse, nil } func validateHash(remoteHash, fileName string) error { hash, err := getSHA256(fileName) if err != nil { return err } if hash == remoteHash { log.Entry().Infof("Hash returned from malwarescan service matches file hash for file '%s' (%s)", fileName, hash) } else { return fmt.Errorf("Hash returned from malwarescan service ('%s') does not match file hash ('%s') for file '%s'", remoteHash, hash, fileName) } return nil } func _getSHA256(fileName string) (string, error) { f, err := open(fileName) if err != nil { return "", err } defer f.Close() hash := sha256.New() _, err = io.Copy(hash, f) if err != nil { return "", err } return fmt.Sprintf("%x", string(hash.Sum(nil))), nil } func prepareHeaders() http.Header { headers := http.Header{} headers.Add("Content-Type", "application/octet-stream") return headers }