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:
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:
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:
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:
npm auditnpm 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:
- name: Audit dependencies
run: npm audit --audit-level=highNow 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:
- uses: github/codeql-action/init@v4
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@v4I also added Semgrep:
- name: Install Semgrep
run: pip install semgrep
- name: Run Semgrep
run: semgrep scan --config=autoDuring 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:
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:
ZAP takes my OpenAPI spec and starts testing the endpoints.
And here a huge difference from SAST showed up.
It can make requests like:
GET /notes/search?q='
Authorization: Bearer <token>and observe what happens.
In one of the scans, that request resulted in:
500 Internal Server ErrorZAP then raised an alert for a possible:
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 UnauthorizedZAP 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:
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:
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:
A found secret can mean that credential already needs to be treated as compromised.
In GitHub Actions it looked something like this:
- 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:
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:
allow_issue_writing: trueThat 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:
GitHub parses that file and turns the results into code scanning alerts inside the repository.
So we started separating two important responsibilities:
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:
If a vulnerability violates the policy:
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:
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:
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:
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.