2020-04-23 09:12:10 +02:00
|
|
|
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"
|
2020-06-03 11:08:34 +02:00
|
|
|
"time"
|
2020-04-23 09:12:10 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
2020-05-06 13:35:40 +02:00
|
|
|
c.Stdout(log.Writer())
|
|
|
|
c.Stderr(log.Writer())
|
2020-04-23 09:12:10 +02:00
|
|
|
|
|
|
|
// 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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-01 11:28:16 +02:00
|
|
|
func runMalwareScan(config *malwareExecuteScanOptions, telemetryData *telemetry.CustomData, command command.ExecRunner,
|
2020-04-23 09:12:10 +02:00
|
|
|
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()
|
|
|
|
|
2020-06-03 11:08:34 +02:00
|
|
|
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,
|
|
|
|
}
|
2020-04-23 09:12:10 +02:00
|
|
|
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
|
|
|
|
}
|