<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3" -->

---
title: Build Your Own Dependency Vulnerability Scanner in 120...
description: A step-by-step walkthrough builds a 120-line offline Python vulnerability scanner that reads pinned requirements.txt entries and checks them against a local...
canonical: https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Build Your Own Dependency Vulnerability Scanner in 120 Lines of Python | daily.dev
og:description: A step-by-step walkthrough builds a 120-line offline Python vulnerability scanner that reads pinned requirements.txt entries and checks them against a local...
og:url: https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3
og:image: https://api.daily.dev/og/posts/E8jiCF6H3.png
og:image:alt: Build Your Own Dependency Vulnerability Scanner in 120 Lines of Python
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Build Your Own Dependency Vulnerability Scanner in 120 Lines of Python

**[Medium](https://daily.dev/sources/medium_js)** · 7 min read · 0 upvotes · 0 comments

## Summary

A step-by-step walkthrough builds a 120-line offline Python vulnerability scanner that reads pinned requirements.txt entries and checks them against a local snapshot of the GitHub Advisory Database (OSV format). It covers package name normalization per PEP 503, correctly parsing OSV version ranges (introduced/fixed/last_affected events), and using PEP 440-compliant version comparison instead of lexical string comparison. The scanner's output is validated against pip-audit on the same requirements file, with the resulting gaps explained by three factors: different source databases (GHSA vs PyPA/PYSEC), alias double-counting across advisory IDs, and pip-audit's ability to flag transitive dependencies that the toy scanner cannot see. The piece closes by noting these same three mechanics explain why commercial scanners like Snyk, Trivy, and Dependabot often report different vulnerability counts for the same repo.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://medium.com/@sachinpk087/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-bd91b6de7a2b>

## Questions this post answers

### Why do pip-audit and other vulnerability scanners report different numbers of findings for the same requirements.txt?

Discrepancies come from three mechanics rather than one scanner being more thorough: differing source databases (pip-audit merges PyPA/PYSEC advisories with GHSA data, while a GHSA-only scanner misses PYSEC-only entries), alias double-counting when the same vulnerability carries both a PYSEC and GHSA ID in merged feeds, and transitive dependency resolution, since pip-audit flags packages like urllib3 and idna pulled in indirectly that never appear in the requirements file itself.

_daily.dev surfaces write-ups like this for developers reconciling conflicting scanner outputs across their dependency stack._

### How does the OSV format represent which versions of a package are affected by a vulnerability?

OSV encodes affected versions as an event list rather than a simple range string: an 'introduced' event opens a vulnerable interval, a 'fixed' event closes it exclusively (the fixed version itself is safe), and a 'last_affected' event closes it inclusively. Some advisories skip ranges entirely and instead enumerate an exact list of affected versions, so a correct parser must handle both shapes and must compare versions using PEP 440 semantics rather than lexical string comparison.

_developers implementing their own OSV parsing logic can find these edge cases surfaced on daily.dev before shipping buggy version checks._

### Why do PyPI package names need to be normalized when matching them against security advisories?

Under PEP 503, PyPI treats package names like Foo-Bar, foo_bar, and foo.bar as identical, so an advisory filed against PyYAML must still match a requirements line written as pyyaml. Scanners that skip this normalization step silently miss findings while appearing to work correctly, since the mismatch produces no error, just an absent result.

_anyone building or auditing dependency tooling can track packaging quirks like this through daily.dev._

## Similar posts on daily.dev

- [Python Supply Chain Security Made Easy](https://daily.dev/posts/python-supply-chain-security-made-easy-tuymgatoc) · Planet Python · 1 upvotes · 0 comments
- [Beyond GHAS: Why I Built a Vulnerability Scanner I Almost Talked Myself Out Of](https://daily.dev/posts/beyond-ghas-why-i-built-a-vulnerability-scanner-i-almost-talked-myself-out-of-duonbhw2i) · Medium · 0 upvotes · 0 comments
- [Detecting Malicious Packages using the OSV API](https://daily.dev/posts/detecting-malicious-packages-using-the-osv-api-zmq3jdhii) · OpenSSF · 0 upvotes · 0 comments
- [Vulnerability and malware checks in uv](https://daily.dev/posts/vulnerability-and-malware-checks-in-uv-wuu9roi2k) · Astral · 47 upvotes · 5 comments
- [DevOps Python Supply Chain Security](https://daily.dev/posts/devops-python-supply-chain-security-faxpt5dnc) · Planet Python · 3 upvotes · 0 comments

---

Tags: [#security](https://daily.dev/tags/security), [#python](https://daily.dev/tags/python)

[View this post on daily.dev](https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Build Your Own Dependency Vulnerability Scanner in 120 Lines of Python","url":"https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3"},"datePublished":"2026-08-16T01:58:33.208Z","dateModified":"2026-09-13T21:09:29.600Z","description":"A step-by-step walkthrough builds a 120-line offline Python vulnerability scanner that reads pinned requirements.txt entries and checks them against a local...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/41dea97e42605a8a5fa307a6243c3cb2?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/41dea97e42605a8a5fa307a6243c3cb2?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Medium","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Medium","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/medium","url":"https://daily.dev/sources/medium_js"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"security,python","timeRequired":"PT7M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Medium","item":"https://daily.dev/sources/medium_js"},{"@type":"ListItem","position":3,"name":"Build Your Own Dependency Vulnerability Scanner in 120 Lines of Python"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/build-your-own-dependency-vulnerability-scanner-in-120-lines-of-python-e8jicf6h3#faq","mainEntity":[{"@type":"Question","name":"Why do pip-audit and other vulnerability scanners report different numbers of findings for the same requirements.txt?","acceptedAnswer":{"@type":"Answer","text":"Discrepancies come from three mechanics rather than one scanner being more thorough: differing source databases (pip-audit merges PyPA/PYSEC advisories with GHSA data, while a GHSA-only scanner misses PYSEC-only entries), alias double-counting when the same vulnerability carries both a PYSEC and GHSA ID in merged feeds, and transitive dependency resolution, since pip-audit flags packages like urllib3 and idna pulled in indirectly that never appear in the requirements file itself. daily.dev surfaces write-ups like this for developers reconciling conflicting scanner outputs across their dependency stack."}},{"@type":"Question","name":"How does the OSV format represent which versions of a package are affected by a vulnerability?","acceptedAnswer":{"@type":"Answer","text":"OSV encodes affected versions as an event list rather than a simple range string: an 'introduced' event opens a vulnerable interval, a 'fixed' event closes it exclusively (the fixed version itself is safe), and a 'last_affected' event closes it inclusively. Some advisories skip ranges entirely and instead enumerate an exact list of affected versions, so a correct parser must handle both shapes and must compare versions using PEP 440 semantics rather than lexical string comparison. developers implementing their own OSV parsing logic can find these edge cases surfaced on daily.dev before shipping buggy version checks."}},{"@type":"Question","name":"Why do PyPI package names need to be normalized when matching them against security advisories?","acceptedAnswer":{"@type":"Answer","text":"Under PEP 503, PyPI treats package names like Foo-Bar, foo_bar, and foo.bar as identical, so an advisory filed against PyYAML must still match a requirements line written as pyyaml. Scanners that skip this normalization step silently miss findings while appearing to work correctly, since the mismatch produces no error, just an absent result. anyone building or auditing dependency tooling can track packaging quirks like this through daily.dev."}}]}
```

