package cmd import ( "encoding/json" "fmt" piperDocker "github.com/SAP/jenkins-library/pkg/docker" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/malwarescan" "github.com/SAP/jenkins-library/pkg/piperutils" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/SAP/jenkins-library/pkg/toolrecord" "github.com/pkg/errors" "io" "os" "strings" "time" ) type malwareScanUtils interface { OpenFile(name string, flag int, perm os.FileMode) (io.ReadCloser, error) SHA256(path string) (string, error) newDockerClient(piperDocker.ClientOptions) piperDocker.Download malwarescan.Client piperutils.FileUtils } type malwareScanUtilsBundle struct { malwarescan.Client *piperutils.Files } func (utils *malwareScanUtilsBundle) OpenFile(name string, flag int, perm os.FileMode) (io.ReadCloser, error) { return utils.Files.FileOpen(name, flag, perm) } func (utils *malwareScanUtilsBundle) newDockerClient(options piperDocker.ClientOptions) piperDocker.Download { dClient := piperDocker.Client{} dClient.SetOptions(options) return &dClient } func newMalwareScanUtilsBundle(config malwareExecuteScanOptions) *malwareScanUtilsBundle { timeout, err := time.ParseDuration(fmt.Sprintf("%ss", config.Timeout)) if err != nil { timeout = 60 log.Entry().Warnf("Unable to parse timeout for malwareScan: '%v'. Falling back to %ds", err, timeout) } httpClientOptions := piperhttp.ClientOptions{ Username: config.Username, Password: config.Password, MaxRequestDuration: timeout, TransportTimeout: timeout, } httpClient := &piperhttp.Client{} httpClient.SetOptions(httpClientOptions) return &malwareScanUtilsBundle{ Client: &malwarescan.ClientImpl{ HTTPClient: httpClient, Host: config.Host, }, Files: &piperutils.Files{}, } } func malwareExecuteScan(config malwareExecuteScanOptions, telemetryData *telemetry.CustomData) { utils := newMalwareScanUtilsBundle(config) err := runMalwareScan(&config, telemetryData, utils) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } func runMalwareScan(config *malwareExecuteScanOptions, telemetryData *telemetry.CustomData, utils malwareScanUtils) error { file, err := selectAndPrepareFileForMalwareScan(config, utils) if err != nil { return err } log.Entry().Infof("Scanning file \"%s\" for malware using service \"%s\"", file, config.Host) candidate, err := utils.OpenFile(file, os.O_RDONLY, 0666) if err != nil { return err } defer candidate.Close() scannerInfo, err := utils.Info() log.Entry().Infof("***************************************") log.Entry().Infof("* Engine: %s", scannerInfo.EngineVersion) log.Entry().Infof("* Signatures: %s", scannerInfo.SignatureTimestamp) log.Entry().Infof("***************************************") if _, err = createToolRecordMalwareScan("./", config, scannerInfo); err != nil { return err } scanResponse, err := utils.Scan(candidate) if err != nil { return err } if err = createMalwareScanReport(config, scanResponse, utils); err != nil { return err } log.Entry().Debugf( "File '%s' has been scanned. MalwareDetected: %t, EncryptedContentDetected: %t, ScanSize: %d, MimeType: '%s', SHA256: '%s', Finding: '%s'", file, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected, scanResponse.ScanSize, scanResponse.MimeType, scanResponse.SHA256, scanResponse.Finding) if err = validateHash(scanResponse.SHA256, file, utils); 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, finding: %v", file, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected, scanResponse.Finding) } log.Entry().Infof("Malware scan succeeded for file '%s'. Malware detected: %t, encrypted content detected: %t", file, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected) return nil } func selectAndPrepareFileForMalwareScan(config *malwareExecuteScanOptions, utils malwareScanUtils) (string, error) { if len(config.ScanFile) > 0 { return config.ScanFile, nil } // automatically detect the file to be scanned depending on the buildtool if len(config.ScanImage) > 0 { saveImageOptions := containerSaveImageOptions{ ContainerImage: config.ScanImage, ContainerRegistryURL: config.ScanImageRegistryURL, ContainerRegistryUser: config.ContainerRegistryUser, ContainerRegistryPassword: config.ContainerRegistryPassword, DockerConfigJSON: config.DockerConfigJSON, ImageFormat: "tarball", } dClientOptions := piperDocker.ClientOptions{ImageName: saveImageOptions.ContainerImage, RegistryURL: saveImageOptions.ContainerRegistryURL, LocalPath: "", ImageFormat: saveImageOptions.ImageFormat} dClient := utils.newDockerClient(dClientOptions) tarFile, err := runContainerSaveImage(&saveImageOptions, &telemetry.CustomData{}, "./cache", "", dClient, utils) if err != nil { if strings.Contains(fmt.Sprint(err), "no image found") { log.SetErrorCategory(log.ErrorConfiguration) } return "", errors.Wrapf(err, "failed to download Docker image %v", config.ScanImage) } return tarFile, nil } return "", fmt.Errorf("Please specify a file to be scanned") } func validateHash(remoteHash, fileName string, utils malwareScanUtils) error { hash, err := utils.SHA256(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 } // create toolrecord file for malwarescan func createToolRecordMalwareScan(workspace string, config *malwareExecuteScanOptions, scanner *malwarescan.Info) (string, error) { record := toolrecord.New(workspace, "malwarescan", config.Host) record.SetOverallDisplayData("Malware Scanner", "") if err := record.AddKeyData("engineVersion", scanner.EngineVersion, "Engine Version", ""); err != nil { return "", err } if err := record.AddKeyData("signatureTimestamp", scanner.SignatureTimestamp, "Signature Timestamp", ""); err != nil { return "", err } if err := record.Persist(); err != nil { return "", err } return record.GetFileName(), nil } func createMalwareScanReport(config *malwareExecuteScanOptions, scanResult *malwarescan.ScanResult, utils malwareScanUtils) error { scanResultJSON, err := json.Marshal(scanResult) if err != nil { return err } return utils.FileWrite(config.ReportFileName, scanResultJSON, 0666) }