Security · 40 min

Audit Dependencies for Vulnerabilities

Take a small project with intentionally outdated dependencies, run a vulnerability audit, read the report like a security engineer, fix the vulnerable packages, and confirm the audit comes back clean — the exact "audit" step of the DARE Ralph Loop.

Problem

Every application you ship is built on top of hundreds of pieces of code you did not write: your dependencies, and *their* dependencies (the "transitive" ones you never even chose directly). Any one of them can carry a known, publicly disclosed vulnerability — a CVE (Common Vulnerabilities and Exposures) — that an attacker can exploit the moment your app is reachable on a network. This is not a hypothetical risk. Supply-chain attacks and known-vulnerable dependencies are one of the most common ways real production systems get compromised, precisely *because* they are invisible in day-to-day code review — nobody reads the diff of a `package-lock.json` line by line. The fix is not "trust your dependencies blindly" nor "review every line of every library" (impractical). It is **automated dependency auditing**: tooling that cross-references every package and version in your project against a vulnerability database and tells you, in seconds, exactly which ones are dangerous, how dangerous, and what to do about it. In this lab you will work with a small sample project that has a handful of dependencies pinned to old, vulnerable versions on purpose. You will run an audit tool, learn to read its report (severity, CVE identifier, the dependency path that pulled the vulnerable package in), fix each finding by upgrading or replacing the dependency, and re-run the audit until it is clean. You will finish by connecting this to the DARE method directly: audit is the fourth and final gate of the **Ralph Loop** (build → test → lint → audit), and a task is never DONE while a HIGH/CRITICAL CVE remains open. > This lab is stack-agnostic. Instructions are given for both a Node.js > sample (using `npm audit`) and a Python sample (using `pip-audit`) — pick > whichever stack you already have installed. Only work against your own > local sample project; never scan or attack a project you do not own.

Objectives

By the end of this lab you will be able to:

Prerequisites

What you will build

A tiny sample project (Node.js or Python, your choice) with a handful of
dependencies pinned to old, known-vulnerable versions. You will audit it,
fix it, and prove — with tool output, not guesswork — that it is clean.

Why "audit" is its own Ralph Loop step

The DARE Ralph Loop is build → test → lint → audit. The first three
steps tell you your own code works. Audit tells you whether the code you
are standing on is safe. These are orthogonal concerns:

This is why the DARE rule is blunt: a CVE rated HIGH or CRITICAL in your
dependency tree means the task is FAILED, not DONE, until it's fixed.

Moderate/low findings may sometimes be accepted with a written justification
(e.g. "only reachable in a dev-only code path"), but HIGH/CRITICAL never
gets a pass.

Anatomy of a vulnerability report

Every audit tool reports roughly the same shape of information per finding:

Field Meaning
Package The name of the vulnerable dependency
Installed version The version currently in your lockfile
Vulnerable range The version range affected (e.g. < 4.17.21)
Patched version The first version where the fix landed
Severity low / moderate / high / critical (derived from a CVSS score)
Identifier A CVE ID (e.g. CVE-2021-23337) or advisory ID (e.g. GHSA-...)
Path The chain of dependencies that pulled this package in — is it direct or transitive?

The path is the single most useful field for deciding your fix strategy.
If your package.json lists the vulnerable package directly, you can often
just bump the version yourself. If it is three levels deep inside someone
else's dependency, you may need to update the parent package instead, or
wait for the maintainer to release a patched version.

How to work through this lab

Pick one stack (Node.js or Python) and follow that track through all
five steps — the sample project, the vulnerable dependencies, and the exact
commands differ slightly by stack, but the workflow (audit → read → fix →
re-audit → connect to Ralph Loop) is identical either way.

Steps

  1. Set up a sample project with vulnerable dependencies

    Create a fresh local directory and pin a handful of dependencies to
    old, known-vulnerable versions on purpose. Pick your stack.

    Node.js track

    mkdir audit-lab && cd audit-lab
    npm init -y
    npm install lodash@4.17.15 minimist@1.2.5 axios@0.21.0
    

    These three specific old versions are a deliberate choice:

    • lodash@4.17.15 — prototype pollution (fixed in 4.17.21).
    • minimist@1.2.5 — prototype pollution (fixed in 1.2.6).
    • axios@0.21.0 — Server-Side Request Forgery via redirect handling
      (fixed in later 0.21.x/1.x releases).

    Python track

    mkdir audit-lab && cd audit-lab
    python -m venv .venv
    source .venv/bin/activate   # Windows: .venv\Scripts\activate
    pip install "requests==2.25.1" "urllib3==1.26.4" "Jinja2==2.11.2"
    pip freeze > requirements.txt
    

    These are also deliberately old:

    • requests==2.25.1 and urllib3==1.26.4 — several CVEs around
      proxy/credential handling and retry behavior in that era.
    • Jinja2==2.11.2 — a sandbox-escape class of vulnerability, fixed in
      later 2.11.x/3.x releases.

    Checkpoint

    Confirm the packages actually installed at the old, pinned versions:

    # Node
    npm ls lodash minimist axios
    
    # Python
    pip show requests urllib3 Jinja2
    

    You should see exactly the old version numbers you specified — if npm
    or pip silently resolved something newer, re-pin with the exact
    version syntax shown above.

    Deliverable for this step: a local project directory with a
    lockfile (package-lock.json or requirements.txt) that pins these
    old, vulnerable versions, confirmed by the checkpoint command output.

  2. Run the audit and read the raw report

    Now run the audit tool for your stack and look at the raw, unfiltered
    output before doing anything else — resist the urge to jump straight to
    --fix. You need to be able to read this report by hand.

    Node.js track

    npm audit
    

    You will see, for each finding, something like:

    # npm audit report
    
    lodash  <4.17.21
    Severity: high
    Prototype Pollution in lodash - https://github.com/advisories/GHSA-...
    fix available via `npm audit fix`
    node_modules/lodash
    
    axios  <=0.21.1
    Severity: moderate
    Server-Side Request Forgery in axios - https://github.com/advisories/GHSA-...
    fix available via `npm audit fix`
    node_modules/axios
    
    2 vulnerabilities (1 moderate, 1 high)
    

    For a machine-readable version (useful for CI or scripting):

    npm audit --json > audit-report.json
    

    Python track

    pip-audit -r requirements.txt
    

    Output looks like:

    Name     Version  ID                  Fix Versions
    -------- -------- ------------------- ------------
    requests 2.25.1   PYSEC-2021-136      2.25.1... (see fix versions)
    urllib3  1.26.4   PYSEC-2021-108      1.26.5
    Jinja2   2.11.2   PYSEC-2021-66       2.11.3
    

    For JSON output:

    pip-audit -r requirements.txt -f json > audit-report.json
    

    Read every column before you touch anything

    For each finding in your report, write down (on paper or in a
    scratch file) these four things:

    1. Package + installed version — what you actually have.
    2. Severity — low / moderate / high / critical.
    3. Identifier — the CVE or GHSA/PYSEC ID, so you could look up the
      full advisory if you needed more detail.
    4. Is it direct or transitive? Check your package.json/requirements.txt
      — if the package is listed there by name, it's direct; if it only
      shows up in the lockfile pulled in by something else, it's transitive.

    Checkpoint

    You should be able to answer, without re-running the tool: "Which
    finding is the most severe, and is it a package I installed directly or
    one that came along for the ride?" If you cannot answer that from your
    notes, re-read the report.

    Deliverable for this step: a short written list of every finding
    (package, severity, ID, direct/transitive) plus the saved JSON report
    file.

  3. Fix each finding — upgrade, auto-fix, or replace

    There are three fix strategies, in order of preference. Try them in
    this order for each finding.

    Strategy 1 — automated fixer

    The audit tools ship a fixer that bumps to the minimum patched version
    that satisfies your existing semver range, and re-installs.

    # Node
    npm audit fix
    
    # Python has no direct equivalent; upgrade manually (Strategy 2)
    

    npm audit fix is safe for patch/minor bumps within your declared
    range. It will not cross a major version bump on its own (that
    needs npm audit fix --force, which you should treat as a real
    upgrade — read the changelog first, it can be a breaking change).

    Strategy 2 — manual version bump

    When the fixer can't reach far enough, or you're on Python, bump the
    version explicitly to (or past) the patched version from the report.

    # Node — bump a direct dependency past the patched version
    npm install lodash@^4.17.21 axios@^0.21.4
    
    # Python — edit requirements.txt, then reinstall
    pip install --upgrade "requests>=2.31.0" "urllib3>=1.26.18" "Jinja2>=3.1.3"
    pip freeze > requirements.txt
    

    Always re-read your project's actual code after a version bump —
    especially across a major version — to check for breaking API
    changes. This is where your test suite earns its keep (see the
    checkpoint below).

    Strategy 3 — replace the dependency

    Sometimes there is no patched version (the package is abandoned) or the
    patched version requires a runtime you can't yet adopt. In that case:

    • Look for an actively maintained fork or an alternative package that
      covers the same functionality.
    • If the vulnerable code path isn't actually reachable from your
      application (e.g. a dev-only tool), you may vendor a patch or accept
      the risk with a written justification — never silently.

    Fix your lab's findings now

    Apply the fixer/upgrade for every package you audited in Step 2. For
    this lab's specific packages:

    # Node
    npm audit fix
    npm install lodash@^4.17.21   # if audit fix didn't reach it
    
    # Python
    pip install --upgrade "requests>=2.31.0" "urllib3>=1.26.18" "Jinja2>=3.1.3"
    pip freeze > requirements.txt
    

    Checkpoint

    After every fix, run whatever passes for "tests" in your sample
    project (even a trivial smoke script that imports/requires each
    package and calls one function) to confirm nothing broke:

    # Node
    node -e "console.log(require('lodash').chunk([1,2,3,4],2))"
    
    # Python
    python -c "import requests, jinja2; print('ok')"
    

    A clean audit that broke your app is not actually done — it just moved
    the problem. This is exactly why "audit" sits after "test" in the
    Ralph Loop, not before: you re-run test after every fix, not just at
    the very end.

    Deliverable for this step: updated lockfile/requirements.txt
    with patched versions, plus confirmation the smoke check still passes.

  4. Re-run the audit and confirm a clean report

    Run the exact same audit command from Step 2 again. This is the
    verification step — you are not done because you think you fixed it,
    you are done because the tool says so.

    # Node
    npm audit
    
    # Python
    pip-audit -r requirements.txt
    

    Expected clean output:

    # npm audit report
    found 0 vulnerabilities
    
    No known vulnerabilities found
    

    If a finding remains

    • Re-check severity. If it's HIGH/CRITICAL, go back to Step 3 —
      this is not optional under the DARE Ralph Loop rule.
    • If it's moderate/low and no fix exists yet, this is the one case
      where you may proceed — but only with a written, dated note
      explaining: which package, which CVE, why it's not reachable or why
      the risk is accepted, and a reminder to re-check next time you touch
      dependencies. Never let an accepted-risk note go unwritten — an
      undocumented "I'll deal with it later" is how vulnerabilities survive
      for years.

    Set the CI gate for real

    In a real project, this check should run automatically and fail the
    build on HIGH/CRITICAL, so no one can merge a newly-vulnerable
    dependency by accident:

    # Node — non-zero exit if HIGH or above is found
    npm audit --audit-level=high
    
    # Python — pip-audit exits non-zero on any finding by default
    pip-audit -r requirements.txt
    

    Try running npm audit --audit-level=high yourself right now and
    check its exit code:

    npm audit --audit-level=high; echo "exit code: $?"
    

    An exit code of 0 means clean; anything else means the audit gate
    would block a CI pipeline — exactly the behavior you want.

    Checkpoint

    You should now have:

    • A "before" report (from Step 2) showing at least one HIGH/moderate
      finding.
    • An "after" report showing zero HIGH/CRITICAL findings (or a
      documented accepted-risk note for anything remaining).
    • A command (npm audit --audit-level=high or pip-audit) that exits
      0 — the exact command you would wire into CI.

    Deliverable for this step: the clean audit output, saved
    side-by-side with your Step 2 "before" report, plus the CI-gate command
    and its exit code.

  5. Wire audit into the DARE Ralph Loop (submission)

    You have practiced the full workflow by hand. Now formalize it as the
    fourth step of your Ralph Loop, the way a real DARE task would define
    it in its Definition of Done.

    Write the Ralph Loop command sequence

    For your sample project, write out the four commands a task's DoD
    would require, in order — this is exactly what an AI executing a DARE
    task would run before marking anything DONE:

    # 1. Build
    # (Node) — no compile step needed for plain JS, or: npm run build
    # (Python) — python -m py_compile *.py, or your framework's build step
    
    # 2. Test
    # (Node) — npm test
    # (Python) — python -m pytest
    
    # 3. Lint
    # (Node) — npx eslint .
    # (Python) — ruff check .
    
    # 4. Audit  <-- what you practiced in this lab
    npm audit --audit-level=high
    # or
    pip-audit -r requirements.txt
    

    Write the DoD rule in plain language

    Add this line to a NOTES.md (or your project's TASKS.md, if you
    have one) — it's the rule you'll now apply to every future task:

    ## Definition of Done — dependency audit
    
    A task is DONE only if:
    - `npm audit --audit-level=high` (or `pip-audit`) exits 0, AND
    - Any remaining moderate/low finding has a written, dated justification
      explaining why it is not reachable or why the risk is accepted.
    
    A HIGH or CRITICAL finding anywhere in the dependency tree means the
    task is FAILED, regardless of how complete the feature code is.
    

    Why this belongs at the end of the loop, not the start

    Audit runs last because it should gate everything else — there is no
    point auditing dependencies before your code even builds. But it must
    still run on every task that touches dependencies, not just once at
    project setup: a new PR that adds a single new package is a new audit
    surface, and skipping it is how vulnerable dependencies slip into
    "trusted" codebases.

    Submission criteria

    Submit when all of the following hold:

    1. A before/after pair of audit reports for your sample project (Step 2
      and Step 4), showing at least one HIGH or moderate finding fixed.
    2. The exact CI-gate command for your stack (npm audit --audit-level=high
      or pip-audit -r requirements.txt) with a confirmed 0 exit code.
    3. A short NOTES.md with the Ralph Loop's four-command sequence and
      the Definition of Done rule for dependency audits, written in your
      own words.
    4. If any finding remains unresolved, a dated, written justification
      for it — never a silently ignored finding.