Post

DevSecOps Security Testing Explained: SAST, SCA, DAST, IAST and More (with a Tool Comparison)

SAST, SCA, secrets, IaC, container, DAST, IAST, CSPM and more, each shown on the same Python Flask API, plus a feature comparison of 24 paid and open-source security tools.

DevSecOps Security Testing Explained: SAST, SCA, DAST, IAST and More (with a Tool Comparison)

Your security team asks whether you “do SAST and SCA”, the vendor deck says the platform covers “CNAPP, ASPM and shift-left”, and you’re left wondering which of these acronyms would actually have caught the bug that matters. They aren’t interchangeable. Each technique looks at a different layer, at a different moment in the pipeline, and misses things the others catch. In this post I take one small application, a Python Flask “orders” API running in a container on AWS, and show what each kind of security testing finds in it. After that comes a comparison of 24 popular paid and open-source tools, with what each one covers.

The example application

Everything below uses the same project, so you can see how each technique looks at a different part of it:

1
2
3
4
5
6
7
8
9
10
orders-api/
├── app/
│   ├── __init__.py
│   ├── routes.py          # Flask endpoints: /orders, /orders/<id>, /search
│   └── db.py              # database access
├── requirements.txt       # Flask, PyYAML, requests ...
├── Dockerfile             # container image
├── infra/
│   └── main.tf            # Terraform: EC2, security group, S3 bucket
└── tests/

And here’s where each technique sits in the delivery pipeline:

%%{init: {"themeVariables": {"fontSize": "14px"}, "flowchart": {"useMaxWidth": false, "nodeSpacing": 30, "rankSpacing": 40, "padding": 8, "wrappingWidth": 150}}}%%
flowchart TB
  subgraph CODE["Code and commit"]
    direction LR
    A[SAST] --> B[Secrets<br/>scanning] --> C[SCA and<br/>licences]
  end
  subgraph BUILD["Build"]
    direction LR
    D[SBOM] --> E[Container<br/>scanning] --> F[IaC scanning] --> G[Signing and<br/>provenance]
  end
  subgraph TEST["Test"]
    direction LR
    H[DAST] --> I[IAST] --> J[API testing] --> K[Fuzzing]
  end
  subgraph RUN["Run"]
    direction LR
    L[CSPM] --> M[Runtime / CWPP<br/>and RASP] --> N[Vulnerability<br/>scanning] --> O[Pen testing]
  end
  CODE --> BUILD --> TEST --> RUN

Code and commit stage

SAST: Static Application Security Testing

SAST reads your source code without running it and looks for insecure patterns: injection, unsafe deserialisation, weak crypto, path traversal. Most tools trace tainted data from inputs (request parameters) to dangerous sinks (SQL queries, shell commands). It runs in the IDE, on pull requests and in CI, and it’s the cheapest place to catch a bug, because it points at the exact line.

In orders-api, the search endpoint builds SQL with an f-string:

1
2
3
4
5
6
# app/routes.py
@bp.get("/search")
def search():
    term = request.args.get("q", "")
    rows = db.execute(f"SELECT id, item FROM orders WHERE item LIKE '%{term}%'")   # SQL injection
    return jsonify([dict(r) for r in rows])
1
2
3
4
5
$ bandit -r app/
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
   Severity: Medium   Confidence: Medium
   CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
   Location: app/routes.py:12

The fix is a parameterised query: db.execute("SELECT id, item FROM orders WHERE item LIKE ?", (f"%{term}%",)).

SAST’s weakness is false positives, plus blindness to anything that only exists at runtime (configuration, authentication flows, the deployed environment). Tune the rules, and fail builds only on high-confidence findings.

Secrets scanning

Secrets scanning looks for credentials committed to code or history: API keys, tokens, private keys, connection strings. The best setups block the push before the secret ever reaches the remote (“push protection”), because once a secret is in git history, deleting the line doesn’t help. You have to rotate it.

In orders-api, someone hard-coded an AWS key in db.py “just for testing”:

1
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"     # AWS's documented example key
1
2
3
4
5
$ gitleaks git -v .
Finding:     AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
RuleID:      aws-access-token
File:        app/db.py
Commit:      3f2a9c1

The fix: rotate the key, load credentials from the environment or an IAM role, and add a pre-commit hook so it can’t happen again.

SCA: Software Composition Analysis

SCA inventories your third-party dependencies, including transitive ones, and matches them against vulnerability databases (NVD, GitHub Advisory, OSV). Most of your application is code you didn’t write, so this is often where the most serious known issues are. Good SCA tools also check licences (a GPL dependency in a proprietary product is a legal problem) and reachability, meaning whether your code actually calls the vulnerable function.

In orders-api, requirements.txt pins an old PyYAML:

1
2
3
Flask==3.0.3
PyYAML==5.3
requests==2.32.3
1
2
3
4
5
6
7
$ trivy fs --scanners vuln .
requirements.txt (pip)
┌─────────┬────────────────┬──────────┬───────────────────┬───────────────┐
│ Library │ Vulnerability  │ Severity │ Installed Version │ Fixed Version │
├─────────┼────────────────┼──────────┼───────────────────┼───────────────┤
│ PyYAML  │ CVE-2020-14343 │ CRITICAL │ 5.3               │ 5.4           │
└─────────┴────────────────┴──────────┴───────────────────┴───────────────┘

CVE-2020-14343 allows arbitrary code execution when untrusted YAML is loaded with full_load. Upgrade the dependency, and use yaml.safe_load regardless. Automate the upgrades with Dependabot or Renovate, or you’ll be fixing the same class of finding forever.

SCA vs vulnerability scanning: SCA looks at the libraries your application pulls in. Vulnerability scanning (covered later) looks at the host or image your application runs on. You need both.

Build stage

SBOM: Software Bill of Materials

An SBOM is a machine-readable inventory of every component in your software: name, version, supplier, licence and hashes, in a standard format (CycloneDX or SPDX). It isn’t a scan in itself. It’s the artifact that makes scanning and incident response fast. When the next Log4Shell-style vulnerability drops, teams with SBOMs answer “are we affected?” with a query instead of a week of grepping.

For orders-api, generate one at build time and store it with the image:

1
2
3
syft dir:. -o cyclonedx-json > sbom.cdx.json
# or, with Trivy
trivy fs --format cyclonedx --output sbom.cdx.json .

Regulations increasingly expect SBOMs for software sold to governments and regulated industries, so it’s worth producing them now even if nobody has asked yet.

Container image scanning

Container scanning checks the image you ship: OS packages in the base image (Debian, Alpine), language packages inside it, and Dockerfile misconfigurations such as running as root or copying secrets in. Scan in CI before pushing, and again in the registry, because new CVEs appear for images that haven’t changed.

In orders-api, the Dockerfile uses an end-of-life Python base image and runs as root:

1
2
3
4
FROM python:3.8
COPY . /app
RUN pip install -r /app/requirements.txt
CMD ["python", "/app/run.py"]
1
2
trivy image --severity HIGH,CRITICAL orders-api:1.0   # vulnerable OS and Python packages
trivy config Dockerfile                               # misconfigurations, e.g. running as root

Python 3.8 reached end of life in October 2024, so its base images stop getting fixes. The fix: move to a supported slim base image (python:3.12-slim), add a non-root USER, and use a multi-stage build so compilers and build tools don’t ship to production.

IaC scanning

Infrastructure-as-code scanning checks Terraform, CloudFormation, Bicep, Kubernetes manifests and Helm charts for insecure configuration before it’s deployed: open security groups, public buckets, missing encryption, privileged pods. It’s SAST for infrastructure, and it prevents the misconfigurations that CSPM would otherwise find in production.

In orders-api, infra/main.tf opens SSH to the world:

1
2
3
4
5
6
7
8
9
resource "aws_security_group" "orders" {
  name = "orders-api"
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
1
2
3
4
$ checkov -d infra/
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
        FAILED for resource: aws_security_group.orders
        File: /main.tf:1-10

The fix: restrict the CIDR to your VPN range, or better, drop SSH and use AWS Systems Manager Session Manager. (If you’ve used tfsec, note that it has been folded into Trivy, which now covers IaC scanning too. See the older tfsec post for context.)

Software supply chain: signing and provenance

Scanning tells you what’s in an artifact. Signing and provenance tell you where it came from and that nobody tampered with it on the way. Sign images in CI (Sigstore’s cosign), attach build provenance (the SLSA framework), and verify signatures at deploy time with an admission controller, so only images built by your pipeline can run.

For orders-api:

1
2
3
4
cosign sign --yes registry.example.com/orders-api@sha256:4f1c...e9a2
cosign verify registry.example.com/orders-api@sha256:4f1c...e9a2 \
  --certificate-identity-regexp 'https://github.com/acme/orders-api/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Test stage

DAST: Dynamic Application Security Testing

DAST attacks the running application from the outside, like an attacker would. It knows nothing about the code. It crawls pages and endpoints, sends malicious inputs, and looks at the responses for XSS, injection, missing security headers, insecure cookies and server misconfiguration. Because it tests the deployed system, it finds problems that only appear at runtime: a proxy that strips headers, a debug mode left on, a misconfigured TLS setup.

Against orders-api running in a test environment:

1
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://staging.orders.internal:5000
1
2
3
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 3
WARN-NEW: Missing Anti-clickjacking Header [10020] x 3
FAIL-NEW: Cross Site Scripting (Reflected) [40012] x 1   -> /search?q=

The baseline scan is passive and safe to run against shared environments. Full active scans send real attack payloads, so run them against a dedicated test environment. DAST tells you what is exploitable, but not which line of code to fix, which is where IAST comes in. There’s more on ZAP in the OWASP ZAP post.

IAST: Interactive Application Security Testing

IAST puts an agent inside the running application (for Python, an instrumented module loaded at startup) while your functional tests or DAST scan exercise it. Because it sees both the incoming HTTP request and the code executing underneath, it can confirm that a request parameter really reaches a SQL query unsanitised, and report the exact line, with far fewer false positives than SAST.

In orders-api, running the integration test suite with an IAST agent enabled would report something like: “SQL injection: untrusted data from request.args['q'] reaches db.execute() at app/routes.py:12 via /search.” That’s the same bug SAST flagged, but now confirmed as exploitable. The trade-offs: an agent per language runtime, some performance overhead, and coverage limited to the code your tests actually execute. Commercial examples include Contrast Assess and Black Duck Seeker.

API security testing

Modern applications are mostly APIs, and the most common API flaws are about authorisation logic, which generic scanners miss. The OWASP API Security Top 10 is led by Broken Object Level Authorization (BOLA): user A can read user B’s data just by changing an ID. API testing tools import your OpenAPI spec, then test every endpoint with different users, tokens and payloads.

In orders-api, logged in as customer 42:

1
2
curl -H "Authorization: Bearer $TOKEN_FOR_CUSTOMER_42" https://staging.orders.internal/orders/1337
# 200 OK  {"id": 1337, "customer_id": 77, "item": "laptop", "address": "..."}   <- someone else's order

The fix belongs in the code: check order.customer_id == current_user.id on every object lookup, ideally in one shared helper. Tools include ZAP’s API scan, Burp Suite, 42Crunch, StackHawk and the API testing in GitLab, Checkmarx and Snyk.

Fuzzing

Fuzzing feeds huge numbers of random or mutated inputs into a function or endpoint and watches for crashes, hangs and unhandled exceptions. It’s excellent at finding the edge cases nobody thought to write a test for: parsers, file formats, protocol handlers. Coverage-guided fuzzers mutate inputs that reach new code paths, so they get deeper over time.

In orders-api, fuzz the order parser with Atheris, Google’s Python fuzzer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# fuzz_parse_order.py
import sys
import atheris

with atheris.instrument_imports():
    from app.orders import parse_order

def test_one_input(data: bytes) -> None:
    fdp = atheris.FuzzedDataProvider(data)
    try:
        parse_order(fdp.ConsumeUnicodeNoSurrogates(512))
    except ValueError:
        pass   # expected for invalid input; anything else is a bug

atheris.Setup(sys.argv, test_one_input)
atheris.Fuzz()

A typical finding: a payload like {"quantity": []} raises TypeError deep inside, which becomes a 500 error, and in other codebases sometimes a denial of service.

Run stage

CSPM: Cloud Security Posture Management

CSPM continuously checks your live cloud accounts against security best practices and compliance frameworks (CIS, PCI DSS, ISO 27001): public storage, disabled logging, overly permissive IAM, unencrypted databases, root accounts without MFA. IaC scanning catches problems before deployment. CSPM catches everything that was clicked together in the console, drifted, or was never in code at all.

For orders-api’s AWS account:

1
prowler aws --services s3 cloudtrail iam
1
2
3
FAIL  s3_bucket_public_access     orders-invoices bucket allows public read
FAIL  cloudtrail_multi_region     CloudTrail is not enabled in all regions
FAIL  iam_root_mfa_enabled        Root account has no MFA

CIEM (Cloud Infrastructure Entitlement Management) is a close relative. It focuses on identities and permissions: unused admin roles, keys that never rotate, and roles that can escalate to admin.

Runtime protection: CWPP and RASP

Scanning finds weaknesses. Runtime protection watches for attacks in progress.

  • CWPP (Cloud Workload Protection Platform) watches hosts, containers and serverless functions at runtime, usually with an eBPF sensor or agent: unexpected processes, shells in containers, crypto-miners, privilege escalation, drift from the original image.
  • RASP (Runtime Application Self-Protection) runs inside the application, like IAST, but in production, and blocks attacks such as a SQL injection payload reaching the database driver.

For orders-api, a Falco rule alerts if anyone gets a shell inside the container, which is a classic sign of compromise:

1
2
3
4
5
- rule: Shell in orders-api container
  desc: Detect a shell spawned inside the orders-api container
  condition: spawned_process and container and container.image.repository endswith "orders-api" and proc.name in (bash, sh)
  output: Shell spawned in orders-api (user=%user.name cmd=%proc.cmdline container=%container.name)
  priority: WARNING

CNAPP and ASPM: the umbrellas

Two more acronyms describe platforms rather than techniques:

  • CNAPP (Cloud-Native Application Protection Platform) bundles CSPM, CWPP, CIEM, container scanning and IaC scanning into one product with a shared view of risk. Wiz, Prisma Cloud (now Cortex Cloud), Aqua and Microsoft Defender for Cloud are CNAPPs.
  • ASPM (Application Security Posture Management) pulls findings from all your scanners (SAST, SCA, DAST, secrets and so on), deduplicates them, maps them to owners and services, and prioritises by real risk, such as “internet-facing, handles payment data, exploitable”. If you have five scanners and 40,000 findings, ASPM is what stops the security team drowning.

Vulnerability scanning (hosts and infrastructure)

Classic vulnerability scanning probes servers, VMs, network devices and their operating systems for known CVEs, missing patches and weak services, either over the network or with an agent. It’s the oldest technique on this list and still essential, because your application is only as safe as the box it runs on.

For orders-api’s EC2 host, an agent-based scan (Amazon Inspector, Qualys, Tenable Nessus or the open-source OpenVAS) might report:

1
2
CRITICAL  CVE-2024-6387  OpenSSH 9.2p1  "regreSSHion" remote code execution  -> upgrade openssh-server
HIGH      Kernel 6.1.0-18 has 3 known privilege escalation CVEs               -> apply OS updates and reboot

Penetration testing

A penetration test is humans attacking your system with permission, chaining small weaknesses into real impact, the way an attacker would. No tool on this list would combine “verbose error reveals internal hostnames”, “BOLA on /orders” and “SSRF in the invoice PDF generator” into “read every customer’s address”. A good tester will. Do it before major launches and at least yearly, and treat findings as input for which automated checks to add.

Summary: which technique finds what

TechniqueLooks atWhenFinds in orders-apiMisses
SASTYour source codeIDE, PR, CISQL injection in /searchRuntime config, auth logic
Secrets scanningCode and git historyPre-commit, push, CIHard-coded AWS keySecrets in running systems
SCAThird-party dependenciesPR, CI, continuouslyPyYAML CVE-2020-14343Bugs in your own code
SBOMComponent inventoryBuild(Enables fast impact analysis)Not a test by itself
Container scanningThe imageCI, registryEOL base image, root userApp logic bugs
IaC scanningTerraform and manifestsPR, CISSH open to 0.0.0.0/0Console changes, drift
DASTRunning app, from outsideTest or stagingReflected XSS, missing headersExact code location
IASTRunning app, from insideDuring testsConfirms SQLi with line numberUntested code paths
API testingAPI endpoints and authTest or stagingBOLA on /orders/<id>Non-API surfaces
FuzzingFunctions and parsersCI, nightlyCrash on malformed inputLogic and auth flaws
CSPMLive cloud accountsContinuouslyPublic S3 bucket, no CloudTrailApp vulnerabilities
CWPP / RASPRunning workloadsProductionShell in container, live SQLiPre-production issues
Vulnerability scanningHosts and OSContinuouslyOpenSSH CVE on the EC2 hostApp-level flaws
Pen testingEverything, by humansBefore launches, yearlyChained attack pathsContinuous coverage

Tool comparison: paid and open source

The landscape consolidates fast. Vendors acquire each other, rename products and add modules every year. The table below reflects the products’ documented capabilities as I researched them. ✓ means the capability is built in, ◐ means partial (an add-on, preview, limited scope, or via integration), and — means it isn’t offered. Check current packaging with the vendor before buying, because modules are often licensed separately.

Commercial platforms

ToolSASTSCASecretsIaCContainerDASTCSPMRuntimeSBOM
GitHub Advanced Security (Code Security + Secret Protection)✓ CodeQL✓ Dependabot✓ push protection◐ via SARIF◐———✓
GitLab Ultimate✓✓✓✓✓✓——✓
Snyk✓ Code✓ Open Source?✓✓✓ API & Web◐—✓
SonarQube (Server / Cloud)✓◐ Advanced Security add-on✓✓◐ Dockerfile rules———◐ add-on
Checkmarx One✓✓✓✓ KICS✓✓——✓
Veracode✓✓◐✓✓✓——✓
OpenText Fortify✓✓ Debricked◐◐◐✓ WebInspect——✓
Black Duck (Coverity, Black Duck SCA)✓ Coverity✓◐◐✓✓——✓
Mend.io✓✓◐—✓?——✓
Wiz (Wiz Code, Wiz Cloud, Wiz Defend)◐ preview✓✓✓✓◐✓✓✓
Prisma Cloud / Cortex Cloud—✓✓✓ Checkov✓—✓✓✓
Aqua Security—✓✓✓✓—✓✓✓
Microsoft Defender for Cloud◐ via DevOps integrations◐◐◐✓—✓✓—

? means I couldn’t confirm the capability in the vendor’s current documentation when researching this post.

Open-source tools

ToolSASTSCASecretsIaCContainerDASTCSPMRuntimeSBOM
Trivy—✓✓✓✓—◐ Kubernetes—✓
Semgrep Community Edition✓——◐ rules—————
Bandit (Python only)✓—◐ hard-coded passwords——————
OWASP Dependency-Check—✓———————
OSV-Scanner—✓——✓———◐ reads SBOMs
Grype + Syft (Anchore)—✓ Grype——✓———✓ Syft
Checkov—◐✓✓◐ Dockerfile————
Gitleaks——✓——————
TruffleHog——✓ verifies live keys——————
OWASP ZAP—————✓———
Prowler——————✓——
Falco———————✓—

Several open-source tools have commercial big siblings: Semgrep’s paid platform adds supply-chain (SCA) and secrets scanning, Trivy comes from Aqua, Checkov from Palo Alto (Bridgecrew), Grype and Syft from Anchore, and ZAP is now maintained with Checkmarx’s backing. Starting with the open-source version and upgrading when you need central management, dashboards and support is a perfectly good strategy.

A free baseline for the orders-api pipeline

If you’re starting from nothing, this combination covers most techniques at zero licence cost:

StageToolCovers
Pre-commit and CIGitleaksSecrets
CISemgrep CE + BanditSAST
CITrivySCA, container, IaC, SBOM
CICheckovExtra IaC policies
StagingZAP baseline scanDAST
Cloud accountProwlerCSPM
KubernetesFalcoRuntime
Repo hygieneDependabot or RenovateAutomated dependency updates

How I’d choose

There’s no single best tool, but there are sensible defaults:

  • Already on GitHub or GitLab? Start with what the platform includes: GitHub Code Security and Secret Protection, or GitLab Ultimate’s scanners. Native integration into pull requests matters more than a few extra rules.
  • Code quality and security together? SonarQube is the familiar choice. See the SonarQube post.
  • Heavy open-source dependency use and a developer-first workflow? Snyk or Mend, both strong on SCA with automated fix PRs.
  • Regulated enterprise with many languages and audit needs? Checkmarx One, Veracode, Fortify or Black Duck, which offer broad language coverage, compliance reporting and DAST in one contract.
  • Cloud-first and container-heavy? A CNAPP (Wiz, Cortex Cloud, Aqua or Defender for Cloud), paired with a code-focused tool for SAST.
  • Tight budget? The free baseline above, and grow from there.

Whatever you pick, two things matter more than the logo: findings must show up where developers already work (pull requests, the IDE), and someone must own triage. A scanner nobody looks at is just an expensive cron job.

Common pitfalls

  • Failing every build on every finding. Developers will route around it. Block on new, high-severity, high-confidence issues, and track the rest.
  • Scanning only at release time. Shift left. A SAST finding on a pull request costs minutes, and the same finding a week before release costs a sprint.
  • Treating SCA alerts as optional. Known vulnerable dependencies are the easiest thing for attackers to exploit, because the exploit is often public.
  • Rotating nothing after a secret leak. Removing the line from git doesn’t revoke the key. Rotate first, then clean up.
  • Scanning images once. New CVEs are published daily. Rescan images in your registry and running clusters continuously.
  • Buying a platform, skipping the process. Tools find issues. Ownership, SLAs and a triage routine fix them.

Conclusion

Each technique looks at a different layer of the same application. SAST, secrets scanning and SCA look at your code and what it pulls in. Container and IaC scanning look at what you build. DAST, IAST, API testing and fuzzing look at how it behaves. CSPM, runtime protection, vulnerability scanning and pen testing look at where it runs. No single tool covers all of it, so map the gaps in your current pipeline against the summary table above, close the cheapest ones first (secrets, SCA, IaC), and add the rest as your risk demands. For the broader picture, see the DevSecOps overview and the DevSecOps interview questions.

References

This post is licensed under CC BY 4.0 by the author.