Back
9/2/20268 min read

SCA, SAST and DAST: how I added real security to my pipeline

For a long time, CI/CD to me was just build, test, deploy. None of those steps tell you if the code is secure. I implemented SCA, SAST and DAST in a real pipeline and learned in practice what each one catches — and what none of them replace.

For a long time, when I thought about CI/CD, I basically thought of this:

Push
Build
Tests
Deploy

If it compiled and the tests passed, great.

But there's a problem: none of these steps necessarily tell you whether the code is secure.

An application can compile, pass every test, and still be vulnerable to SQL Injection, use a dependency with a known CVE, or even have a secret committed to the repository.

While studying application security, I started building a DevSecOps pipeline in a vulnerable project I created just for that.

And three concepts kept coming up:

SCA, SAST, and DAST.

They seem to do the same thing, but they analyze completely different parts of the application.

First: what's the difference between SCA, SAST, and DAST?

The simplest way I found to think about them is:

SCA analyzes what you installed. SAST analyzes what you wrote. DAST analyzes how your application behaves when someone tries to break it.

In a pipeline, we can picture it like this:

Application
SCA
Dependencies
SAST
Code
DAST
Running app

They're not competitors.

One finds problems the other often can't even see.

1. SCA — Software Composition Analysis

Today, almost no application is written from scratch.

In my Node.js backend, for example, I have dependencies like Express, JWT libraries, PostgreSQL access, and many others.

The problem is:

My code
express
jsonwebtoken
drizzle
dozens/hundreds of transitive dependencies

A vulnerability doesn't have to be in the code I wrote.

It can be in a library I installed.

That's exactly where SCA comes in.

In the project, I started with:

bash
npm audit

npm audit compares the project's dependency tree against known vulnerability advisories.

That's how I found, for example, a vulnerable version of jsonwebtoken.

In other words: I could review my entire codebase and never find that problem.

The problem was in the composition of the software.

In the pipeline, I added:

yaml
- name: Audit dependencies
  run: npm audit --audit-level=high

Now CI also checks dependencies.

And --audit-level=high starts to introduce another important concept: security gate.

I can decide that minor vulnerabilities get reported, while High or Critical vulnerabilities block that pipeline run from passing.

2. SAST — Static Application Security Testing

After dependencies comes the code we wrote ourselves.

That's when I added SAST.

Its main feature is that SAST can look for potentially vulnerable patterns without needing to attack a running application.

I used two tools:

  • CodeQL
  • Semgrep

CodeQL is particularly interesting because it turns the code into a queryable representation and looks for flows that match security issues.

In the pipeline:

yaml
- uses: github/codeql-action/init@v4
  with:
    languages: javascript-typescript
    queries: security-extended
 
- uses: github/codeql-action/analyze@v4

I also added Semgrep:

yaml
- name: Install Semgrep
  run: pip install semgrep
 
- name: Run Semgrep
  run: semgrep scan --config=auto

During testing, CodeQL managed to flag problems that would easily go unnoticed in a quick review, like log injection.

The idea here is different from SCA:

SCA
package.json
package-lock.json
vulnerable dependency
SAST
source code
vulnerable flow/pattern

But there's still an important limitation.

We're analyzing the code.

We're not necessarily observing the application's actual behavior while it's running.

That's where the part I found most interesting comes in.

3. DAST — Dynamic Application Security Testing

With DAST, I stopped just analyzing files.

I spun up the application and let a tool try to attack it.

I used OWASP ZAP.

My pipeline started doing roughly this:

GitHub Actions
spins up PostgreSQL
runs migrations
spins up backend
creates test user
logs in
captures JWT
runs OWASP ZAP

ZAP takes my OpenAPI spec and starts testing the endpoints.

And here a huge difference from SAST showed up.

It can make requests like:

http
GET /notes/search?q='
Authorization: Bearer <token>

and observe what happens.

In one of the scans, that request resulted in:

500 Internal Server Error

ZAP then raised an alert for a possible:

SQL Injection
Risk:High
CWE-89
Parameter:q

That doesn't automatically mean every scanner finding is a confirmed vulnerability.

In this case, for example, the reported confidence was low. The next step is analyzing the implementation, reproducing the behavior, and determining whether SQL Injection actually exists.

This part is fundamental:

A scanner finds hints. The developer still has to understand the finding.

A problem I ran into implementing DAST

At first, the scan seemed to work.

Except almost all requests were coming back:

401 Unauthorized

ZAP was "testing" my API, but couldn't actually reach the endpoints I wanted it to test.

I had to create a specific user for DAST during the pipeline run:

POST /auth/register
POST /auth/login
JWT
Authorization: Bearer <JWT>
OWASP ZAP

Then another problem showed up.

Rate limiting.

I had added rate limits precisely as a security measure, but a scanner sends a huge number of requests.

Result:

ZAP
request ×4
429 Too Many Requests

The scanner lost coverage again.

This was an important lesson: it's not enough to drop a security tool into CI and assume it's actually testing your application correctly.

A green scan with broken authentication can be much worse than a red one.

It gives a false sense of security.

Secret scanning also joined the pipeline

Besides the three main ones, I also added Gitleaks.

The goal is different:

Commit → Gitleaks
API key?
Token?
Password?
Credential?

A found secret can mean that credential already needs to be treated as compromised.

In GitHub Actions it looked something like this:

yaml
- uses: gitleaks/gitleaks-action@v3
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

That complements the other scanners.

How the pipeline turned out

After putting it all together:

Pull RequestGITHUB ACTIONSBuild✓SCAnpm auditSASTCodeQL+SemgrepSecret ScanGitleaksDASTOWASP ZAP

But one thing was still missing.

What happens when a tool finds a vulnerability?

Findings also need somewhere to go

Another thing I implemented was making sure scanner results didn't just get lost in the GitHub Actions log.

With OWASP ZAP, for example, you can let the Action itself log results in a GitHub Issue:

yaml
allow_issue_writing: true

That way, the finding doesn't only exist during that one pipeline run. It becomes something that can be tracked and handled inside the repository itself.

With CodeQL, the integration is even more direct.

Analysis results get sent to GitHub's code scanning and show up under Security and quality, where you can see alerts, severity, affected file, and track the fix.

Other static analysis tools can also integrate their results with GitHub through the SARIF format:

Scanner
SARIF
GitHub Code Scanning
Security and quality

GitHub parses that file and turns the results into code scanning alerts inside the repository.

So we started separating two important responsibilities:

Find vulnerability → Scanner
Report
Issue / Security and quality
Block
Pipeline fails

Because reporting a problem and preventing it from reaching production are different things.

That's where security gates come in.

Finding a vulnerability isn't enough

A scanner can simply generate a report and let the pipeline continue.

To turn that into an actual security policy, we need to define security gates.

For example:

Informationalreport
Lowreport
Mediumproject policy
Highblock
Criticalblock

If a vulnerability violates the policy:

Scanner
Finding
Security Policy
exit code != 0
GitHub Action ❌
PR blocked

And there's an important distinction here.

A failing pipeline doesn't automatically mean nobody can merge.

The repository also needs to require that status check through branch protection rules.

So the full flow becomes:

Pull RequestSecurity PipelinePASSFAILMerge allowedMerge blocked

This is where security stops being just a report nobody reads and starts being part of the development process.

None of these tools replace the others

This was probably the biggest conclusion I got out of building all this.

Imagine I install a vulnerable library:

SCA can find it.

Now imagine I write a SQL query concatenating user input:

SAST can find it.

Now imagine a given payload causes unexpected behavior only when the database, auth, middleware, and application are all working together:

DAST can find it.

And imagine I have an authorization flaw where:

User A
GET /notes/10
Note belongs to user B

Maybe no scanner reliably finds that.

That's where human review, specific tests, and Threat Modeling come in.

Security isn't:

"I installed ZAP, so now my application is secure."

It's much closer to:

DependenciesSCA
CodeSAST
SecretsSecret Scanning
Running appDAST
ArchitectureThreat Modeling
human review

Each layer covers a different part of the problem.

What changed in how I look at CI/CD

Before, I used to think:

"The pipeline checks whether my code works."

Now I prefer to think:

"The pipeline checks some of the conditions I need to trust that this code can move forward."

A passing build doesn't mean correct code.

A passing test doesn't mean secure code.

A passing SAST doesn't mean a secure application.

A passing DAST doesn't mean a secure application either.

But when we combine several of these techniques, we significantly increase our ability to find problems before they reach production.

And, to me, that was the most interesting part of studying DevSecOps in practice:

security stops being a step done at the end of the project and becomes part of the normal development cycle.