github.com/kubeshop/testkube@v1.17.23/contrib/executor/zap/pkg/runner/runner.go (about)

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    13  	"github.com/kubeshop/testkube/pkg/envs"
    14  	"github.com/kubeshop/testkube/pkg/executor"
    15  	"github.com/kubeshop/testkube/pkg/executor/agent"
    16  	"github.com/kubeshop/testkube/pkg/executor/content"
    17  	"github.com/kubeshop/testkube/pkg/executor/env"
    18  	"github.com/kubeshop/testkube/pkg/executor/output"
    19  	"github.com/kubeshop/testkube/pkg/executor/runner"
    20  	"github.com/kubeshop/testkube/pkg/executor/scraper"
    21  	"github.com/kubeshop/testkube/pkg/executor/scraper/factory"
    22  	"github.com/kubeshop/testkube/pkg/ui"
    23  )
    24  
    25  // ZapRunner runs ZAP tests
    26  type ZapRunner struct {
    27  	Params  envs.Params
    28  	ZapHome string
    29  	Scraper scraper.Scraper
    30  }
    31  
    32  var _ runner.Runner = &ZapRunner{}
    33  
    34  // NewRunner creates a new ZapRunner
    35  func NewRunner(ctx context.Context, params envs.Params) (*ZapRunner, error) {
    36  	output.PrintLogf("%s Preparing test runner", ui.IconTruck)
    37  
    38  	var err error
    39  	r := &ZapRunner{
    40  		Params:  params,
    41  		ZapHome: os.Getenv("ZAP_HOME"),
    42  	}
    43  
    44  	output.PrintLogf("%s Preparing scraper", ui.IconTruck)
    45  	r.Scraper, err = factory.TryGetScrapper(ctx, params)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	return r, nil
    51  }
    52  
    53  // Run executes the test and returns the test results
    54  func (r *ZapRunner) Run(ctx context.Context, execution testkube.Execution) (result testkube.ExecutionResult, err error) {
    55  	if r.Scraper != nil {
    56  		defer r.Scraper.Close()
    57  	}
    58  	output.PrintLogf("%s Preparing for test run", ui.IconTruck)
    59  
    60  	testFile, _, err := content.GetPathAndWorkingDir(execution.Content, r.Params.DataDir)
    61  	if err != nil {
    62  		output.PrintLogf("%s Failed to resolve absolute directory for %s, using the path directly", ui.IconWarning, r.Params.DataDir)
    63  	}
    64  
    65  	fileInfo, err := os.Stat(testFile)
    66  	if err != nil {
    67  		return result, err
    68  	}
    69  
    70  	var zapConfig string
    71  	workingDir := r.Params.DataDir
    72  	if fileInfo.IsDir() {
    73  		// assume the ZAP config YAML has been passed as test argument
    74  		zapConfig = filepath.Join(testFile, execution.Args[len(execution.Args)-1])
    75  	} else {
    76  		// use the given file config as ZAP config YAML
    77  		zapConfig = testFile
    78  	}
    79  
    80  	// determine the ZAP script
    81  	output.PrintLogf("%s Processing test type", ui.IconWorld)
    82  	scanType := strings.Split(execution.TestType, "/")[1]
    83  	for i := range execution.Command {
    84  		if execution.Command[i] == "<pythonScriptPath>" {
    85  			execution.Command[i] = zapScript(scanType)
    86  		}
    87  	}
    88  	output.PrintLogf("%s Using command: %s", ui.IconCheckMark, strings.Join(execution.Command, " "))
    89  
    90  	output.PrintLogf("%s Preparing reports folder", ui.IconFile)
    91  	reportFolder := filepath.Join(r.Params.DataDir, "reports")
    92  	err = os.Mkdir(reportFolder, 0700)
    93  	if err != nil {
    94  		return *result.WithErrors(err), nil
    95  	}
    96  	reportFile := filepath.Join(reportFolder, fmt.Sprintf("%s-report.html", execution.TestName))
    97  
    98  	output.PrintLogf("%s Building arguments", ui.IconWorld)
    99  	output.PrintLogf("%s Reading options from file", ui.IconWorld)
   100  	options := Options{}
   101  	err = options.UnmarshalYAML(zapConfig)
   102  	if err != nil {
   103  		return *result.WithErrors(err), nil
   104  	}
   105  
   106  	output.PrintLogf("%s Preparing variables", ui.IconWorld)
   107  	envManager := env.NewManagerWithVars(execution.Variables)
   108  	envManager.GetReferenceVars(envManager.Variables)
   109  	output.PrintLogf("%s Variables are prepared", ui.IconCheckMark)
   110  
   111  	args := zapArgs(scanType, options, reportFile)
   112  	output.PrintLogf("%s Reading execution arguments", ui.IconWorld)
   113  	args = MergeArgs(args, reportFile, execution)
   114  	output.PrintLogf("%s Arguments are ready: %s", ui.IconCheckMark, envManager.ObfuscateStringSlice(args))
   115  
   116  	// when using file based ZAP parameters it expects a /zap/wrk directory
   117  	// we simply symlink the directory
   118  	os.Symlink(workingDir, filepath.Join(r.ZapHome, "wrk"))
   119  
   120  	output.PrintLogf("%s Running ZAP test", ui.IconMicroscope)
   121  	command, args := executor.MergeCommandAndArgs(execution.Command, args)
   122  	logs, err := executor.Run(r.ZapHome, command, envManager, args...)
   123  	logs = envManager.ObfuscateSecrets(logs)
   124  
   125  	output.PrintLogf("%s Calculating results", ui.IconMicroscope)
   126  	if err == nil {
   127  		result.Status = testkube.ExecutionStatusPassed
   128  	} else {
   129  		result.Status = testkube.ExecutionStatusFailed
   130  		result.ErrorMessage = err.Error()
   131  		if strings.Contains(result.ErrorMessage, "exit status 1") || strings.Contains(result.ErrorMessage, "exit status 2") {
   132  			result.ErrorMessage = "security issues found during scan"
   133  		} else {
   134  			// ZAP was unable to run at all, wrong args?
   135  			return result, nil
   136  		}
   137  	}
   138  
   139  	result.Output = string(logs)
   140  	result.OutputType = "text/plain"
   141  
   142  	// prepare step results based on output
   143  	result.Steps = []testkube.ExecutionStepResult{}
   144  	lines := strings.Split(result.Output, "\n")
   145  	for _, line := range lines {
   146  		if strings.Index(line, "PASS") == 0 || strings.Index(line, "INFO") == 0 {
   147  			result.Steps = append(result.Steps, testkube.ExecutionStepResult{
   148  				Name: stepName(line),
   149  				// always success
   150  				Status: string(testkube.PASSED_ExecutionStatus),
   151  			})
   152  		} else if strings.Index(line, "WARN") == 0 {
   153  			result.Steps = append(result.Steps, testkube.ExecutionStepResult{
   154  				Name: stepName(line),
   155  				// depends on the options if WARN will fail or not
   156  				Status: warnStatus(scanType, options),
   157  			})
   158  		} else if strings.Index(line, "FAIL") == 0 {
   159  			result.Steps = append(result.Steps, testkube.ExecutionStepResult{
   160  				Name: stepName(line),
   161  				// always error
   162  				Status: string(testkube.FAILED_ExecutionStatus),
   163  			})
   164  		}
   165  	}
   166  
   167  	var rerr error
   168  	if execution.PostRunScript != "" && execution.ExecutePostRunScriptBeforeScraping {
   169  		output.PrintLog(fmt.Sprintf("%s Running post run script...", ui.IconCheckMark))
   170  
   171  		if rerr = agent.RunScript(execution.PostRunScript, r.Params.WorkingDir); rerr != nil {
   172  			output.PrintLogf("%s Failed to execute post run script %s", ui.IconWarning, rerr)
   173  		}
   174  	}
   175  
   176  	if r.Params.ScrapperEnabled {
   177  		directories := []string{reportFolder}
   178  		var masks []string
   179  		if execution.ArtifactRequest != nil {
   180  			directories = append(directories, execution.ArtifactRequest.Dirs...)
   181  			masks = execution.ArtifactRequest.Masks
   182  		}
   183  
   184  		output.PrintLogf("%s Scraping directories: %v with masks: %v", ui.IconCabinet, directories, masks)
   185  
   186  		if err := r.Scraper.Scrape(ctx, directories, masks, execution); err != nil {
   187  			return *result.Err(err), errors.Wrap(err, "error scraping artifacts from ZAP executor")
   188  		}
   189  	}
   190  
   191  	if rerr != nil {
   192  		return *result.Err(rerr), nil
   193  	}
   194  
   195  	return result, err
   196  }
   197  
   198  // GetType returns runner type
   199  func (r *ZapRunner) GetType() runner.Type {
   200  	return runner.TypeMain
   201  }
   202  
   203  const API = "api"
   204  const BASELINE = "baseline"
   205  const FULL = "full"
   206  
   207  func zapScript(scanType string) string {
   208  	switch {
   209  	case scanType == BASELINE:
   210  		return "./zap-baseline.py"
   211  	default:
   212  		return fmt.Sprintf("./zap-%s-scan.py", scanType)
   213  	}
   214  }
   215  
   216  func zapArgs(scanType string, options Options, reportFile string) (args []string) {
   217  	switch {
   218  	case scanType == API:
   219  		args = options.ToApiScanArgs(reportFile)
   220  	case scanType == BASELINE:
   221  		args = options.ToBaselineScanArgs(reportFile)
   222  	case scanType == FULL:
   223  		args = options.ToFullScanArgs(reportFile)
   224  	}
   225  	return args
   226  }
   227  
   228  func stepName(line string) string {
   229  	return strings.TrimSpace(strings.SplitAfter(line, ":")[1])
   230  }
   231  
   232  func warnStatus(scanType string, options Options) string {
   233  	var fail bool
   234  
   235  	switch {
   236  	case scanType == API:
   237  		fail = options.API.FailOnWarn
   238  	case scanType == BASELINE:
   239  		fail = options.Baseline.FailOnWarn
   240  	case scanType == FULL:
   241  		fail = options.Full.FailOnWarn
   242  	}
   243  
   244  	if fail {
   245  		return string(testkube.FAILED_ExecutionStatus)
   246  	} else {
   247  		return string(testkube.PASSED_ExecutionStatus)
   248  	}
   249  }
   250  
   251  // MergeArgs merges the arguments read from file with the arguments read from the execution
   252  func MergeArgs(fileArgs []string, reportFile string, execution testkube.Execution) []string {
   253  	output.PrintLogf("%s Merging file arguments with execution arguments", ui.IconWorld)
   254  
   255  	args := execution.Args
   256  	for i := range args {
   257  		if args[i] == "<fileArgs>" {
   258  			newArgs := make([]string, len(args)+len(fileArgs)-1)
   259  			copy(newArgs, args[:i])
   260  			copy(newArgs[i:], fileArgs)
   261  			copy(newArgs[i+len(fileArgs):], args[i+1:])
   262  			args = newArgs
   263  			break
   264  		}
   265  	}
   266  
   267  	for i := range args {
   268  		args[i] = os.ExpandEnv(args[i])
   269  	}
   270  
   271  	return args
   272  }