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.
| Severity | CVSS score | What to do |
|---|---|---|
| Critical | 9.0-10.0 | Fix today, especially if internet-facing |
| High | 7.0-8.9 | Fix this week |
| Medium | 4.0-6.9 | Schedule into the next sprint |
| Low | 0.1-3.9 | Fix 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:
| Ecosystem | Command | Notes |
|---|---|---|
| Node.js | npm audit | Built into npm |
| Python | pip-audit | Install with pip install pip-audit |
| Go | govulncheck ./... | Only flags code paths you actually call |
| Any / generic | trivy 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-codeto"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 auditsays 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.