Security Testing for SDETs: Automate Vulnerability Scans with OWASP ZAP

Security Testing for SDETs: Automate Vulnerability Scans with OWASP ZAP

posted Originally published at bugnificent.medium.com 6 min read

As Software Development Engineers in Test (SDETs), we’re no strangers to automation. We build frameworks to validate functionality, performance, and usability. But in today’s threat landscape, security testing can no longer be an afterthought – and that’s where OWASP ZAP (Zed Attack Proxy) shines. Let’s explore why integrating ZAP into your automated tests matters and how it transforms your role in securing applications.

OWASP ZAP

Why Security Testing Matters for SDETs

  1. Shift-Left Security
    Security vulnerabilities caught late in the SDLC cost 6x more to fix than those identified early. By integrating security testing into automation, SDETs enable proactive risk mitigation.

  2. Beyond Functional Testing
    Modern SDETs are guardians of quality and security. A feature might work perfectly but leak sensitive data or expose SQL injection points.

  3. Compliance & Reputation
    GDPR, PCI-DSS, and other regulations demand security rigor. Automated security tests provide auditable proof of due diligence.


OWASP ZAP: The SDET’s Security Swiss Army Knife

ZAP isn’t just another scanner – it’s a developer-centric toolkit designed for automation:

  • Passive Scanning: Analyze traffic during functional tests
  • Active Scanning: Simulate attacks (XSS, SQLi, etc.)
  • API-first Design: Perfect for CI/CD pipelines
  • Customizable: Addons for OAuth, GraphQL, and more

Integrating ZAP into Your Framework: Key Benefits

Seamless Automation
Trigger scans programmatically via ZAP’s REST API during Selenium test suites.

Context-Aware Testing
Use authenticated sessions from your functional tests to scan post-login workflows.

CI/CD Friendly
Fail builds automatically when ZAP detects high-risk vulnerabilities.

Allure-Ready Reports
Combine ZAP’s HTML/JSON reports with Allure’s trend analysis for security metrics.


How to Integrate ZAP with Selenium + TestNG


Architecture Overview

  1. ZAP Proxy Setup: Route Selenium traffic through ZAP
  2. Passive Scan During Tests: Let ZAP analyze HTTP/S traffic
  3. Active Scan Post-Test: Trigger attacks after critical workflows
  4. Report Generation: Export results to Allure

Note: Headless mode requires additional steps and for normal test ZAP GUI should be opened before test starts.


Example Code Snippets (Dependencies)

<!-- Selenium -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>4.30.0</version>
        </dependency>

<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-java -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>7.21.1</version>
        </dependency>

<!-- https://mvnrepository.com/artifact/org.zaproxy/zap-clientapi -->
        <dependency>
            <groupId>org.zaproxy</groupId>
            <artifactId>zap-clientapi</artifactId>
            <version>1.16.0</version>
        </dependency>

<!-- https://mvnrepository.com/artifact/org.testng/testng -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>7.10.2</version>
            <scope>test</scope>
        </dependency>

<!-- Allure -->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-cucumber7-jvm</artifactId>
            <scope>test</scope>
        </dependency>

<!-- https://mvnrepository.com/artifact/io.qameta.allure/allure-testng -->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-testng</artifactId>
            <version>2.29.1</version>
        </dependency>

Feature File


Feature: ZAP Security Check

  @sec
  Scenario: I need to run security check
    When i started security test


Step Definition

package com.velespit.step_definitions;

import com.velespit.utilities.BrowserUtils;
import com.velespit.utilities.ConfigurationReader;
import io.qameta.allure.Allure;
import io.qameta.allure.Description;
import io.qameta.allure.testng.Tag;
import org.testng.annotations.Test;
import org.zaproxy.clientapi.core.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zaproxy.clientapi.core.ClientApi;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;

public class ZapSecurityTest {

    private static final Logger logger = LoggerFactory.getLogger(ZapSecurityTest.class);
    private static final String ZAP_PROXY_HOST = "localhost";
    private static final int ZAP_PROXY_PORT = 8080;
    private static final String ZAP_API_KEY = ConfigurationReader.getProperty("zap_api_key"); // Replace with your ZAP API key

    private static ClientApi zapClient;

    public ZapSecurityTest() {
        zapClient = new ClientApi(ZAP_PROXY_HOST, ZAP_PROXY_PORT, ZAP_API_KEY);
    }

    public static void startZAPHeadless() {
        try {
            ProcessBuilder processBuilder = new ProcessBuilder(
                    "cmd.exe", "/c", ConfigurationReader.getProperty("zap_path"), "-daemon", "-port", "8080", "-host", "localhost"
            );

            processBuilder.directory(new File(ConfigurationReader.getProperty("zap_path")));
            processBuilder.redirectErrorStream(true);

            Process zapProcess = processBuilder.start();
            logger.info("ZAP started in headless mode.");

            // Wait until ZAP is fully initialized before continuing
            waitForZapStartup();

        } catch (Exception e) {
            logger.error("Failed to start ZAP in headless mode.", e);
        }
    }

    private static void waitForZapStartup() throws InterruptedException {
        logger.info("Waiting for ZAP to initialize...");

        int retries = 10;
        while (retries > 0) {
            if (isZapRunning()) {
                logger.info("ZAP is ready!");
                return;
            }
            BrowserUtils.sleep(5); // Wait 5 seconds
            retries--;
        }
        throw new RuntimeException("ZAP did not start within the expected time!");
    }

    private static boolean isZapRunning() {
        try {
            ProcessBuilder checkProcess = new ProcessBuilder("curl", "-s", "http://localhost:8080/");
            Process process = checkProcess.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

            String response;
            while ((response = reader.readLine()) != null) {
                if (response.contains("ZAP")) {
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    public void startZapSecurityTest(String targetUrl) {
        try {
            // Start a new session
            zapClient.core.newSession("ZAP Security Test", "true");

            // Spider the target URL to discover links
            logger.info("Starting ZAP Spider for: {}", targetUrl);
            ApiResponse spiderResponse = zapClient.spider.scan(targetUrl, null, null, null, null);
            String scanId = ((ApiResponseElement) spiderResponse).getValue();
            waitForSpiderToComplete(scanId);

            // Active scan for vulnerabilities
            logger.info("Starting ZAP Active Scan for: {}", targetUrl);
            ApiResponse activeScanResponse = zapClient.ascan.scan(targetUrl, "true", "false", null, null, null);
            String activeScanId = ((ApiResponseElement) activeScanResponse).getValue();
            waitForActiveScanToComplete(activeScanId);

            // Generate and report security findings
            reportSecurityFindings(targetUrl);

        } catch (ClientApiException | InterruptedException e) {
            logger.error("An error occurred during ZAP security testing: ", e);
            Allure.addAttachment("Error", "text/plain", "An error occurred: " + e.getMessage());
        }
    }

    private void waitForSpiderToComplete(String scanId) throws ClientApiException, InterruptedException {
        while (true) {
            BrowserUtils.sleep(2); // Poll every 2 seconds
            ApiResponse spiderStatus = zapClient.spider.status(scanId);
            int progress = Integer.parseInt(((ApiResponseElement) spiderStatus).getValue());
            logger.info("ZAP Spider progress: {}%", progress);
            if (progress >= 100) {
                break;
            }
        }
        logger.info("ZAP Spider completed.");
    }

    private void waitForActiveScanToComplete(String scanId) throws ClientApiException, InterruptedException {
        while (true) {
            BrowserUtils.sleep(2); // Poll every 2 seconds
            ApiResponse activeScanStatus = zapClient.ascan.status(scanId);
            int progress = Integer.parseInt(((ApiResponseElement) activeScanStatus).getValue());
            logger.info("ZAP Active Scan progress: {}%", progress);
            if (progress >= 100) {
                break;
            }
        }
        logger.info("ZAP Active Scan completed.");
    }

    private void reportSecurityFindings(String targetUrl) throws ClientApiException {
        // Get alerts (vulnerabilities) using the new API method
        ApiResponse alertsResponse = zapClient.alert.alerts(targetUrl, null, null, null);
        List<Map<String, String>> alerts = parseAlerts((ApiResponseList) alertsResponse);
        if (alerts.isEmpty()) {
            Allure.addAttachment("ZAP Security Result", "text/plain", "No security issues found for: " + targetUrl);
        } else {
            Allure.addAttachment("ZAP Security Result", "text/plain", "Security Issues Found for: " + targetUrl);
            for (Map<String, String> alert : alerts) {
                String alertDetails = formatAlertDetails(alert);

                if (alertDetails != null) {
                    Allure.addAttachment("Alert: " + alert.get("alert"), "text/plain", alertDetails);
                }
            }
        }
    }

    private List<Map<String, String>> parseAlerts(ApiResponseList alertsResponse) {
        List<Map<String, String>> alerts = new ArrayList<>();

        for (ApiResponse response : alertsResponse.getItems()) {
            if (response instanceof ApiResponseSet alertSet) {
                if (alertSet.getValuesMap() != null && !alertSet.getValuesMap().isEmpty()) {
                    Map<String, String> alertDetails = new HashMap<>();

                    for (Map.Entry<String, ApiResponse> entry : alertSet.getValuesMap().entrySet()) {
                        if (entry.getValue() != null) {
                            alertDetails.put(entry.getKey(), entry.getValue().toString());
                        } else {
                            alertDetails.put(entry.getKey(), "null"); // null kontrolü
                        }
                    }
                    alerts.add(alertDetails);
                }
            }
        }
        return alerts;
    }


    private String formatAlertDetails(Map<String, String> alert) {
        if (alert == null || alert.isEmpty()) {
            return null; // Return null if map is empty or null
        }

        String risk = alert.get("risk");
        if (risk == null || risk.equals("Low") || risk.equals("Informational")) {
            return null; // Return null if low or informational risk
        }

        // Check if necessary alert keys are available
        if (!alert.containsKey("alert") || !alert.containsKey("confidence") ||
                !alert.containsKey("description") || !alert.containsKey("solution") ||
                !alert.containsKey("reference") || !alert.containsKey("url")) {
            return "Missing required alert details."; // Return error message if missing information
        }

        return "Alert: " + alert.get("alert") + "\n" +
                "Risk: " + risk + "\n" +
                "Confidence: " + alert.get("confidence") + "\n" +
                "Description: " + alert.get("description") + "\n" +
                "Solution: " + alert.get("solution") + "\n" +
                "Reference: " + alert.get("reference") + "\n" +
                "URL: " + alert.get("url") + "\n" +
                "--------------------";
    }

    public static void shutDownZAP() {
        // Shutdown ZAP after testing
        try {
            zapClient.core.shutdown();
            logger.info("ZAP has been shut down.");
        } catch (ClientApiException e) {
            logger.error("Failed to shutdown ZAP.", e);
        }
    }

    @Tag("Security")
    @Description("Security Test with ZAP API using TestNG")
    @Test
    public void secTest() {

        startZAPHeadless();

        startZapSecurityTest("https://yusufasik.com");

        shutDownZAP();

    }


Challenges & Best Practices

⚠️ Common Pitfalls

  • False Positives: Always triage results

  • Performance: Schedule aggressive scans off-peak

  • Session Handling: Reuse authenticated contexts


Pro Tips

  1. Start with passive scans to avoid test flakiness

  2. Use ZAP’s @Tags to filter irrelevant vulnerabilities (Or my if statement if you dont want to deal with it)

  3. Update ZAP weekly – new vulnerabilities emerge constantly


Conclusion: Security is Your Responsibility Now

SDETs are uniquely positioned to democratize security testing. By embedding ZAP into your automation framework, you:

  • Catch vulnerabilities before they reach production

  • Build security into the team’s daily workflow

  • Elevate your role from test author to quality architect

Ready to try it? Drop your ZAP questions below or share how you’ve integrated security testing!

If you read this far, tweet to the author to show them you care. Tweet a Thanks

Great article on integrating ZAP into automated testing! It's really important for SDETs to take proactive steps in security. Have you encountered any particular challenges when setting up ZAP with CI/CD pipelines, and how did you overcome them?

Hello James! Yes my code needs little bit retouch especially with ports, you cannot use localhost and default 8080(Same with Jenkins) DevOps should give you IP and open port to run tests, also its better practice to contain API_KEY with System.getEnv instead of properties file to test different URLs and apps.
Its especially very good approach for Docker or Kubernetes or both.

And if you dont filter results (Low and Informational risks) depends on the application heaviness you will spend time to search crucial security risks especially from Allure, so in that case you can consider to convert this results to JUnit XML format for better integration with CI/CD pipelines. But this requires another method(wont add here yet i still look for best practices) and stage to export that results and send to the both your mail and Docker Volume if needed. Here is the preview of how it will look like on Jenkinsfile:

stage('OWASP ZAP Security Scan') {
        steps {
            script {
                // Start Docker and Scan
                sh '''
                docker run --rm \
                    -v $(pwd)/zap-reports:/zap/reports \
                    -e ZAP_API_KEY=your_api_key \
                    owasp/zap2docker-stable \
                    zap-scan.java\
                    -t http://testapp:8080 \
                    -r junit.xml \
                    -l INFO
                '''
            }
        }
        post {
            always {
                // Save report to the Jenkins
                junit 'zap-reports/junit.xml'
                // Save HTML report as artifact(optional)
                archiveArtifacts artifacts: 'zap-reports/*.html', allowEmptyArchive: true
            }
        }

More Posts

API Security Testing with Damn Vulnerable API (DVAPI)

ByteHackr - Oct 14, 2024

The Ultimate Guide to Web Accessibility Testing: From Screen Readers to LighthouseCI

bugnificent - Mar 27

Shift Left Security: DAST with Dastardly in CI/CD

bugnificent - Apr 1

API Performance Testing with k6 - A Quick Start Guide

nadirbasalamah - Feb 4

How to use Builder design Pattern for test data generation in automation testing

Faisal khatri - Oct 14, 2024
chevron_left