The DockSec Series, Part 2: Inside DockSec—Architecture and Pipeline
7:05
author photo
By Advait Patel
Tue | Jul 14, 2026 | 5:22 AM PDT

In Part 1 of this series, we argued that container security fails at the last mile: detection is mature, but turning findings into fixes is not. This article opens the hood to show how DockSec closes that gap. Understanding the architecture is not academic—it explains why the tool behaves the way it does, where you can extend it, and why the same scan can produce a terminal summary, a JSON payload, a SARIF file, and a PDF report all from one run.

A four-stage pipeline 

At the highest level, DockSec runs a four-stage pipeline: scan, analyze, recommend, report. Trivy, Hadolint, and Docker Scout do the scanning locally. An LLM pass correlates and explains the findings. A scoring stage produces a 0-100 posture number.

A reporting stage emits the results in whatever formats you asked for. What makes this composable rather than a tangle is that every stage reads from and writes to a single shared data structure.

The results dict: One contract to rule them all 

The most important design decision in DockSec is also the least glamorous: there is exactly one results dictionary that flows through the whole system, and every component agrees on its shape.

The scanner produces this dict. It carries the Dockerfile lint results, the image scan results, and—the key field—<json_data>, a list of normalized vulnerability records. Each record has a stable shape regardless of which scanner produced it: a vulnerability ID, the target, the package name and installed version, a severity, a title and description, a status, a CVSS score, and a reference URL. Trivy findings are filtered into this shape; Docker Compose misconfiguration findings reuse the exact same shape with an added remediation field.

Because everything downstream consumes this one contract, the scoring calculator, the report generators, the SARIF writer, and the <--json> output do not need to know anything about Trivy's or Hadolint's native formats. They read <json_data> and the AI findings and do their job. Add a new scanner tomorrow and, as long as it emits records in this shape, the entire reporting and scoring stack works unchanged. This is why the codebase can support five output formats without five times the complexity.

The scanning layer

DockerSecurityScanner is the workhorse. It wraps three tools, each with a distinct job:

  • Trivy enumerates known CVEs in the image’s OS and language packages. It is the primary source of vulnerability findings and honors the severity filter you pass.

  • Hadolint lints the Dockerfile itself against a large rule set—use COPY instead of ADD, pin package versions, avoid running as root, and so on. These are the best-practice findings.

  • Docker Scout provides an image-versus-base-image comparison and suggests updated base images, which is often the single highest-leverage fix.

The scanner also owns a results cache keyed on the image name and the requested severity, so repeated scans of the same image at the same severity are fast, while a scan at a different severity correctly triggers a fresh run rather than serving stale data.
The AI layer

When an API key and provider are configured, the CLI runs an AI pass. It loads the Dockerfile, truncates it to a token-sensible size, and feeds it to the configured model through a structured-output chain. "Structured output" is the important phrase: the model is not asked for free-form prose. It is asked to populate a defined schema with fields for vulnerabilities, best practices, security risks, exposed credentials, and remediation steps. The result is predictable, parseable, and mergeable back into the results dict as <ai_findings>.

Provider handling is abstracted behind a single <get_llm()> factory. OpenAI uses JSON mode for structured output; Anthropic, Google, and Ollama use tool-calling, which LangChain selects automatically. Sensible model defaults are applied per provider, and newer Claude and Gemini models that no longer accept a temperature parameter are handled transparently. From the user’s perspective, switching providers is one flag; the complexity is contained in one function.

The scoring layer

DockSec produces a single 0-100 security score, and there are two paths to it.

When an LLM is available, the model can produce a holistic score from a summary of the findings. When it is not—in <--scan-only> mode, or with <--skip-ai-scoring>—a local, deterministic calculator takes over. The local score is a weighted blend of three axes: the Dockerfile quality (from lint results), the vulnerability burden (a severity-weighted deduction over <json_data>), and a configuration score derived by reading the Dockerfile directly and deducting for concrete misconfigurations: running as root, credential-looking <ENV> variables, unpinned base images, missing health checks, sensitive exposed ports, <ADD> over <COPY>, and privileged flags.

The local calculator is deliberately transparent and tunable, and it has been hardened based on real testing. Hardcoded credentials, for example, now cap the overall score regardless of how the rest of the blend comes out— because shipping a plaintext secret in an image is not a middling problem to be averaged away. Part 5 returns to scoring in depth.

The reporting layer

A single ReportGenerator is the canonical writer for JSON, CSV, PDF, and HTML. It runs silently and returns the paths it wrote; the CLI then renders one clean summary rather than interleaving progress bars with scan output. HTML uses a template with placeholder substitution. PDF routes all text through a sanitizer so that bullets, smart quotes, em dashes, and emoji in vulnerability titles never crash generation. SARIF is written by the same generator but is opt-in and independent of the <--format> bundle, because it targets CI and code-scanning rather than human reading.

The reason all of these can coexist is, again, the shared results dict. Each writer is a pure function from that dict to a file.

How the CLI ties it together

<cli.py> is the orchestrator. It parses arguments, resolves the provider and severity once, decides the mode—full analysis, AI-only, scan-only, image-only, or compose—and sets three booleans: run AI, run scan, run compose. From there it invokes the AI pass and the scan pass, merges their outputs into the one results dict, computes the score, generates the requested reports, and renders the summary. Exit codes are honest: a clean run exits 0, a triggered gate exits 1, a usage error exits 2, and a tool or runtime failure—including a failed AI pass—exits 3, so a broken pipeline never masquerades as a passing one.

How the CLI ties it together

Three practical consequences fall out of this design:

  1. Extensibility: Because of the single results contract, adding a scanner or an output format is a local change, not a rewrite.

  2. Honesty: The separation of scan, score, and report means the score reflects real findings and the exit code reflects real outcomes.

  3. Control: The provider abstraction and the scan-only path mean you decide where your data goes and whether an external model is involved at all.

With the architecture clear, Part 3 gets hands-on: We will scan a deliberately vulnerable Dockerfile, an image, and a Compose stack, and read the output line by line.

This is the second in a five-part series. Watch for coming installments on Tuesdays.

Comments