This content originally appeared on DEV Community and was authored by Bruno Soares
Security is a fundamental aspect of software engineering, and it’s made up of multiple layers – secure coding practices, infrastructure hardening, data protection, and proactive monitoring – including a critical layer: managing the security of third-party dependencies.
Modern applications rarely exist in isolation; they are built on top of large ecosystems of open-source libraries. Each new dependency introduces both functionality and risk.
Tools like GitHub’s Dependabot can automatically monitor known vulnerabilities and open pull requests to update dependencies, reducing some of the operational burden. However, automated updates are not a silver bullet. More comprehensive scanning tools, such as Apiiro, allow organizations to detect vulnerabilities across the entire dependency graph — while still leaving engineering teams in charge of triaging, prioritizing, and patching vulnerabilities.
This article takes as practical example a React with the Yarn package manager, but the steps we’ll cover are universal practices. Whether you’re working in Node.js, Python, Java, Go, or Rust, the same security posture applies: dependency hygiene, regular updates, and careful resolution of transitive risks.
Tool Discrepancies: Apiiro vs Yarn Audit
Apiiro recently flagged two critical vulnerabilities in a React application owned by my team.
My first intuition, run yarn audit:
$ yarn audit --groups dependencies \
--level high \
--frozen-lockfile
Yarn reported over twenty critical and high vulnerabilities. But where does the discrepancy in the results between each tool come from?
Data sources:
yarn audit
(in Yarn Classic) relies on the npm security advisories database, whereas Apiiro aggregates multiple vulnerability databases and correlates them with application context.Severity scoring: Apiiro may apply additional risk scoring or filtering rules defined by the InfoSec team, surfacing only the issues deemed most relevant or critical for the business.
Configuration: It’s common for security teams to configure Apiiro to focus on certain severity levels or to suppress vulnerabilities that have compensating controls, whereas package manager audits usually report everything.
In short, the differences don’t mean one tool is “wrong”. They reflect different purposes: Yarn’s audit is a raw report of known CVEs, while Apiiro provides a filtered, risk-based perspective.
An structured process
Vulnerabilities are often flagged deep within nested dependencies, not just at the direct dependency level. To handle them effectively, engineers need to understand their application’s lock file and how dependency resolution works — an area often overlooked by less experienced developers.
From my perspective, the process of patching dependency vulnerabilities can be broken down into three clear steps. After each step, you should re-run your package manager’s audit command (or an external tool like Apiiro) to measure the reduction in vulnerabilities and confirm that you’re making progress.
1. Dependency Hygiene
More often than not, unused dependencies are left dangling in applications. These may come from refactored features, copy-pasted snippets, or forgotten experiments. Regardless of the reason, each dependency added should be interpreted—through an InfoSec lens—as an expansion of the application’s attack surface.
Good hygiene means regularly scanning for unused dependencies and removing them. This is not only a best practice for security but also helps reduce application complexity, speed up builds, and minimize supply chain risks.
2. Updating Dependencies
Once your dependency list is clean, the next step is to update dependencies to their latest non-breaking versions. This is trickier than it sounds because package managers handle installation and updates differently.
YARN CLASSIC (v1)
Manually editing versions in
package.json
and runningyarn install
sets the version range for the dependency but does not necessarily update transitive dependencies. The lock file may still point to older, vulnerable versions.Running
yarn upgrade {package-name}
updates the dependency to the latest version that satisfies the defined range, and rewrites the lock file accordingly.
YARN 2+ (BERRY)
yarn install
behaves similarly to Yarn Classic – it respects the lock file and only resolves new versions if the ranges don’t match.yarn up {package-name}
is the equivalent command for updating.
This difference becomes visible in the lock file. While install
preserves existing versions unless ranges demand otherwise, upgrade
(or up
in Yarn 2+) actively pulls in newer versions and ensures your lock file reflects those updates. Less experienced engineers often miss this nuance, leading to a false sense of security when vulnerabilities persist after a manual update.
3. Resolving Nested Dependencies
Sometimes, even after updating direct dependencies, vulnerabilities remain because they exist as nested dependencies one or many levels deep. In these cases, one solution is to use package resolutions. It instructs Yarn to use a specific resolution (specific package version) instead of anything the resolver would normally pick.
The property resolutions
is defined at the root of package.json
:
{
"resolutions": {
"lodash": "4.17.21"
}
}
The previous example forces Yarn to use a single version of lodash
across the entire dependency graph, regardless of which direct dependency introduced it.
Another approach is being laser focused and scope resolutions to a specific dependency paths:
{
"resolutions": {
"react-scripts/**/lodash": "4.17.21"
}
}
After updating your application’s resolutions run yarn install
to update the dependency resolutions in your lock file.
Done! At this point your application should have been adequately patched up, unless no patch is yet available for one of the compromised nested dependencies.
Such cases should be communicated to InfoSec, ideally including alternatives to replace the compromised library and the respective engineering effort to refactor the application. It would be on them to weight the vulnerability risk, understand wether the compromised dependency is actively maintained and how often security patches are released, and if a refactor would be indeed required.
Final Thoughts
Dependency management is often treated as a background task — until a vulnerability surfaces and suddenly becomes a production risk. By following a structured approach — hygiene, updates, and resolutions — engineering teams can take control of their software supply chain and meaningfully reduce their exposure.
Automated tools like Dependabot and Apiiro are powerful allies, but ultimately, engineers must understand the nuances of their dependency graph, lock files, and package manager behaviors. With this knowledge, vulnerabilities stop being chaotic surprises and become manageable parts of a continuous security process.
This content originally appeared on DEV Community and was authored by Bruno Soares