Skip to main content

🛡️ SafePyGuard

Python dependency license & vulnerability scanner — one report, one CI gate.

CI Python Coverage License: MIT

SafePyGuard reads your requirements.txt and checks every dependency for two kinds of risk:

Risk Example What SafePyGuard does
⚖️ Legal PyQt5 is GPL-3.0, which can force you to open-source your own code Classifies each license as Approved / Restricted / Denied / Unknown using your policy
🔓 Security requests==2.19.0 leaks credentials (CVE-2018-18074) Finds known vulnerabilities via OSV.dev, with severity and the version that fixes them

Both results land in one report, and the exit code tells your CI pipeline whether to block the merge.


Contents


Demo

$ safepyguard scan -r examples/requirements-demo.txt

SafePyGuard: examples/requirements-demo.txt
┌──────────┬──────────────────┬──────────────┬────────────┬──────────────────────┐
│ Package  │ Version          │ License      │ Category   │ Vulnerabilities      │
├──────────┼──────────────────┼──────────────┼────────────┼──────────────────────┤
│ requests │ 2.19.0           │ Apache-2.0   │ APPROVED   │ 5 (HIGH) -> 2.33.0   │
│ django   │ 3.2.0            │ BSD-3-Clause │ APPROVED   │ 38 (CRITICAL)        │
│ PyQt5    │ 5.15.0           │ GPL-3.0-only │ DENIED     │ none                 │
│ certifi  │ 2024.8.30        │ MPL-2.0      │ RESTRICTED │ none                 │
│ numpy    │ 2.5.4 (unpinned) │ BSD-3-Clause │ APPROVED   │ none                 │
│ click    │ 8.1.7            │ BSD-3-Clause │ APPROVED   │ none                 │
└──────────┴──────────────────┴──────────────┴────────────┴──────────────────────┘

Vulnerability details
requests 2.19.0
  HIGH      GHSA-x84v-xcm2-53pg (CVE-2018-18074)  fixed in 2.20.0
            Insufficiently Protected Credentials in Requests
  ...

6 package(s) | licenses: 4 approved, 1 restricted, 1 denied | vulnerabilities: 43 (6 critical, 17 high, ...)
FAILED
  - denied license: PyQt5
  - vulnerabilities at or above high severity: requests (1), django (22)

Features

License compliance

  • Normalizes messy license text ("Apache 2.0", "BSD License", "GPLv3+") to standard SPDX IDs
  • Reads every place a package can declare a license: PEP 639 license_expression, the license field, trove classifiers, even a full license text pasted into metadata
  • Understands SPDX expressions — MIT OR Apache-2.0, (MIT AND BSD-3-Clause), WITH exceptions
  • Configurable TOML policy with wildcards (GPL-*) and per-package exceptions with a recorded reason

Vulnerability scanning

  • Queries OSV.dev in one batch request (no API key needed)
  • Merges duplicate advisories (GHSA / PYSEC / CVE) into a single finding
  • Severity from GitHub advisories, falling back to a built-in CVSS v3.1 calculator
  • Shows the smallest version that fixes each issue

Built for CI

  • Exit codes: 0 passed · 1 policy violated · 2 scan could not run
  • Output as a terminal table, JSON, Markdown (PR comments) or SARIF (GitHub Security tab)
  • Ready-made GitHub Action and pre-commit hook
  • Concurrent lookups and a 24-hour on-disk cache for fast repeat runs

Installation

Requires Python 3.10+.

pip install safepyguard
Install from a local clone (for development)
git clone https://github.com/glichy-gem/SafePyGuard.git
cd SafePyGuard
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

Quick start

# 1. Scan your project's requirements
safepyguard scan -r requirements.txt

# 2. (Optional) create an editable policy file in the current directory
safepyguard init-policy

# 3. Scan again — safepyguard.toml is picked up automatically
safepyguard scan

Try it on the bundled demo file:

safepyguard scan -r examples/requirements-demo.txt

CLI reference

safepyguard scan [OPTIONS]
Option Description Default
-r, --requirements PATH Requirements file to scan requirements.txt
-p, --policy PATH Policy TOML file ./safepyguard.toml if present, else built-in
-f, --format table, json, markdown, sarif table
-o, --output PATH Write the report to a file terminal
--fail-on-license Categories that fail the scan, e.g. denied,unknown, or none from policy (denied)
--fail-on-severity low, moderate, high, critical, none from policy (high)
--ignore-vuln ID Ignore a vulnerability ID or alias (repeatable) —
--no-licenses Skip the license check —
--no-vulns Skip the vulnerability check —
--no-cache Don't use the on-disk cache —
--timeout SECONDS Network timeout 15

Other commands: safepyguard init-policy [--path FILE] [--force] · safepyguard --version

Exit codes

Code Meaning
0 Every dependency passed the policy
1 The policy was violated (CI should fail)
2 The scan could not run (missing file, invalid policy, network down)

Unpinned requirements such as numpy>=1.26 are checked against the newest matching release (pre-releases and yanked releases are skipped). Pin your dependencies for reproducible results.


Policy file

Generate one with safepyguard init-policy, then edit it:

[licenses]
approved   = ["MIT", "BSD-3-Clause", "Apache-2.0", "ISC", "PSF-2.0"]
restricted = ["LGPL-*", "MPL-*"]      # allowed, but needs a review
denied     = ["GPL-*", "AGPL-*"]      # wildcards are supported

[exceptions]                          # per-package overrides, with a reason for auditors
"pyqt5" = { category = "approved", reason = "Commercial license purchased, LEGAL-123" }

[vulnerabilities]
ignore = ["GHSA-xxxx-xxxx-xxxx"]      # accepted risks (IDs or CVE aliases)

[gate]
fail_on_license  = ["denied"]         # approved | restricted | denied | unknown
fail_on_severity = "high"             # low | moderate | high | critical | none

Classification rules

Situation Result Why
MIT OR GPL-3.0-only Approved With OR you may choose, so the most permissive option wins
MIT AND GPL-3.0-only Denied With AND you must comply with both, so the strictest wins
Exact entry vs. wildcard Exact wins approved = ["GPL-2.0-only"] overrides denied = ["GPL-*"]
Same precision in two lists Stricter wins denied > restricted > approved
License not in any list / unrecognized Unknown Add "unknown" to fail_on_license to require a review
Vulnerability with unknown severity Fails the gate If severity can't be determined, a human should look

See examples/policy-strict.toml for a production-style policy.


CI integration

GitHub Actions

- uses: actions/checkout@v4
- uses: glichy-gem/SafePyGuard@v0.1.0
  with:
    requirements: requirements.txt
    fail-on-severity: high

To show findings in the repository's Security tab, export SARIF and upload it — see the complete workflow in examples/github-workflow.yml.

Action input Default
requirements requirements.txt
policy auto-detect
format table
output —
fail-on-license from policy
fail-on-severity from policy
python-version 3.12

pre-commit

repos:
  - repo: https://github.com/glichy-gem/SafePyGuard
    rev: v0.1.0
    hooks:
      - id: safepyguard

Any other CI (GitLab, Jenkins, Azure DevOps…)

pip install safepyguard
safepyguard scan -r requirements.txt -f markdown -o safepyguard-report.md

The non-zero exit code fails the job on its own.


Python API

from safepyguard import evaluate, scan

report = scan("requirements.txt", policy="safepyguard.toml")
gate = evaluate(report)

for package in report.packages:
    print(package.name, package.version, package.license.category.value)
    for vuln in package.vulnerabilities:
        print(f"  {vuln.severity.label:<9} {vuln.id}  fix: {vuln.fix_version}")

print("PASSED" if gate.passed else gate.reasons)

How it works

flowchart LR
    A[requirements.txt] --> B[Parse<br/>pins, -r includes, markers]
    B --> C[PyPI JSON API<br/>version + license metadata]
    C --> D[Normalize<br/>→ SPDX expression]
    D --> E[Policy<br/>Approved / Restricted / Denied / Unknown]
    B --> F[OSV.dev batch query]
    F --> G[Merge duplicates<br/>severity + fix version]
    E --> H[Unified report]
    G --> H
    H --> I{Gate}
    I -->|pass| J[exit 0]
    I -->|fail| K[exit 1]
    H --> L[table · JSON · Markdown · SARIF]

Project structure

SafePyGuard/
├── src/safepyguard/            # the installable package
│   ├── cli.py                  #   command-line interface (click)
│   ├── scanner.py              #   orchestrates the whole scan
│   ├── gate.py                 #   pass/fail decision and exit codes
│   ├── models.py               #   data classes: Dependency, Vulnerability, Report…
│   ├── cache.py                #   24-hour on-disk JSON cache
│   ├── errors.py               #   exception hierarchy
│   ├── parsers/
│   │   └── requirements.py     #   requirements.txt → dependencies
│   ├── sources/
│   │   ├── pypi.py             #   PyPI JSON API client
│   │   ├── osv.py              #   OSV.dev client, de-duplication, CVSS scoring
│   │   └── http.py             #   shared HTTP client settings
│   ├── licenses/
│   │   ├── normalize.py        #   license text → SPDX expression
│   │   ├── policy.py           #   TOML policy loading and classification
│   │   └── default_policy.toml #   built-in policy
│   └── reporters/              #   table, JSON, Markdown and SARIF output
├── tests/
│   ├── unit/                   # one test file per module, fully offline
│   ├── integration/            # end-to-end scanner and CLI tests (+ one live test)
│   ├── fakes.py                # in-memory fake PyPI and OSV servers
│   └── conftest.py             # shared fixtures
├── examples/                   # demo requirements, strict policy, CI workflow
├── .github/workflows/          # CI (lint, type-check, tests) and PyPI release
├── action.yml                  # the reusable GitHub Action
├── .pre-commit-hooks.yaml      # the reusable pre-commit hook
├── pyproject.toml              # packaging + tool configuration
├── CHANGELOG.md
└── LICENSE

Development

pip install -e ".[dev]"

pytest                      # 159 offline tests against fake PyPI/OSV servers
pytest -m network           # live test against the real APIs
ruff check . && ruff format --check .
mypy                        # strict type checking

CI runs the test suite on Python 3.10–3.13 on both Ubuntu and Windows.

Releasing: bump src/safepyguard/_version.py, update CHANGELOG.md, then push a tag such as v0.1.0. The release workflow builds the package and publishes it to PyPI via trusted publishing.


License

MIT © 2026 Shivam Yadav

Metadata

Release files for safepyguard 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for safepyguard 0.1.0
File Size Uploaded Provenance
safepyguard-0.1.0.tar.gz 44.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for safepyguard 0.1.0
File Interpreter ABI Platform
safepyguard-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 83.3 kB

Release files / safepyguard-0.1.0.tar.gz

Download URL safepyguard-0.1.0.tar.gz
Size 44.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f160642db05afcad8135f07fce0ac85b3e69d725bcb22b1bed73dd0a3bb69a5d
BLAKE2b-256 checksum
How to use checksums
5d07ad55f3a26d52353793d636540a93c8c6e93a82771f793fc031e0eb889596
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / safepyguard-0.1.0-py3-none-any.whl

Download URL safepyguard-0.1.0-py3-none-any.whl
Size 39.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8badfdab5a45d9d20989e1387027c2549964d5834e427659b517bea30ab7324e
BLAKE2b-256 checksum
How to use checksums
637d1a2e8d666705a05f477aef42435edee468493d0186474e50215e31074203
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Security for safepyguard 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for safepyguard-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page