From 8bec6632190946fdb03c27d7c6c755f4cad4ae5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Carpintero?=
Date: Tue, 14 Jan 2025 15:01:24 +0100
Subject: [PATCH 1/2] refactor zap check
---
cmd/vulcan-zap/Dockerfile | 3 +-
cmd/vulcan-zap/main.go | 637 ++++++++++++++++-------------------
cmd/vulcan-zap/main_test.go | 117 +++++++
cmd/vulcan-zap/manifest.toml | 1 +
cmd/vulcan-zap/zap.go | 144 --------
go.mod | 1 -
go.sum | 2 -
7 files changed, 415 insertions(+), 490 deletions(-)
create mode 100644 cmd/vulcan-zap/main_test.go
delete mode 100644 cmd/vulcan-zap/zap.go
diff --git a/cmd/vulcan-zap/Dockerfile b/cmd/vulcan-zap/Dockerfile
index 6a26cb612..d363e3253 100644
--- a/cmd/vulcan-zap/Dockerfile
+++ b/cmd/vulcan-zap/Dockerfile
@@ -1,7 +1,8 @@
# Copyright 2019 Adevinta
-FROM softwaresecurityproject/zap-bare:2.16.0
+FROM zaproxy/zap-bare:2.16.0
USER root
+
RUN chown -R zap /zap/
USER zap
diff --git a/cmd/vulcan-zap/main.go b/cmd/vulcan-zap/main.go
index 2c3204993..cf5b1a051 100644
--- a/cmd/vulcan-zap/main.go
+++ b/cmd/vulcan-zap/main.go
@@ -7,42 +7,38 @@ package main
import (
"context"
"encoding/json"
- "errors"
"fmt"
- "net"
"net/url"
- "os/exec"
+ "os"
"sort"
"strconv"
"strings"
- "time"
+ "sync"
+ "text/template"
check "github.com/adevinta/vulcan-check-sdk"
"github.com/adevinta/vulcan-check-sdk/helpers"
+ "github.com/adevinta/vulcan-check-sdk/helpers/command"
checkstate "github.com/adevinta/vulcan-check-sdk/state"
report "github.com/adevinta/vulcan-report"
"github.com/sirupsen/logrus"
- "github.com/zaproxy/zap-api-go/zap"
)
-var (
- checkName = "vulcan-zap"
- logger = check.NewCheckLog(checkName)
- client zap.Interface
+const (
+ checkName = "vulcan-zap"
+ contextName = "target"
+ userAgent = "Vulcan - Security Scanner"
)
-const contextName = "target"
-
type options struct {
Depth int `json:"depth"`
Active bool `json:"active"`
Port int `json:"port"`
- Username string `json:"username"`
- Password string `json:"password"`
MinScore float32 `json:"min_score"`
// List of active/passive scanners to disable by their identifiers:
// https://www.zaproxy.org/docs/alerts/
DisabledScanners []string `json:"disabled_scanners"`
+ DisabledActiveScanners []string `json:"disabled_active_scanners"`
IgnoredFingerprintScanners []string `json:"ignored_fingerprint_scanners"`
MaxSpiderDuration int `json:"max_spider_duration"`
MaxScanDuration int `json:"max_scan_duration"` // In minutes
@@ -51,8 +47,134 @@ type options struct {
OpenapiHost string `json:"openapi_host"`
}
+type tmplOptions struct {
+ Opts options
+ URL string
+ IncludePath string
+ Dir string
+ UserAgent string
+}
+
+const configTemplate = `
+env:
+ contexts:
+ - name: Default Context
+ urls: [ "{{ .URL }}" ]
+ includePaths: [ "{{ .IncludePath }}" ]
+jobs:
+{{ if .Opts.OpenapiUrl }}
+- type: openapi
+ parameters:
+ apiUrl: "{{ .Opts.OpenapiUrl }}"
+ targetUrl: "{{ .Opts.OpenapiHost }}"
+{{ end }}
+- type: passiveScan-config
+ parameters:
+ scanOnlyInScope: true
+ maxAlertsPerRule: 5
+ enableTags: false
+ rules:
+{{ range .Opts.DisabledScanners }}
+ - id: {{ . }}
+ threshold: off
+{{ end }}
+- type: spider
+ parameters:
+ maxDuration: {{ .Opts.MaxSpiderDuration }}
+ maxDepth: {{ .Opts.Depth }}
+ userAgent: "{{ .UserAgent }}"
+- type: spiderAjax
+ parameters:
+ maxDuration: {{ .Opts.MaxSpiderDuration }}
+ maxCrawlDepth: {{ .Opts.Depth }}
+ browserId: htmlunit
+- type: passiveScan-wait
+ parameters: {}
+{{ if .Opts.Active }}
+- type: activeScan
+ parameters:
+ maxRuleDurationInMins: {{ .Opts.MaxRuleDuration }}
+ maxScanDurationInMins: {{ .Opts.MaxScanDuration }}
+ maxAlertsPerRule: 5
+ policyDefinition:
+ defaultStrength: medium
+ defaultThreshold: medium
+ rules:
+{{ range .Opts.DisabledActiveScanners }}
+ - id: {{ . }}
+ threshold: off
+ strength: default
+{{ end }}
+{{ end }}
+- name: report.json
+ type: report
+ parameters:
+ template: traditional-json
+ reportDir: "{{ .Dir }}"
+ reportFile: report.json
+ displayReport: false
+ risks:
+ - info
+ - low
+ - medium
+ - high
+ confidences:
+ - falsepositive
+ - low
+ - medium
+ - high
+ - confirmed
+`
+
+type Report struct {
+ ProgramName string `json:"@programName"`
+ Version string `json:"@version"`
+ Generated string `json:"@generated"`
+ Site []struct {
+ Name string `json:"@name"`
+ Host string `json:"@host"`
+ Port string `json:"@port"`
+ Ssl string `json:"@ssl"`
+ Alerts []struct {
+ Pluginid string `json:"pluginid"`
+ AlertRef string `json:"alertRef"`
+ Alert string `json:"alert"`
+ Name string `json:"name"`
+ Riskcode string `json:"riskcode"`
+ Confidence string `json:"confidence"`
+ Riskdesc string `json:"riskdesc"`
+ Desc string `json:"desc"`
+ Instances []struct {
+ URI string `json:"uri"`
+ Method string `json:"method"`
+ Param string `json:"param"`
+ Attack string `json:"attack"`
+ Evidence string `json:"evidence"`
+ Otherinfo string `json:"otherinfo"`
+ } `json:"instances"`
+ Count string `json:"count"`
+ Solution string `json:"solution"`
+ Otherinfo string `json:"otherinfo"`
+ Reference string `json:"reference"`
+ Cweid string `json:"cweid"`
+ Wascid string `json:"wascid"`
+ Sourceid string `json:"sourceid"`
+ } `json:"alerts"`
+ } `json:"site"`
+}
+
+type pool struct {
+ l []int
+ m sync.Mutex
+}
+
+// Create a pool of 5 ports starting from 13000.
+// 5 defines the max number of concurrent scans to allow before a "too many requests error"
+var portPool *pool = createPortPool(13000, 5)
+
func main() {
- run := func(_ context.Context, target, assetType, optJSON string, state checkstate.State) (err error) {
+ run := func(ctx context.Context, target, assetType, optJSON string, state checkstate.State) (err error) {
+ logger := check.NewCheckLogFromContext(ctx, checkName)
var opt options
if optJSON != "" {
if err = json.Unmarshal([]byte(optJSON), &opt); err != nil {
@@ -60,8 +182,6 @@ func main() {
}
}
- disabledScanners := strings.Join(opt.DisabledScanners, ",")
-
isReachable, err := helpers.IsReachable(target, assetType, nil)
if err != nil {
logger.Warnf("Can not check asset reachability: %v", err)
@@ -70,299 +190,176 @@ func main() {
return checkstate.ErrAssetUnreachable
}
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- // Execute ZAP daemon.
- go func() {
- logger.Print("Executing for ZAP daemon...")
- out, err := exec.Command(
- "/zap/zap.sh",
- "-daemon", "-host", "127.0.0.1", "-port", "8080",
- "-config", "api.disablekey=true",
- "-config", "database.recoverylog=false", // Reduce disk usage
- "-notel", // Disables telemetry
- "-silent", // Prevents from checking for addon updates
- ).Output()
-
- logger.Debugf("Error executing ZAP daemon: %v", err)
- logger.Debugf("Output of the ZAP daemon: %s", string(out))
-
- ctxCancel()
- }()
-
- // Wait for ZAP to be available.
- logger.Print("Waiting for ZAP proxy...")
- ticker := time.NewTicker(time.Second)
- proxyLoop:
- for {
- select {
- case <-ctx.Done():
- return errors.New("ZAP exited while waiting for proxy")
- case <-ticker.C:
- conn, _ := net.DialTimeout("tcp", "127.0.0.1:8080", time.Second)
- if conn != nil {
- conn.Close()
- break proxyLoop
- }
- }
- }
-
- logger.Print("Initiating ZAP client...")
-
- cfg := &zap.Config{
- Proxy: "http://127.0.0.1:8080",
- Base: "http://127.0.0.1:8080/JSON/",
- BaseOther: "http://127.0.0.1:8080/OTHER/",
- }
- client, err = zap.NewClient(cfg)
- if err != nil {
- return fmt.Errorf("error configuring the ZAP proxy client: %w", err)
- }
-
- client.Core().SetOptionDefaultUserAgent("Vulcan - Security Scanner - vulcan@adevinta.com")
-
targetURL, err := url.Parse(target)
if err != nil {
return fmt.Errorf("error parsing target URL: %w", err)
}
- cx, err := client.Context().NewContext(contextName)
- if err != nil {
- return fmt.Errorf("error creating scope context: %w", err)
- }
- contextID, err := getStringAttribute(cx, "contextId")
- if err != nil {
- return err
- }
-
// Add base URL to the scope.
targetPort := ""
if targetURL.Port() != "" {
targetPort = fmt.Sprintf(":%s", targetURL.Port())
}
- hostnameRegExQuote := strings.Replace(targetURL.Hostname(), `.`, `\.`, -1)
- includeInContextRegEx := fmt.Sprintf(`http(s)?:\/\/%s%s\/.*`, hostnameRegExQuote, targetPort)
- logger.Printf("include in context regexp: %s", includeInContextRegEx)
- _, err = client.Context().IncludeInContext(contextName, includeInContextRegEx)
- if err != nil {
- return fmt.Errorf("error including target URL to context: %w", err)
- }
+ includePathRegex := fmt.Sprintf(`http(s)?://%s%s/.*`, strings.Replace(targetURL.Hostname(), `.`, `\\.`, -1), targetPort)
- _, err = client.Context().SetContextInScope(contextName, "True")
+ tempDir, err := os.MkdirTemp(os.TempDir(), "zap-")
if err != nil {
- return fmt.Errorf("error setting context in scope: %w", err)
- }
-
- if opt.Username != "" {
- auth := client.Authentication()
- auth.SetAuthenticationMethod("1", "httpAuthentication", fmt.Sprintf("hostname=%v&port=%v", targetURL.Hostname(), targetURL.Port()))
-
- users := client.Users()
- users.NewUser("1", opt.Username)
- users.SetAuthenticationCredentials("1", "0", fmt.Sprintf("username=%v&password=%v", opt.Username, opt.Password))
- users.SetUserEnabled("1", "0", "True")
- }
-
- if opt.OpenapiUrl != "" {
- _, err = client.Openapi().ImportUrl(opt.OpenapiUrl, opt.OpenapiHost, contextID)
- if err != nil {
- return fmt.Errorf("error importing openapi url: %w", err)
- }
+ return fmt.Errorf("unable to create tmp dir: %w", err)
}
- _, err = client.Pscan().DisableScanners(disabledScanners)
- if err != nil {
- return fmt.Errorf("error disabling scanners for passive scan: %w", err)
- }
+ defer os.RemoveAll(tempDir)
- _, err = client.Spider().SetOptionMaxDepth(opt.Depth)
- if err != nil {
- return fmt.Errorf("error setting spider max depth: %w", err)
- }
- _, err = client.Spider().SetOptionMaxDuration(opt.MaxSpiderDuration)
+ tmpl, err := template.New("config").Parse(configTemplate)
if err != nil {
- return fmt.Errorf("error setting spider max duration: %w", err)
- }
-
- _, err = client.Pscan().DisableAllTags()
+ panic(err)
+ }
+ sb := new(strings.Builder)
+ err = tmpl.Execute(sb, tmplOptions{
+ Opts: opt,
+ URL: targetURL.String(),
+ Dir: tempDir,
+ IncludePath: includePathRegex,
+ UserAgent: userAgent,
+ })
if err != nil {
- return fmt.Errorf("error disabling all tags: %w", err)
+ return fmt.Errorf("unable to execute template: %w", err)
}
- logger.Printf("Running spider %v levels deep, max duration %v, ...", opt.Depth, opt.MaxSpiderDuration)
+ file := fmt.Sprintf("%s/auto.yaml", tempDir)
- resp, err := client.Spider().Scan(targetURL.String(), "", contextName, "", "")
+ err = os.WriteFile(file, []byte(sb.String()), 0600)
if err != nil {
- return fmt.Errorf("error executing the spider: %w", err)
- }
-
- v, ok := resp["scan"]
- if !ok {
- // Scan has not been executed. Due to the ZAP proxy behaviour
- // (the request to the ZAP API does not return the status codes)
- // we can not be sure whether it was because a non existant target
- // or because an error accessing the ZAP API. Therefore, we will
- // terminate the check without errors.
- logger.WithFields(logrus.Fields{"resp": resp}).Warn("Scan not present in response body when calling Spider().Scan()")
- return nil
- }
-
- scanid, ok := v.(string)
- if !ok {
- return errors.New("scan is present in response body when calling Spider().Scan() but it is not a string")
+ return fmt.Errorf("unable to create auto.yaml file: %w", err)
}
- ticker = time.NewTicker(10 * time.Second)
- spiderLoop:
- for {
- select {
- case <-ctx.Done():
- return errors.New("ZAP exited while waiting for spider")
- case <-ticker.C:
- resp, err := client.Spider().Status(scanid)
- if err != nil {
- return fmt.Errorf("error getting the status of the spider: %w", err)
- }
- v, ok := resp["status"]
- if !ok {
- // In this case if we can not get the status let's fail.
- return fmt.Errorf("cannot retrieve the status of the spider: %v", resp)
- }
- status, ok := v.(string)
- if !ok {
- return errors.New("status is present in response body when calling Spider().Scatus() but it is not a string")
- }
-
- progress, err := strconv.Atoi(status)
- if err != nil {
- return fmt.Errorf("can not convert status value %s into an int", status)
- }
+ logger.WithField("autorun", sb.String()).Info("cmd config")
- logger.Debugf("Spider at %v progress.", progress)
-
- if opt.Active {
- state.SetProgress(float32(progress) / 200)
- } else {
- state.SetProgress(float32(progress) / 100)
- }
-
- if progress >= 100 {
- break spiderLoop
- }
- }
- }
-
- logger.Print("Waiting for spider results...")
- time.Sleep(5 * time.Second)
-
- resp, err = client.Spider().AllUrls()
- if err != nil {
- return fmt.Errorf("error getting the list of URLs from spider: %w", err)
- }
- logger.Printf("Spider found the following URLs: %+v", resp)
-
- _, err = client.AjaxSpider().SetOptionMaxDuration(opt.MaxSpiderDuration)
+ zapPort, err := portPool.getPort()
if err != nil {
- return fmt.Errorf("error setting ajax spider max duration: %w", err)
- }
-
- logger.Printf("Running AJAX spider %v levels deep, max duration %v...", opt.Depth, opt.MaxSpiderDuration)
-
- client.AjaxSpider().SetOptionMaxCrawlDepth(opt.Depth)
- _, err = client.AjaxSpider().Scan(targetURL.String(), "", contextName, "")
+ return fmt.Errorf("too many requests: %w", err)
+ }
+ defer portPool.releasePort(zapPort)
+
+ out, outErr, exitCode, err := command.ExecuteWithStdErr(ctx, logger,
+ "/zap/zap.sh",
+ "-dir", tempDir,
+ "-cmd", "-autorun", file,
+ "-config", "database.recoverylog=false", // Reduce disk usage
+ "-config", fmt.Sprintf("connection.defaultUserAgent='%s'", userAgent),
+ "-port", strconv.Itoa(zapPort),
+ "-notel", // Disables telemetry
+ "-silent", // Prevents from checking for addon updates
+ )
if err != nil {
- return fmt.Errorf("error executing the AJAX spider: %w", err)
+ logger.Errorf("Output of the ZAP daemon: %s", string(out))
+ return fmt.Errorf("running zap: %w", err)
}
- ticker = time.NewTicker(10 * time.Second)
- ajaxSpiderLoop:
- for {
- select {
- case <-ctx.Done():
- return errors.New("ZAP exited while waiting for AJAX spider")
- case <-ticker.C:
- resp, err := client.AjaxSpider().Status()
- if err != nil {
- return fmt.Errorf("error getting the status of the AJAX spider: %w", err)
- }
-
- v, ok := resp["status"]
- if !ok {
- // In this case if we can not get the status let's fail.
- return errors.New("can not retrieve the status of the AJAX spider")
- }
- status, ok := v.(string)
- if !ok {
- return errors.New("status is present in response body when calling AjaxSpider().Scatus() but it is not a string")
- }
-
- if status >= "running" {
- break ajaxSpiderLoop
- }
- }
- }
-
- logger.Print("Waiting for AJAX spider results...")
- time.Sleep(5 * time.Second)
-
- resp, err = client.AjaxSpider().FullResults()
- if err != nil {
- return fmt.Errorf("error getting the list of URLs from AJAX spider: %w", err)
+ select {
+ case <-ctx.Done():
+ logger.Warn("Context canceled or deadline exceeded")
+ return ctx.Err()
+ default:
}
- logger.Printf("AJAX spider found the following URLs: %+v", resp)
- // Scan actively only if explicitly indicated.
- if opt.Active {
- logger.Print("Running active scan...")
- err := activeScan(ctx, targetURL, state, disabledScanners, opt.MaxScanDuration, opt.MaxRuleDuration, contextID)
- if err != nil {
- return err
- }
- logger.Print("Waiting for active scan results...")
- time.Sleep(5 * time.Second)
+ // Exit codes:
+ // 0: Success
+ // 1: At least 1 FAIL
+ // 2: At least one WARN and no FAILs
+ // 3: Any other failure
+ if exitCode != 0 {
+ logger.WithField("exitCode", exitCode).WithField("stdOut", string(out)).WithField("stdErr", string(outErr)).Info("Zap finished")
}
- // Retrieve alerts.
- alerts, err := client.Core().Alerts("", "", "", "")
+ res, err := os.ReadFile(fmt.Sprintf("%s/report.json", tempDir))
if err != nil {
- return fmt.Errorf("error retrieving alerts: %v", alerts)
+ return fmt.Errorf("unable to read report.json: %v", err)
}
- alertsSlice, ok := alerts["alerts"].([]interface{})
- if !ok {
- return errors.New("alerts does not exist or it is not an array of interface{}")
+ r := Report{}
+ if err = json.Unmarshal(res, &r); err != nil {
+ return fmt.Errorf("unable to parse report: %v", err)
}
vulnerabilities := make(map[string]*report.Vulnerability)
vulnSummary2PluginID := make(map[string]string)
- for _, alert := range alertsSlice {
- a, ok := alert.(map[string]interface{})
- if !ok {
- return errors.New("alert it is not a map[string]interface{}")
+ for _, site := range r.Site {
+ logger.WithFields(logrus.Fields{
+ "site.host": site.Host,
+ "site.num_alerts": len(site.Alerts)}).Info("alerts")
+ if !strings.Contains(target, site.Host) {
+ // This can happen i.e. when the openapi target url is other than the target.
+ // DOUBT: Filter? exclude?
+ logger.Warnf("Reporting alerts from an outside target %s %s", target, site.Host)
}
+ for _, a := range site.Alerts {
+
+ cwe := 0
+ if a.Cweid != "-1" {
+ cwe, err = strconv.Atoi(a.Cweid)
+ if err != nil {
+ logger.Warnf("Wrong number Cweid %d", cwe)
+ }
+ }
+ v := report.Vulnerability{
+ Summary: a.Name,
+ Description: trimP(a.Desc),
+ Details: a.Otherinfo,
+ Recommendations: splitP(a.Solution),
+ References: splitP(a.Reference),
+ Labels: []string{"issue", "web", "zap"},
+ CWEID: uint32(cwe),
+ Score: func(risk string) float32 {
+ switch risk {
+ case "0":
+ return report.SeverityThresholdNone
+ case "1":
+ return report.SeverityThresholdLow
+ case "2":
+ return report.SeverityThresholdMedium
+ case "3":
+ return report.SeverityThresholdHigh
+ }
+ return float32(report.SeverityNone)
+ }(a.Riskcode),
+ }
- v, err := processAlert(a)
- if err != nil {
- logger.WithError(err).Warn("can not process alert")
- continue
- }
- pluginID, err := parsePluginID(a)
- if err != nil {
- logger.WithError(err).Warn("can not parse plugin ID")
- continue
- }
- vulnSummary2PluginID[v.Summary] = pluginID
-
- if _, ok := vulnerabilities[v.Summary]; ok {
- vulnerabilities[v.Summary].Resources[0].Rows = append(
- vulnerabilities[v.Summary].Resources[0].Rows,
- v.Resources[0].Rows...,
- )
- } else {
- vulnerabilities[v.Summary] = &v
+ // DOUBT: Only the fist instance?
+ if len(a.Instances) > 0 {
+ i := a.Instances[0]
+ v.Resources = []report.ResourcesGroup{
+ {
+ Name: "Affected Requests",
+ Header: []string{
+ "Method",
+ "URL",
+ "Parameter",
+ "Attack",
+ "Evidence",
+ },
+ Rows: []map[string]string{
+ {
+ "Method": i.Method,
+ "URL": i.URI,
+ "Parameter": i.Param,
+ "Attack": i.Attack,
+ "Evidence": i.Evidence,
+ },
+ },
+ },
+ }
+ }
+ vulnSummary2PluginID[v.Summary] = a.Pluginid
+ if _, ok := vulnerabilities[v.Summary]; ok {
+ vulnerabilities[v.Summary].Resources[0].Rows = append(
+ vulnerabilities[v.Summary].Resources[0].Rows,
+ v.Resources[0].Rows...,
+ )
+ } else {
+ vulnerabilities[v.Summary] = &v
+ }
}
}
-
for _, v := range vulnerabilities {
// NOTE: Due to a signifcant number of false positive findings
// reported for low severity issues by ZAP, the MinScore option
@@ -390,6 +387,14 @@ func main() {
c.RunAndServe()
}
+func trimP(html string) string {
+ return strings.TrimSuffix(strings.TrimPrefix(html, ""), "
")
+}
+
+func splitP(html string) []string {
+ return strings.Split(trimP(html), "
")
+}
+
func fingerprintFromResources(resources []map[string]string) string {
var empty struct{}
occurrences := []string{}
@@ -423,74 +428,6 @@ func fingerprintFromResources(resources []map[string]string) string {
return strings.Join(occurrences, "#")
}
-func activeScan(ctx context.Context, targetURL *url.URL, state checkstate.State, disabledScanners string, maxScanDuration, maxRuleDuration int, contextID string) error {
- _, err := client.Ascan().DisableScanners(disabledScanners, "")
- if err != nil {
- return fmt.Errorf("error disabling scanners for active scan: %w", err)
- }
-
- _, err = client.Ascan().SetOptionMaxScanDurationInMins(maxScanDuration)
- if err != nil {
- return fmt.Errorf("error setting max scan duration for active scan: %w", err)
- }
-
- _, err = client.Ascan().SetOptionMaxRuleDurationInMins(maxRuleDuration)
- if err != nil {
- return fmt.Errorf("error setting max rule duration for active scan: %w", err)
- }
-
- resp, err := client.Ascan().Scan("", "True", "", "", "", "", contextID)
- if err != nil {
- return fmt.Errorf("error executing the active scan: %w", err)
- }
-
- v, ok := resp["scan"]
- if !ok {
- return fmt.Errorf("scan is not present in response body when calling Ascan().Scan()")
- }
-
- scanid, ok := v.(string)
- if !ok {
- return errors.New("scan is present in response body when calling Ascan().Scan() but it is not a string")
- }
-
- ticker := time.NewTicker(60 * time.Second)
- for {
- select {
- case <-ctx.Done():
- return errors.New("ZAP exited while waiting for active scan")
- case <-ticker.C:
- ascan := client.Ascan()
-
- resp, err := ascan.Status(scanid)
- if err != nil {
- return fmt.Errorf("error getting the status of the scan: %w", err)
- }
-
- v, ok := resp["status"]
- if !ok {
- // In this case if we can not get the status let's fail.
- return errors.New("can not retrieve the status of the scan")
- }
- status, ok := v.(string)
- if !ok {
- return errors.New("status is present in response body when calling Ascan().Scatus() but it is not a string")
- }
- progress, err := strconv.Atoi(status)
- if err != nil {
- return fmt.Errorf("can not convert status value %s into an int", status)
- }
-
- state.SetProgress((1 + float32(progress)) / 200)
-
- logger.Debugf("Active scan at %v progress.", progress)
- if progress >= 100 {
- return nil
- }
- }
- }
-}
-
func isPluginIgnoredForFingerprint(opt options, pluginID string) bool {
for _, ignoredID := range opt.IgnoredFingerprintScanners {
if pluginID == ignoredID {
@@ -500,14 +437,30 @@ func isPluginIgnoredForFingerprint(opt options, pluginID string) bool {
return false
}
-func getStringAttribute(m map[string]any, name string) (string, error) {
- v, ok := m[name]
- if !ok {
- return "", fmt.Errorf("%s not found", name)
+func createPortPool(first, count int) *pool {
+ pp := pool{
+ l: make([]int, count),
+ m: sync.Mutex{},
}
- str, ok := v.(string)
- if !ok {
- return "", fmt.Errorf("%s value [%v] is not a string", name, v)
+ for i := range pp.l {
+ pp.l[i] = first + i
}
- return str, nil
+ return &pp
+}
+
+func (p *pool) getPort() (int, error) {
+ p.m.Lock()
+ defer p.m.Unlock()
+ if len(p.l) == 0 {
+ return 0, fmt.Errorf("no ports available")
+ }
+ port := p.l[len(p.l)-1]
+ p.l = p.l[:len(p.l)-1]
+ return port, nil
+}
+
+func (p *pool) releasePort(port int) {
+ p.m.Lock()
+ defer p.m.Unlock()
+ p.l = append(p.l, port)
}
diff --git a/cmd/vulcan-zap/main_test.go b/cmd/vulcan-zap/main_test.go
new file mode 100644
index 000000000..46041383d
--- /dev/null
+++ b/cmd/vulcan-zap/main_test.go
@@ -0,0 +1,117 @@
+/*
+Copyright 2025 Adevinta
+*/
+
+package main
+
+import (
+ "strings"
+ "testing"
+ "text/template"
+)
+
+func TestTemplate(t *testing.T) {
+ if strings.Contains(configTemplate, "\t") {
+ t.Errorf("template contains tabs: %s", strings.ReplaceAll(configTemplate, "\t", "***"))
+ }
+
+ tmpl, err := template.New("config").Parse(configTemplate)
+ if err != nil {
+ panic(err)
+ }
+
+ tests := []struct {
+ name string
+ opts tmplOptions
+ wantContains []string
+ wantNotContains []string
+ }{
+ {
+ name: "Simple",
+ opts: tmplOptions{
+ URL: "http://my.url",
+ Dir: "/tmp",
+ IncludePath: `http://my\.url/.*`,
+ },
+ wantContains: []string{
+ "maxDuration: 0",
+ `reportDir: "/tmp"`,
+ `r: "/tmp"`,
+ `urls: [ "http://my.url" ]`,
+ `includePaths: [ "http://my\.url/.*" ]`,
+ },
+ wantNotContains: []string{
+ "credentials:",
+ "type: openapi",
+ "type: activeScan",
+ "maxDuration: 10",
+ },
+ },
+ {
+ name: "Active",
+ opts: tmplOptions{
+ Opts: options{
+ Active: true,
+ }},
+ wantContains: []string{
+ "type: activeScan",
+ },
+ },
+ {
+ name: "Openapi",
+ opts: tmplOptions{
+ Opts: options{OpenapiUrl: "http://my.url/openapi"},
+ },
+ wantContains: []string{
+ "apiUrl: \"http://my.url/openapi\"",
+ },
+ },
+ {
+ name: "Constants",
+ opts: tmplOptions{
+ Opts: options{
+ Active: true,
+ MaxSpiderDuration: 10,
+ MaxRuleDuration: 100,
+ MaxScanDuration: 1000,
+ },
+ },
+ wantContains: []string{
+ "maxDuration: 10",
+ "maxRuleDurationInMins: 100",
+ "maxScanDurationInMins: 1000",
+ },
+ },
+ {
+ name: "DisabledScanners",
+ opts: tmplOptions{
+ Opts: options{
+ DisabledScanners: []string{"S1", "123"},
+ },
+ },
+ wantContains: []string{
+ "- id: S1",
+ "- id: 123",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ sb := new(strings.Builder)
+ err = tmpl.Execute(sb, tt.opts)
+ if err != nil {
+ t.Errorf("error: %v", err)
+ }
+ for _, sub := range tt.wantContains {
+ if !strings.Contains(sb.String(), sub) {
+ t.Errorf("not contains: %s vs %s", sb.String(), sub)
+ }
+ }
+ for _, sub := range tt.wantNotContains {
+ if strings.Contains(sb.String(), sub) {
+ t.Errorf("contains: %s vs %s", sb.String(), sub)
+ }
+ }
+ })
+ }
+}
diff --git a/cmd/vulcan-zap/manifest.toml b/cmd/vulcan-zap/manifest.toml
index 8a1629d80..cffbf8de4 100644
--- a/cmd/vulcan-zap/manifest.toml
+++ b/cmd/vulcan-zap/manifest.toml
@@ -16,6 +16,7 @@ Options = """{
"active": true,
"min_score": 0,
"disabled_scanners": ["10062", "10003", "10108"],
+ "disabled_active_scanners": [],
"ignored_fingerprint_scanners": ["40018", "40024"],
"max_spider_duration": 0,
"max_scan_duration": 540,
diff --git a/cmd/vulcan-zap/zap.go b/cmd/vulcan-zap/zap.go
deleted file mode 100644
index 170d6a599..000000000
--- a/cmd/vulcan-zap/zap.go
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
-Copyright 2019 Adevinta
-*/
-
-package main
-
-import (
- "errors"
- "fmt"
- "math"
- "strconv"
- "strings"
-
- report "github.com/adevinta/vulcan-report"
-)
-
-func riskToScore(risk string) float32 {
- switch risk {
- case "Informational":
- return float32(report.SeverityThresholdNone)
- case "Low":
- return float32(report.SeverityThresholdLow)
- case "Medium":
- return float32(report.SeverityThresholdMedium)
- case "High":
- return float32(report.SeverityThresholdHigh)
- }
-
- return float32(report.SeverityNone)
-}
-
-func processAlert(a map[string]interface{}) (report.Vulnerability, error) {
- var ok bool
- var err error
-
- v := report.Vulnerability{}
-
- v.Summary, ok = a["name"].(string)
- if !ok {
- return report.Vulnerability{}, errors.New("Error retrieving vulnerability summary.")
- }
-
- v.Description, ok = a["description"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving description for \"%v\".", v.Summary)
- }
-
- v.Details, ok = a["other"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving details for \"%v\".", v.Summary)
- }
-
- recommendations, ok := a["solution"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving recommendations for \"%v\".", v.Summary)
- }
- v.Recommendations = strings.Split(recommendations, "\n")
-
- references, ok := a["reference"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving references for \"%v\".", v.Summary)
- }
- v.References = strings.Split(references, "\n")
-
- risk, ok := a["risk"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving score for \"%v\".", v.Summary)
- }
- v.Score = riskToScore(risk)
-
- v.Labels = []string{"issue", "web", "zap"}
-
- cweID, ok := a["cweid"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving CWE ID for \"%v\".", v.Summary)
- }
- cweIDInt, err := strconv.Atoi(cweID)
- if err != nil {
- return report.Vulnerability{}, fmt.Errorf("Error converting CWE ID for \"%v\".", v.Summary)
- }
- // ZAP uses 2^32-1 as CWE ID for vulnerabilities without a known CWE.
- if cweIDInt >= 0 && cweIDInt < math.MaxInt32-1 {
- v.CWEID = uint32(cweIDInt)
- } else {
- v.CWEID = 0
- }
-
- resMethod, ok := a["method"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving method for \"%v\".", v.Summary)
- }
-
- resURL, ok := a["url"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving URL for \"%v\".", v.Summary)
- }
-
- resParam, ok := a["param"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving parameter for \"%v\".", v.Summary)
- }
-
- resAttack, ok := a["attack"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving attack for \"%v\".", v.Summary)
- }
-
- resEvidence, ok := a["evidence"].(string)
- if !ok {
- return report.Vulnerability{}, fmt.Errorf("Error retrieving evidence for \"%v\".", v.Summary)
- }
-
- v.Resources = []report.ResourcesGroup{
- {
- Name: "Affected Requests",
- Header: []string{
- "Method",
- "URL",
- "Parameter",
- "Attack",
- "Evidence",
- },
- Rows: []map[string]string{
- {
- "Method": resMethod,
- "URL": resURL,
- "Parameter": resParam,
- "Attack": resAttack,
- "Evidence": resEvidence,
- },
- },
- },
- }
-
- return v, nil
-}
-
-func parsePluginID(a map[string]interface{}) (string, error) {
- pluginID, ok := a["pluginId"].(string)
- if !ok {
- return "", errors.New("error parsing alert plugin ID")
- }
- return pluginID, nil
-}
diff --git a/go.mod b/go.mod
index 8b36f385e..7e0e944bc 100644
--- a/go.mod
+++ b/go.mod
@@ -27,7 +27,6 @@ require (
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.9.3
github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945
- github.com/zaproxy/zap-api-go v0.0.0-20231219145106-e9ebb9695484
golang.org/x/net v0.35.0
)
diff --git a/go.sum b/go.sum
index f0a6ecabd..f874a18b8 100644
--- a/go.sum
+++ b/go.sum
@@ -170,8 +170,6 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945 h1:6Ju8pZBYFTN9FaV/JvNBiIHcsgEmP4z4laciqjfjY8E=
github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945/go.mod h1:4vRFPPNYllgCacoj+0FoKOjTW68rUhEfqPLiEJaK2w8=
-github.com/zaproxy/zap-api-go v0.0.0-20231219145106-e9ebb9695484 h1:V///T0Z9mRKDi75TFSCFdaPr+WHI3lp9q3MKYhL9mmw=
-github.com/zaproxy/zap-api-go v0.0.0-20231219145106-e9ebb9695484/go.mod h1:M+9bOOP3ffPDcPJ6N8oILPBSUEY/pbhk9emtkt2iHB8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
From f817dd6c214a9df73c114389917dba36e07b4e8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jes=C3=BAs=20Carpintero?=
Date: Fri, 14 Feb 2025 14:02:48 +0100
Subject: [PATCH 2/2] Include zap logs
---
cmd/vulcan-zap/main.go | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/cmd/vulcan-zap/main.go b/cmd/vulcan-zap/main.go
index cf5b1a051..8d16eaefc 100644
--- a/cmd/vulcan-zap/main.go
+++ b/cmd/vulcan-zap/main.go
@@ -268,11 +268,30 @@ func main() {
// 2: At least one WARN and no FAILs
// 3: Any other failure
if exitCode != 0 {
- logger.WithField("exitCode", exitCode).WithField("stdOut", string(out)).WithField("stdErr", string(outErr)).Info("Zap finished")
+ logger.WithField("exitCode", exitCode).Info("Zap finished")
+ logger := logger.WithField("dump", "zap.out")
+ for _, line := range strings.Split(string(out), "\n") {
+ logger.Info(line)
+ }
+ for _, line := range strings.Split(string(outErr), "\n") {
+ logger.Error(line)
+ }
}
res, err := os.ReadFile(fmt.Sprintf("%s/report.json", tempDir))
if err != nil {
+ if res, err := os.ReadFile(fmt.Sprintf("%s/zap.log", tempDir)); err != nil {
+ logger.Error("unable to read zap.log")
+ } else {
+ logger := logger.WithField("dump", "zap.log")
+ lines := strings.Split(string(res), "\n")
+ if len(lines) > 50 {
+ lines = lines[:50]
+ }
+ for _, line := range lines {
+ logger.Info(line)
+ }
+ }
return fmt.Errorf("unable to read report.json: %v", err)
}