Page cover

githubGitHub Actions Security Scanning - Walkthrough

Learn Github Actions in action!

Duration: 60-90 minutes Requirements: GitHub account

Step 1: Fork the Repository

  1. Click the Fork button (top-right corner)

  2. Select your GitHub account as destination

  3. Wait for GitHub to create your copy

After forking, you will have your own copy at: https://github.com/YOUR-USERNAME/Github-Actions-Lab

Step 2: Configure Repository Settings

Before we start, you need to check if important settings are configured in your repo.

Make Repository Public

Code scanning (Security tab integration) is only available for public repositories on free GitHub plans. Private repositories would require GitHub Advanced Security (paid enterprise feature).

But forking public repo should already be public. If not check this setting:

  1. Go to Settings (tab at the top of your repository)

  2. Scroll to the bottom → Danger Zone

  3. Click Change visibility → Select Public

  4. Type the repository name to confirm

Step 3: Clone Repository to Your Computer

Open terminal and run:

Replace YOUR-USERNAME with your actual GitHub username.

Create the workflows directory:

Step 4: Exercise 1 - Secret Detection with Gitleaks

What is Gitleaks?

Gitleaks scans your code and Git history for hardcoded secrets like API keys, passwords, and tokens. This is critical because:

  • Secrets committed to Git remain in history forever (even if deleted later)

  • Attackers use automated bots to scan public repos for leaked credentials

  • Leaked AWS keys can be exploited within minutes of being pushed

Create the Workflow File

Create file .github/workflows/gitleaks.yml:

Line-by-Line Explanation

Workflow Name:

This name appears in the GitHub Actions tab. Choose descriptive names so you can identify what each workflow does.

Triggers (when the workflow runs):

Trigger
When it fires
Purpose

push: branches: [main]

Every push to main branch

Scan all new commits for secrets

pull_request: branches: [main]

When PR targeting main is opened/updated

Catch secrets BEFORE they reach main

workflow_dispatch

Manual "Run workflow" button

Testing and on-demand scans

Permissions Block:

This is CRITICAL for Security tab integration:

Permission
What it allows
Why needed

contents: read

Read repository files

Scanner needs to access code

security-events: write

Upload SARIF to Security tab

Results appear in Code scanning

actions: read

Read workflow information

Required for Gitleaks action

Without security-events: write, the workflow runs but results don't appear in Security tab!

Job Definition:

  • scan - internal job ID (can be anything)

  • name: Scan for Secrets - display name in Actions UI

  • runs-on: ubuntu-latest - use latest Ubuntu runner (free for public repos)

Checkout Step:

fetch-depth: 0 is CRUCIAL for Gitleaks! Here's why:

fetch-depth value
What it does
Use case

1 (default)

Only latest commit

Fast, but misses secrets in history

0

Full Git history

Gitleaks can find secrets in ANY past commit

Why full history? Imagine you committed an AWS key 6 months ago, then deleted it. With fetch-depth: 1, Gitleaks only sees current files - the secret isn't there. With fetch-depth: 0, Gitleaks scans all commits and finds the key in the old commit where it was added.

Important: fetch-depth: 0 is NOT the same as "scan everything". It only means the full history is available on the runner. What Gitleaks actually scans depends on the trigger event:

Trigger
What Gitleaks scans
Explanation

push

Only the commits in that specific push

If you push 1 commit that adds the workflow file, Gitleaks only scans that 1 commit (not the history)

pull_request

Only the commits in the PR

Same logic: only new changes

workflow_dispatch (manual)

Full repository history

This is when fetch-depth: 0 truly matters

This means: When you first add the Gitleaks workflow and push it, the scan will likely show "No leaks detected" because the push only contains the new .yml file. The secrets in config.js were committed in earlier commits that are NOT part of this push.

To trigger a full history scan, go to Actions > Gitleaks Secret Scan > click Run workflow (manual trigger). This time Gitleaks will scan every commit in the repository and detect the secrets in config.js.

Gitleaks Action:

Environment variables:

Variable
Value
Purpose

GITHUB_TOKEN

${{ secrets.GITHUB_TOKEN }}

Auto-provided token for GitHub API access and SARIF upload

GITLEAKS_ENABLE_UPLOAD_ARTIFACT

false

Don't save results as downloadable artifact (we use SARIF)

GITLEAKS_ENABLE_SUMMARY

true

Show summary in workflow run page

Commit and Push

Where to Find Results

In Actions Tab (detailed logs):

  1. Go to your repository on GitHub

  2. Click Actions tab

  3. Click "Gitleaks Secret Scan" in the left sidebar

  4. Click on the latest run

  5. Click "Scan for Secrets" job

  6. Expand "Run Gitleaks" step

You will see output like:

Expected Result: The workflow will FAIL (red X) because config.js contains hardcoded secrets.

In Security Tab (aggregated view):

  1. Click Security tab (top menu)

  2. Click Code scanning in the left sidebar

  3. You will see alerts for each detected secret

Each alert shows:

  • Secret type (AWS Access Key, Generic Password, etc.)

  • File and line number where found

  • Commit where secret was introduced

  • Severity level

Note: It may take a minute for results to appear in Security tab after the workflow completes.

Step 5: Exercise 2 - Dependency Scanning with Trivy

What is Trivy?

Trivy is an open-source vulnerability scanner that can scan:

  • Dependencies (npm, pip, Maven, etc.) - finds CVEs in libraries

  • Container images - finds OS and app vulnerabilities

  • Infrastructure as Code - finds misconfigurations in Terraform, CloudFormation

What are CVEs?

CVE (Common Vulnerabilities and Exposures) is a standardized identifier for security vulnerabilities:

  • CVE-2021-44906 = specific vulnerability in minimist package (Prototype Pollution)

  • Each CVE has a severity: CRITICAL, HIGH, MEDIUM, LOW

  • Trivy checks your dependencies against CVE databases

Create the Workflow File

Create file .github/workflows/trivy-deps.yml:

Line-by-Line Explanation

Schedule Trigger:

This runs the workflow automatically every day at 6:00 AM UTC.

Why schedule scans? New CVEs are discovered daily. A library that was "safe" yesterday might have a critical vulnerability disclosed today. Daily scans catch new vulnerabilities without manual intervention.

Cron syntax: minute hour day-of-month month day-of-week

  • 0 6 * * * = minute 0, hour 6, every day, every month, every weekday

  • 0 0 * * 0 = every Sunday at midnight

  • 0 */4 * * * = every 4 hours

Why Generate Lock File?

Trivy needs a lock file (package-lock.json) to scan npm dependencies. Here's why:

File
Contains
Example

package.json

Version ranges

"lodash": "^4.17.0" (any 4.17.x)

package-lock.json

Exact versions

"lodash": "4.17.15" (exactly this)

Without the lock file, Trivy can't know which exact version you're using, so it reports Number of language-specific files: 0 (nothing to scan).

The --package-lock-only flag generates the lock file without downloading packages (faster).

First Trivy Step (Console Output):

Parameter
Value
Meaning

scan-type: 'fs'

filesystem

Scan local files (not container image)

scan-ref: '.'

current directory

Scan everything in repo

scanners: 'vuln'

vulnerabilities only

Don't scan for secrets or misconfigs

severity: 'CRITICAL,HIGH'

filter

Only show serious issues

format: 'table'

human-readable

Easy to read in logs

exit-code: '0'

don't fail

Workflow continues even if vulns found

Why exit-code: '0'? During initial setup, you want to SEE vulnerabilities without failing the build. Later, change to '1' to enforce security gates.

Second Trivy Step (SARIF for Security Tab):

Why run Trivy twice?

  1. First run with format: 'table' - human-readable output in Actions logs

  2. Second run with format: 'sarif' - machine-readable format for Security tab

SARIF (Static Analysis Results Interchange Format) is a standard JSON format that GitHub understands. It enables:

  • Aggregated view in Security tab

  • Filtering by severity, tool, file

  • Tracking when vulnerabilities were introduced/fixed

Upload SARIF Step:

Parameter
Purpose

if: always()

Upload even if previous steps failed

sarif_file

Path to the SARIF file generated by Trivy

category

Label in Security tab (helps filter results by tool)

Why if: always()? If Trivy finds vulnerabilities with exit-code: '1', the workflow fails. Without if: always(), the upload step would be skipped and you'd never see results in Security tab.

Commit and Push

Where to Find Results

In Actions Tab:

  1. ActionsTrivy Dependency Scan → click latest run

  2. Click "Scan Dependencies" job

  3. Expand "Run Trivy (Console Output)" step

You will see a table like:

Understanding the output:

  • Library - which npm package has the vulnerability

  • Vulnerability - CVE identifier (click to see details on nvd.nist.gov)

  • Severity - CRITICAL/HIGH/MEDIUM/LOW

  • Status - fixed means a patched version exists

  • Installed Version - what you currently have

  • Fixed Version - minimum version that fixes the issue

In Security Tab:

  1. SecurityCode scanning

  2. Filter by Tool: "Trivy" or look for category "trivy-dependencies"

Step 6: Exercise 3 - Container Scanning with Trivy

Why Scan Container Images?

Docker images contain not just your code, but also:

  • Base OS packages (Alpine, Debian, Ubuntu)

  • Runtime dependencies

  • System libraries

Each of these can have vulnerabilities. A "secure" app in a vulnerable container is still vulnerable.

Create the Workflow File

Create file .github/workflows/trivy-container.yml:

Line-by-Line Explanation

Path-Based Triggers:

This workflow ONLY runs when:

  • Push is to main branch, AND

  • The push changes Dockerfile OR any file matching package*.json

Why use paths? Container builds are slow (minutes). No point rebuilding if you only changed README.md. This saves CI minutes and speeds up feedback.

Docker Buildx Setup:

Buildx is Docker's extended build capabilities. It enables:

  • Multi-platform builds (arm64, amd64)

  • Better caching

  • More efficient builds

Build the Image:

Parameter
Value
Purpose

context: .

Current directory

Where Dockerfile is located

push: false

Don't push to registry

We only scan, not deploy

load: true

Load into Docker daemon

So Trivy can scan it

tags: app:${{ github.sha }}

Unique tag

Uses commit hash for identification

Container Scan Parameters:

Parameter
Value
Purpose

scan-type: 'image'

Docker image

Different from fs (filesystem)

image-ref

The image we just built

Must match the tag from build step

scanners: 'vuln,secret'

Both scans

Check for vulns AND secrets in image

timeout: '10m'

10 minutes

Container scans take longer

Why scanners: 'vuln,secret'? Container images can accidentally include:

  • Secrets baked into environment variables

  • .env files copied during build

  • SSH keys in home directories

Commit and Push

circle-info

Note: Since we use path-based triggers, you may need to manually run the workflow from Actions tab for the first test.

Where to Find the Results

After the workflow completes, your results appear in two places:

1. Console Output (Actions tab)

Go to Actions > click the "Build and Scan Image" run > expand the "Scan image (Console Output)" step. You will see a table listing every package inside the Docker image along with the number of vulnerabilities and secrets found. Pay attention to:

  • The base image (e.g. Alpine) often has the most vulnerabilities because it includes OS-level packages

  • The Secrets column shows whether Trivy found any credentials baked into the image

2. Security Tab (Code Scanning alerts)

Go to Security > Code scanning. New alerts from the container scan appear alongside the dependency scan alerts. All of them show "Trivy" as the tool.

How to tell container alerts apart from dependency alerts? Look at the file path under each alert:

Path pattern
Source

/app/..., /usr/..., library/...

Container scan -- these are paths inside the Docker image

package-lock.json, package.json (no container prefix)

Dependency scan -- these are files from your repository

Container scan paths start with the image's internal directory structure (like /app/ from the Dockerfile's WORKDIR). GitHub will show "Preview unavailable" for these because the file does not exist in the repository -- it only exists inside the built Docker image.

Step 7: Exercise 4 - Infrastructure as Code Scanning

What is IaC Scanning?

Infrastructure as Code (IaC) tools like Terraform let you define cloud resources in code. But misconfigurations can create security holes:

  • Public S3 buckets → data breaches

  • Open SSH (0.0.0.0/0) → brute force attacks

  • Unencrypted databases → data exposure

Trivy can scan Terraform, CloudFormation, and Kubernetes manifests to find these issues BEFORE deployment.

Create the Workflow File

Create file .github/workflows/trivy-iac.yml:

Line-by-Line Explanation

IaC Scan Parameters:

Parameter
Value
Purpose

scan-type: 'config'

Configuration scan

For IaC files (not dependencies)

scanners: 'misconfig'

Misconfigurations

Look for security issues in config

severity: 'CRITICAL,HIGH,MEDIUM'

Include MEDIUM

IaC issues often MEDIUM but still dangerous

Why include MEDIUM for IaC? Many infrastructure misconfigurations are rated MEDIUM individually but create serious risks when combined:

Issue
Severity
Combined Risk

No S3 access logging

MEDIUM

Can't detect breaches

No VPC flow logs

MEDIUM

Can't trace attacks

Combined:

Attacker exfiltrates data undetected

Commit and Push

Where to Find IaC Results

In Actions Tab:

Expand "Scan IaC (Console Output)" to see:

Step 8: Understanding the Results

Actions Tab Structure

Security Tab Structure

Workflow Status Icons

Icon
Meaning
Action

Yellow dot

Running

Wait for completion

Green check

All steps passed

No blocking issues found

Red X

Failed

Click to see which step failed

Gray circle

Skipped or cancelled

Check workflow conditions

Step 9: Fixing the Vulnerabilities

Now that you have identified all security issues, let's fix them.

Fix 1: Vulnerable Dependencies

Edit package.json to use secure versions:

How to find safe versions:

  1. Check Trivy output - shows "Fixed Version" column

  2. Visit npmjs.comarrow-up-right - each package page shows versions

  3. Run npm audit locally - gives recommendations

Delete the old lock file so it regenerates:

Fix 2: Hardcoded Secrets

Replace config.js with environment variable references:

Why environment variables?

  • Secrets stay out of Git history

  • Different values for dev/staging/production

  • Can be managed via GitHub Secrets, AWS Secrets Manager, etc.

Fix 3: Terraform Misconfigurations

Replace terraform/main.tf:

Commit and Verify

Go to the Actions tab and wait for all workflows to complete. They should now pass (green checkmarks) because:

  • No more hardcoded secrets in config.js

  • Dependencies updated to secure versions

  • Terraform misconfigurations fixed

Summary

You have completed:

Exercise
Tool
What it Scans
Key Parameters

1

Gitleaks

Secrets in code and Git history

fetch-depth: 0

2

Trivy (fs)

npm dependency CVEs

scan-type: 'fs', scanners: 'vuln'

3

Trivy (image)

Container vulnerabilities

scan-type: 'image'

4

Trivy (config)

IaC misconfigurations

scan-type: 'config', scanners: 'misconfig'

Resources

Last updated