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 }