AI-Powered DevSecOps Pipeline

Build a CI/CD pipeline that scans containers for vulnerabilities with AI analysis.

Introduction

30 Second Summary

Imagine pushing code that accidentally exposes a security vulnerability. Without automated checks, that flaw could reach production before anyone notices.

In this project, you will build a DevSecOps pipeline that automatically scans every pull request for container vulnerabilities, Dockerfile misconfigurations, and hardcoded secrets. An AI-powered analysis stage reads those findings and posts human-readable remediation advice directly on the pull request.

What You'll Build

A GitHub Actions workflow that builds a Docker image, runs Trivy security scans, and feeds the results to Google Gemini for AI-powered security analysis.

By the end of this project, you'll have:

  • A fully automated GitHub Actions pipeline that triggers security scans on every pull request.
  • Trivy scanning for container vulnerabilities, Dockerfile misconfigurations, and hardcoded secrets, with results posted as structured PR comments.
  • An AI analysis stage that reads Trivy's security findings and generates human-readable remediation advice posted directly on the pull request.
  • Secret Mission: Add a security gate that blocks PR merges when CRITICAL vulnerabilities are found and an AI-generated security posture score.

Are there any prerequisites?

This project assumes basic familiarity with Docker, Git, and Python. You'll also need a GitHub account and a Google AI Studio account (for a free Gemini API key).

Before We Start

Before We Start...

Set Up the Project Repository

Every automated pipeline needs a home. Before you can scan containers for vulnerabilities or generate AI-powered security reports, you need a repository that holds your application code, your scanning configuration, and your workflow definitions.

In this step, you will create the GitHub repository for the project, set up its folder structure, and securely store the Gemini API key that the pipeline will use later.

In this step, get ready to:
  • Create and clone the GitHub repository.
  • Create the sample Flask application files.
  • Add your Gemini API key as a repository secret.
Create and clone the GitHub repository

The pipeline you are building runs on GitHub Actions. It needs a public repository so the free Actions minutes apply.

  • Open your browser and go to github.com/new.
  • In the Repository name field, type devsecops-ai-pipeline.
  • Set the visibility to Public.
  • Leave all other options unchecked (no README, no .gitignore, no license).
  • Click Create repository.

Why leave it empty?

You will push all files from your local machine. Starting with an empty repository avoids merge conflicts from auto-generated files like a README.

  • Open a terminal by pressing Ctrl+Alt+T.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the repository and move into it by running these commands:
git clone https://github.com/[[GITHUB_USERNAME="your-github-username"]]/devsecops-ai-pipeline.git
cd devsecops-ai-pipeline
  • Create the project directory structure by running this command:
mkdir -p scripts .github/workflows
  • Confirm the directories exist by running:
ls -la

You should see scripts and .github listed in the output.

  • Open the project in VS Code by running:
code .

Don't see the directories?

Make sure you ran mkdir -p scripts .github/workflows from inside the devsecops-ai-pipeline folder. Run pwd to confirm you are in ~/Desktop/devsecops-ai-pipeline.

help me troubleshoot my directory structure

Create the sample application files

The pipeline needs something to scan. You will create a minimal Flask web application that acts as the target for container vulnerability scanning.

  • In VS Code, create a new file called app.py in the root of the devsecops-ai-pipeline folder.
  • Paste the following code into app.py:
from flask import Flask, jsonify
import os

app = Flask(__name__)


@app.route("/")
def home():
    return jsonify({
        "service": "devsecops-demo",
        "status": "healthy",
        "version": "1.0.0"
    })


@app.route("/health")
def health():
    return jsonify({"status": "ok"}), 200


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(host="0.0.0.0", port=port)

What does this code do?

  • Creates a Flask web server with two endpoints: / returns service info and /health returns a simple OK status.
  • The app itself is not the focus of this project. It exists as a realistic scan target that Trivy will inspect for vulnerabilities inside its container.
  • Create a new file called requirements.txt in the root folder.
  • Paste the following into requirements.txt:
flask==3.1.1
gunicorn==23.0.0

Why pin versions?

Pinning exact versions (flask==3.1.1) ensures reproducible builds. Without pins, a new release could introduce vulnerabilities between scans, making your results inconsistent.

  • Create a new file called .dockerignore in the root folder.
  • Paste the following into .dockerignore:
.git
.github
scripts
*.md
.trivyignore
.dockerignore

What does .dockerignore do?

It tells Docker which files to exclude when building an image. Keeping the .git directory and workflow files out of the image reduces its size and avoids leaking repository metadata into the container.

  • Save all three files (app.py, requirements.txt, .dockerignore).
  • In the VS Code Explorer sidebar on the left, confirm you can see all three files listed at the root level.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is each file for comparison:

from flask import Flask, jsonify
import os

app = Flask(__name__)


@app.route("/")
def home():
    return jsonify({
        "service": "devsecops-demo",
        "status": "healthy",
        "version": "1.0.0"
    })


@app.route("/health")
def health():
    return jsonify({"status": "ok"}), 200


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(host="0.0.0.0", port=port)
flask==3.1.1
gunicorn==23.0.0
.git
.github
scripts
*.md
.trivyignore
.dockerignore
Add the Gemini API key as a GitHub Actions secret

The AI analysis job in your pipeline needs access to the Gemini API. Storing the key as a GitHub Actions secret keeps it encrypted and out of your code.

  • In your browser, go to aistudio.google.com/apikey.
  • Click Create API Key and select a Google Cloud project (or use the default one).
  • Copy the generated key. You will only see it once, so keep the tab open until you paste it.

What is Google AI Studio?

Google AI Studio is a web interface for managing Gemini API keys and testing prompts. Each key is tied to a Google Cloud project and gives you access to the free tier of the Gemini API.

  • In a new browser tab, navigate to your devsecops-ai-pipeline repository on GitHub.
  • Click the Settings tab at the top of the repository page.
  • In the left sidebar under Security, click Secrets and variables, then click Actions.
  • Click the Secrets tab.
  • Click New repository secret.
  • In the Name field, type GEMINI_API_KEY.
  • In the Secret field, paste the API key you copied from Google AI Studio.
  • Click Add secret.

You should see GEMINI_API_KEY listed on the repository secrets page with an Updated just now timestamp.

Why a repository secret?

GitHub Actions secrets are encrypted at rest and only exposed to workflow runs as environment variables. Your key never appears in logs or in your code. The workflow will reference it as ${{ secrets.GEMINI_API_KEY }}.

Don't see the Settings tab?

The Settings tab only appears if you are the repository owner. Confirm you are signed in to the GitHub account that created the repository.

help me find the repository Settings tab on GitHub

Your repository is set up with the right structure, sample app files, and a securely stored API key. Next up, you will build the Dockerfiles that define how this application gets packaged into a container.

Build the Dockerfiles

Your project repository is set up with the Flask app and all the supporting files. Now it's time to containerize the application with Docker.

You'll create two Dockerfiles in this step. One is deliberately insecure to give Trivy something meaningful to detect, and the other follows container security best practices.

In this step, get ready to:
  • Create a vulnerable Dockerfile with deliberate security issues.
  • Build a hardened Dockerfile and verify it with Docker.
  • Set up a .trivyignore file for managing accepted risks.
Create the vulnerable Dockerfile

Security scanners need something to find. This Dockerfile deliberately includes common security mistakes that Trivy will flag during scans.

  • In VS Code, create a new file called Dockerfile.vulnerable in the root of your devsecops-ai-pipeline folder.
  • Define the intentionally insecure container by pasting this code into Dockerfile.vulnerable:
FROM python:3.12

RUN apt-get update && apt-get install -y curl wget netcat-openbsd

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

What does this code do?

FROM python:3.12 uses the full Python image, which includes hundreds of pre-installed packages you don't need. This creates a large attack surface with many potential vulnerabilities.

RUN apt-get install -y curl wget netcat-openbsd installs networking tools that attackers commonly exploit once inside a container.

The container runs as root (there's no USER directive), which means any exploit gives full container access. The pip install also runs without --no-cache-dir, leaving cached package data inside the image.

  • Save Dockerfile.vulnerable.
  • You should see Dockerfile.vulnerable appear in the VS Code Explorer sidebar.

Don't see Dockerfile.vulnerable in the sidebar?

Make sure you created the file in the root of your devsecops-ai-pipeline folder, not inside a subdirectory like scripts/.

Help me find where my Dockerfile.vulnerable ended up.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and move on to the next Dockerfile.

⨯ I'd like to double check the full code

FROM python:3.12

RUN apt-get update && apt-get install -y curl wget netcat-openbsd

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]
Create the hardened Dockerfile

This Dockerfile follows container security best practices. When you compare its scan results against the vulnerable version later, you'll see a dramatic reduction in findings.

  • Create a new file called Dockerfile (no extension) in the root of your devsecops-ai-pipeline folder.
  • Add the secure container configuration by pasting this code into Dockerfile:
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

What does this code do?

python:3.12-slim is a minimal base image with far fewer pre-installed packages. Fewer packages means fewer potential vulnerabilities for Trivy to find.

--no-cache-dir tells pip not to store downloaded packages, keeping the final image smaller and reducing unnecessary files.

gunicorn is a production-grade WSGI server, replacing the development-only Flask server that the vulnerable Dockerfile uses.

  • Save Dockerfile.
  • Verify the hardened image builds successfully by running this command in your terminal:
docker build -t devsecops-demo:test .

You should see Docker pull the python:3.12-slim base image, install your Python dependencies, and finish without errors.

Build failing?

Make sure Docker is running on your machine. You can check by running docker info in your terminal.

If you see a COPY failed error, confirm that requirements.txt and app.py exist in the same folder as your Dockerfile.

Help me troubleshoot my Docker build failure.

✔️ Awesome, I've got everything!

Great. Double check you've saved Dockerfile and confirm the build completed.

⨯ I'd like to double check the full code

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Why two Dockerfiles?

The vulnerable Dockerfile exists purely to give Trivy interesting findings to report. Your GitHub Actions pipeline will build it on every PR so the AI analyst always has real vulnerabilities to analyze.

The hardened Dockerfile is what you'd actually deploy. In Step 7, you'll switch the pipeline to use it and compare the before/after results.

Create a .trivyignore file for accepted risks

In real projects, some vulnerability findings are acceptable risks. Teams document these decisions in a .trivyignore file so Trivy skips them during scans.

  • Create a new file called .trivyignore in the root of your devsecops-ai-pipeline folder.
  • Add the following template to .trivyignore:
# Accepted risks - document reason for each
# Example: CVE-XXXX-YYYY - mitigated by network policy, no external exposure

What is a .trivyignore file?

When Trivy scans your project, it checks this file for CVE IDs to skip. Teams list one CVE per line with a comment explaining why the risk is accepted.

For now, the file only contains example comments. In a real project, you'd add specific CVE IDs as your team reviews and accepts findings.

  • Save .trivyignore.
  • Confirm all your new files are in place by running:
ls -a

You should see Dockerfile, Dockerfile.vulnerable, .trivyignore, app.py, requirements.txt, and .dockerignore listed in the output.

Don't see all the files?

Files starting with a dot (like .trivyignore) are hidden by default. Make sure you used ls -a (not just ls) to show them.

If a file is missing, check that you created it in the root of devsecops-ai-pipeline, not inside a subfolder.

Help me figure out why some files are missing from my project.

Your Dockerfiles are ready. Next, you'll create the GitHub Actions workflow that builds the vulnerable image and runs Trivy security scans against it.

Create the Trivy Security Scanning Workflow

Your Dockerfiles are ready. The vulnerable one has deliberate security issues waiting to be caught, and the hardened one shows best practices.

Now you need something to automatically scan those images on every pull request. In this step, you'll create a GitHub Actions workflow that builds the vulnerable Dockerfile and runs Trivy security scans to catch vulnerabilities and misconfigurations.

In this step, get ready to:
  • Create a GitHub Actions workflow file with pull request triggers.
  • Add Trivy scanning for vulnerabilities and misconfigurations.
  • Post scan results as a formatted PR comment.
Create the workflow file with trigger and build configuration

GitHub Actions workflows live in the .github/workflows/ directory as YAML files. Each file defines when the workflow runs, what permissions it needs, and what jobs to execute.

  • In VS Code, create a new file at .github/workflows/security-scan.yml inside your devsecops-ai-pipeline project folder.
  • Paste the following code into the file:
name: DevSecOps Security Pipeline

on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

jobs:
  build-and-scan:
    name: Build & Security Scan
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t devsecops-demo:${{ github.sha }} -f Dockerfile.vulnerable .

What does this workflow configuration do?

  • on: pull_request: branches: [main] triggers this workflow whenever someone opens a PR targeting the main branch.
  • permissions grants the workflow's GITHUB_TOKEN read access to the repo and write access to pull requests (needed to post comments later).
  • The build step uses Dockerfile.vulnerable on purpose. This generates interesting scan results so you can see Trivy in action.
  • github.sha tags the image with the commit hash, making each build uniquely identifiable.

Why pull_request instead of push?

Security scanning on pull requests gives you feedback before code reaches main. This is the "shift-left" philosophy: catch issues early when they're cheapest to fix. The scan results appear as comments directly on the PR where the developer is already reviewing changes.

  • Save security-scan.yml.

You won't see a visible change yet. The next chunk adds the actual scanning steps.

Add the Trivy security scan steps

Trivy can scan container images for known vulnerabilities and scan your filesystem for misconfigurations and secrets. You'll add both scan types as separate steps in the workflow.

  • In security-scan.yml, find the Build Docker image step you just added. Copy the following code below it (keeping the same indentation level as the other steps):
      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@0.33.1
        with:
          image-ref: "devsecops-demo:${{ github.sha }}"
          format: "json"
          output: "trivy-vuln-results.json"
          severity: "LOW,MEDIUM,HIGH,CRITICAL"
          vuln-type: "os,library"

      - name: Run Trivy misconfiguration scan
        uses: aquasecurity/trivy-action@0.33.1
        with:
          scan-type: "fs"
          scan-ref: "."
          format: "json"
          output: "trivy-misconfig-results.json"
          scanners: "misconfig,secret"
          skip-setup-trivy: "true"

What do these scan steps do?

The first step scans the Docker image you just built. It checks for known CVEs in the OS packages and Python libraries installed inside the container. Results are saved as JSON for processing later.

The second step scans the filesystem (your repo files) for Dockerfile misconfigurations and hardcoded secrets. The skip-setup-trivy: "true" option reuses the Trivy binary from the first step instead of downloading it again.

  • Save security-scan.yml.

You won't see a visible change yet. The next chunk adds the steps that upload results and post a comment on the PR.

Seeing YAML indentation errors?

YAML is sensitive to whitespace. Each step must start with exactly 6 spaces (or 3 tabs of 2 spaces). Make sure the - name: lines align with the existing steps above them.

help me fix my YAML indentation in security-scan.yml

Add artifact upload and PR comment posting

The scan results need to be shared with the AI review job (which you'll build in the next step). You also want a quick summary table posted directly on the PR so developers can see the findings at a glance.

  • In security-scan.yml, find the Run Trivy misconfiguration scan step. Copy the following code below it:
      - name: Upload scan results
        uses: actions/upload-artifact@v4
        with:
          name: trivy-results
          path: |
            trivy-vuln-results.json
            trivy-misconfig-results.json

      - name: Post scan summary comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          MEDIUM=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          TOTAL=$((CRITICAL + HIGH + MEDIUM))

          gh pr comment ${{ github.event.pull_request.number }} --body "## \U0001f512 Trivy Security Scan Results
          
          | Severity | Count |
          |----------|-------|
          | \U0001f534 Critical | ${CRITICAL} |
          | \U0001f7e0 High | ${HIGH} |
          | \U0001f7e1 Medium | ${MEDIUM} |
          | **Total** | **${TOTAL}** |
          
          Full AI-powered analysis will follow in the next comment."

What does this code do?

The upload step saves both JSON result files as a GitHub Actions artifact. This makes them available to download in later jobs (the AI review job will need them).

The comment step uses jq to count vulnerabilities by severity from the JSON results. Then it uses the gh CLI (pre-installed on GitHub runners) to post a formatted Markdown table as a comment on the pull request.

  • Save security-scan.yml.
  • In the VS Code Explorer sidebar, confirm that .github/workflows/security-scan.yml appears in your file tree.

Don't see the file in the sidebar?

Make sure you created the file inside the .github/workflows/ directory (with the dot prefix). Folders starting with a dot are hidden by default on some systems. In VS Code's Explorer, they should still appear.

help me find or create the .github/workflows directory in my project

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and it's in the correct location.

ⓧ I'd like to double check the full code

Here is the complete .github/workflows/security-scan.yml file so far:

name: DevSecOps Security Pipeline

on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

jobs:
  build-and-scan:
    name: Build & Security Scan
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t devsecops-demo:${{ github.sha }} -f Dockerfile.vulnerable .

      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@0.33.1
        with:
          image-ref: "devsecops-demo:${{ github.sha }}"
          format: "json"
          output: "trivy-vuln-results.json"
          severity: "LOW,MEDIUM,HIGH,CRITICAL"
          vuln-type: "os,library"

      - name: Run Trivy misconfiguration scan
        uses: aquasecurity/trivy-action@0.33.1
        with:
          scan-type: "fs"
          scan-ref: "."
          format: "json"
          output: "trivy-misconfig-results.json"
          scanners: "misconfig,secret"
          skip-setup-trivy: "true"

      - name: Upload scan results
        uses: actions/upload-artifact@v4
        with:
          name: trivy-results
          path: |
            trivy-vuln-results.json
            trivy-misconfig-results.json

      - name: Post scan summary comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          MEDIUM=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          TOTAL=$((CRITICAL + HIGH + MEDIUM))

          gh pr comment ${{ github.event.pull_request.number }} --body "## \U0001f512 Trivy Security Scan Results
          
          | Severity | Count |
          |----------|-------|
          | \U0001f534 Critical | ${CRITICAL} |
          | \U0001f7e0 High | ${HIGH} |
          | \U0001f7e1 Medium | ${MEDIUM} |
          | **Total** | **${TOTAL}** |
          
          Full AI-powered analysis will follow in the next comment."

Your security scanning workflow is ready. Next up, you'll build the AI security analyst script that reads Trivy's JSON output and generates human-readable remediation advice using Gemini.

Build the AI Security Analyst Script

Your Trivy workflow now produces JSON scan results and uploads them as artifacts.

Raw JSON is hard to read during code review. In this step, you'll build a Python script that reads Trivy's output, sends it to Google Gemini for analysis, and posts a human-readable remediation summary directly on the pull request.

In this step, get ready to:
  • Create a Python script that loads Trivy JSON results and constructs a structured prompt.
  • Implement the Gemini API call to analyze security findings.
  • Add GitHub API integration to post AI-generated analysis as a PR comment.
Create the script with Trivy result loading and prompt construction

The AI security review script needs to read Trivy's JSON output files and assemble them into a prompt that gives Gemini enough context to provide useful remediation advice.

  • In VS Code, create a new file called ai_security_review.py inside the scripts folder you created earlier.
  • Add the imports and the load_trivy_results() function by pasting the following code:
import json
import os
import sys
import urllib.request

from google import genai
from google.genai import types


def load_trivy_results(file_path):
    """Load Trivy JSON results from file."""
    if not os.path.exists(file_path):
        return None
    with open(file_path, "r") as f:
        return json.load(f)

What does this code do?

  • The standard library imports (json, os, sys, urllib.request) handle file I/O, environment variables, and HTTP requests without external dependencies.
  • from google import genai and from google.genai import types import the google-genai SDK for calling the Gemini API.
  • load_trivy_results() safely checks if a file exists before reading it. If Trivy didn't produce output for a scan type, the function returns None instead of crashing.

Now add the build_prompt() function below load_trivy_results(). This constructs the structured prompt that tells Gemini how to analyze the findings.

  • Paste this function below the load_trivy_results() function:

def build_prompt(vuln_results, misconfig_results):
    """Construct the analysis prompt for Gemini."""
    prompt = """You are a senior DevSecOps engineer reviewing container security scan results.
Analyze the following Trivy scan output and provide:

1. **Executive Summary** - A 2-3 sentence overview of the security posture
2. **Critical Findings** - List any HIGH or CRITICAL vulnerabilities with:
   - What the vulnerability is
   - Why it matters
   - How to fix it (specific commands or Dockerfile changes)
3. **Dockerfile Best Practices** - Any misconfigurations found and how to fix them
4. **Recommendations** - Prioritized list of actions to improve security

Format your response in GitHub-flavored Markdown suitable for a PR comment.
Keep it concise and actionable - this will be read by developers during code review.

---

## Vulnerability Scan Results:
"""
    if vuln_results:
        prompt += f"\n```json\n{json.dumps(vuln_results, indent=2)[:8000]}\n```\n"
    else:
        prompt += "\nNo vulnerability scan results available.\n"

    prompt += "\n## Misconfiguration Scan Results:\n"
    if misconfig_results:
        prompt += f"\n```json\n{json.dumps(misconfig_results, indent=2)[:4000]}\n```\n"
    else:
        prompt += "\nNo misconfiguration results available.\n"

    return prompt

What does this code do?

The prompt gives Gemini a clear role (senior DevSecOps engineer) and asks for a structured response with four sections: executive summary, critical findings, Dockerfile best practices, and recommendations.

The JSON results are truncated to 8000 and 4000 characters respectively. This keeps the total prompt within Gemini's context window while still providing enough detail for meaningful analysis.

  • Save scripts/ai_security_review.py.

Seeing a red underline on the google import?

Your local Python environment may not have google-genai installed. The import will resolve in GitHub Actions where the workflow installs the package. You can ignore the local warning, or install it locally with pip install google-genai if you prefer.

Help me fix the google-genai import error in my ai_security_review.py script.

Implement the Gemini API call

With the prompt ready, you need a function that sends it to Gemini and returns the analysis. The google-genai SDK uses a Client-based architecture where you create a client with your API key, then call generate_content on it.

  • Add the analyze_with_gemini() function below build_prompt():

def analyze_with_gemini(prompt):
    """Send the prompt to Gemini and return the analysis."""
    api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        print("Error: GEMINI_API_KEY environment variable not set")
        sys.exit(1)

    client = genai.Client(api_key=api_key)

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            max_output_tokens=2048,
            temperature=0.3,
        ),
    )
    return response.text

What does this code do?

  • os.environ.get("GEMINI_API_KEY") reads the API key from the environment variable you stored as a GitHub Actions secret.
  • genai.Client(api_key=api_key) creates a client instance. The google-genai SDK requires this client-based pattern.
  • temperature=0.3 keeps the output focused and consistent. Lower temperature means less creative variation in the response, which is what you want for security advice.
  • max_output_tokens=2048 caps the response length so the PR comment stays readable.
  • Save scripts/ai_security_review.py.

Why gemini-2.5-flash?

The gemini-2.5-flash model is available in the free tier with free input and output tokens. It has fast response times suitable for CI/CD pipelines where latency matters. Its quality is more than sufficient for security analysis tasks.

Add the GitHub API integration to post the PR comment

The final piece connects your AI analysis to the pull request. GitHub's REST API lets you post comments on PRs using the Issues endpoint, since every pull request is also an issue in GitHub's data model.

  • Add the post_pr_comment() function below analyze_with_gemini():

def post_pr_comment(comment_body):
    """Post a comment on the pull request using GitHub API."""
    github_token = os.environ.get("GITHUB_TOKEN")
    repo = os.environ.get("GITHUB_REPOSITORY")
    pr_number = os.environ.get("PR_NUMBER")

    if not all([github_token, repo, pr_number]):
        print("Missing GitHub environment variables, printing to stdout instead:")
        print(comment_body)
        return

    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    data = json.dumps({"body": comment_body}).encode("utf-8")

    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": f"Bearer {github_token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req) as resp:
            if resp.status == 201:
                print("Successfully posted AI security review comment")
            else:
                print(f"Unexpected status: {resp.status}")
    except Exception as e:
        print(f"Failed to post comment: {e}")
        print("AI Analysis Output:")
        print(comment_body)

What does this code do?

This function posts the AI analysis as a comment on the pull request using GitHub's REST API. It reads GITHUB_TOKEN, GITHUB_REPOSITORY, and PR_NUMBER from environment variables that GitHub Actions provides automatically.

The fallback logic is important. If any environment variable is missing (like when running locally for testing), the script prints the analysis to stdout instead of crashing. This makes the script safe to run outside of CI.

The API returns status 201 on successful comment creation. The X-GitHub-Api-Version: 2022-11-28 header pins the API version for stability.

Now add the main() function that orchestrates the full flow.

  • Paste this at the bottom of scripts/ai_security_review.py:

def main():
    vuln_file = os.environ.get("VULN_RESULTS", "trivy-vuln-results.json")
    misconfig_file = os.environ.get("MISCONFIG_RESULTS", "trivy-misconfig-results.json")

    print("Loading Trivy scan results...")
    vuln_results = load_trivy_results(vuln_file)
    misconfig_results = load_trivy_results(misconfig_file)

    if not vuln_results and not misconfig_results:
        print("No scan results found. Skipping AI analysis.")
        return

    print("Building analysis prompt...")
    prompt = build_prompt(vuln_results, misconfig_results)

    print("Sending to Gemini for analysis...")
    analysis = analyze_with_gemini(prompt)

    comment = (
        "## \U0001f916 AI Security Analysis\n\n"
        f"{analysis}\n\n"
        "---\n"
        f"*Generated by AI Security Analyst powered by Google Gemini | "
        f"[Pipeline Run](https://github.com/{os.environ.get('GITHUB_REPOSITORY', '')}"
        f"/actions/runs/{os.environ.get('GITHUB_RUN_ID', '')})*\n"
    )

    post_pr_comment(comment)


if __name__ == "__main__":
    main()

What does this code do?

  • main() reads file paths from environment variables with sensible defaults, so the script works both in CI and locally.
  • If no scan results exist, the script exits gracefully instead of sending an empty prompt to Gemini.
  • The comment is wrapped in a Markdown template with a header, the AI analysis, and a footer linking back to the pipeline run for traceability.
  • if __name__ == "__main__" ensures the script only runs when executed directly, not when imported.
  • Save scripts/ai_security_review.py.
  • Confirm the file exists by running:
ls scripts/

You should see ai_security_review.py in the output.

Don't see the file?

Make sure you created the file inside the scripts folder, not in the project root. The path should be scripts/ai_security_review.py.

Help me find where my ai_security_review.py file ended up.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

import json
import os
import sys
import urllib.request

from google import genai
from google.genai import types


def load_trivy_results(file_path):
    """Load Trivy JSON results from file."""
    if not os.path.exists(file_path):
        return None
    with open(file_path, "r") as f:
        return json.load(f)


def build_prompt(vuln_results, misconfig_results):
    """Construct the analysis prompt for Gemini."""
    prompt = """You are a senior DevSecOps engineer reviewing container security scan results.
Analyze the following Trivy scan output and provide:

1. **Executive Summary** - A 2-3 sentence overview of the security posture
2. **Critical Findings** - List any HIGH or CRITICAL vulnerabilities with:
   - What the vulnerability is
   - Why it matters
   - How to fix it (specific commands or Dockerfile changes)
3. **Dockerfile Best Practices** - Any misconfigurations found and how to fix them
4. **Recommendations** - Prioritized list of actions to improve security

Format your response in GitHub-flavored Markdown suitable for a PR comment.
Keep it concise and actionable - this will be read by developers during code review.

---

## Vulnerability Scan Results:
"""
    if vuln_results:
        prompt += f"\n```json\n{json.dumps(vuln_results, indent=2)[:8000]}\n```\n"
    else:
        prompt += "\nNo vulnerability scan results available.\n"

    prompt += "\n## Misconfiguration Scan Results:\n"
    if misconfig_results:
        prompt += f"\n```json\n{json.dumps(misconfig_results, indent=2)[:4000]}\n```\n"
    else:
        prompt += "\nNo misconfiguration results available.\n"

    return prompt


def analyze_with_gemini(prompt):
    """Send the prompt to Gemini and return the analysis."""
    api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        print("Error: GEMINI_API_KEY environment variable not set")
        sys.exit(1)

    client = genai.Client(api_key=api_key)

    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            max_output_tokens=2048,
            temperature=0.3,
        ),
    )
    return response.text


def post_pr_comment(comment_body):
    """Post a comment on the pull request using GitHub API."""
    github_token = os.environ.get("GITHUB_TOKEN")
    repo = os.environ.get("GITHUB_REPOSITORY")
    pr_number = os.environ.get("PR_NUMBER")

    if not all([github_token, repo, pr_number]):
        print("Missing GitHub environment variables, printing to stdout instead:")
        print(comment_body)
        return

    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    data = json.dumps({"body": comment_body}).encode("utf-8")

    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": f"Bearer {github_token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req) as resp:
            if resp.status == 201:
                print("Successfully posted AI security review comment")
            else:
                print(f"Unexpected status: {resp.status}")
    except Exception as e:
        print(f"Failed to post comment: {e}")
        print("AI Analysis Output:")
        print(comment_body)


def main():
    vuln_file = os.environ.get("VULN_RESULTS", "trivy-vuln-results.json")
    misconfig_file = os.environ.get("MISCONFIG_RESULTS", "trivy-misconfig-results.json")

    print("Loading Trivy scan results...")
    vuln_results = load_trivy_results(vuln_file)
    misconfig_results = load_trivy_results(misconfig_file)

    if not vuln_results and not misconfig_results:
        print("No scan results found. Skipping AI analysis.")
        return

    print("Building analysis prompt...")
    prompt = build_prompt(vuln_results, misconfig_results)

    print("Sending to Gemini for analysis...")
    analysis = analyze_with_gemini(prompt)

    comment = (
        "## \U0001f916 AI Security Analysis\n\n"
        f"{analysis}\n\n"
        "---\n"
        f"*Generated by AI Security Analyst powered by Google Gemini | "
        f"[Pipeline Run](https://github.com/{os.environ.get('GITHUB_REPOSITORY', '')}"
        f"/actions/runs/{os.environ.get('GITHUB_RUN_ID', '')})*\n"
    )

    post_pr_comment(comment)


if __name__ == "__main__":
    main()

Your AI security analyst script is complete. Next up, you'll wire this script into the GitHub Actions workflow so it runs automatically after every Trivy scan.

Integrate the AI Review into the Pipeline

You have a Trivy scanning job that produces JSON results and a Python script ready to analyze those results with Gemini.

Now you need to connect these two pieces. In this step, you will add a second job to your GitHub Actions workflow that runs after the scan completes, downloads the scan artifacts, and executes your AI analysis script.

In this step, get ready to:
  • Add the AI security review job to the workflow with job dependencies and artifact passing.
  • Verify the project file structure is correct before pushing.
  • Push everything to the main branch and confirm the workflow is valid in GitHub Actions.
Add the AI security review job to the workflow

GitHub Actions workflows can have multiple jobs. The needs keyword tells a job to wait for another job to finish before starting.

  • In VS Code, open .github/workflows/security-scan.yml.
  • Scroll to the very bottom of the file, after the closing of the Post scan summary comment step.
  • Add the following new job below the build-and-scan job by pasting this code at the end of the file:
  ai-security-review:
    name: AI Security Analysis
    runs-on: ubuntu-24.04
    needs: build-and-scan
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Download scan results
        uses: actions/download-artifact@v4
        with:
          name: trivy-results

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pip install google-genai

      - name: Run AI security review
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          GITHUB_RUN_ID: ${{ github.run_id }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          VULN_RESULTS: "trivy-vuln-results.json"
          MISCONFIG_RESULTS: "trivy-misconfig-results.json"
        run: python scripts/ai_security_review.py

What does this job do?

  • needs: build-and-scan tells GitHub Actions to wait until the scan job finishes successfully before running this job.
  • actions/download-artifact@v4 retrieves the Trivy JSON files that the scan job uploaded. This is how separate jobs share data.
  • actions/setup-python@v5 installs Python 3.12 on the runner so the AI script can execute.
  • The env block passes secrets and GitHub context variables into the script's environment. The script reads these to call Gemini and post comments back to the PR.

YAML indentation matters

The ai-security-review job must be indented at the same level as build-and-scan (two spaces under jobs:). If the indentation is off, the workflow will fail to parse.

  • Save security-scan.yml.

YAML syntax errors?

  • Make sure there is a blank line between the end of the build-and-scan job and the start of ai-security-review.
  • Check that ai-security-review: starts with exactly two spaces (same level as build-and-scan:).
  • Verify that the env block values use double quotes around strings containing ${{ }} expressions.

Why is my YAML indentation causing errors?

Verify the project structure locally

Before pushing, confirm that all project files are in the right places. A missing file or wrong directory means the pipeline will fail at runtime.

  • Check your project structure by running this command in your terminal:
find . -type f -not -path './.git/*'

You should see the following files listed:

./.dockerignore
./.github/workflows/security-scan.yml
./.trivyignore
./app.py
./Dockerfile
./Dockerfile.vulnerable
./requirements.txt
./scripts/ai_security_review.py

Missing a file?

  • If scripts/ai_security_review.py is missing, check that you created it inside a scripts folder (not at the project root).
  • If .github/workflows/security-scan.yml is missing, verify the full directory path exists: .github/workflows/ (both folders needed).

Help me find where my missing project files went.

Push the pipeline to the main branch

With all files verified, you can push the complete project to GitHub. This initial commit puts everything on main so the workflow is available when you open a pull request later.

  • Stage, commit, and push all your files by running these commands:
git add .
git commit -m "Add DevSecOps pipeline with Trivy and AI review"
git push origin main

Why push to main first?

The workflow triggers on pull requests targeting main. For that trigger to work, the workflow file must already exist on the main branch. This initial push establishes the baseline.

  • Open your repository on GitHub in your browser.
  • Click the Actions tab at the top of the repository.
  • Confirm you see DevSecOps Security Pipeline listed as a workflow on the left sidebar.

If the workflow appears without any error banners, your YAML is valid and ready to run.

Workflow not showing up in Actions?

  • Check that the file is at exactly .github/workflows/security-scan.yml (the path is case-sensitive).
  • Verify your push succeeded by checking git status shows no uncommitted changes and git log shows your commit.
  • If GitHub shows a YAML parsing error banner, open the file and check for indentation issues at the boundary between the two jobs.

Help me figure out why my workflow is not appearing in the GitHub Actions tab.

✔️ Awesome, I've got everything!

Great. Double check you have saved security-scan.yml and that your push completed without errors.

ⓧ I'd like to double check the full code

Here is the complete .github/workflows/security-scan.yml file after this step:

name: DevSecOps Security Pipeline

on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

jobs:
  build-and-scan:
    name: Build & Security Scan
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t devsecops-demo:${{ github.sha }} -f Dockerfile.vulnerable .

      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@0.33.1
        with:
          image-ref: "devsecops-demo:${{ github.sha }}"
          format: "json"
          output: "trivy-vuln-results.json"
          severity: "LOW,MEDIUM,HIGH,CRITICAL"
          vuln-type: "os,library"

      - name: Run Trivy misconfiguration scan
        uses: aquasecurity/trivy-action@0.33.1
        with:
          scan-type: "fs"
          scan-ref: "."
          format: "json"
          output: "trivy-misconfig-results.json"
          scanners: "misconfig,secret"
          skip-setup-trivy: "true"

      - name: Upload scan results
        uses: actions/upload-artifact@v4
        with:
          name: trivy-results
          path: |
            trivy-vuln-results.json
            trivy-misconfig-results.json

      - name: Post scan summary comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          MEDIUM=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' trivy-vuln-results.json 2>/dev/null || echo "0")
          TOTAL=$((CRITICAL + HIGH + MEDIUM))

          gh pr comment ${{ github.event.pull_request.number }} --body "## 🔒 Trivy Security Scan Results
          
          | Severity | Count |
          |----------|-------|
          | 🔴 Critical | ${CRITICAL} |
          | 🟠 High | ${HIGH} |
          | 🟡 Medium | ${MEDIUM} |
          | **Total** | **${TOTAL}** |
          
          Full AI-powered analysis will follow in the next comment."

  ai-security-review:
    name: AI Security Analysis
    runs-on: ubuntu-24.04
    needs: build-and-scan
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Download scan results
        uses: actions/download-artifact@v4
        with:
          name: trivy-results

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pip install google-genai

      - name: Run AI security review
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          GITHUB_RUN_ID: ${{ github.run_id }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          VULN_RESULTS: "trivy-vuln-results.json"
          MISCONFIG_RESULTS: "trivy-misconfig-results.json"
        run: python scripts/ai_security_review.py

Your pipeline is fully wired up. Next, you will trigger the full pipeline by opening a pull request and watch both jobs run end to end.

Trigger and Verify the Full Pipeline

Your pipeline code is on GitHub, but it hasn't actually run yet. The workflow trigger is set to pull_request, which means it only fires when someone opens a PR against main.

In this step, you'll simulate the real developer workflow: create a feature branch, push a change, and open a pull request. This triggers the full pipeline and produces the security comments that make the whole project worth building.

In this step, get ready to:
  • Create a feature branch with a minor code change and open a pull request.
  • Watch the GitHub Actions pipeline run both jobs to completion.
  • Review the AI-generated security analysis posted as a PR comment.
Create a feature branch and open a pull request

In a real team workflow, every code change goes through a pull request before merging. Your pipeline is wired to scan every PR automatically, so opening one is all it takes to trigger the security scan.

  • Switch back to your terminal in VS Code.
  • Create a new feature branch by running this command:
git checkout -b feature/update-version
  • Open app.py in VS Code.
  • Find this line:
        "version": "1.0.0"
  • Change "1.0.0" to "1.1.0", so the line now looks like this:
        "version": "1.1.0"

Why this change?

The specific code change doesn't matter for triggering the pipeline. You just need any diff between your branch and main so GitHub has something to compare in the pull request. A version bump is a realistic, minimal change.

  • Save app.py.
  • Stage, commit, and push the change by running these commands:
git add app.py
git commit -m "Bump version to 1.1.0"
git push origin feature/update-version

What do these commands do?

The first command stages your changed file. The second creates a commit with a descriptive message. The third pushes your new branch to GitHub, making it available for a pull request.

  • After the push completes, open your devsecops-ai-pipeline repository on GitHub in your browser.
  • You should see a yellow banner at the top saying your branch had recent pushes, with a Compare & pull request button.
  • Click Compare & pull request.
  • Leave the title and description as-is (or add your own).
  • Click Create pull request.

Your pull request is now open. The pipeline triggers automatically within a few seconds.

Don't see the yellow banner?

If the banner doesn't appear, click the Pull requests tab, then click New pull request. Set the base branch to main and the compare branch to feature/update-version.

If your branch doesn't appear in the dropdown, double-check that the git push command completed without errors in your terminal.

Still stuck? Help me troubleshoot why my branch isn't showing up on GitHub after pushing.

Watch the pipeline run and verify both jobs complete

Your workflow has two jobs: build-and-scan runs first (building the Docker image and scanning it with Trivy), then ai-security-review runs after (analyzing the results with Gemini).

  • On your pull request page, scroll down to the checks section near the bottom.
  • You should see the workflow running with a yellow spinner icon.
  • Click Details next to any check to open the Actions run view.

What should I see in the Actions view?

You'll see a workflow graph with two jobs connected by an arrow. The Build & Security Scan job runs first. Once it completes with a green checkmark, the AI Security Analysis job starts.

The full pipeline typically takes 3-5 minutes. The Docker build and Trivy scan take the most time.

  • Click on the Build & Security Scan job to expand its steps.
  • Watch each step complete: checkout, Docker build, Trivy vulnerability scan, Trivy misconfiguration scan, artifact upload, and PR comment.
  • Once the first job finishes, click the AI Security Analysis job to watch it run.
  • Wait until both jobs show green checkmarks.

Seeing a red X on one of the jobs?

Click the failed job to see which step failed. Common causes:

  • If the Build Docker image step fails, check that Dockerfile.vulnerable exists on your branch and has valid syntax.
  • If the Run AI security review step fails, verify your GEMINI_API_KEY secret is set correctly in your repository settings.
  • If the Post scan summary comment step fails, ensure the workflow has pull-requests: write in the permissions block.

Need more help? Help me debug my failed GitHub Actions job.

Review the AI-generated security analysis comment

Once both jobs complete, your pull request should have two new comments posted automatically by the pipeline.

  • Navigate back to your pull request page (click Pull requests tab, then click your open PR).
  • Scroll down to the comments section.
  • Look for the first comment titled Trivy Security Scan Results. This is the summary table showing vulnerability counts by severity (Critical, High, Medium).
  • Look for the second comment titled AI Security Analysis. This is the Gemini-generated remediation advice.

What should the AI analysis contain?

Because your pipeline builds Dockerfile.vulnerable (the full python:3.12 base image with unnecessary packages), Trivy will find real vulnerabilities. The AI analysis should identify:

  • Vulnerabilities in the base image's OS packages (the full image includes hundreds of libraries with known CVEs).
  • Dockerfile misconfigurations like running as root and installing unnecessary tools.
  • Specific remediation steps (switching to python:3.12-slim, removing unused packages, adding a non-root user).

The exact vulnerabilities and CVE counts will vary depending on when you run the scan, because new CVEs are published daily.

  • Read through the AI analysis and confirm it includes an executive summary, critical findings with fix instructions, and prioritized recommendations.

Your pipeline is working end-to-end. Every pull request to this repository now gets an automated security review with AI-powered remediation advice.

Only see one comment, or no comments at all?

If you see the Trivy summary but not the AI analysis, the ai-security-review job may still be running. Wait for it to complete, then refresh the page.

If you see no comments at all, check that both jobs passed in the Actions tab. A failed job won't post its comment.

Still need help? Help me figure out why my pipeline isn't posting PR comments.

Your DevSecOps pipeline is delivering real value on every PR. Next up, you'll swap to the hardened Dockerfile and see how dramatically the security posture improves.

Harden the Dockerfile and See the Improvement

Your pipeline just proved it can catch vulnerabilities in a deliberately insecure container. Now it's time to prove the pipeline can also track improvements. In a real team, this is how you demonstrate to reviewers that a PR actually fixed the security issues it claims to fix.

You already have a hardened Dockerfile sitting in your repo. In this step, you'll switch the pipeline to build that hardened version and open a new PR so you can compare the scan results side by side.

In this step, get ready to:
  • Create a branch that switches the workflow to use the hardened Dockerfile.
  • Push the branch and open a new PR to trigger a clean scan.
  • Compare before/after AI analysis comments across both PRs.
Create a new branch that switches to the hardened Dockerfile

The workflow currently builds Dockerfile.vulnerable on every PR. To show the security improvement, you'll create a branch that changes the build target to the slim, hardened Dockerfile you wrote in Step 2.

  • Switch back to your terminal in VS Code.
  • Create and switch to a new branch by running this command:
git checkout main
git pull origin main
git checkout -b hardened-dockerfile

What does this do?

This ensures you're starting from the latest main branch, then creates a fresh branch called hardened-dockerfile where your workflow change will live.

Now you'll make one small change to the workflow file. The pipeline currently references Dockerfile.vulnerable in the Docker build step. You'll swap it to the hardened Dockerfile.

  • In VS Code, open .github/workflows/security-scan.yml.
  • Find this line inside the Build Docker image step:
run: docker build -t devsecops-demo:${{ github.sha }} -f Dockerfile.vulnerable .
  • Replace Dockerfile.vulnerable with Dockerfile, so the line now looks like this:
run: docker build -t devsecops-demo:${{ github.sha }} -f Dockerfile .
  • Save security-scan.yml.

Why does this one-line change matter?

The hardened Dockerfile uses python:3.12-slim (a minimal base image), removes unnecessary system packages, and uses gunicorn as the production server. These changes drastically reduce the attack surface that Trivy scans.

Push the branch and open a new PR
  • Stage, commit, and push your change by running these commands:
git add .github/workflows/security-scan.yml
git commit -m "Switch to hardened Dockerfile for security scan"
git push origin hardened-dockerfile
  • Open your devsecops-ai-pipeline repository on GitHub in your browser.
  • You should see a yellow banner at the top saying your branch had recent pushes. Click Compare & pull request.
  • Give the PR a title like Switch to hardened Dockerfile.
  • Click Create pull request.

This triggers the pipeline again. The workflow will now build your slim, hardened image instead of the bloated vulnerable one.

Don't see the banner?

If the yellow banner doesn't appear, click the Pull requests tab, then click New pull request. Set the compare branch to hardened-dockerfile and the base branch to main.

Still stuck? Help me open a pull request from my hardened-dockerfile branch

Compare the before/after AI analysis comments

Wait for both jobs (Build & Security Scan and AI Security Analysis) to complete. This typically takes 2-3 minutes.

  • Click the Actions tab to watch the pipeline run in real time.
  • Once both jobs show green checkmarks, return to your new PR's Conversation tab.
  • Scroll down to see the new Trivy summary comment and AI analysis comment.

You should see significantly fewer (or zero) CRITICAL and HIGH vulnerabilities compared to your first PR.

  • Open your first PR (the one from Step 6) in a new browser tab.
  • Compare the vulnerability counts in the Trivy summary tables side by side.
  • Read both AI analysis comments to see how Gemini's recommendations change when the image is hardened.

What should I see?

Your first PR (vulnerable) likely showed dozens of HIGH/CRITICAL findings from the full python:3.12 base image and its installed system packages (curl, wget, netcat-openbsd).

Your second PR (hardened) should show far fewer vulnerabilities because python:3.12-slim strips out most system libraries and you're not installing any unnecessary packages. The AI analysis should reflect this improvement with a more positive tone and fewer recommended actions.

Why this matters in the real world

This before/after pattern is exactly how security teams track progress. Each PR gets its own scan results, creating a history of your security posture over time. When a developer says they've hardened a container, the pipeline proves it with data.

Pipeline failed on the hardened build?

Make sure you only changed Dockerfile.vulnerable to Dockerfile in the -f flag of the build step. The rest of the line should remain the same.

Also confirm that the hardened Dockerfile exists in your repository root on the main branch (it was created in Step 2).

Need help debugging? Help me figure out why my hardened Dockerfile build is failing in GitHub Actions

Secret mission

Add a Security Gate and Posture Score

Your pipeline scans for vulnerabilities and explains them. But what if a developer ignores the warnings and merges anyway? In this secret mission, you'll add a security gate that blocks PR merges when CRITICAL vulnerabilities are found, extend the AI analysis to score the overall security posture on a scale of 1-10, and add a workflow status badge to your README.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on free-tier services. GitHub Actions is free for public repositories, and the Gemini API free tier covers all usage from this pipeline. There are no ongoing costs to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • GitHub repository devsecops-ai-pipeline (public, with GitHub Actions workflow).
  • Local project folder devsecops-ai-pipeline on your Desktop.
  • Docker image devsecops-demo:test built locally.
  • Gemini API key stored as a GitHub Actions secret (GEMINI_API_KEY).
  • Google AI Studio API key used to generate the secret.

Keep everything running

No action needed. Choose this if you plan to keep iterating on the pipeline, adding new scanning rules, or using this repo as a template for other projects.

  • Your GitHub Actions workflow only runs on pull requests, so it will not consume minutes unless you open a PR.
  • The Gemini API key remains stored securely in your repository secrets and will not be used unless the pipeline triggers.
  • The local Docker image takes up disk space but has no running cost.

Pause - I'll come back to this later

Shut down running processes to free up disk space while keeping your code intact.

  • Remove the local Docker image to reclaim disk space by running:
  • Your GitHub repository and secrets stay untouched for when you return.
docker rmi devsecops-demo:test

Everything else remains in place and costs nothing while idle.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Delete the local project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf devsecops-ai-pipeline
  • On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force devsecops-ai-pipeline
  • Remove the local Docker image by running:
docker rmi devsecops-demo:test
  • Delete the GitHub repository:
  • Go to your devsecops-ai-pipeline repository on GitHub.
  • Click Settings in the top navigation bar.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Type the repository name to confirm, then click Delete this repository again.
  • Delete your Gemini API key from Google AI Studio:
  • Go to the API Keys page in Google AI Studio.
  • Find the key you created for this project.
  • Delete the key to revoke access.

Nice Work!

Nice Work!

You just built a production-grade DevSecOps pipeline that automatically scans container images for security vulnerabilities and uses AI to generate actionable remediation advice. This is exactly the kind of automation that security-focused engineering teams rely on every day.

Here's what you learned:

  • Built a GitHub Actions CI/CD pipeline from scratch with multi-job orchestration, artifact passing, and automated PR comments.
  • Integrated Trivy for container vulnerability scanning, Dockerfile misconfiguration detection, and secret scanning, demonstrating shift-left security practices.
  • Created an AI-powered security analyst using Google Gemini that interprets scan results and posts human-readable remediation advice directly on pull requests.
  • Added a security gate that blocks PR merges when CRITICAL vulnerabilities are found, plus an AI-generated security posture score rated on a scale of 1-10.

Ready to quiz yourself?