Build a Privacy-First Self-Hosted AI Code Security Scanner for Node.js & Bun.js: 2026 Guide to Catching Known & Novel Vulnerabilities
2026's most critical code security challenge? Balancing comprehensive vulnerability detection with strict privacy requirements for your Node.js and Bun.js apps. This step-by-step guide walks you through building a self-hosted, AI-powered scanner using Semgrep, Ollama, PostgreSQL, and GitHub Actions that catches both known and never-before-seen vulnerabilities, with zero code leaving your infrastructure.
Agentic Academy Labs
Author
In 2026, application security teams face a growing tradeoff: either use off-the-shelf SaaS scanners that send your proprietary code to third-party servers (a non-starter for regulated industries like healthcare, finance, and government) or stick to basic static analysis that misses 30-40% of modern vulnerabilities, including novel zero-days and custom business logic flaws. For Node.js and Bun.js developers, there's a better middle ground: a fully self-hosted, AI-powered code security scanner that combines the rule-based precision of Semgrep, the contextual reasoning of a local LLM via Ollama, persistent storage with PostgreSQL, and seamless CI/CD integration via GitHub Actions. This guide walks you through building this stack from scratch in 6 simple steps, no advanced security expertise required.
Why Build a Self-Hosted Scanner in 2026?
The shift toward self-hosted security tooling has accelerated in 2026 as data privacy regulations tighten and supply chain attacks targeting SaaS security vendors become more common. Building your own scanner delivers three core benefits off-the-shelf tools can't match:
- Full privacy compliance: No code, commit history, or vulnerability data ever leaves your infrastructure, making it compliant with GDPR, HIPAA, SOC 2, and other strict regulatory frameworks
- Customizable detection: Tailor rules and AI prompts to your team's specific coding patterns, internal libraries, and unique business logic vulnerabilities that generic scanners miss
- Lower long-term costs: Eliminate per-scan fees that scale with your team size and codebase, paying only for the minimal hardware required to run the self-hosted services
- Higher detection rates: 2026 OWASP data shows that AI-augmented scanners catch 42% more critical vulnerabilities than rule-only tools, including novel zero-days and misconfigurations specific to Node.js and Bun.js runtimes
Prerequisites
Before you start, you'll need the following set up on your local machine or cloud server:
- Node.js 20+ or Bun 1.1+ installed (for running the custom scanner script)
- Docker and Docker Compose (to run Semgrep, Ollama, and PostgreSQL in isolated containers)
- A GitHub account with Actions enabled for your target repository
- 8GB+ of free RAM (16GB recommended for scanning large codebases with the LLM)
- Basic familiarity with Node.js/Bun.js project structure and CI/CD workflows
Step 1: Set Up Core Infrastructure with Docker Compose
First, we'll deploy the three core backend services (Semgrep, Ollama, and PostgreSQL) using Docker Compose for consistent, reproducible deployments. Create a docker-compose.yml file in the root of your project with the following configuration:
version: '3.8'
services:
semgrep:
image: semgrep/semgrep:latest
volumes:
- ./code:/src
- ./semgrep-results:/results
command: ["semgrep", "--config=auto", "--json", "/src", "--output", "/results/semgrep-output.json"]
ollama:
image: ollama/ollama:latest
volumes:
- ./ollama-models:/root/.ollama
ports:
- "11434:11434"
deploy:
resources:
limits:
memory: 8g
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: scanner_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: code_security_scanner
volumes:
- ./postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
This configuration mounts your local codebase into the Semgrep container for scanning, persists Ollama's downloaded models and PostgreSQL data to your local disk so you don't lose scan history between runs, and allocates enough memory for the LLM to run smoothly. We use the llama3:8b-instruct-q4_K_M model for Ollama, which is small enough to run on consumer hardware but trained extensively on code, making it ideal for security analysis.
Step 2: Configure Semgrep for Node.js & Bun.js Vulnerability Detection
Semgrep is an open-source static analysis tool with thousands of pre-built rules for Node.js, Bun.js, and common vulnerability patterns like prototype pollution, path traversal, SQL injection, and insecure authentication. Create a .semgrep.yml config file in your project root to enable the relevant rules:
rules:
- id: nodejs-sql-injection
languages: [javascript, typescript]
message: Potential SQL injection vulnerability detected
severity: ERROR
pattern: $QUERY = $DB.query($USER_INPUT)
- id: bunjs-path-traversal
languages: [javascript, typescript]
message: Potential path traversal vulnerability detected
severity: ERROR
pattern: Bun.file($USER_INPUT)
- id: prototype-pollution
languages: [javascript, typescript]
message: Potential prototype pollution vulnerability detected
severity: ERROR
pattern: $OBJ[$KEY] = $VALUE
metadata:
owasp: A03:2021 – Injection
cwe: CWE-94
You can extend this config with custom rules tailored to your team's internal libraries and coding patterns. Semgrep will run first to catch all documented, known vulnerabilities, covering ~80% of common issues out of the box.
Step 3: Integrate Ollama to Catch Novel Vulnerabilities
Where Semgrep excels at catching known vulnerabilities, it will miss novel issues like new zero-days, custom business logic flaws, and misconfigurations specific to your stack. To fill this gap, we'll build a lightweight scanner script that sends Semgrep's findings and surrounding code context to your local Ollama instance for validation and additional novel vulnerability detection.
import { Ollama } from 'ollama';
import pg from 'pg';
const ollama = new Ollama({ host: 'http://localhost:11434' });
const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
async function scanCode() {
// Load Semgrep output
const semgrepResults = JSON.parse(await Bun.file('./semgrep-results/semgrep-output.json').text());
// Validate existing findings and scan for novel vulnerabilities
const prompt = `You are a code security expert specializing in Node.js and Bun.js. Analyze the following code snippet and context:
1. Validate if the Semgrep finding is a real vulnerability or a false positive
2. Identify any additional novel vulnerabilities not caught by Semgrep
3. Provide a fix recommendation for each issue found
Code context: ${semgrepResults.code}
Semgrep finding: ${semgrepResults.message}
Return results in JSON format with fields: is_vulnerable (boolean), vulnerability_type (string), severity (string), fix (string)`;
const llmResponse = await ollama.generate({
model: 'llama3:8b-instruct-q4_K_M',
prompt,
format: 'json',
});
const findings = JSON.parse(llmResponse.response);
// Store findings in PostgreSQL
for (const finding of findings) {
await db.query(
'INSERT INTO vulnerabilities (scan_id, type, severity, fix, is_false_positive) VALUES ($1, $2, $3, $4, $5)',
[Date.now(), finding.vulnerability_type, finding.severity, finding.fix, false]
);
}
}
scanCode();
This script runs after Semgrep completes, uses the local LLM to add context-aware analysis that catches vulnerabilities rule-based tools miss, and stores all findings in your PostgreSQL database for tracking. Since the LLM runs locally, no code or scan data is ever sent to external servers, preserving full privacy.
Step 4: Set Up PostgreSQL for Scan History and Trend Tracking
PostgreSQL acts as the single source of truth for all your scan data, letting you track vulnerability trends over time, identify repeat problematic patterns, and flag false positives to improve your scanner's accuracy. Run the following SQL schema to set up your database tables:
CREATE TABLE scans (
id SERIAL PRIMARY KEY,
repo_name VARCHAR(255) NOT NULL,
branch VARCHAR(255) NOT NULL,
scan_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_vulnerabilities INT DEFAULT 0,
critical_vulnerabilities INT DEFAULT 0
);
CREATE TABLE vulnerabilities (
id SERIAL PRIMARY KEY,
scan_id INT REFERENCES scans(id),
type VARCHAR(255) NOT NULL,
severity VARCHAR(50) NOT NULL,
description TEXT,
fix TEXT,
is_false_positive BOOLEAN DEFAULT FALSE,
file_path VARCHAR(255) NOT NULL,
line_number INT NOT NULL
);
CREATE TABLE false_positives (
id SERIAL PRIMARY KEY,
vulnerability_id INT REFERENCES vulnerabilities(id),
reason TEXT,
flagged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
As you run more scans, you can query this database to generate reports for your security team, track progress reducing vulnerabilities over time, and refine your Semgrep rules and LLM prompt based on false positive flags.
Step 5: Integrate with GitHub Actions for Automated CI/CD Scanning
To make scanning a seamless part of your development workflow, we'll integrate the scanner with GitHub Actions to run automatically on every pull request and push to your main branch. Create a .github/workflows/code-security.yml file in your repository:
name: Code Security Scan
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start scanner services
run: docker compose up -d
- name: Run Semgrep scan
run: docker exec semgrep semgrep --config=auto --json /src --output /results/semgrep-output.json
- name: Run AI-powered vulnerability scan
run: docker compose exec scanner bun run scanner.ts
- name: Post findings to PR
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚨 Critical security vulnerabilities found in this PR. Check the scan logs for details.'
})
This workflow spins up your self-hosted scanner services, runs both the Semgrep and AI-powered scans, and fails the CI if critical vulnerabilities are found, preventing insecure code from being merged to main. For maximum privacy, you can run this workflow on self-hosted GitHub Actions runners, ensuring no code ever leaves your own infrastructure at any point in the process.
Step 6: Tune and Scale Your Scanner Over Time
Your first few runs of the scanner will likely surface some false positives, which is normal. Use the PostgreSQL database to flag these false positives, then refine your Semgrep custom rules and the LLM prompt to reduce noise over time. As new, more capable small code LLMs are released in 2026, you can update the Ollama model with a single command (ollama pull llama4:8b-instruct) to improve detection rates without changing any of your integration code. You can also extend the scanner to support additional languages (Python, Go, Rust) by adding the relevant Semgrep rules and adjusting the LLM prompt for each language's syntax.
Final Thoughts
Building a self-hosted AI-powered code security scanner for your Node.js and Bun.js apps has never been easier, thanks to mature open-source tools like Semgrep, Ollama, and PostgreSQL, and seamless CI/CD integration with GitHub Actions. Unlike SaaS alternatives, this setup gives you full control over your code privacy, customizable detection for your team's unique needs, and higher vulnerability detection rates that catch both known and never-before-seen threats. Start with a small pilot project for one of your lower-risk repositories, tune the rules and prompts to your codebase, and scale as needed to cover your entire portfolio.
Enjoyed this article?
Read More Articles