Skip to main content

Command Palette

Search for a command to run...

How I Built a Local AI Docker Vulnerability Scanner (No API Costs, No Cloud)

Updated
4 min read
V
I build AI+DevOps tools that run 100% locally — no API costs, no cloud required. Currently building a series of 12 projects combining Ollama, Python, and core DevOps tools like Docker, Kubernetes, Terraform, and GitHub Actions. YouTube: ThinkWithOps

The Problem with Trivy Output

Running Trivy gives you a wall of CVE numbers. Most developers copy-paste them into Google and spend 20 minutes figuring out if each one actually matters for their use case.

I built a tool that fixes this.


What I Built

A local AI wrapper around Trivy that:

  • Scans any Docker image

  • Takes the raw CVE output

  • Feeds it to Ollama (local LLM — no API costs)

  • Returns plain English explanations + specific fix recommendations


The Interesting Finding

nginx:1.27-alpine: 14 vulnerabilities
nginx:alpine:       3 vulnerabilities

Same base image family — pinned version had 4.5x more CVEs. The AI caught this pattern and recommended variants to compare automatically.


Tech Stack

  • Python 3.11

  • Trivy (vulnerability scanner)

  • Ollama + Llama 3.2 (local LLM)

  • Zero cloud dependencies


How It Works (Code Walkthrough)

The scanner has three moving parts: Trivy does the heavy lifting of CVE detection, Python orchestrates everything, and Ollama explains what it all means.

Step 1 — Scan with Trivy and parse the JSON:

def scan_image(self, image_name: str) -> Optional[Dict]:
    result = subprocess.run(
        ["trivy", "image", "--format", "json", "--severity", "HIGH,CRITICAL", image_name],
        capture_output=True, text=True, check=True
    )
    return json.loads(result.stdout)

def extract_vulnerabilities(self, scan_data: Dict) -> List[Dict]:
    vulnerabilities = []
    seen_vulns = set()  # deduplicate by CVE ID

    for result in scan_data.get("Results", []):
        for vuln in result.get("Vulnerabilities", []):
            vuln_id = vuln.get("VulnerabilityID", "N/A")
            if vuln_id in seen_vulns:
                continue
            seen_vulns.add(vuln_id)
            vulnerabilities.append({
                "id": vuln_id,
                "package": vuln.get("PkgName", "N/A"),
                "severity": vuln.get("Severity", "N/A"),
                "fixed_version": vuln.get("FixedVersion", "Not available")
            })
    return vulnerabilities

Step 2 — Send each CVE to Ollama for a plain English explanation:

def explain_vulnerability(self, vuln: Dict) -> str:
    prompt = f"""You are a security expert explaining vulnerabilities to developers.

Vulnerability Details:
- ID: {vuln['id']}
- Package: {vuln['package']} (version {vuln['version']})
- Severity: {vuln['severity']}
- Title: {vuln['title']}

Explain in 2-3 sentences:
1. What this vulnerability means in simple terms
2. Why it's dangerous
3. How to fix it (fixed version: {vuln['fixed_version']})

Keep it concise and actionable. Use analogies if helpful."""

    response = requests.post(
        f"{self.ollama_host}/api/generate",
        json={"model": "llama3.2", "prompt": prompt, "stream": False},
        timeout=60
    )
    return response.json().get("response", "")

Step 3 — Generate an overall summary with structured output:

The summary prompt forces Ollama into a key-value format so we can parse it reliably and build a comparison command on the fly — more on that in the next section.


The Trickiest Part

Getting Ollama to return structured output consistently was harder than expected. Free-form responses were great for individual CVE explanations, but the security summary needed to be parseable — I needed specific fields like SECURITY_POSTURE and VARIANTS_TO_TEST to programmatically build the comparison command.

The solution was strict prompt formatting: I told the model to respond in KEY: value pairs and gave it an explicit example. Then I split each line on : and built a dict. When parsing failed I fell back to a hardcoded comparison command. The other challenge was Llama 3.2 sometimes repeating itself — I solved that with a deduplication pass that checks for repeated section headers (**1., **Vulnerability, etc.) and drops them before printing.


Results

Before — Raw Trivy output:

CVE-2024-1234 (CRITICAL)
Package: openssl 1.1.1k
Description: Use-after-free in X509_verify_cert function

😕 "What does this mean? Do I need to care about this?"

After — AI-enhanced output:

🤖 AI Explanation:
This is like leaving your house key under the doormat.
OpenSSL handles your HTTPS connections, and this bug lets
attackers potentially decrypt traffic. Fix: update your
Dockerfile base image to get openssl 1.1.1w or later.

"Got it, I'll update the base image today."

Metric Value
Avg scan time 15–30 seconds
AI explanation per CVE ~3 seconds
Cloud API cost $0
Images tested 50+ (nginx, node, python, ubuntu)

Manual CVE triage that used to take 20+ minutes per image now takes under a minute for the top 5 vulnerabilities.


Try It Yourself

GitHub: https://github.com/ThinkWithOps/ai-devops-projects Full demo video: https://youtu.be/J6fmU6t9jUU

# Prerequisites: Docker, Trivy, Ollama + llama3.2 pulled
git clone https://github.com/ThinkWithOps/ai-devops-projects.git
cd 01-ai-docker-scanner
pip install -r requirements.txt
python src/docker_scanner.py nginx:latest

What's Next

This is Project 1 in my AI+DevOps series. Next I built an AI K8s Pod Debugger — link in my profile.

AI+DevOps Projects

Part 1 of 10

A series of 12 AI+DevOps tools built with Python and Ollama — all running locally, zero cloud costs, zero API fees. Each project combines a core DevOps tool with local AI to solve real engineering problems.

Up next

I Built an AI AWS Cost Detective That Found $900/Year in Waste — Here's How

The Problem AWS Cost Explorer shows you data. It doesn't tell you what to do about it. I was paying $127/month and knew I was wasting money but couldn't quickly identify where. What the AI Found Runn

More from this blog

ThinkWithOps

11 posts