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:

  1. Run secret scanning and SAST on pull requests
  2. Run dependency and container scans on pushes to main
  3. Run DAST on a staging or preview environment
  4. 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_request runs checks before code is merged
  • push to main runs 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: read allows it to read the repository
  • security-events: write allows 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: 0 gives 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:

  1. Check out the code
  2. Initialize CodeQL
  3. Build the project automatically
  4. 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 sast and dependencies jobs 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 main or 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:

  1. Begin with secret scanning and SAST on every pull request
  2. Add dependency and IaC scanning on pushes to main
  3. Run DAST only in staging or preview environments
  4. Fail builds only on high and critical issues at first
  5. Upload SARIF findings to GitHub for centralized visibility
  6. Add scheduled weekly deep scans
  7. Review false positives and tune rules regularly
  8. 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.