DEV Community

Cover image for Security Testing for SDETs: Automate Vulnerability Scans with OWASP ZAP
Yusuf Aşık
Yusuf Aşık

Posted on

1

Security Testing for SDETs: Automate Vulnerability Scans with OWASP ZAP

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>
Enter fullscreen mode Exit fullscreen mode

Feature File


Feature: ZAP Security Check

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


Enter fullscreen mode Exit fullscreen mode

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();

    }

Enter fullscreen mode Exit fullscreen mode

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! 👇

Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay