1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00
sap-jenkins-library/cmd/malwareExecuteScan.go
Marcus Holl 0ad38b8621
Timeout for malwarescan (#1623)
Provide a timeout parameters to malwarescan step. This is forwarded to the piper http layer. The default used there is 10 seconds with is not useable for that use case for larger files.
2020-06-03 11:08:34 +02:00

205 lines
5.5 KiB
Go

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
}