🚀 Executive Summary

TL;DR: Security scanners frequently block deployments by flagging CVEs in development-only packages that are not part of the final production artifact. This common issue can be effectively resolved through technical solutions like multi-stage Dockerfiles or by establishing formal risk acceptance processes with security teams.

🎯 Key Takeaways

  • Security scanners often misinterpret the build lifecycle, flagging vulnerable `devDependencies` present during the build environment but absent from the final production container.
  • Implementing a multi-stage Dockerfile is the most robust technical solution, creating a clean separation between build-time tools and the lean production image, ensuring only relevant dependencies are scanned.
  • The ‘Diplomatic Solution’ involves leveraging scanner ignore/suppression features (e.g., `.snyk` file) and formalizing risk acceptance with the security team, providing an auditable process for managing dev-only CVEs.

Summary: Security scanners flagging unused dev packages can halt deployments. Learn three practical fixes—from quick hacks to robust architectural changes—to resolve false positive CVEs and get your pipeline flowing again.

Security Blocked Your Deploy? How to Exorcise Phantom CVEs from Your Pipeline

It’s 10 PM on a Thursday. The ‘go-live’ for our new microservice, `auth-service-v2`, is in 30 minutes. Everything is green on the `ci-runner-prod-03`… until it isn’t. A red alert from our scanner pops up in Slack: Deployment Blocked – Critical CVE-2022-XXXX in `some- obscure-parser` v1.2.0. My heart sinks. We don’t directly use that package. After a frantic 20 minutes of `grep`’ing through the codebase, I find the culprit. It’s a dependency of a dependency… of a linting tool we only use at build time. It never gets close to the final production container running on `prod-app-cluster-01`. The deployment was blocked by a ghost, and the security team, following their playbook, put up a hard stop. If you’re in DevOps, you’ve lived this nightmare.

So, What’s Actually Happening Here? The “Why” Behind the Ghost

This isn’t just a scanner bug; it’s a misunderstanding of the build lifecycle. Most modern projects have two types of dependencies:

  • Production Dependencies: The code your application actually needs to run. The engine of the car. (e.g., Express, Django, a database driver).
  • Development Dependencies: The tools you need to build, test, and package your application. The wrenches, lifts, and diagnostic tools in the garage. (e.g., Jest, ESLint, Webpack, Nodemon).

The problem arises when your security scanner runs at a stage where both sets of dependencies are present. It scans your `package-lock.json` or your build environment’s `node_modules` directory, sees the vulnerable dev package, and slams on the brakes. It has no context to know that the offending code is just a build-time tool that gets thrown away before the final artifact is created. It sees a threat and acts accordingly.

Let’s fix it. Here are three ways to handle this, from a quick patch to a permanent architectural solution.

Solution 1: The Quick Fix (The “–production” Hammer)

This is the “I need to get this deployed 5 minutes ago” solution. The idea is to force your package manager to install only the production dependencies before the security scan step in your CI/CD pipeline. It’s a blunt instrument, but it often works.

For a Node.js project, you’d modify your build script. Instead of a simple `npm install`, you’d use a flag to omit the dev dependencies:


# In your CI YAML file (e.g., .gitlab-ci.yml)

- npm ci --omit=dev # For newer NPM versions
# or
- npm install --production # For older NPM versions
- run_security_scan.sh

Warning: This approach is hacky. If any part of your actual build process (like transpiling TypeScript or bundling with Webpack) relies on a `devDependency`, this will break your build entirely. Use it to get unblocked, but don’t stop here.

Solution 2: The Permanent Fix (The “Multi-Stage Dockerfile” Scalpel)

This is the right way to solve the problem for containerized applications. A multi-stage Dockerfile creates a clean, definitive separation between your build environment and your final production image. The security scanner is then configured to only scan the lean, final image.

Here’s what it looks like in practice:


# Stage 1: The "Builder" - where all the dev tools live
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files and install ALL dependencies, including dev
COPY package.json package-lock.json ./
RUN npm ci

# Copy the rest of the source code
COPY . .

# Run tests, linting, and build the application
RUN npm run test
RUN npm run build

# --- This is the critical separation ---

# Stage 2: The "Production" Image - lean and clean
FROM node:18-alpine

WORKDIR /app

# Only copy the production dependencies definition
COPY package.json package-lock.json ./
# Install ONLY production dependencies
RUN npm ci --omit=dev

# Copy the built application artifacts from the "builder" stage
COPY --from=builder /app/dist ./dist

# Expose port and define the run command
EXPOSE 3000
CMD [ "node", "dist/main.js" ]

With this setup, your security scanner (like Trivy or Snyk) scans the final, small image. The vulnerable linting package from the `builder` stage is long gone. It never existed in the final artifact. Problem solved, permanently.

Solution 3: The ‘Nuclear’ Option (The Diplomatic Solution)

Sometimes, the problem isn’t the code; it’s the process. This solution involves working with your security team, not around them. The goal is to tune the scanner and establish a formal process for accepting certain risks.

Your steps are:

  1. Isolate and Identify: Prove that the CVE is in a dev-only dependency that is not present in the final production artifact. Use the multi-stage build as your evidence.
  2. Use Scanner Features: Most modern security scanners have ignore/suppression features. For example, with Snyk, you can create a `.snyk` file to ignore specific vulnerabilities with a justification and an expiration date.
  3. 
    # .snyk file example
    # Snyk policy file, patches or ignores known vulnerabilities
    version: v1.2.3
    ignore:
      'npm:some-obscure-parser@1.2.0':
        - CVE-2022-XXXX:
            reason: 'This is a dev dependency used only for code linting and is not present in the production Docker image.'
            expires: '2024-12-31T00:00:00.000Z'
    
  4. Formalize Risk Acceptance: Get this policy checked into your source control. Create a ticket (e.g., in Jira) that links to the pull request where the ignore rule was added. Get formal sign-off from the security team on that ticket. This creates an audit trail.

Pro Tip: This path requires building trust with your security team. Come to them with a solution, not just a complaint. Show them your multi-stage Dockerfile and explain why the risk is mitigated. This turns an adversarial relationship into a collaborative one.

Comparing The Solutions

Solution Speed to Implement Robustness Effort Level
1. The “–production” Hammer Very Fast (Minutes) Low (Brittle) Low
2. The Multi-Stage Dockerfile Moderate (Hours) High (Best Practice) Medium
3. The Diplomatic Solution Slow (Days/Weeks) High (Process-driven) High (Requires collaboration)

Ultimately, a blocked deployment over a phantom CVE is a symptom of a process problem. While a quick fix might get you through the night, investing in a proper multi-stage build (Solution 2) is the true engineering solution. And building a good process with your security team (Solution 3) will pay dividends for years to come. Don’t let the ghosts win.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why do security scanners flag CVEs in packages not used in production?

Scanners typically run at a stage where both production and development dependencies are present (e.g., `node_modules` or `package-lock.json`), lacking the context to know that `devDependencies` are build-time tools discarded before the final production artifact is created.

âť“ How do the different solutions for phantom CVEs compare in terms of robustness and effort?

The ‘–production’ flag is a very fast, low-effort but low-robustness hack. Multi-stage Dockerfiles offer moderate implementation time, medium effort, and high robustness as a best practice. The ‘Diplomatic Solution’ is slow, high-effort, but provides high robustness through process-driven risk acceptance and collaboration.

âť“ What is a common implementation pitfall when using the ‘–production’ flag for a quick fix?

A common pitfall is that if any part of your actual build process (like transpiling TypeScript or bundling with Webpack) relies on a `devDependency`, using `npm install –production` or `npm ci –omit=dev` will break your build entirely.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading