Skip to content

Because Security is Not a “Non-Functional Requirement”

Related Article: BDD-style Acceptance Testing

Security is not a non-functional requirement

Too often we treat security as a nice-to-have; a check box buried deep in a backlog, languishing behind feature flags and performance tickets. But when data is leaked, when credentials are exposed, security suddenly becomes painfully real. All at once, the consequences land squarely in your inbox, your boardroom — and the public square. That’s when “Wish You Were Here?” isn’t a postcard caption — it’s a solemn announcement.

We have tools today — Static Application Security Testing (SAST), Dynamic Application Security Testing (DAST), dependency scanners, secret detection, fuzzers, and more. They’re powerful, but they can become rigid gatekeepers if they’re not an integral part of the continuous integration pipeline.

In my book The Effective Software Engineer, I emphasize something that often gets lost:

CI is behavior, not a button you install.

A healthy CI pipeline isn’t just a server and a report; it’s a culture of continuous feedback and responsibility. And while culture is human, the tools must be technical and automated.

🛠️ You should absolutely embed SAST and DAST into your technical pipeline — not as an afterthought, not as a separate sprint task, but as standard quality feedback that developers see before code is merged.

Here’s an example of what that looks like in practice: 🔗 https://github.com/my-ellersdorfer-at/my-stuff

If security is treated only as a “non-functional requirement,” then we will experience it most acutely when it fails. Instead, let’s bake secure thinking into how we build, test, and deliver software — every commit, every integration, every day.


What is SAST?

Static Application Security Testing (SAST) analyzes your source code without executing it. It looks for security vulnerabilities, reliability issues, and maintainability problems such as:

  • Injection risks
  • Insecure cryptography usage
  • Broken error handling
  • Code paths that lead to undefined or unsafe behavior

SAST shines early in the development lifecycle. It gives fast feedback while code is still cheap to change — ideally before it ever reaches main.

--

SAST in the book’s example project

In the example project accompanying The Effective Software Engineer, SAST is part of the commit stage of the CI pipeline, not an optional scan.

  • Executed automatically on every push
  • Combined with unit and integration tests
  • Enforced via a quality gate

This is implemented using Sonar analysis directly in CI.

SAST - SonarQube

The important part is not the tool itself, but where it runs: Security feedback arrives at the same time as test failures — while the developer is still context-loaded.


What is DAST?

Dynamic Application Security Testing (DAST) analyzes a running system. Instead of inspecting code, it behaves like an attacker and probes the application from the outside.

DAST is especially effective at finding:

  • Misconfigurations
  • Authentication and authorization flaws
  • Missing security headers
  • Runtime exposure issues that static analysis cannot see

DAST answers a different question than SAST:

“Given what we built and deployed — how does it behave under attack?”


DAST in the book’s example project

In the book’s example project, DAST is implemented as a scheduled CI workflow, not a manual penetration test and not a one-off audit.

  • The application is built and started automatically
  • Supporting infrastructure (e.g. identity provider) is provisioned
  • A baseline OWASP ZAP scan is executed against the running system
  • Reports are archived as CI artifacts

This setup is fully automated using GitHub Actions and OWASP ZAP

name: DAST - OWASP ZAP against local app (daily)

on:
  schedule:
    - cron: "0 1 * * *" # daily 01:00 UTC
  workflow_dispatch: {}

jobs:
  zap_dast:
    runs-on: ubuntu-latest

    env:
      BOOT_MODULE: "application"
      APP_URL: "http://localhost:8888"
      KEYSTORE_DIR: "${{ github.workspace }}/keystore"
      KEYSTORE_PASSWORD: "changeit"

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Java 25
        uses: actions/setup-java@v4
        with:
          distribution: "temurin"
          java-version: "25"
          cache: "maven"

      - name: (Optional) Show realm file exists
        run: |
          ls -la acceptance/src/test/resources/keycloak/my-stuff.realm.json

      - name: Maven build + acceptance tests
        run: mvn -B clean verify --file pom.xml -Dgroups=acceptanceTest

      - name: Start Keycloak via docker compose
        run: docker compose -f docker-compose.yaml up -d

      - name: Wait for Keycloak realm (OIDC discovery)
        run: |
          set -e
          URL="https://localhost:8443/realms/my-stuff/.well-known/openid-configuration"
          for i in {1..120}; do
            if curl -kfsS "$URL" >/dev/null; then
              echo "Realm is imported and OIDC discovery is reachable."
              exit 0
            fi
            sleep 2
          done
          echo "Realm import not ready in time."
          docker compose -f docker-compose.yaml logs --no-color keycloak | tail -n 300 || true
          exit 1

      - name: Package Spring Boot app (reactor build)
        run: mvn -B -DskipTests -pl "${BOOT_MODULE}" -am package

      - name: Start Spring Boot (background, java -jar)
        run: |
          set -e
          JAR_PATH="$(ls -1 ${BOOT_MODULE}/target/*.jar | grep -v 'original-' | head -n 1)"
          echo "Starting ${JAR_PATH}"

          nohup java \
            -Djavax.net.ssl.keyStore=${KEYSTORE_DIR}/cacerts \
            -Djavax.net.ssl.keyStorePassword=${KEYSTORE_PASSWORD} \
            -Djavax.net.ssl.trustStore=${KEYSTORE_DIR}/cacerts \
            -Djavax.net.ssl.trustStorePassword=${KEYSTORE_PASSWORD} \
            -jar "${JAR_PATH}" \
            --server.port=8888 \
            > app.log 2>&1 &

      - name: Wait for app to be ready
        run: |
          set -e
          echo "Waiting for ${APP_URL} ..."
          for i in {1..120}; do
            if curl -fsS "${APP_URL}" >/dev/null; then
              echo "App is up."
              exit 0
            fi
            sleep 1
          done
          echo "App did not become ready in time."
          echo "---- app.log (tail) ----"
          tail -n 250 app.log || true
          exit 1

      - name: OWASP ZAP Baseline Scan
        run: |
          mkdir -p zap-reports

          ZAP_UID="$(docker run --rm zaproxy/zap-stable id -u)"
          ZAP_GID="$(docker run --rm zaproxy/zap-stable id -g)"

          sudo chown -R "${ZAP_UID}:${ZAP_GID}" zap-reports

          docker run --rm --network host \
            -v "${PWD}/zap-reports:/zap/wrk:rw" \
            zaproxy/zap-stable \
            zap-baseline.py \
            -t "${APP_URL}" \
            -r zap-baseline-report.html \
            -J zap-baseline-report.json \
            -w zap-baseline-warn.md \
            || true

      - name: Upload ZAP reports
        uses: actions/upload-artifact@v4
        with:
          name: zap-reports
          path: zap-reports

      - name: Upload app log
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: app-log
          path: app.log
          if-no-files-found: ignore

The key idea: DAST is repeatable, visible, and boring — exactly what you want from security checks.


Why this matters for Continuous Integration

Security does not fail because teams lack tools. It fails because feedback arrives too late, too detached, or outside the normal development flow.

When SAST and DAST are:

  • Part of CI
  • Run automatically
  • Visible to developers
  • Treated like tests

…security stops being an abstract requirement and becomes daily engineering behavior.

And that is the core message of The Effective Software Engineer:

Sustainable software quality — including security — emerges from habits, feedback loops, and responsibility, not from documents or checklists.


How this connects to The Effective Software Engineer

This article directly builds on Chapter 4 — Test Driven Development and Chapter 5 — Useful goals for software development teams in The Effective Software Engineer.

In those chapters, I argue that:

  • Quality is not something we “add” at the end of development
  • Fast feedback is the primary economic driver of sustainable software
  • Continuous Integration is a behavioral system, not a tooling checklist

SAST and DAST fit naturally into this model. They are not “security phases” or specialized audits — they are feedback mechanisms, no different in principle from unit tests, acceptance tests, or build verification.

In the book’s example project, you’ll see this philosophy applied consistently:

  • SAST runs in the commit stage, alongside tests, enforcing security and maintainability as first-class quality signals
  • DAST runs against a fully started system, exercising real authentication, real infrastructure, and real attack surfaces
  • Both are automated, repeatable, and visible to the team — not delegated to a separate role or postponed to “later”

If this article resonates with you, the book goes deeper into why these practices work, how they shape team behavior, and what organizational conditions are required so that security, quality, and delivery speed reinforce each other instead of competing.

Related Article: BDD-style Acceptance Testing