Skip to content

Releases: binuka200/package-doctor

package-doctor 1.0.2

Choose a tag to compare

@binuka200 binuka200 released this 16 Sep 22:00

Upgrading

If you copied the Claude Code hook configuration from an earlier README, add
Bash to the PostToolUse matcher so the lockfile check runs after shell
commands:

"PostToolUse": [
  { "matcher": "Bash|Edit|Write|MultiEdit",
    "hooks": [{ "type": "command", "command": "package-doctor hook claude-code", "timeout": 60 }] }
]

Without it, only edits to dependency files are checked after the fact.

Fixed

  • The install hook checks what an install pulls in, not just what it
    names.
    uv add, poetry add and pipenv install now trigger the
    lockfile check that previously ran only after resolves such as uv sync, so
    a vulnerable or abandoned package two levels down the tree reaches the model
    as context. Names the command typed are left out, since they were checked
    before it ran.
  • The documented hook configuration ran the lockfile check for nobody. Its
    PostToolUse matcher listed only Edit|Write|MultiEdit, so the check after
    shell commands never fired. It is now Bash|Edit|Write|MultiEdit, in the
    README and the agent guardrail page. The page also stopped claiming coverage
    after pip, pip-tools, PDM and Rye commands, whose lockfiles are not read.
  • check and the hook honour accepted risks. Only scan read
    package-doctor.toml, although the hook's own block message told the agent
    to ask for an acceptance and rerun. The same rules apply as in scan: an
    accepted package is allowed with its reason passed to the model, an expired
    acceptance blocks again and says so, a version-tied one covers only that
    version, and provenance checks are never overridden. check gains
    --config, and its JSON an accepted field.

package-doctor 1.0.1

Choose a tag to compare

@binuka200 binuka200 released this 16 Sep 20:25

Documentation and metadata. Nothing covered by the 1.0 compatibility promise
changes.

Changed

  • The README is a landing page. It went from 1,006 lines to 169: what the
    tool does, the report sections, install, quick start, CI and Claude Code
    setup, and the headline accuracy numbers. The detail moved unchanged into
    docs/usage.md, docs/agent-guardrail.md, docs/how-it-works.md,
    docs/exposure-map.md and docs/accuracy.md, linked by absolute URL so
    PyPI renders them.
  • The summary says what the tool is for: which Python dependencies to fix
    first, the ones being exploited and the ones nobody is left to patch. The
    same wording is in the PyPI summary, the GitHub Action description and
    package-doctor --help.
  • The accuracy numbers were re-measured on 1.0.0, on fresh clones: 6,897 of
    6,898 pinned pairs agree with OSV, pip-audit finds nothing package-doctor
    misses, and the exposure map's comparison now excludes entries decided from
    advisories, which would otherwise build the outcome into the test.
  • PyPI keywords drop sbom, which the tool does not do, and add
    vulnerability, osv, cisa-kev, epss, sarif, pip-audit and
    claude-code.

Fixed

  • --stale-release-days and --stale-push-days have help text, with their
    defaults, in scan, explain, check and hook.
  • The pre-commit hook runs when setup.py or setup.cfg changes, both of
    which the scanner reads.
  • Stale documentation, checked against the code: the Action and pre-commit
    examples pinned v0.3.0; a check example showed a warning the tool no longer
    gives; the scan example listed its sections out of order; joblib was named
    as a model-loading package; the map size and test time were out of date.
  • Undocumented behaviour is documented: research/audit_map.py, building
    the dataset suggest_map.py reads, GitHub as a data source, the --json,
    --output, --fail-on, --all and --depth options, and every install and
    resolve command the agent guardrail reads.

package-doctor 1.0.0

Choose a tag to compare

@binuka200 binuka200 released this 16 Sep 18:47

No behaviour changes from 0.9.1. This release marks the interfaces other
tools build on as stable.

Stable from 1.0

Covered by semantic versioning, so a change that breaks any of these waits
for 2.0:

  • the scan, explain, check, hook and cache commands and their options;
  • the --json output at schema_version 3 - fields may be added, none
    removed or renamed;
  • the verdict names exploited, replace, mitigate, upgrade, quiet,
    unchecked and ok, and the --fail-on levels;
  • exit codes: 0 nothing that fails the build, 1 something does, 2 a
    usage error;
  • the Claude Code hook's input and output.

Not frozen: the data. The exposure map, the advisories, the CISA KEV list
and EPSS scores change continuously, and map updates ship in minor and patch
releases. Upgrading package-doctor can therefore change the verdict on a
package you have not touched - that is the tool getting more accurate, not an
interface change. Pin package-doctor itself when a build must be reproducible.

Changed

  • The package is marked Development Status :: 5 - Production/Stable,
    replacing Alpha.
  • The README shows real output. Screenshots of package-doctor scan and
    package-doctor explain pillow on a demo project replace two text blocks,
    one of which still showed the pre-0.9 EXPLOITED header. They are served
    from GitHub, so PyPI renders them too.

Fixed

  • The sdist no longer carries the README screenshots. docs/ is excluded,
    keeping the sdist at around 317 KB rather than 1.9 MB.

package-doctor 0.9.1

Choose a tag to compare

@binuka200 binuka200 released this 16 Sep 11:19

Added

  • The exposure map is audited against its own advisories.
    research/audit_map.py reads each mapped package's OSV advisories against
    the answer the map gives and reports three disagreements: a package cleared
    as not at a boundary whose advisories cite a trust-boundary weakness, an
    exposed package whose advisories point to a worse consequence than any of
    its categories, and a why citing an advisory id OSV has never heard of.
    Naming the advisory in a why settles a finding either way, so silence is
    the only thing reported. It runs weekly in the live contract workflow with
    --strict; an OSV outage is listed, not failed. A wrong entry is worse than
    a missing one - a missing entry says "boundary not reviewed", a wrong one
    looks exactly like a reviewed answer - and this is the check for it.

Changed

  • 66 map entries the first audit disagreed with were decided from the
    advisory text.
    28 packages gained the category their advisories reached:
    code execution for langchain, llama-index, smolagents, semantic-kernel,
    pillow, chromadb, pdfminer-six, apache-airflow, litellm and open-webui, and
    auth for signature verifiers (ecdsa, rsa, signxml, starkbank-ecdsa) and for
    mcp, fastmcp and mitmproxy. 26 kept their call with the reason recorded -
    mislabelled CWEs such as uvicorn's log injection, opt-in features, trusted or
    local preconditions, and langchain-core, whose serialization flaw stops
    short of code execution. Nine cleared packages now cite the advisories they
    were cleared despite.
  • 47 unreviewed packages were decided, found in a 110-repository random
    sample, most because their own advisories cite a trust-boundary weakness.
    31 are now mapped - among them fugue's pickling RPC server,
    nemo-toolkit and torchgeo's model loaders, lightrag-hku's JWT handling,
    gdown's archive extraction, homeassistant, zeroconf, rpyc and django-ses -
    and 16 are reviewed as not exposed, including vyper, whose advisories are
    miscompiled contracts, and pyngrok, which never sees tunnel traffic.
  • HTML and XML classifiers no longer drive inference. Of the unreviewed
    packages they guessed as parsing markup, about one in four did; the rest
    generate it (dominate, htmlmin, pytablewriter) or are documentation tooling.
    The five that parse outside markup - inscriptis, mf2py, sickle, onvif-python
    and feedgen - have curated entries instead.

Fixed

  • Map citations that named no real advisory. babel cited CVE-2021-20095
    and diskcache GHSA-r8gq-9x9w-jcpg; OSV has neither. They now cite
    CVE-2021-42771 and GHSA-w8v5-vhqr-4h9v. open-webui's reason said it was
    listed under auth, and it never was.

package-doctor 0.9.0

Choose a tag to compare

@binuka200 binuka200 released this 16 Sep 01:27

Changed

  • The report is grouped by trust boundary, and the verdicts are named for
    what to do.
    Sections used to be a diagnosis to decode ("exposed + no one
    home"), and the boundary was baked into the verdict, so the same work had two
    names depending on whether a human had curated the package yet. Now the group
    carries the priority and the verdict carries the action:

    FIX TODAY             known exploited, and your version is affected
    AT A TRUST BOUNDARY   replace and upgrade fail the build
    NOT AT A TRUST BOUNDARY   reviewed: worth knowing, not blocking
    BOUNDARY NOT REVIEWED     nobody has judged these yet
    

    The verdicts are exploited, replace, mitigate, upgrade, quiet,
    unchecked and ok, replacing act/watch/low/unknown. The two lower groups
    print as a count; --all lists them.

  • "Quiet for years" no longer asks for a replacement. Two weak signals are a
    forecast about who would answer, not evidence that anything is wrong, so they
    now produce quiet, which never fails a build. Across sixty repositories 19
    of 26 "replace" verdicts rested on age alone with no advisory against the
    version in use. Replace now needs proof - an archived repository, the
    maintainer's Inactive classifier, or an unfixable advisory in a project that
    has also gone quiet.

  • An advisory nobody fixed, in a project that is still shipping, is
    mitigate.
    chromadb, mariadb and nltk were being told to plan a
    replacement while committing daily. Upgrading cannot clear these, so the call
    is a human one and it never fails a build.

  • The trust boundary decides what blocks, not what a finding is called.
    Exploitation blocks wherever it is found; replace and upgrade block at a
    reviewed boundary and inform away from one. An inferred exposure is
    unreviewed, so it can never fail a build.

  • --fail-on takes exploited, boundary (default), vulnerable, all or
    never. act, watch and the short-lived replace/upgrade/review/bump
    names still work. The action's fail-on input follows.

  • JSON schema_version is 3: findings carry boundary ("at", "clear",
    "unreviewed") and blocks, and counts uses the new verdict names. SARIF
    rules are package-doctor/exploited, /replace, /upgrade, /mitigate and
    /quiet, and a finding that does not fail the build is downgraded from
    error to warning so a dashboard shows what a pipeline would.

  • check and the Claude Code hook map one-to-one onto the report. They
    block exactly what a scan fails on - fix today anywhere, replace or
    upgrade at a reviewed trust boundary - and warn on mitigate, on the same
    facts away from a boundary, and on a boundary package that has gone quiet.

  • The hook reads the install shapes it was missing. uv run --with,
    uvx, uv tool run, uv tool install, pipx run and rye add were all
    invisible to it; the first three fetch a package and execute it in one step,
    which is the case the guardrail exists for. uvx ruff check . names ruff
    and not check, and an option value is never read as a package.

  • The hook is aware of where an install would fetch from. A non-PyPI
    --index-url or --extra-index-url is stated, and one over plain HTTP - or
    with TLS waived for its host by --trusted-host - says so. With a private
    index configured, not on PyPI is a warning rather than a block: a package
    missing from PyPI is what an internal package looks like.

  • A block names a requirement that would pass - -> retry with pillow==12.3.0 - so a refusal is a corrected command rather than a reason
    to try the same one again.

  • A warning is said once per session, keyed on the event's session_id.
    Blocks still repeat every time the command is tried.

  • A PostToolUse hook checks what a resolve locked. uv sync,
    poetry lock and pip install -r type no package name; this diffs the
    lockfile against the last commit and checks what was added, up to twenty
    packages, counting the rest.

  • A healthy package at a trust boundary no longer warns. Inherited from the
    old watch tier, it fired on seven of the ten packages an agent most often
    adds - httpx, fastapi, jinja2 and requests among them, each with a
    spotless advisory record - and said nothing anyone could act on.

Fixed

  • Claude Code hook silently allowed non-git/http(s) VCS installs. The
    PreToolUse hook's URL/VCS block only recognized four prefixes
    (http://, https://, git+, ssh://). pip install also accepts
    hg+, bzr+, svn+, and bare git:// references, including the
    bzr+lp:name Launchpad shorthand; none of those matched the allowlist,
    and none parse as a valid PyPI requirement string either, so the hook
    produced no block and no warning at all. is_remote_install_target()
    now classifies every scheme pip fetches over the network, and the hook's
    filter uses it instead of the narrower inline tuple.

v0.8.4

Choose a tag to compare

@binuka200 binuka200 released this 13 Sep 21:13

Fixed

  • Hash-pinned requirements files were read as empty. pip-compile --generate-hashes and uv export write every requirement as a
    backslash-continued block with one --hash per line. The requirements
    parser read the file physically, so no line was a requirement and a
    459-package file scanned as "No dependencies found". Continued lines are
    now joined the way pip does - a trailing backslash continues the logical
    line, a comment ends it - and per-requirement options such as --hash=...
    are stripped before the line reaches the requirement parser. -r includes
    split across a continuation line are followed as before.

Measured

The sixty-repository run (research/eval-repos.txt) was repeated on 0.8.3,
and the README's Accuracy section now carries these figures:

  • 13,042 packages, 6,879 distinct pinned pairs; advisory matching agrees
    with OSV on 6,878 of them, the one disagreement still langsmith 0.3.45.
  • Against pip-audit --no-deps --disable-pip -s osv on the same pins:
    331 packages flagged by each, 1,661 vulnerabilities found by both, zero
    found only by pip-audit, one only by this tool.
  • 77,148 import sites, down from 108,193, because 0.8.1 stopped counting
    files under overlapping source roots twice; 77,119 verify, the other 29
    are pytest's _pytest and py modules.
  • 2,257 advisories affecting pinned versions, 27 on CISA KEV or above 10%
    EPSS (13 distinct CVEs).
  • Curated coverage 75%, up from 45%, after the 249 map entries in 0.8.3.
  • 552 act verdicts, 132 of them on maintenance signals across 42 packages,
    up from 97 on 28; the increase is the larger map, and the README names the
    entrants that sit closest to the release and commit thresholds.
  • letta-ai/letta no longer has a dependency file in its repository, so 59
    of 60 repositories are read, as before.

v0.8.3

Choose a tag to compare

@binuka200 binuka200 released this 13 Sep 11:57

Added

  • Families as data. [reviewed] families holds the name patterns the
    map's header used to describe in a comment - google-cloud-*, types-*,
    pytest-*, opentelemetry-* and twelve others - each with a reason.
    is_reviewed honours them, explain shows the family's reason, and the
    suggestion tooling stops proposing their members. An explicit entry beats
    the pattern, which is how apache-airflow-providers-fab (auth manager),
    google-cloud-aiplatform (stored XSS) and opentelemetry-instrumentation
    (request-attribute cardinality DoS) stay exposed inside not-exposed
    families. Exposure map schema_version is 6.
  • A reason on every entry. All 997 category entries, the
    reviewed-and-cleared list and the stable list record what convinced the
    curator; tests/exposure_unexplained.txt is empty and stays that way.
  • 249 new category entries, worked from the CWE-backed candidate list
    and the rank 101-1,000 gap: every one of the top 100 and 998 of the top
    1,000 packages by downloads now have a curated call (1,774 of 3,000). New
    shelves with a family test each: inbound webhook verifiers (stripe,
    twilio, slack-sdk, svix, sendgrid, django-anymail), rate
    limiters (flask-limiter, slowapi, django-ratelimit, limits),
    broker consumers (confluent-kafka, kafka-python, aiokafka,
    nats-py, faststream, arq, taskiq), expression evaluators
    (simpleeval, asteval, restrictedpython, numexpr, sympy),
    hostile-by-design parsers (pefile, oletools, yara-python,
    pyelftools, lief, capstone), HDF5, netCDF and GDAL readers, notebook
    and app servers (jupyterlab, notebook, marimo, voila, panel,
    bokeh, nicegui), the packaging toolchain (uv, poetry, pipenv,
    installer), and email parsing.

Changed

  • Placements. A package may now carry several categories and the report
    takes the worst consequence: mlflow, vllm, sglang, bentoml and
    ray add the deserialization, model-loading, framework or remote-access
    categories their advisories describe instead of only llm/agent;
    pandas adds deserialization for read_pickle; reportlab adds
    templating for CVE-2023-33733; litellm and gradio add web framework.
    Moved: django-redis to deserialization (pickle by default),
    flask-session to auth, dateparser, lark, pyparsing and webcolors
    out of markup into data parsing, pathvalidate to archive extraction,
    lmdb to file parsing (its advisories are crafted database files),
    faiss-cpu to model loading, docxtpl to templating, babel to
    deserialization (CVE-2021-20095 loaded pickled locale data by
    Accept-Language). svgwrite, xlsxwriter and xlwt write and parse
    nothing and move to reviewed-not-exposed.

Fixed

  • A package locked at more than one version is scanned at the newest.
    uv.lock forks a package by Python version or extra, and the first entry
    used to win - which uv lists oldest. GitGuardian/ggshield was scanned as its
    Python 3.9 environment, with act verdicts on cryptography, marshmallow,
    requests and urllib3 that a 3.10+ install does not have. The older
    versions are now named in a scan note rather than dropped.
  • UTF-16 and BOM-prefixed dependency files are read. pip freeze > requirements.txt in PowerShell writes UTF-16; decoded as UTF-8 nothing
    parsed, and microsoft/Table-Pretraining's 31 pins reported as "No
    dependencies found".
  • setup.py is read, and never run. install_requires and
    extras_require written as literals - or as a list bound to a name first -
    are taken from the syntax tree. Three of forty randomly sampled
    repositories declared their dependencies nowhere else and scanned as "No
    dependencies found". A list computed in Python is invisible to a parser, so
    the scan names the file as not fully read instead of coming back clean.
  • research/evaluate_repos.py no longer crashes on cache rows with no
    response body, which OSV lookups for unknown packages leave behind.
  • A lockfile's version beats a different pin in another file. The
    newest-wins rule above applied across files too, so a requirements.txt
    newer than the lock would have been scanned instead of what installs.
    the-paperless-project/paperless ships a Pipfile.lock and a requirements.txt
    that disagree on 49 pins, and installs from the lock.
  • A root whose files declare nothing no longer stops discovery. Mailu's
    root pyproject.toml holds only towncrier settings, so the nested search
    never ran and its pins in core/base/requirements-prod.txt went unscanned:
    "No dependencies found". The same fallback now runs, and says so.

v0.8.2

Choose a tag to compare

@binuka200 binuka200 released this 13 Sep 10:18

Added

  • Nested discovery. When the project root has no dependency files,
    scan looks up to two directories down - never into tests, docs,
    examples, fixtures, vendored code or hidden directories - and says which
    files it used. A root with files sees no change. scan . now works for
    a project whose requirements live in configs/.
  • setup.cfg. install_requires and extras_require are read.
    setup.py is deliberately not: a computed install_requires is
    invisible to a parser, and a partial answer that looks complete is the
    failure this tool exists to avoid.
  • explain --src. The same repeatable option scan has, through the
    same root pruning and merge, so both commands report identical sites.
  • Map entries, each with a reason: flower (auth; GHSA-q4qm-xhf9-4p8f is
    an OAuth bypass) and flask-jwt (auth); uwsgi (http); flask-passlib
    (crypto); flask-restplus and flask-restx (web framework). Three new
    families: Flask API frameworks, WSGI and ASGI servers, Celery and its
    tooling.

Full changelog: v0.8.1...v0.8.2

v0.8.1

Choose a tag to compare

@binuka200 binuka200 released this 13 Sep 09:50

Fixed

  • Overlapping source roots double-counted import sites. Auto-detection
    returns each package directory and the project root when it holds a
    script - core and . for a layout with core/__init__.py and
    app.py - and a file under both was indexed and reported twice, so
    explain listed every site twice and one package claimed 65 sites
    where there were 33. Overlapping --src values did the same. Only the
    outermost roots are walked now, and sites from several roots are merged
    by resolved path and line rather than by their rendered string.

Full changelog: v0.8.0...v0.8.1

v0.8.0

Choose a tag to compare

@binuka200 binuka200 released this 13 Sep 09:37

Added

  • Map entries from a second outside review, each with a reason:
    flask-httpauth (CVE-2026-34531 is an authentication bypass) and
    flask-ipfilter (auth); netaddr (url: before 1.0 it accepted
    inet_aton forms, so 010.0.0.1 parsed as 8.0.0.1 - no CVE, 1.0 simply
    stopped); flask-expects-json (data parsing); flask-mysql (query);
    legacycrypt (crypto); gearman3 and python3-gearman (http - the
    wire protocol is the boundary; nothing is unpickled); and
    configparser, json-log-formatter, log-with-context, rollbar,
    blinker as reviewed and not exposed. Two new families guard the
    Flask auth extensions and the wire-protocol job queues.

Changed

  • The commit date is the default branch's, not the repository's push
    time.
    GitHub's pushed_at advances on a push to any branch or tag and
    read fourteen months newer than the code for flask-restful. The last
    commit on the default branch is now read from GitHub's commit feed
    whenever the push time is inside the stale window, and is what the
    staleness signal uses; the push time is kept as the fallback and shown
    in explain labelled as such.
  • The watch tier reads as what it is. Its hint is now nothing to do
    today: someone is home
    , and a clean advisory record is worded
    healthy record: N of M fixed at or before disclosure rather than
    presented as the reason for a warning.

Fixed

  • --src FILE with a file outside the scanned project rendered import
    sites as .:LINE. The path is now given relative to the project, the
    working directory, or in full.
  • A name @ https://…tar.gz requirement was classified by its suffix as a
    local path named by the whole line. Lines the requirement parser accepts
    are now its to handle; only lines it rejects are classified as git, URL
    or path.
  • A bare wheel or sdist URL was named by normalising the whole filename
    (foo-1-0-py3-none-any-whl). It is now named by its distribution.
  • A renamed repository read as "repository metadata unavailable". GitHub's
    redirect is followed to the new address.
  • A failed repository lookup showed only in explain. The report table
    and Markdown now carry missing signal: … on the row, so a row is not
    mistaken for complete.

Full changelog: v0.7.0...v0.8.0