Skip to content
DevOps devops security 5 min read

Scanning for Vulnerabilities

Every piece of software you ship is built on top of other people’s code: the libraries your app imports, the base image your container starts from, and the packages installed on your Ubuntu server. Any of those can contain a known security flaw that an attacker can exploit. Vulnerability scanning is the practice of automatically checking all of those layers against public databases of known flaws, so you find problems before an attacker does. This page shows you how to scan dependencies, container images, and servers, and how to wire scans into your pipeline so they run automatically.

What is a CVE?

A CVE (Common Vulnerabilities and Exposures) is a unique ID for one publicly known security flaw, for example CVE-2024-3094. When a researcher finds a bug in a library or program, it gets a CVE number so everyone refers to the same issue. Scanners work by listing the exact versions of software you use, then looking each one up in CVE databases to see if any known flaws match.

Each CVE also has a severity score from the CVSS (Common Vulnerability Scoring System), a 0-10 number where higher means more dangerous. You will not have time to fix everything at once, so severity is how you decide what to fix first.

SeverityCVSS scoreWhat to do
Critical9.0-10.0Fix today, especially if internet-facing
High7.0-8.9Fix this week
Medium4.0-6.9Schedule into the next sprint
Low0.1-3.9Fix opportunistically

Severity is not the whole story. A “critical” flaw in a library you never actually call may be less urgent than a “high” flaw on your public login page. Always ask: is this code path reachable, and is it exposed to the internet?

Shift-left means moving security checks earlier (“left”) in your workflow, so you catch flaws on your laptop or in CI rather than in production. That is the goal of everything below.

Scanning your dependencies

Your application code pulls in third-party libraries, and those are the most common source of vulnerabilities. Most package managers have a built-in scanner.

For a Node.js project, use npm audit:

npm audit

Output:

# npm audit report

axios  <1.6.0
Severity: high
Server-Side Request Forgery in axios - https://github.com/advisories/GHSA-8hc4-vh64-cxmj
fix available via `npm audit fix`
node_modules/axios

1 high severity vulnerability

To apply safe fixes automatically (upgrades that will not break compatibility):

npm audit fix

Other ecosystems have equivalents. Use the one that matches your project:

EcosystemCommandNotes
Node.jsnpm auditBuilt into npm
Pythonpip-auditInstall with pip install pip-audit
Gogovulncheck ./...Only flags code paths you actually call
Any / generictrivy fs .Scans the whole project folder

When to use this: run a dependency scan on every project, every time you update packages. When NOT to: do not block on npm audit warnings for dev-only dependencies that never ship to production.

Scanning container images with Trivy

If you build Docker images, the base image (like node:20) brings along an entire mini operating system, and those OS packages can have CVEs too. Trivy is a free, fast scanner that checks both the OS packages and your app dependencies inside an image.

Install Trivy on Ubuntu 22.04/24.04:

sudo apt-get install -y wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install -y trivy

Now scan an image. The --severity flag filters the noise so you only see what matters:

trivy image --severity HIGH,CRITICAL node:20

Output:

node:20 (debian 12.5)
Total: 23 (HIGH: 21, CRITICAL: 2)

┌──────────────┬────────────────┬──────────┬───────────────────┬───────────────┐
│   Library    │ Vulnerability  │ Severity │ Installed Version │ Fixed Version │
├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┤
│ libssl3      │ CVE-2024-0727  │ HIGH     │ 3.0.11-1          │ 3.0.13-1      │
│ zlib1g       │ CVE-2023-45853 │ CRITICAL │ 1:1.2.13.dfsg-1   │               │
└──────────────┴────────────────┴──────────┴───────────────────┴───────────────┘

A blank “Fixed Version” means there is no patch yet, so you cannot fix it by upgrading. The usual cure for image CVEs is to rebuild on a newer or slimmer base image (for example node:20-slim or node:20-alpine), then re-scan.

Scanning the server itself with Lynis

Your Ubuntu server has its own configuration and installed packages. Lynis is an auditing tool that checks the running system for misconfigurations and missing hardening, and gives you a hardening score.

sudo apt update
sudo apt install -y lynis
sudo lynis audit system

Output:

[+] Hardening
    - Installed compiler(s)                        [ FOUND ]
    - Hardening index : 67 [############        ]

  Warnings (2):
  ! No password set for single user mode [AUTH-9308]
  ! iptables module(s) loaded, but no rules active [FIRE-4513]

  Suggestions (15):
  * Install a file integrity tool to monitor changes [FINT-4350]

Read the warnings and suggestions, fix what applies, then re-run to watch the hardening index climb. When to use this: run Lynis after first setting up a server and then monthly. It pairs naturally with the steps in server hardening.

Wiring scans into CI

The real power of scanning is automation: run it on every commit so a vulnerable dependency can never quietly merge. Here is a GitHub Actions workflow that fails the build if Trivy finds a HIGH or CRITICAL flaw. Put it in .github/workflows/security.yml:

name: Security scan
on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Audit npm dependencies
        run: npm audit --audit-level=high

      - name: Scan filesystem with Trivy
        uses: aquasecurity/[email protected]
        with:
          scan-type: fs
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: "1"

The exit-code: "1" line is what turns a finding into a failed build. Without it, the scan reports problems but lets the pipeline pass, which defeats the point.

Be careful turning scans into hard build-blockers on day one. A brand-new repo can surface dozens of pre-existing CVEs and block every merge. Start in report-only mode, clear the backlog, then flip exit-code to "1" so only new issues fail the build.

Best Practices

  • Scan all three layers: app dependencies, container images, and the server OS. A clean npm audit says nothing about your base image.
  • Filter to HIGH and CRITICAL first so the output is actionable instead of a wall of low-priority noise.
  • Automate scanning in CI with a failing exit code, so vulnerable code cannot merge unnoticed.
  • Update your scanner’s vulnerability database regularly (Trivy auto-updates; for Lynis run apt upgrade lynis) or you will miss newly published CVEs.
  • Prefer slim base images (-slim, -alpine, or distroless) to shrink the number of OS packages that can ever be vulnerable.
  • Track and triage findings rather than ignoring them; a known unpatched CVE on an internet-facing service is an open door.
Last updated June 15, 2026
Was this helpful?