How to Add OWASP Security Checks to GitHub Actions
Security works best when it is part of your normal development flow. A good way to do that is to place security checks directly inside your CI pipeline.
GitHub Actions is a strong choice for this because it runs automatically on pull requests, pushes, and schedules. That means every code change can get security feedback before it reaches production.
In this tutorial, you will learn how to encode OWASP-style secure coding checks into GitHub Actions. We will build a practical pipeline that covers:
- Secret scanning
- Static application security testing (SAST)
- Dependency scanning
- Infrastructure as Code (IaC) scanning
- Dynamic application security testing (DAST)
By the end, you will have a simple but useful security workflow that your team can expand over time.
Why Put OWASP Checks in GitHub Actions?
OWASP provides guidance on secure coding, secure CI/CD, and application testing. The main idea is simple:
- Catch security issues early
- Make checks repeatable
- Give developers fast feedback
- Block only the most serious risks
- Keep results visible for triage and cleanup
Instead of relying on manual reviews alone, you can automate common checks inside your pipeline.
What Each Type of Scan Does
Before we build the workflow, let us look at the major pieces.
1. Secret scanning
Secret scanning looks for things that should never be committed to source control, such as:
- API keys
- Access tokens
- Database passwords
- Cloud credentials
This is often the fastest security win because leaked secrets can cause immediate damage.
2. SAST
SAST stands for Static Application Security Testing.
It looks at your source code without running the app. It is useful for finding patterns such as:
- SQL injection risks
- Unsafe input handling
- Hardcoded credentials
- Insecure coding patterns
Tools like CodeQL are commonly used for this.
3. Dependency scanning
Modern applications depend on third-party packages. If one of those packages has a known vulnerability, your app may be exposed even if your own code is clean.
Dependency scanning helps you find:
- Vulnerable npm packages
- Outdated libraries
- Packages with known CVEs
4. IaC scanning
IaC means Infrastructure as Code. Examples include:
- Terraform
- CloudFormation
- Kubernetes YAML
IaC scanning checks for risky infrastructure settings, such as:
- Publicly exposed storage
- Weak network rules
- Missing encryption
- Overly broad IAM permissions
5. DAST
DAST stands for Dynamic Application Security Testing.
Unlike SAST, DAST runs against a live application. It checks the app from the outside, like an attacker or tester would.
It is useful for detecting issues such as:
- Missing security headers
- Weak authentication flows
- Exposed endpoints
- Common web security weaknesses
OWASP ZAP is one of the most common tools for this.
A Practical Security Workflow
A simple and effective pattern looks like this:
- Run secret scanning and SAST on pull requests
- Run dependency and container scans on pushes to
main - Run DAST on a staging or preview environment
- Upload results into GitHub Security for visibility
This pattern balances speed and coverage.
- Pull request checks stay fast
- Deep scans happen after merge
- DAST runs only where a live app is available
- High-risk issues can fail the pipeline
Starter GitHub Actions Workflow
Below is a simple workflow you can adapt for a web application.
name: security-pipeline
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm audit --audit-level=high
iac:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
output_format: sarif
output_file_path: checkov.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarif
dast:
needs: [sast, dependencies]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d
- uses: zaproxy/action-baseline@v0.11.0
with:
target: 'http://localhost:3000'
Understanding the Workflow Step by Step
Let us break this down so it is easier to understand.
Trigger section
on:
pull_request:
push:
branches: [main]
This tells GitHub Actions when to run the workflow.
pull_requestruns checks before code is mergedpushtomainruns checks after merge on the main branch
This is useful because not every scan needs to run at every stage.
Permissions
permissions:
contents: read
security-events: write
This gives the workflow the minimum access it needs.
contents: readallows it to read the repositorysecurity-events: writeallows it to upload SARIF security results to GitHub
Keeping permissions small is an important security habit.
Secret scanning job
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
This job checks the repository for secrets using Gitleaks.
Important detail:
fetch-depth: 0gives the scanner more Git history to inspect
That helps catch secrets that may not be in the latest commit only.
SAST job
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
This job uses CodeQL to scan JavaScript code.
The flow is:
- Check out the code
- Initialize CodeQL
- Build the project automatically
- Analyze the code for issues
If your repository uses another language, update the languages field.
Example:
with:
languages: python
Or for multiple languages:
with:
languages: javascript,python
Dependency scanning job
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm audit --audit-level=high
This job installs dependencies and checks them for known vulnerabilities.
The important part is:
npm audit --audit-level=high
That means the job fails only when high-severity or critical issues are found.
This is a good default because it avoids blocking the team on every minor warning.
IaC scanning job
iac:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
output_format: sarif
output_file_path: checkov.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarif
This job scans Terraform code using Checkov.
It also exports the findings in SARIF format, which GitHub understands for security reporting.
That gives you a better experience because findings show up in GitHub Security views instead of only in raw log output.
DAST job
dast:
needs: [sast, dependencies]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d
- uses: zaproxy/action-baseline@v0.11.0
with:
target: 'http://localhost:3000'
This job starts the application and runs an OWASP ZAP baseline scan.
The needs line means:
- Run DAST only after the
sastanddependenciesjobs complete
The baseline scan is a good starting point because it is lighter and easier to add than a full active scan.
How This Maps to OWASP Secure Coding Goals
You can think of each OWASP concern as something you connect to a tool or policy.
Here is a simple mental map:
- Input validation and injection risks → SAST
- Secrets handling → Secret scanning
- Unsafe third-party packages → Dependency scanning
- Insecure cloud or infrastructure config → IaC scanning
- Runtime web issues → DAST
This approach helps teams turn abstract secure coding guidance into concrete automated checks.
A Better Version for Real Teams
The starter workflow is useful, but many teams will want to improve it.
Here is a stronger version of the strategy.
On pull requests
Run checks that are fast and helpful to developers:
- Secret scanning
- SAST
- Basic linting and tests
Goal: fast feedback before merge.
On push to main
Run deeper checks that may take longer:
- Dependency scanning
- Container scanning
- IaC scanning
- Full CodeQL analysis
Goal: stronger enforcement on the main branch.
On staging or preview
Run DAST only against a safe environment:
- Staging deployment
- Ephemeral preview app
- Test environment with seeded data
Goal: find runtime security issues without impacting production.
On a schedule
Add weekly or nightly deep scans:
- Full dependency review
- Full ZAP scan
- Container image rescans
- License and policy checks
Goal: catch issues that appear after the original merge.
Example: Failing Only on High-Risk Findings
One of the biggest mistakes teams make is failing the pipeline on everything.
That sounds strict, but it often creates noise and frustration.
A better policy is:
- Fail on critical and high
- Warn on medium
- Log or track low
- Require manual approval for exceptions
This keeps the signal strong.
For example, in npm audit:
- run: npm audit --audit-level=high
For other tools, look for similar severity thresholds.
Uploading Results to GitHub Security
GitHub can display security findings when tools output SARIF files.
This is valuable because it gives you:
- Central visibility
- Better triage
- Historical tracking
- A cleaner workflow for developers and security teams
Example SARIF upload step:
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarif
You can use the same pattern for other supported tools.
Adding Container Scanning
Many modern apps run in containers, so container image scanning is often worth adding.
Here is a simple example using Trivy:
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:latest .
- uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:latest
format: sarif
output: trivy-results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
This is especially useful when your app depends on base images or operating system packages.
Adding Scheduled Security Scans
You can also run full scans regularly with a schedule.
Example:
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '0 2 * * 1'
This runs the workflow every Monday at 02:00 UTC.
Scheduled scans are useful because vulnerabilities can appear in dependencies even when your code has not changed.
Tips for Making the Pipeline Fast
Security checks should be helpful, not painful. Here are some practical ways to keep the workflow fast:
- Run fast checks on pull requests
- Move heavier scans to
mainor scheduled jobs - Use caching where possible
- Scan only changed directories when supported
- Use baseline DAST before full active DAST
- Keep severity thresholds realistic
Fast feedback helps developers fix issues while the code is still fresh in their minds.
Common Mistakes to Avoid
Running DAST against production
DAST can be noisy and sometimes disruptive. Use it on staging or preview environments instead.
Failing on low-value findings
Too many blockers lead to alert fatigue. Focus on high-risk issues first.
Giving workflows too many permissions
Use least privilege. Do not grant broad write access unless it is truly required.
Forgetting visibility
A scan that runs but produces logs nobody reads is much less useful than one that uploads findings into GitHub Security.
Treating tools as complete security coverage
Scanners help a lot, but they do not replace secure design, code review, threat modeling, and testing.
Key Considerations
When you design a GitHub Actions security pipeline, keep these points in mind:
- Speed matters: slow pipelines reduce adoption
- Severity matters: fail only when risk is meaningful
- Environment matters: DAST needs a running app
- Visibility matters: upload findings where teams can actually review them
- Maintenance matters: tools, rules, and dependencies need regular updates
- Context matters: not every finding should be handled the same way
A good pipeline is not just strict. It is useful, understandable, and sustainable.
Recommendations
Here are practical recommendations for most teams starting out:
- Begin with secret scanning and SAST on every pull request
- Add dependency and IaC scanning on pushes to
main - Run DAST only in staging or preview environments
- Fail builds only on high and critical issues at first
- Upload SARIF findings to GitHub for centralized visibility
- Add scheduled weekly deep scans
- Review false positives and tune rules regularly
- Treat the workflow as code and improve it over time
For small teams, even a simple version of this setup is much better than having no automated security checks at all.
A More Complete Example
Here is an expanded workflow that includes a schedule and container scanning.
name: security-pipeline
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '0 2 * * 1'
permissions:
contents: read
security-events: write
jobs:
secrets:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
sast:
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
dependencies:
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm audit --audit-level=high
iac:
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
output_format: sarif
output_file_path: checkov.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarif
container-scan:
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:latest .
- uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:latest
format: sarif
output: trivy-results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
dast:
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
needs: [sast, dependencies]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d
- uses: zaproxy/action-baseline@v0.11.0
with:
target: 'http://localhost:3000'
Final Thoughts
GitHub Actions gives you a practical way to turn OWASP security guidance into automated guardrails.
Start small. Add the checks that give the biggest value first:
- Secret scanning
- SAST
- Dependency scanning
Then grow into:
- IaC scanning
- Container scanning
- DAST
- Scheduled deep scans
The best setup for most teams is simple: fast checks on pull requests, deeper checks on merges, and regular full scans on a schedule.
That approach keeps security visible, consistent, and mostly automated without slowing developers down too much.