Build a Dependency Vulnerability Scanner

Build a CLI tool in Python that scans npm packages for known vulnerabilities.

Introduction

30 Second Summary

Every app you use relies on dozens of third-party packages written by strangers on the internet. If even one of those packages has a known security flaw, attackers can exploit it to break into the systems that depend on it.

In this project, you will build a command-line tool in Python that scans a project's dependencies against a real vulnerability database and reports which ones have known security issues.

What You'll Build

A Python CLI tool called dep-audit that reads package.json files, checks each dependency against the OSV vulnerability database, and outputs a formatted security report in your terminal.

By the end of this project, you'll have:

  • A working vulnerability scanner that checks npm packages against a real security database used by professionals worldwide.
  • Hands-on experience calling a REST API with Python and processing JSON responses.
  • A reusable security tool you can run against any project's package.json to check for known vulnerabilities.
  • Secret Mission: Extend your scanner to support Python's requirements.txt files, making it a multi-ecosystem tool.

Are there any prerequisites?

None. This guide walks you through installing Python and setting up everything from scratch. No prior coding or security experience is required.

Set Up Python and Your Project Folder

Before you can build your vulnerability scanner, you need the right tools on your machine. Python is the language security professionals use for tooling, and it comes with everything you need to talk to web APIs and process data.

In this step, you will install Python, create a dedicated project folder, and set up an isolated environment for your scanner's dependencies.

In this step, get ready to:
  • Install Python 3.14 and verify it works.
  • Create the project folder and a virtual environment.
  • Install the requests library for making API calls.
Install Python on Windows

Python is the engine that runs your scanner script. You need version 3.14 or higher installed and accessible from your terminal.

  • Open your terminal by pressing the Windows key, typing Terminal, and pressing Enter.
  • Check if Python is already installed by running this command:
python --version

✔️ I see version 3.14 or higher

You are all set. Python is installed and ready to go. Skip ahead to the next substep.

ⓧ I see an older version

You have Python installed, but your version is too old for this project. The new installer will replace your current version.

  • Open your browser and go to python.org/downloads.
  • Click the large Download Python 3.14 button at the top of the page.
  • Run the downloaded installer.
  • On the first screen of the installer, check the box that says Add python.exe to PATH.
  • Click Install Now.
  • Close your current terminal and open a brand new one.
  • Run python --version again to confirm it now shows 3.14.

Why a new terminal?

Your terminal loads the PATH variable when it first opens. If you installed Python while the terminal was already open, it will not know about the new installation until you restart it.

ⓧ Command not found

Python is not installed on your machine yet. Follow these steps to install it.

  • Open your browser and go to python.org/downloads.
  • Click the large Download Python 3.14 button at the top of the page.
  • Run the downloaded installer.
  • On the first screen of the installer, check the box that says Add python.exe to PATH. This is critical.
  • Click Install Now.
  • Once the installer finishes, close your terminal and open a brand new one.
  • Run python --version to confirm it shows 3.14.

Still seeing 'command not found' after installing?

Make sure you checked Add python.exe to PATH during installation. If you missed it, run the installer again and select Modify, then check the PATH box.

Also confirm you opened a completely new terminal window after installing.

help me troubleshoot why Python isn't recognized in my terminal after installing it

Create your project folder and virtual environment

A virtual environment is an isolated space where your project's packages live, separate from the rest of your system. This prevents version conflicts between different projects.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir dep-audit
cd dep-audit
  • Confirm the folder exists by running:
ls

You should see the dep-audit folder listed in the output.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the dep-audit folder, and click Open.

Why open the folder in VS Code?

Opening the folder (not just a file) tells VS Code to treat dep-audit as your workspace. The integrated terminal will automatically start inside this folder, so every command you run is in the right place.

  • Open a new terminal inside VS Code by pressing Ctrl+` (backtick key, usually above Tab).
  • Create your virtual environment by running this command:
python -m venv venv

What does this command do?

python -m venv tells Python to run its built-in virtual environment module. The second venv is the name of the folder that will hold the environment files.

  • Activate the virtual environment by running:
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the environment is active.

macOS or Linux users

If you are on macOS or Linux, use source venv/bin/activate instead.

Seeing a 'scripts is disabled' error?

PowerShell sometimes blocks scripts by default. If you see an error about running scripts being disabled on this system, switch your VS Code terminal to Command Prompt instead.

  • Click the dropdown arrow next to the + icon in the terminal panel.
  • Select Command Prompt.
  • Run venv\Scripts\activate again in the new terminal.

help me fix the PowerShell execution policy error when activating my Python virtual environment

Install the requests library

The requests library is what your scanner will use to talk to the vulnerability database over the internet. It makes sending HTTP requests as simple as one line of Python.

  • Install requests inside your activated virtual environment by running:
pip install requests
  • Verify the installation by running:
pip show requests

You should see output showing the package name, version number, and other details. This confirms requests is installed and ready to use.

Seeing 'No matching distribution found'?

Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If it is missing, run the activate command again before retrying the install.

help me install the requests library in my Python virtual environment

Your environment is locked and loaded. Next up, you will create the Python script and a test file to scan.

Create Your Project Files

Your scanner needs two things to work: a Python script that knows how to read dependency files, and a sample project to scan.

In this step, you will create both files. scanner.py will contain the functions that parse package.json files and clean up version strings. A test package.json with intentionally outdated packages will give your scanner something real to find later.

In this step, get ready to:
  • Create the scanner script with functions to read dependencies and clean version numbers.
  • Create a sample package.json with packages known to have vulnerabilities.
Create the scanner script with the dependency reader

The first job of your scanner is to open a package.json file and extract every dependency listed inside it. JSON files store data in a structured format that Python can read natively with its built-in json library.

  • In VS Code, create a new file called scanner.py inside your dep-audit folder by clicking the new file icon in the Explorer sidebar.
  • Add the imports and the load_dependencies() function by pasting the following code:
import json
import requests
import sys


def load_dependencies(filepath):
    """Read a package.json file and return a dictionary of dependencies with their versions."""
    with open(filepath, "r") as file:
        package_data = json.load(file)

    dependencies = {}

    if "dependencies" in package_data:
        dependencies.update(package_data["dependencies"])
    if "devDependencies" in package_data:
        dependencies.update(package_data["devDependencies"])

    return dependencies

What does this code do?

The three imports bring in tools you will use throughout the project. json reads structured data files, requests makes HTTP calls to APIs, and sys lets your script accept command-line arguments.

load_dependencies() opens the file at filepath, parses it as JSON, then merges both dependencies and devDependencies into a single Python dictionary. This means your scanner checks every package a project uses, not just the production ones.

  • Save scanner.py.
  • You will not see any output yet. load_dependencies() needs a package.json file to read, which you will create shortly.

Next, you need a helper that strips version prefixes. In package.json files, versions often start with characters like ^ or ~ that tell npm how to resolve updates. The vulnerability database expects a plain version number without those prefixes.

  • Add the clean_version() function below load_dependencies() by pasting this code at the bottom of scanner.py:

def clean_version(version_string):
    """Remove version prefixes like ^, ~, >=, etc. to get a clean version number."""
    prefixes = ["^", "~", ">=", "<=", ">", "<", "="]
    cleaned = version_string.strip()
    for prefix in prefixes:
        if cleaned.startswith(prefix):
            cleaned = cleaned[len(prefix):]
    return cleaned

What does this code do?

clean_version() loops through a list of common prefixes (^, ~, >=, etc.) and removes whichever one appears at the start of the version string.

For example, ^4.17.1 becomes 4.17.1. This gives the OSV vulnerability database a clean version to look up.

  • Save scanner.py.
  • Test clean_version() by running this command in your terminal:
python -c "from scanner import clean_version; print(clean_version('^4.17.1'))"
  • You should see 4.17.1 printed in your terminal. The ^ prefix has been stripped off.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt. If it disappeared, reactivate with venv\Scripts\activate.

Also check that your terminal is in the dep-audit folder. Python needs to be in the same directory as scanner.py to import it. help me fix this ModuleNotFoundError

✔️ Awesome, I've got everything!

Great. Double check you have saved scanner.py before moving on.

ⓧ I'd like to double check the full code

Here is the full scanner.py so far. Make sure your file matches this exactly:

import json
import requests
import sys


def load_dependencies(filepath):
    """Read a package.json file and return a dictionary of dependencies with their versions."""
    with open(filepath, "r") as file:
        package_data = json.load(file)

    dependencies = {}

    if "dependencies" in package_data:
        dependencies.update(package_data["dependencies"])
    if "devDependencies" in package_data:
        dependencies.update(package_data["devDependencies"])

    return dependencies


def clean_version(version_string):
    """Remove version prefixes like ^, ~, >=, etc. to get a clean version number."""
    prefixes = ["^", "~", ">=", "<=", ">", "<", "="]
    cleaned = version_string.strip()
    for prefix in prefixes:
        if cleaned.startswith(prefix):
            cleaned = cleaned[len(prefix):]
    return cleaned
Create a sample package.json for testing

Your scanner needs a file to scan. You will create a package.json with intentionally outdated packages that are known to have security vulnerabilities. This guarantees your scanner will find real results when you run it.

  • Create a new file called package.json in your dep-audit folder by clicking the new file icon in the Explorer sidebar.
  • Paste the following content into package.json:
{
  "name": "test-project",
  "version": "1.0.0",
  "description": "A test project for vulnerability scanning",
  "dependencies": {
    "express": "4.17.1",
    "lodash": "4.17.19",
    "axios": "0.21.0",
    "jsonwebtoken": "8.5.1"
  },
  "devDependencies": {
    "nodemon": "2.0.7"
  }
}

Why these specific packages?

Each of these older versions has documented vulnerabilities in the OSV database. express 4.17.1, lodash 4.17.19, axios 0.21.0, and jsonwebtoken 8.5.1 all have known security issues that your scanner will detect.

The file also includes a devDependencies section with nodemon to test that your scanner checks both dependency types.

  • Save package.json.
  • Verify that load_dependencies() can read your new file by running this command in your terminal:
python -c "from scanner import load_dependencies; print(load_dependencies('package.json'))"
  • You should see a Python dictionary printed with all five packages and their versions merged together.

Seeing a FileNotFoundError?

This means Python cannot find package.json in the current folder. Confirm both scanner.py and package.json are saved in the same dep-audit folder.

Check that your terminal is in the dep-audit directory by looking at the path in your prompt. help me fix this FileNotFoundError

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full package.json. Make sure your file matches this exactly:

{
  "name": "test-project",
  "version": "1.0.0",
  "description": "A test project for vulnerability scanning",
  "dependencies": {
    "express": "4.17.1",
    "lodash": "4.17.19",
    "axios": "0.21.0",
    "jsonwebtoken": "8.5.1"
  },
  "devDependencies": {
    "nodemon": "2.0.7"
  }
}

Both project files are ready. Next up, you will connect your scanner to a real vulnerability database so it can check whether these packages have known security issues.

Connect to the OSV Vulnerability Database

Your scanner can now read a package.json file and extract clean version numbers from every dependency. The next step is to actually check whether those packages have known security holes.

The OSV (Open Source Vulnerabilities) database is a free, public REST API that aggregates vulnerability data from multiple sources. In this step, you will write the code that sends each package to OSV and interprets the response.

In this step, get ready to:
  • Write a function that queries the OSV vulnerability database for a given package.
  • Write a helper function that extracts severity levels from vulnerability records.
Write the API query function

Each dependency needs to be checked individually against the OSV database. The API accepts a POST request with the package name, its ecosystem (like npm), and the version number.

  • In scanner.py, find the clean_version() function and add the following code below it:
def check_package(package_name, version):
    """Query the OSV API to check if a package version has known vulnerabilities."""
    url = "https://api.osv.dev/v1/query"
    payload = {
        "package": {
            "name": package_name,
            "ecosystem": "npm"
        },
        "version": version
    }

    response = requests.post(url, json=payload)

    if response.status_code == 200:
        data = response.json()
        return data.get("vulns", [])
    else:
        print(f"  Warning: Could not check {package_name} (HTTP {response.status_code})")
        return []

What does this code do?

This function talks to the OSV database to check one package at a time.

  • url stores the API endpoint your scanner sends requests to.
  • payload is the JSON body that tells OSV which package and version to look up. The ecosystem field tells OSV this is an npm package.
  • requests.post(url, json=payload) sends the request and automatically converts your Python dictionary into JSON.
  • data.get("vulns", []) pulls out the list of vulnerabilities from the response. If none are found, it returns an empty list.
  • Save scanner.py.
  • Test the function by opening your terminal and running this command:
python -c "from scanner import check_package; result = check_package('lodash', '4.17.19'); print(len(result), 'vulnerabilities found')"

You should see a number greater than 0 followed by vulnerabilities found, confirming your function can reach the OSV database.

Seeing 0 vulnerabilities or an error?

  • Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
  • Check that you have an internet connection. The OSV API is a live web service.
  • Confirm the ecosystem value is exactly "npm" (lowercase). The API is case-sensitive.

Help me figure out why my check_package function returns 0 vulnerabilities or throws an error.

Write the severity extractor

Vulnerability records from OSV contain severity information buried in nested JSON. The severity can live in different places depending on the data source, so the function needs to check multiple locations.

  • In scanner.py, add the following function below check_package():
def get_severity(vuln):
    """Extract the severity level from a vulnerability record."""
    for affected in vuln.get("affected", []):
        eco_specific = affected.get("ecosystem_specific", {})
        if "severity" in eco_specific:
            return eco_specific["severity"]

    severity_list = vuln.get("severity", [])
    if severity_list:
        return severity_list[0].get("score", "UNKNOWN")

    return "UNKNOWN"

What does this code do?

This function tries three places to find a severity level, in order of specificity:

  • First, it checks ecosystem_specific inside each affected entry. This is where npm-specific severity data lives.
  • If that is empty, it falls back to the top-level severity list and grabs the first score.
  • If neither location has data, it returns "UNKNOWN" so the report always has something to display.
  • Save scanner.py.

You will not see a visible change yet. The next step uses this function to display severity in your final report.

Seeing a syntax error when you save?

  • Make sure the function is not indented inside check_package(). It should start at the same indentation level (no leading spaces).
  • Check that the for loop and both return statements are indented consistently. Use 4 spaces per level.

Help me fix a syntax error in my get_severity function in scanner.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved scanner.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete scanner.py at this point:

import json
import requests
import sys


def load_dependencies(filepath):
    """Read a package.json file and return a dictionary of dependencies with their versions."""
    with open(filepath, "r") as file:
        package_data = json.load(file)

    dependencies = {}

    if "dependencies" in package_data:
        dependencies.update(package_data["dependencies"])
    if "devDependencies" in package_data:
        dependencies.update(package_data["devDependencies"])

    return dependencies


def clean_version(version_string):
    """Remove version prefixes like ^, ~, >=, etc. to get a clean version number."""
    prefixes = ["^", "~", ">=", "<=", ">", "<", "="]
    cleaned = version_string.strip()
    for prefix in prefixes:
        if cleaned.startswith(prefix):
            cleaned = cleaned[len(prefix):]
    return cleaned


def check_package(package_name, version):
    """Query the OSV API to check if a package version has known vulnerabilities."""
    url = "https://api.osv.dev/v1/query"
    payload = {
        "package": {
            "name": package_name,
            "ecosystem": "npm"
        },
        "version": version
    }

    response = requests.post(url, json=payload)

    if response.status_code == 200:
        data = response.json()
        return data.get("vulns", [])
    else:
        print(f"  Warning: Could not check {package_name} (HTTP {response.status_code})")
        return []


def get_severity(vuln):
    """Extract the severity level from a vulnerability record."""
    for affected in vuln.get("affected", []):
        eco_specific = affected.get("ecosystem_specific", {})
        if "severity" in eco_specific:
            return eco_specific["severity"]

    severity_list = vuln.get("severity", [])
    if severity_list:
        return severity_list[0].get("score", "UNKNOWN")

    return "UNKNOWN"

Your scanner can now talk to a real vulnerability database and understand the severity of what it finds. Next up, you will build the report that turns all this raw data into a clear, readable output.

Build the Report Output

Your scanner can now query the OSV database and pull severity data from vulnerability records.

Right now, those results disappear into the void. In this step, you'll build the functions that tie everything together and present findings in a clean, readable terminal report.

In this step, get ready to:
  • Write a report printer that formats vulnerability findings.
  • Write a main function that orchestrates the full scan.
  • Add the entry point so the script runs from the terminal.
Write the report printer

A security scanner is only useful if a human can read the results quickly. The print_report() function takes the raw vulnerability data your scanner collects and formats it into a structured terminal report.

  • In scanner.py, find the get_severity() function you wrote in the previous step.
  • Add the following print_report() function below it:
def print_report(results):
    """Print a formatted vulnerability report."""
    print("\n" + "=" * 60)
    print("  DEPENDENCY VULNERABILITY REPORT")
    print("=" * 60)

    vulnerable_count = 0
    total_vulns = 0

    for package_name, vulns in results.items():
        if vulns:
            vulnerable_count += 1
            total_vulns += len(vulns)
            print(f"\n[!] {package_name}")
            print(f"    Found {len(vulns)} vulnerability(ies):")
            for vuln in vulns:
                vuln_id = vuln.get("id", "Unknown ID")
                summary = vuln.get("summary", "No description available")
                severity = get_severity(vuln)
                print(f"    - [{severity}] {vuln_id}")
                print(f"      {summary[:80]}")
                refs = vuln.get("references", [])
                if refs:
                    print(f"      Link: {refs[0].get('url', 'N/A')}")

    print("\n" + "-" * 60)
    total_packages = len(results)
    safe_count = total_packages - vulnerable_count
    print(f"\n  Packages scanned: {total_packages}")
    print(f"  Vulnerable:       {vulnerable_count}")
    print(f"  No issues found:  {safe_count}")
    print(f"  Total vulns:      {total_vulns}")
    print("\n" + "=" * 60)

What does this code do?

This function loops through the results dictionary (where each key is a package name and each value is a list of vulnerabilities found for it).

  • For each vulnerable package, it prints the vulnerability ID, a severity tag from get_severity(), a truncated summary (capped at 80 characters), and the first reference URL so you can read the full advisory.
  • At the bottom, it prints a summary section with total packages scanned, how many are vulnerable, how many are safe, and the total number of individual vulnerabilities found.
  • Save scanner.py.

You won't see any output yet. The main() function you write next will call print_report() to display results.

Seeing a syntax error on save?

  • Check that the function is indented at the same level as your other functions (no extra indentation).
  • Make sure all f-strings use matching quote types. The inner refs[0].get('url', 'N/A') uses single quotes because the outer f-string uses double quotes.
  • Verify you have a blank line between the end of get_severity() and the start of print_report().

Why is my print_report function showing a syntax error?

Write the main function

The main() function is the orchestrator. It reads the file path, loads dependencies, loops through each package calling the API, and passes everything to your report printer.

  • In scanner.py, add the following main() function below print_report():
def main():
    """Main function - entry point of the scanner."""
    # Default to package.json in current folder
    filepath = "package.json"

    # Allow the user to pass a different file path as a command-line argument
    if len(sys.argv) > 1:
        filepath = sys.argv[1]

    print(f"\nScanning dependencies in: {filepath}")
    print("-" * 40)

    # Try to load the dependencies, handle common errors
    try:
        dependencies = load_dependencies(filepath)
    except FileNotFoundError:
        print(f"Error: Could not find '{filepath}'")
        print("Make sure you are in the right folder, or provide the path as an argument.")
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"Error: '{filepath}' is not valid JSON.")
        sys.exit(1)

    if not dependencies:
        print("No dependencies found in the file.")
        sys.exit(0)

    print(f"Found {len(dependencies)} dependencies. Checking for vulnerabilities...\n")

    # Loop through each dependency and check for vulnerabilities
    results = {}
    for package_name, version_string in dependencies.items():
        version = clean_version(version_string)
        print(f"  Checking {package_name}@{version}...")
        vulns = check_package(package_name, version)
        results[package_name] = vulns

    # Display the formatted report
    print_report(results)

What does this code do?

  • filepath defaults to package.json but accepts a custom path via sys.argv[1] so you can scan any project's dependencies.
  • The try/except block catches the two most common issues: the file not existing (FileNotFoundError) and the file containing broken JSON (json.JSONDecodeError). Both print a helpful message instead of crashing with a traceback.
  • The loop calls clean_version() to strip prefixes, then check_package() to query the OSV API. Results accumulate in a dictionary keyed by package name.
Add the entry point

Python scripts need a special guard at the bottom to run when called directly from the terminal. Without it, your main() function would never execute.

  • At the very bottom of scanner.py, below the main() function, add this entry point:
if __name__ == "__main__":
    main()

What is __name__ == "__main__"?

When you run a Python file directly (like python scanner.py), Python sets a special variable called __name__ to the string "__main__". This guard ensures main() only runs when you execute the script directly, not when another file imports it.

  • Save scanner.py.

Your scanner is now complete. In the next step, you'll run it and see the vulnerability report in action.

VS Code showing errors in the file?

  • Make sure if __name__ == "__main__": is at the very bottom of the file with no indentation (it sits at the module level, not inside a function).
  • Check that there is a blank line between the end of main() and the if __name__ line.
  • Confirm your virtual environment is still active. You should see (venv) at the start of your terminal prompt.

Help me fix errors in my completed scanner.py file.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete scanner.py file at this point:

import json
import requests
import sys


def load_dependencies(filepath):
    """Read a package.json file and return a dictionary of dependencies with their versions."""
    with open(filepath, "r") as file:
        package_data = json.load(file)

    dependencies = {}

    if "dependencies" in package_data:
        dependencies.update(package_data["dependencies"])
    if "devDependencies" in package_data:
        dependencies.update(package_data["devDependencies"])

    return dependencies


def clean_version(version_string):
    """Remove version prefixes like ^, ~, >=, etc. to get a clean version number."""
    prefixes = ["^", "~", ">=", "<=", ">", "<", "="]
    cleaned = version_string.strip()
    for prefix in prefixes:
        if cleaned.startswith(prefix):
            cleaned = cleaned[len(prefix):]
    return cleaned


def check_package(package_name, version):
    """Query the OSV API to check if a package version has known vulnerabilities."""
    url = "https://api.osv.dev/v1/query"
    payload = {
        "package": {
            "name": package_name,
            "ecosystem": "npm"
        },
        "version": version
    }

    response = requests.post(url, json=payload)

    if response.status_code == 200:
        data = response.json()
        return data.get("vulns", [])
    else:
        print(f"  Warning: Could not check {package_name} (HTTP {response.status_code})")
        return []


def get_severity(vuln):
    """Extract the severity level from a vulnerability record."""
    for affected in vuln.get("affected", []):
        eco_specific = affected.get("ecosystem_specific", {})
        if "severity" in eco_specific:
            return eco_specific["severity"]

    severity_list = vuln.get("severity", [])
    if severity_list:
        return severity_list[0].get("score", "UNKNOWN")

    return "UNKNOWN"


def print_report(results):
    """Print a formatted vulnerability report."""
    print("\n" + "=" * 60)
    print("  DEPENDENCY VULNERABILITY REPORT")
    print("=" * 60)

    vulnerable_count = 0
    total_vulns = 0

    for package_name, vulns in results.items():
        if vulns:
            vulnerable_count += 1
            total_vulns += len(vulns)
            print(f"\n[!] {package_name}")
            print(f"    Found {len(vulns)} vulnerability(ies):")
            for vuln in vulns:
                vuln_id = vuln.get("id", "Unknown ID")
                summary = vuln.get("summary", "No description available")
                severity = get_severity(vuln)
                print(f"    - [{severity}] {vuln_id}")
                print(f"      {summary[:80]}")
                refs = vuln.get("references", [])
                if refs:
                    print(f"      Link: {refs[0].get('url', 'N/A')}")

    print("\n" + "-" * 60)
    total_packages = len(results)
    safe_count = total_packages - vulnerable_count
    print(f"\n  Packages scanned: {total_packages}")
    print(f"  Vulnerable:       {vulnerable_count}")
    print(f"  No issues found:  {safe_count}")
    print(f"  Total vulns:      {total_vulns}")
    print("\n" + "=" * 60)


def main():
    """Main function - entry point of the scanner."""
    filepath = "package.json"

    if len(sys.argv) > 1:
        filepath = sys.argv[1]

    print(f"\nScanning dependencies in: {filepath}")
    print("-" * 40)

    try:
        dependencies = load_dependencies(filepath)
    except FileNotFoundError:
        print(f"Error: Could not find '{filepath}'")
        print("Make sure you are in the right folder, or provide the path as an argument.")
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"Error: '{filepath}' is not valid JSON.")
        sys.exit(1)

    if not dependencies:
        print("No dependencies found in the file.")
        sys.exit(0)

    print(f"Found {len(dependencies)} dependencies. Checking for vulnerabilities...\n")

    results = {}
    for package_name, version_string in dependencies.items():
        version = clean_version(version_string)
        print(f"  Checking {package_name}@{version}...")
        vulns = check_package(package_name, version)
        results[package_name] = vulns

    print_report(results)


if __name__ == "__main__":
    main()

Your scanner is fully assembled. Next up, you'll run it against your test package.json and see real vulnerabilities appear in your terminal.

Run Your Scanner and Analyze Results

Your scanner is fully built. Every function is in place, from reading the package.json file to querying the OSV database and printing a formatted report.

Now it is time to actually run it and see what vulnerabilities are hiding in those outdated packages. This is where your tool proves its value.

In this step, get ready to:
  • Run your scanner against the test package.json and see real vulnerabilities.
  • Analyze the report output to understand severity levels and advisory details.
  • Test your scanner against a real project's dependencies.
Run the scanner against your test file

Your package.json contains intentionally outdated packages that are known to have security issues. Running the scanner against this file will prove your tool works and show you what real vulnerability data looks like.

  • Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
  • Run the scanner by executing this command:
python scanner.py

Watch the terminal as your scanner checks each package one by one against the OSV database. You should see lines like Checking express@4.17.1... appear in sequence, followed by the full vulnerability report.

What should I see?

Your terminal will first show each package being checked, then print a formatted report titled DEPENDENCY VULNERABILITY REPORT. Packages with vulnerabilities are marked with [!] and list each issue with its severity, ID, and a link to the full advisory.

At the bottom, a summary shows how many packages were scanned, how many are vulnerable, and the total number of vulnerabilities found. The scan takes a few seconds because it makes a separate API call for each package.

Seeing an error instead of a report?

  • If you see Error: Could not find 'package.json', make sure your terminal is in the dep-audit folder where both files live.
  • If you see ModuleNotFoundError: No module named 'requests', your virtual environment is not active. Run venv\Scripts\activate and try again.
  • If you see connection errors, check your internet connection. The scanner needs to reach https://api.osv.dev/v1/query to work.

Help me debug my scanner output

Read and understand the report

A vulnerability scanner is only useful if you can interpret its output. Each entry in the report tells you something specific about the risk a package introduces to a project.

  • Look at the vulnerability IDs in your report (they start with GHSA- for GitHub Security Advisories). Each ID uniquely identifies a specific security issue.
  • Check the severity levels shown in square brackets. These tell you how dangerous the vulnerability is.
  • Read the summary text for each vulnerability. This explains what an attacker could do by exploiting the issue.
  • Click or copy one of the reference links from the report and open it in your browser. This takes you to the full advisory with remediation advice.

What do severity levels mean?

Severity levels indicate how much damage a vulnerability could cause if exploited. HIGH means an attacker could steal data or take control of a system. MODERATE means there is a real risk but it requires specific conditions to exploit.

In a real audit, HIGH and CRITICAL vulnerabilities need immediate attention. MODERATE ones should be fixed in the next release cycle. LOW severity issues can be tracked but are not urgent.

Why do some packages have multiple vulnerabilities?

A single outdated package version can accumulate several vulnerabilities over time. Each one was discovered and reported separately. This is exactly why keeping dependencies updated matters. The longer a package stays at an old version, the more known attack vectors pile up against it.

Test with a real project

The sample file proved your scanner works. Now try it on something real to see what it finds in actual project dependencies.

  • Find a package.json from one of your own projects or any open-source project on GitHub.
  • Copy that file into your dep-audit folder. Give it a different name so it does not overwrite your test file (for example, real-project.json).
  • Run the scanner against your real file by passing its name as an argument:
python scanner.py [[REAL_FILE="your-real-package-json-filename.json"]]

What if the real project has no vulnerabilities?

That is a good sign for that project. If your report shows zero vulnerable packages, it means the dependencies are up to date. Try scanning a project with older dependencies for more interesting results.

Find package.json files on GitHub

If you do not have a real project handy, browse any popular open-source Node.js project on GitHub and download its package.json. Projects that have not been updated recently are more likely to have interesting findings.

Scanner not finding your file?

  • Make sure the filename you pass matches exactly what you named the file, including the .json extension.
  • The file must be in the same dep-audit folder, or you need to provide the full path to the file.

Help me run my scanner against a different file

Secret mission

Support Multiple Ecosystems

Your scanner only handles npm right now. But real projects use multiple languages and package managers. What if you could scan Python dependencies too, using the same vulnerability database?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses a free public API, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • dep-audit project folder (contains scanner.py, package.json, and your virtual environment).
  • Python 3.14 (if you installed it for this project).

Keep everything running

No action needed. Choose this if you plan to extend your scanner or use Python for other security projects.

  • Your dep-audit folder stays on your Desktop with all files intact.
  • Python 3.14 remains installed and available for other projects.
  • The requests library stays inside your virtual environment, isolated from any other Python projects.

Pause - I'll come back to this later

Deactivate your virtual environment to free up your terminal session. Your files and installed packages remain on disk for next time.

  • If your virtual environment is still active, deactivate it by running:
deactivate
  • Your dep-audit folder stays on your Desktop.
  • When you return, open VS Code, open the folder, activate the virtual environment again, and pick up where you left off.

Delete - I don't want to use this again

Remove all project files and optionally uninstall Python if you no longer need it.

  • Delete the project folder by running the command for your operating system:

macOS

rm -rf ~/Desktop/dep-audit

Windows

Remove-Item -Recurse -Force ~\Desktop\dep-audit
  • This removes scanner.py, package.json, the virtual environment, and the requests library installed inside it.

To uninstall Python 3.14 (skip this if you use Python for other work):

macOS

The Python installer places three things on your system. Remove all three to fully uninstall.

sudo rm -rf /Library/Frameworks/Python.framework/Versions/3.14
sudo rm -rf "/Applications/Python 3.14"
sudo rm -f /usr/local/bin/python3.14

Windows

  • Open Settings.
  • Select Apps, then Installed apps.
  • Find Python 3.14 in the list.
  • Click it, then click Uninstall.

That's a Wrap!

That's a Wrap!

Nice work! You just built a real security tool from scratch using Python.

Your dep-audit scanner can now check any npm project's dependencies against a live vulnerability database and produce a clear report. That is exactly what professional security engineers do every day.

Here's what you learned:

  • Built a CLI vulnerability scanner that checks npm packages against the OSV database and outputs a formatted report with severity levels and reference links.
  • Called a REST API with Python using the requests library, sending POST requests with JSON payloads and parsing structured responses.
  • Understood how supply chain vulnerabilities work and why dependency auditing is one of the most critical tasks in modern security engineering.
  • Extended your scanner to handle multiple ecosystems (npm + PyPI), making it a reusable tool across different tech stacks.

Ready to quiz yourself?