DEV Community

Cover image for Building PackageGuard: Dependency Traversal, External APIs, and Explainable Risk Scoring in TypeScript
Ishika Thakur
Ishika Thakur

Posted on

Building PackageGuard: Dependency Traversal, External APIs, and Explainable Risk Scoring in TypeScript

First, what exactly is PackageGuard?

PackageGuard is a TypeScript command-line tool that scans a Node.js project's dependencies and flags packages that may deserve security scrutiny.

The problem it addresses is dependency risk.

Imagine you're building a backend service. You install a package to make an HTTP request, parse a file, or handle authentication. That package may itself depend on other packages, and those packages may have dependencies of their own.
Your application therefore relies on more code than the code you wrote personally.

Some packages might be:

  • Recently published, with little history.
  • Downloaded very infrequently.
  • Missing repository information.
  • Configured with installation scripts that execute during installation.
  • No longer available in the npm registry.

These characteristics do not automatically mean a package is malicious. However, they can help identify dependencies that deserve further investigation.

PackageGuard uses a set of heuristics to assign risk scores to dependencies and summarizes their severity.

The important distinction is this:

PackageGuard is a dependency-risk triage tool, not a malware detector. It helps prioritize investigation; it does not prove that a package is safe or malicious.

Conceptual Architecture

What each component does?

Component 1 — CLI entry point

I built a command-line interface using TypeScript and Commander.
The user runs:
node dist/index.js scan

The dist directory contains the compiled JavaScript output.

Why Commander?
Commander helps define commands, arguments, options, and command-line help without requiring you to write all the argument parsing yourself.

Engineering decision: use an existing CLI library rather than spending the project building a custom command parser.

Commander handles the interface, but it does not perform the dependency scan or calculate the risk score. Those are application responsibilities.

Component 2 — Project and dependency discovery
PackageGuard needs to know which packages to evaluate.
Consider this hypothetical package.json:
{
"dependencies": {
"express": "^5.0.0",
"axios": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}

These are the dependencies declared by the project.
But there's a complication: express and axios can have their own dependencies.

PackageGuard traverses the installed node_modules tree recursively, with a maximum traversal depth of five and a duplicate guard.
Three concepts matter here.

  • Recursion: a function can call itself to explore nested dependencies.
  • Depth limit: restricts how far traversal proceeds.
  • Duplicate guard: tracks packages already visited so repeated encounters don't cause redundant traversal.

Why do this?
Without a depth limit, a malformed or unusually deep directory structure could consume excessive resources. Without a duplicate guard, shared dependencies might be revisited repeatedly.
There is a trade-off: a depth limit of five bounds work but can miss dependencies deeper in the tree. A duplicate guard also needs to use an appropriate package identity; two versions of the same package are not necessarily interchangeable.

Important: package.json describes declared dependencies, while node_modules reflects the installed layout. They answer related but different questions. My implementation uses dependency information and recursively examines the installed tree; before claiming exact coverage of every declared, optional, peer, or platform-specific dependency, we should verify the traversal code.

Component 3 — External npm APIs
Once a package has been identified, PackageGuard gathers evidence from npm.
It uses two sources for different kinds of information:

npm Registry API
Retrieves package metadata, such as whether a package exists, publication information, repository information, and package configuration details.

npm Downloads API
Provides download statistics used as a popularity signal.

*Why use external APIs instead of looking only at local files?
*

A local dependency directory can tell you what is installed, but it cannot reliably tell you the package's current registry status, its wider popularity, or all the metadata used by your scoring rules.

Why two APIs rather than one?
Because package metadata and download statistics are different categories of evidence. Separating the sources makes their roles clearer and lets the analyzer reason about different signals.

This introduces an engineering trade-off: better evidence comes with network latency and external failure modes.

For example:

  • The registry may be unavailable.
  • A request might time out.
  • A response could be malformed.
  • Rate limits could affect scanning.
  • Package metadata could change between scans. We should inspect the actual HTTP-client and error-handling code before claiming that all of these cases are handled robustly. Using an API does not automatically mean you have implemented retries, caching, concurrency limits, or graceful degradation.

Component 4 — Risk analyzer

This is the decision-making core of PackageGuard.
Instead of displaying raw npm metadata and expecting the developer to interpret it, the analyzer converts evidence into a numerical score.

The known heuristic rules are:

Signal Score contribution
Package is very new: under 7 days old +30
Fewer than 100 downloads per week +20
Repository information missing +15
Installation scripts present +20
Package does not exist in the registry Directly assigned 100

The score is capped at 100. The thresholds are:
Score Severity
0–29 LOW
30–59 MEDIUM
60–79 HIGH
80–100 CRITICAL

Let's calculate a hypothetical example.

Suppose a package is three days old, has fewer than 100 weekly downloads, and has no repository information.
[
R = 30 + 20 + 15 = 65
]

Its score would be 65, placing it in the HIGH category.

There is another important case. A package that doesn't exist in the registry is assigned a score of 100 directly. That is different from adding the ordinary signal contributions together.

Why?
A package that cannot be found may be a typo, an unpublished package, a removed package, a private-registry dependency, or something else. It is a significant finding, but the score alone does not establish malicious intent.

The trade-off is that the rules are crude. Legitimate new packages can score highly, while malicious packages can have many downloads and complete metadata.

The exact aggregation, missing-data handling, and precedence of the individual rules should be confirmed against the source before we make stronger claims about them.

Component 5 — Reporting

The last stage converts the analyzer's findings into something a developer can act on.

Instead of forcing the user to examine multiple API responses for every package, PackageGuard summarizes the findings by risk and severity.
The value of the report is not simply that it contains a score. It makes a potentially large set of dependency metadata easier to prioritize.

A strong report should help answer:

  1. Which packages deserve attention first?
  2. What evidence triggered the warning?
  3. What should the developer investigate next?

The first is supported by the scoring and severity levels. The second depends on how much explanatory detail the report exposes. The third is an area where a future version could offer remediation guidance.

PackageGuard started as an idea about npm dependency visibility. It became an opportunity to work through the full lifecycle of a small developer tool: defining the problem, dividing responsibilities, building modules, connecting to an external service, implementing explainable rules, testing, packaging, publishing, and validating the result.

The most valuable lesson was that shipping software requires more than making the happy path work.

You need to think about what the program knows, what it doesn't know, how its parts interact, how users invoke it, and how to communicate the limits of its output.

PackageGuard is an early experiment, not a complete supply-chain security solution. But it is now a real npm package that other developers can install and try.

Try it out:

npm: [https://www.npmjs.com/package/packageguard]

Source code: [https://github.com/ishika-thakur7802/packageguard]

I'm looking forward to improving the implementation, strengthening its tests, and learning from what the next iteration reveals.

Thanks for reading!

Top comments (0)