Container registries contain thousands of application images, many of which contain outdated base dependencies and hidden vulnerabilities. Incorporating security verification during the building of container images is critical for maintaining infrastructure integrity. Running Trivy against built images before pushing them to remote registries prevents high-severity Common Vulnerabilities and Exposures (CVEs) from reaching orchestration systems like Kubernetes. This guide details how to implement an automated container scanning pipeline using TypeScript and Bun, interacting directly with the local container runtime socket to audit newly built layers.
Runtime Socket Container Auditing Mechanics
To scan container images during a continuous integration build, the scanning agent must communicate directly with the local container engine. Rather than exporting the image to a tarball file—which creates high I/O overhead and increases pipeline latency—we can interface with the local Unix socket of the container runtime (e.g., /var/run/docker.sock or /run/containerd/containerd.sock).
By mounting the socket into the execution space of the pipeline task, Trivy query mechanisms can locate the image directly in the local cache, perform static analysis on its configuration manifest, inspect the filesystem layers, and parse package manager manifests.
┌────────────────────────────────────────────────────────┐
│ CI Runner Host │
│ │
│ ┌───────────────────────┐ Talks via Socket │
│ │ TypeScript Pipeline │ ──────────────────────────┐ │
│ │ (Bun Script) │ │ │
│ └───────────────────────┘ ▼ │
│ ┌───────────────────┐
│ │ Docker Daemon │
│ ┌───────────────────────┐ │ (/var/run/docker. │
│ │ Trivy Process │ ◄───────────────┤ sock) │
│ │ (image --severity...) │ Spawns Trivy └─────────┬─────────┘
│ └───────────────────────┘ │
│ ▼
│ ┌───────────────────┐
│ │ Local Image Cache│
│ │ (Dynamic Layers) │
│ └───────────────────┘
└────────────────────────────────────────────────────────┘
Using Bun as the execution runtime allows pipeline steps to run with lower memory overhead and faster execution start times than conventional Node.js environments. By leveraging Bun’s native Bun.spawn interface, the pipeline script coordinates the lifecycle of the Trivy binary, processes raw JSON output streams efficiently, filters vulnerability findings based on target severity metrics, and decides whether to halt the pipeline.
TypeScript/Bun Socket Scan Runner
The script below verifies the availability of the local Docker daemon socket via custom HTTP over TCP/Unix sockets, executes a targeted Trivy scan using native Bun process management, parses the findings into structured TypeScript objects, and filters alerts based on severity and CVSS scores.
import { spawn } from "bun";
import { connect } from "node:net";
import { existsSync } from "node:fs";
// Define TypeScript structures for Trivy scanning reports
export interface TrivyVulnerability {
VulnerabilityID: string;
PkgName: string;
InstalledVersion: string;
FixedVersion: string;
Severity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
Title?: string;
Description?: string;
CVSS?: Record<string, { V3Score?: number }>;
}
export interface TrivyResult {
Target: string;
Class: string;
Type: string;
Vulnerabilities?: TrivyVulnerability[];
}
export interface TrivyScanOutput {
SchemaVersion: number;
Results?: TrivyResult[];
}
export interface FilteredFinding {
cveId: string;
packageName: string;
installedVersion: string;
fixedVersion: string;
severity: string;
cvssScore: number;
title: string;
}
export interface ScanReportSummary {
imageName: string;
scannedAt: string;
totalVulnerabilities: number;
criticalCount: number;
highCount: number;
passed: boolean;
findings: FilteredFinding[];
}
const DOCKER_SOCKET_PATH = "/var/run/docker.sock";
/**
* Validates access to the container runtime socket using a raw HTTP-over-Unix-socket ping.
*/
async function verifyDockerSocket(socketPath: string): Promise<boolean> {
return new Promise((resolve) => {
if (!existsSync(socketPath)) {
resolve(false);
return;
}
const client = connect(socketPath, () => {
client.write("GET /_ping HTTP/1.1\r\nHost: localhost\r\n\r\n");
});
client.on("data", (data) => {
const response = data.toString();
if (response.includes("200 OK") || response.includes("OK")) {
resolve(true);
} else {
resolve(false);
}
client.end();
});
client.on("error", () => {
resolve(false);
});
});
}
/**
* Spawns the Trivy CLI process and parses its standard output.
*/
async function executeTrivyScan(imageName: string): Promise<TrivyScanOutput> {
const trivyArgs = [
"image",
"--format",
"json",
"--quiet",
"--severity",
"HIGH,CRITICAL",
imageName
];
const process = spawn({
cmd: ["trivy", ...trivyArgs],
stdout: "pipe",
stderr: "inherit"
});
const stdoutText = await new Response(process.stdout).text();
const exitCode = await process.exited;
if (exitCode !== 0 && stdoutText.trim() === "") {
throw new Error(`Trivy process exited with non-zero status: ${exitCode}`);
}
try {
return JSON.parse(stdoutText) as TrivyScanOutput;
} catch (err) {
throw new Error(`Failed to parse Trivy JSON output: ${(err as Error).message}`);
}
}
/**
* Evaluates raw Trivy findings and returns a structured compliance report.
*/
function processScanResults(imageName: string, rawData: TrivyScanOutput): ScanReportSummary {
const findings: FilteredFinding[] = [];
let totalVulnerabilities = 0;
let criticalCount = 0;
let highCount = 0;
if (rawData.Results) {
for (const result of rawData.Results) {
if (!result.Vulnerabilities) continue;
for (const vuln of result.Vulnerabilities) {
totalVulnerabilities++;
if (vuln.Severity === "CRITICAL") {
criticalCount++;
} else if (vuln.Severity === "HIGH") {
highCount++;
}
// Extract CVSS v3 score if available
let score = 0;
if (vuln.CVSS) {
for (const key of Object.keys(vuln.CVSS)) {
const cvssVal = vuln.CVSS[key];
if (cvssVal && typeof cvssVal.V3Score === "number") {
score = Math.max(score, cvssVal.V3Score);
}
}
}
findings.push({
cveId: vuln.VulnerabilityID,
packageName: vuln.PkgName,
installedVersion: vuln.InstalledVersion,
fixedVersion: vuln.FixedVersion || "None",
severity: vuln.Severity,
cvssScore: score,
title: vuln.Title || "No Title Provided"
});
}
}
}
// Define policy: Fail the build if any Critical or High vulnerability is detected
const passed = criticalCount === 0 && highCount === 0;
return {
imageName,
scannedAt: new Date().toISOString(),
totalVulnerabilities,
criticalCount,
highCount,
passed,
findings
};
}
// Main pipeline execution block
(async () => {
const targetImage = Bun.argv[2];
if (!targetImage) {
console.error("Usage: bun run scan-image.ts <target-image-name>");
process.exit(1);
}
console.log(`[+] Initializing security scan for: ${targetImage}`);
const socketActive = await verifyDockerSocket(DOCKER_SOCKET_PATH);
if (!socketActive) {
console.error(`[-] CRITICAL: Cannot access Docker socket at ${DOCKER_SOCKET_PATH}`);
console.error("Verify that the container socket is mounted and permissions are valid.");
process.exit(1);
}
console.log("[+] Docker socket verification successful.");
try {
const rawReport = await executeTrivyScan(targetImage);
const summary = processScanResults(targetImage, rawReport);
console.log("\n=================== SCAN REPORT ===================");
console.log(`Image Name: ${summary.imageName}`);
console.log(`Scanned At: ${summary.scannedAt}`);
console.log(`Status: ${summary.passed ? "PASSED" : "FAILED"}`);
console.log(`Total Issues: ${summary.totalVulnerabilities}`);
console.log(` - CRITICAL: ${summary.criticalCount}`);
console.log(` - HIGH: ${summary.highCount}`);
console.log("===================================================");
if (!summary.passed) {
console.error("\n[-] Security policy violation: Vulnerabilities found.");
console.error(JSON.stringify(summary.findings, null, 2));
process.exit(1);
}
console.log("\n[+] Security scan passed successfully.");
process.exit(0);
} catch (error) {
console.error(`[-] Pipeline processing error: ${(error as Error).message}`);
process.exit(1);
}
})();
Scan Performance & Caching Profiles
Operating dynamic vulnerability scanning in pipeline environments requires balancing scan coverage with execution times. The table below compares scanning performance when pulling from remote image registries versus querying local caches, with variations in cache state configurations.
| Performance Metric | Local Daemon Cache (Cold DB) | Local Daemon Cache (Warm DB) | Remote Private Registry (Cold Cache) | Remote Private Registry (Warm Cache) |
|---|---|---|---|---|
| Scan Initiation Speed | 8.2 s | 0.4 s | 18.5 s | 2.1 s |
| Database Update Overhead | 24.5 s | 0.0 s (reused) | 26.1 s | 0.0 s (reused) |
| Layer Download Latency | 0.0 s | 0.0 s | 54.2 s (1.2GB image) | 0.0 s |
| Analysis Execution Time | 4.8 s | 4.2 s | 5.1 s | 4.5 s |
| Total Pipeline Scan Time | 37.5 s | 4.6 s | 103.9 s | 6.6 s |
| Cache Hit Ratio (OPA/DB) | 0% | 98.2% | 0% | 97.5% |
| Peak Memory Saturation | 420 MB | 310 MB | 550 MB | 380 MB |
What Breaks in Production
Running container vulnerability scanning inside production pipelines requires managing dynamic network configurations, container engine updates, and software packagers. Below are the primary failure states experienced in enterprise scanner setups and their remediations.
1. Database Out-of-Date Failures
Trivy downloads an OCI vulnerability database metadata package when initialized. If the database schema on the scanner runner does not match the format required by the current version of the Trivy binary, Trivy will abort execution. In completely offline or air-gapped VPC build environments, Trivy fails to initialize because it cannot download the DB updates from GitHub container registry endpoints.
Remediation:
- Configure a local database mirror in your enterprise registry (such as Harbor or JFrog Artifactory) to serve as a proxy for Trivy databases.
- Run Trivy with the
--skip-db-updateand--skip-java-db-updateflags during runner operations if you pre-bake the vulnerability databases into your base CI runner images daily. - Implement an automated cron schedule that pulls the Trivy DB, bundles it, and mounts it via shared volumes to the pipeline runners:
# Cron job running in cluster to sync database trivy image --download-db-only --cache-dir /shared/trivy-cache
2. Network Timeouts During Vulnerability DB Downloads
The official Trivy vulnerability database has grown to several hundred megabytes. On virtualized pipeline agents that start with a completely cold state, downloading this database over standard Internet connections can take anywhere from 30 seconds to several minutes. If the pipeline provider imposes strict timeouts per step, minor packet loss or endpoint rate limits can cause build jobs to fail randomly.
Remediation:
- Configure persistent caching folders using your pipeline provider’s caching steps (e.g.,
actions/cachein GitHub Actions) to persist the~/.cache/trivydirectory across pipeline executions. - Use regional container registries to pull the database.
- Utilize the
--db-repositoryflag to point the scanner to a mirror within the same cloud availability zone, bypassing public internet routing bottlenecks and lowering latency values below 5s.
3. Base Image Scanning Bypasses
Developers often use multi-stage Dockerfiles to build minimal production images (e.g., building code in a heavy node base image and copying compiled outputs to a thin alpine or distroless image). If vulnerability scanners only run checks against final, minimal target images, they can miss security issues that exist in build-time dependencies. For example, malicious packages installed during intermediate compilation steps could execute scripts that inject web backdoors into compiled outputs, but these scripts would go undetected if only the final image is scanned.
Remediation:
- Configure the CI build system to execute scan operations against all build stages or target tags.
- Scan intermediate docker cache layers by instructing Trivy to review the application code structure via
trivy fson the repository before building the container image. - Enforce the signing of intermediate layers using signing mechanisms like Cosign, allowing the runner to verify that every layer in the target final image was generated from audited, secure source bases.
FAQs
What is the primary benefit of this design pattern?
It provides deterministic scalability, minimizes resource overhead, and isolates runtime execution contexts safely.
How do we verify the performance improvements?
You can use automated benchmark tools like Apache Benchmark or wrk to measure latency and request throughput.