SAST and DAST Tools Compared
AI generated
OWASP
0x00
Security · SAST · DAST · Application Security Testing
SAST and DAST Tools Compared
Combining static and dynamic analysis the right way

Teams that build Magento applications without systematic security testing rely on luck instead of tools that reliably surface vulnerabilities. Static analysis with PHPStan and Psalm catches dangerous data flows directly in the source code, while dynamic scanners like OWASP ZAP run real attacks against a running staging environment. This article shows how both approaches work together, where their limits lie, and when each investment pays off.

18 min read SAST · DAST · PHPStan · Psalm OWASP ZAP · CI/CD Security

1. Static vs. dynamic analysis: the underlying mechanics

SAST (Static Application Security Testing) and DAST (Dynamic Application Security Testing) solve the same problem, finding vulnerabilities before an attacker does, through fundamentally different mechanics. SAST analyzes source code without executing it: a parser builds an Abstract Syntax Tree, then an analyzer traces control flow and data flow across functions, classes and call chains. The tool sees every line of code, regardless of whether it is ever reached at runtime. DAST instead treats the application as a black box: it sends real HTTP requests against a running instance and evaluates the actual responses, exactly as an external attacker would.

The difference is clearest with complex code: SAST flags a SQL injection as soon as unsanitized user input reaches a query string, regardless of whether that code path is ever invoked in practice. DAST only catches the same flaw if the scanner actually finds the affected endpoint and tests it with a matching payload, but it evaluates the finding in the real runtime context, with an active session and a genuine HTTP response.

2. How SAST works: AST, control flow and taint tracking

Modern SAST tools for PHP such as PHPStan and Psalm operate on several layers at once. First the code is turned into an Abstract Syntax Tree that captures the syntactic structure of every file. On top of that, the analyzer computes a control flow graph to understand which code paths are reachable under which conditions. The actual security check happens through taint analysis: variables originating from an untrusted source such as $_GET or a request object are marked as tainted and tracked through the entire data flow.

If such a tainted variable reaches a dangerous sink such as a SQL query, an echo call, or an unserialize() function without first passing through a recognized sanitizer, the tool reports a finding. This mechanism works purely on the code, requires no running application and no test data, but it does need precise type information and well maintained stub definitions for frameworks like Magento.


<?php

declare(strict_types=1);

namespace Mironsoft\SeoSuite\Model;

use Magento\Framework\App\ResourceConnection;

/**
 * Example: SAST-detectable SQL injection via string concatenation.
 * A static analyzer flags this because tainted request data flows
 * directly into a raw SQL query without going through a sanitizer.
 */
final class ProductLookup
{
    public function __construct(
        private readonly ResourceConnection $resourceConnection
    ) {
    }

    /**
     * VULNERABLE: user-controlled $sku is concatenated into raw SQL.
     * Taint source: HTTP request parameter.
     * Taint sink: Zend_Db_Adapter::query().
     *
     * @param string $sku Raw SKU value taken directly from $_GET.
     * @return array<int, array<string, mixed>>
     */
    public function findBySkuUnsafe(string $sku): array
    {
        $connection = $this->resourceConnection->getConnection();

        // TaintedSql: never build queries via string concatenation
        $query = "SELECT * FROM catalog_product_entity WHERE sku = '" . $sku . "'";

        return $connection->fetchAll($query);
    }

    /**
     * SAFE: parameter binding removes the taint before it reaches the sink.
     *
     * @param string $sku SKU value, still untrusted at this point.
     * @return array<int, array<string, mixed>>
     */
    public function findBySkuSafe(string $sku): array
    {
        $connection = $this->resourceConnection->getConnection();
        $select = $connection->select()
            ->from('catalog_product_entity')
            ->where('sku = ?', $sku);

        return $connection->fetchAll($select);
    }
}

3. How DAST works: crawling, fuzzing and real HTTP attacks

A DAST scanner starts with no knowledge of the source code and has to discover the attack surface itself. In the first step, the tool crawls the application like a browser: it follows links, fills in forms, calls JavaScript-generated routes, and logs every endpoint it finds along with its parameters. This catalog becomes the site tree, the basis for the subsequent active scan, where every parameter is fed with a library of known attack payloads, for example SQL metacharacters, XSS payloads, or path traversal sequences.

Unlike SAST, DAST evaluates the actual behavior of the system: changed response times, different status codes, reflected payloads in the HTML, or SQL error messages in the response body all count as evidence of a real, exploitable vulnerability. This black box perspective is technology agnostic, so it works regardless of whether the application is written in PHP, Java, or Node.js, but it only ever finds what the crawler actually reaches and can test with the credentials it has available.

4. What SAST catches and what DAST finds: blind spots compared

SAST excels at vulnerability classes that follow directly from data flow in the code: SQL injection through string concatenation, cross-site scripting through missing output encoding, unsafe deserialization, and hardcoded credentials. The key advantage is completeness: a code path reached only under a rare error condition is still analyzed, because SAST does not depend on actual traffic. The flip side: runtime configuration, server headers, TLS settings, or the interaction between multiple microservices sit outside the field of view of a pure code scanner.

DAST covers exactly that gap: missing security headers, weak TLS configuration, session fixation, authentication bypasses through manipulated cookies, and misconfigurations that only arise from the interplay of web server, framework, and infrastructure. What DAST systematically misses in turn is code paths the crawler never reaches, such as admin functions without a linked entry point, plus deeper business logic flaws that no generic payload triggers. Both tool classes complement each other rather than replacing one another.

5. False positive rates and tuning

SAST tools tend toward high false positive rates without tuning, because they conservatively flag every potential data flow, even when sanitization happens at a point the analyzer does not recognize. A typical example: a custom escaping helper module that PHPStan or Psalm does not know as a trusted sanitizer. The fix lies in baselines, which freeze the current set of findings so only new violations break the build, combined with explicit annotations such as @psalm-taint-escape that tell the analyzer which functions actually clean the data.

False positives are rarer with DAST scanners, but more expensive to triage, because every finding must be manually verified against the real application. OWASP ZAP offers alert filters that permanently hide known, deliberately accepted findings, plus a context configuration that precisely scopes authentication and target boundaries. Without this ongoing maintenance, both tool classes accumulate so many ignored notifications over months that real findings drown in the noise, a state commonly known as alert fatigue.

6. Integrating SAST into the CI pipeline: PHPStan security rules

PHPStan itself is primarily a type checker, but it becomes a security tool when combined with specialized rule sets such as phpstan/phpstan-security or custom rules that flag dangerous function calls like eval(), unserialize() without allowed_classes, or system() with dynamic arguments. For Magento projects it is worth adding a rule that detects direct SQL string concatenation outside the Zend Db Select object, since this pattern shows up especially often in legacy modules. The key is to raise the analysis level gradually so existing code does not immediately produce hundreds of findings.

Wired in as a quality gate, a failing PHPStan run blocks the merge before insecure code ever reaches the main branch, much earlier than any DAST scan, which can only run against a deployed environment. The combination of a baseline file and a strict gate for new violations keeps adoption practical even in mature codebases with thousands of existing files.


# phpstan.neon: security-relevant rules for a Magento 2 codebase
parameters:
    level: 6
    paths:
        - app/code/Mironsoft

    # Freeze existing findings, only fail the build on new violations
    baselineFile: phpstan-baseline.neon

    ignoreErrors:
        # Known Magento interface gap, documented and accepted
        - '#Call to an undefined method .*PageInterface::getData\(\)#'

includes:
    - vendor/phpstan/phpstan-security/extension.neon

services:
    -
        class: Mironsoft\PhpStanRules\NoRawSqlConcatenationRule
        tags:
            - phpstan.rules.rule
    -
        class: Mironsoft\PhpStanRules\NoUnsafeUnserializeRule
        tags:
            - phpstan.rules.rule

# .github/workflows/sast.yml: run static analysis as a required check
name: SAST

on:
  pull_request:
    branches: [main]

jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          tools: composer:v2

      - name: Install dependencies
        run: composer install --no-progress --prefer-dist

      # Fail the pipeline on any error above the current baseline
      - name: Run PHPStan security analysis
        run: vendor/bin/phpstan analyse app/code/Mironsoft --level=6 --error-format=github

      - name: Run Psalm taint analysis
        run: vendor/bin/psalm --taint-analysis --report=psalm-report.sarif

7. Psalm taint analysis: tracking data flow from source to sink

Psalm's taint analysis engine takes a more explicit approach than PHPStan: instead of just checking types, Psalm models the complete data flow between defined sources and sinks. Sources include superglobals, request objects, and database results; sinks are dangerous functions like exec(), echo in an HTML context, or PDO::query(). Once taint analysis is enabled, Psalm reports concrete finding types such as TaintedSql for SQL injection risk or TaintedHtml for reflected XSS, each with a complete call path from the entry point to the sink.

The advantage over generic rules lies in traceability: a developer does not just see that a function is unsafe, but the exact chain of method calls through which the unsafe input got there. That makes it easier both to judge whether a finding is real and to fix it precisely. Custom sanitization functions can be marked as trusted through @psalm-taint-escape annotations, so the false positive rate drops with every maintained annotation.


<?xml version="1.0"?>
<!-- psalm.xml: enable taint analysis for the whole codebase -->
<psalm
    errorLevel="4"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
    <projectFiles>
        <directory name="app/code/Mironsoft" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>

    <!-- Run with: vendor/bin/psalm --taint-analysis --
         Findings surface as TaintedSql, TaintedHtml, TaintedShell, etc. -->
    <issueHandlers>
        <TaintedSql errorLevel="error" />
        <TaintedHtml errorLevel="error" />
        <TaintedShell errorLevel="error" />
        <TaintedUnserialize errorLevel="error" />
    </issueHandlers>
</psalm>

8. DAST against a running staging environment

A DAST scan should never target production, only a staging environment that resembles the live system as closely as possible, including realistic test data but never real customer data. OWASP ZAP can be run as a CLI inside a Docker container and distinguishes between a fast baseline scan, which only passively observes traffic, and a full active scan, which fires payloads against every discovered parameter and generates significantly more time and load on the target environment. For authenticated areas, a context file with a login sequence and session recognition is required, otherwise ZAP only scans the publicly reachable pages.

An active scan should run outside core working hours, since it noticeably loads the staging environment and in the worst case alters test data. Results are written as an HTML or JSON report, ideally automated as a CI pipeline artifact, so findings do not get lost in a separate console but land directly in the usual review process.


#!/usr/bin/env bash
# run-dast-scan.sh: run an OWASP ZAP scan against the staging environment
set -euo pipefail

readonly STAGING_URL="https://staging.mironsoft.de"
readonly CONTEXT_FILE="zap/mironsoft-staging.context"
readonly REPORT_DIR="zap-reports/$(date +%Y%m%d-%H%M%S)"

mkdir -p "$REPORT_DIR"

# Fast baseline scan: passive checks only, safe to run on every deploy
docker run --rm -v "$(pwd)/${REPORT_DIR}:/zap/wrk:rw" \
  ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t "$STAGING_URL" \
  -r baseline-report.html \
  -J baseline-report.json

# Full active scan: authenticated, off-hours only, higher load
docker run --rm \
  -v "$(pwd)/${CONTEXT_FILE}:/zap/context.context:ro" \
  -v "$(pwd)/${REPORT_DIR}:/zap/wrk:rw" \
  ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
  -t "$STAGING_URL" \
  -n /zap/context.context \
  -r active-report.html \
  -J active-report.json \
  -z "-config api.disablekey=true"

echo "[OK] Reports written to ${REPORT_DIR}"

9. SAST and DAST head to head

Both tool classes cost time, infrastructure, and maintenance effort, but at different points in the development cycle. SAST is almost always worth adopting first, because the barrier to entry is low: no running environment needed, results within seconds, direct integration into every pull request. DAST, by contrast, requires a stable staging environment, test data, an authentication setup, and time for the scan to run, but in return it delivers statements about the system as a whole that no code scanner can make.

Dimension SAST DAST
Detection scope Entire source code, including unreached paths Only endpoints actually reachable at runtime
False positive rate High without a baseline and tuning Lower, since real server responses are verified
Earliest CI integration point Pre-commit or pre-merge Only after deployment to staging
Runtime per pass Seconds to a few minutes Minutes to hours depending on scan scope
Typical vulnerability classes Taint flows, SQLi/XSS in code, unsafe deserialization Auth bypass, server misconfiguration, session handling
Business logic flaws Barely detectable without contextual knowledge Only with deliberately written test scenarios

Teams with budget for only one tool should start with SAST, because it stops vulnerabilities before the merge and is the cheapest per issue found. DAST becomes mandatory at the latest once an application is publicly reachable and compliance requirements such as PCI DSS call for regular external scans. The most economical strategy is rarely "either or", but rather SAST on every commit and DAST on a fixed cadence, for example weekly or before every major release.

Mironsoft

Application security testing for Magento and Hyva projects

Want SAST and DAST established in your Magento pipeline?

We set up PHPStan security rules and Psalm taint analysis in your CI pipeline, configure OWASP ZAP scans against your staging environment, and help keep false positives permanently under control.

Security audit

PHPStan and Psalm configuration with security rules for your codebase

CI integration

Set up taint analysis and quality gates in GitLab CI or GitHub Actions

DAST scans

Plan OWASP ZAP scans against staging environments and triage the results

10. Summary

SAST and DAST tools solve complementary, not competing, tasks. SAST analyzes the source code without executing it and finds taint flows, unsafe function calls, and data leaks before code is even merged, but it does not cover runtime configuration. DAST tests the running application like a real attacker and finds server misconfigurations, auth bypasses, and session issues, but it misses code paths the crawler never reaches. PHPStan security rules and Psalm taint analysis belong in every CI pipeline, because they are fast, cheap, and catch problems early in the development process.

OWASP ZAP against a realistic staging environment complements this early testing with an outside-in perspective, especially for misconfigurations that can never be derived from source code alone. The decisive success factor is consistent tuning on both sides: baselines and sanitization annotations for SAST, alert filters and precise context configuration for DAST, so real findings do not drown in a sea of false positives.

SAST and DAST Tools Compared: the essentials at a glance

SAST: source code analysis

Catches taint flows and unsafe function calls before the merge. PHPStan and Psalm are the standard tools for PHP.

DAST: runtime analysis

Uncovers real attack surfaces and misconfigurations on staging. OWASP ZAP simulates real HTTP attacks.

Keeping false positives in check

Baselines, sanitization annotations, and alert filters noticeably reduce the noise level on both sides.

Combined strategy

SAST on every commit, DAST regularly against staging. Both approaches complement each other, neither replaces the other.

11. FAQ: SAST and DAST Tools Compared

1What is the difference between SAST and DAST?
SAST analyzes source code statically via taint analysis. DAST tests a running application from the outside with real HTTP requests, similar to a real attacker.
2Which vulnerabilities does SAST catch that DAST misses?
Rarely reached code paths like error handling or unlinked admin functions, because SAST analyzes the entire source code rather than only reachable traffic.
3Which vulnerabilities does DAST catch that SAST misses?
Runtime and infrastructure problems like missing security headers, weak TLS configuration, and session fixation, none derivable from code alone.
4How high is the false positive rate of SAST tools really?
Substantial without tuning, since every potential data flow is conservatively flagged. Baselines and sanitization annotations lower the rate significantly.
5How do I reduce false positives in PHPStan and Psalm?
Baseline file for existing code, mark sanitization functions as trusted via annotation, raise the analysis level gradually instead of maxing it out immediately.
6What is taint analysis and how does it work in Psalm?
Tracks data from untrusted sources to dangerous sinks. Psalm reports types such as TaintedSql or TaintedHtml with the complete call path.
7How do I integrate PHPStan security rules into the CI pipeline?
As a dedicated job with a baseline file and a strict gate for new violations, blocking the merge whenever new findings appear.
8How do I safely run a DAST scan with OWASP ZAP against staging?
With a Docker image, a context file for authentication and scope, and an active scan outside core working hours. Never scan production.
9Should I introduce SAST or DAST first?
Usually SAST first due to its low barrier to entry. DAST becomes mandatory once an application is publicly reachable and compliance requirements apply.
10Does DAST replace a manual penetration test?
No. DAST finds known vulnerability patterns automatically, but does not detect complex business logic flaws that require deliberate human reasoning.