Constraint-range advisory scanning, dependency refresh, and Release-Notes reset recipe #41
No reviewers
Labels
No labels
bug
contribution welcome
duplicate
enhancement
good first issue
help wanted
invalid
question
upstream
No milestone
No assignees
3 participants
Notifications
Due date
No due date set.
Reference
marvin8/zaojun!41
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/issue-37-39-stage2-and-vuln-range"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Three pieces of work in one PR, as requested:
Constraint-range advisory scanning (#39)
The package-level PyPI endpoint only lists advisories against the newest release, so a constraint like
~=2.3looked fully up to date while still admitting known-vulnerable versions (exactly what hid the httpx2 advisories from a library-mode run against longwei 2.1.2 — six advisories, invisible). zaojun now fetches the version-level PyPI JSON response for each spec's lower bound (>=,==,~=) and reports advisories affecting it:--short) +Advisories in declared rangeblock with a suggested floor (highest per-advisory minimumfixed_in)vuln-ignore; never silenced by--libraryor--compat-okpackage@lowerboundkey with the same 24 h TTL; failing fetches degrade to no advisories with a stderr warning; JSON gainsrange_vulnerabilities+suggested_floordocs/usage/command-reference.md) and Release-Notes updatedKnown limitation (documented): advisories introduced after the floor aren't caught by a lower-bound-only fetch — a full-range OSV query would be the follow-up.
Dependency refresh + security posture (#37)
Most of #37 landed via merged PR #38 (Codeberg references clean; stale ROADMAP trackers deliberately removed in
952d57b, so tracker completion = closing the issue with stage 1 = PR #36 and stage 2 = PR #38 recorded as merged). What was genuinely stale after ~4 weeks:zaojun --format json(min-age 7) → cyclopts ~=4.25.0, complexipy ~=8.0.0, mkdocstrings-python ~=2.0.8, prek ~=0.5.2, ruff ~=0.16.6, ty ~=0.0.78, uv ~=0.12.10; pre-commit revs aligned;uv.lock+pylock.tomlregeneratedRelease-Notes reset recipe (#40)
just fediverse-postno longer owns the reset logic: a newjust reset-release-notesrecipe does the template substitution, andfediverse-postdelegates to it.just releaseuntouched.Checks
ruff check/format, ty, complexipy, interrogate, deptry clean; 312 tests pass (95% coverage, min 90);
taibai thread --dry-runvalid.WuMing
Found 9 issue(s). See inline comments below.
@ -15,6 +15,8 @@`zaojun` is a command-line (CLI) tool to check versions of your dependencies as defined in `pyproject.toml` against the latest versions published on [PyPI](https://pypi.org). Full documentation is available at [zaojun.marvin8.zone](https://zaojun.marvin8.zone/latest/).**New**: Constraint-range advisory scanning — zaojun checks the oldest version your constraint allows, not just the newest release. When that floor has known security advisories, the dependency line gains a 🚨 inline indicator and an `Advisories in declared range` block lists them with a suggested floor that clears every fixable advisory. One extra cached HTTP request per constrained package; `vuln-ignore` rules apply.docs [MEDIUM]
This new item breaks the convention used by the two entries below it, which are both labelled with a version (
**New in v1.6.0**,**New in v1.5.0**). A bare**New**leaves the reader unable to tell which release introduced the feature and will need re-editing on the next release. Label it with the release it ships in (e.g.**New in v1.7.5**, matchingRelease-Notes.md). While you are here, consider mentioning the same behaviour the other entries state for parity: that range advisories set exit code 1 and are never silenced by--library/--compat-ok(currently onlyvuln-ignoreis mentioned).@ -8,3 +10,4 @@---zaojun's vulnerability feed only looked at the newest release on PyPI, so a constraint like `~=2.3` could look fully up to date while still admitting six known-vulnerable versions. zaojun now fetches the advisory list for each constraint's lower bound as well. Anything your range still allows shows up as a 🚨 warning inline plus an "Advisories in declared range" section, with a suggested floor that clears every fixable advisory — the exact gap that hid the httpx2 advisories from library-mode runs. Range warnings respect `vuln-ignore`, always set exit code 1, and are never silenced by `--library` or `--compat-ok`.docs [LOW]
"admitting six known-vulnerable versions" misstates the verified case: the longwei 2.1.2 run surfaced six advisories (see the README wording, "six advisories"), not six distinct vulnerable versions. Reword to something like "while still admitting versions with six known advisories" so the release notes agree with the README and the PR description.
@ -160,0 +165,4 @@package-level PyPI endpoint only lists advisories against the newest release, so aconstraint like `~=2.3` can look fully up to date while still admitting known-vulnerablereleases. zaojun therefore also fetches the version-level PyPI JSON response for eachspec's lower bound (`>=`, `==`, `~=`) and reports advisories that affect it:docs [LOW]
The operator list (
>=,==,~=) reads as exhaustive but is never stated as such, so a user with a>2.3(or other) constraint cannot tell whether their range is scanned. For>2.3the bound version is not itself allowed by the constraint, so scanning it would produce false positives; whatever the actual behaviour (skip vs. scan), state it explicitly here, e.g. "constraints whose operator excludes the bound version (>) are not scanned".@ -160,0 +179,4 @@```The 🚨 inline indicator is always shown (even in `--short` mode); the block follows thesame suppression rules as `Vulnerabilities`. Advisories already reported against thedocs [LOW]
"the block follows the same suppression rules as
Vulnerabilities" is ambiguous, because the preceding sentence has just said the inline indicator is always shown even in--short, whileVulnerabilitiessuppression rules are not spelled out in this section. A reader cannot tell whether theAdvisories in declared rangedetail block is hidden in--short/--libraryor not, which conflicts with the "never silenced by--libraryor--compat-ok" claim three lines later. Replace with an explicit statement of exactly which flags hide the block and which do not.@ -516,6 +547,8 @@ human-readable text. The JSON format is stable and suitable for scripting and CI`groups` is only present when `--groups` is used. `--short` has no effect in JSON mode.`vulnerabilities` is only present on records that have open (non-ignored) advisories.`range_vulnerabilities` and `suggested_floor` are only present on records whosedocs [LOW]
The two new JSON fields are named but never described: neither the shape of
range_vulnerabilities(array of objects with which keys — advisory id, aliases,fixed_in?) nor the type ofsuggested_floor(string, like2.12.0) is documented. Also, the stated presence condition covers onlyrange_vulnerabilities;suggested_flooris additionally omitted when no in-range advisory has a listedfixed_in, as noted in the section above. Document the element shape and the exact presence conditions for each field.@ -207,0 +217,4 @@index_url=index_url,)latest_ids = {vuln.id for vuln in result.vulns} | {alias for vuln in result.vulns for alias in vuln.aliases}result.range_vulns = [vuln for vuln in range_vulns if vuln.id not in latest_ids]code [LOW]
Deduplication checks only the range advisory's primary
idagainstlatest_ids, butlatest_idsalso contains aliases. If a range advisory has a different primary ID but an alias matching a latest advisory ID or alias, it will still appear in bothvulnsandrange_vulns. Include the range advisory's aliases in the membership check to avoid duplicate advisories.@ -124,0 +176,4 @@response = await client.get(f"{index_url.rstrip('/')}/{package_name}/{lower_bound}/json")response.raise_for_status()payload = response.json()except (httpx2.HTTPStatusError, httpx2.RequestError, json.JSONDecodeError):security [MEDIUM]
The range-advisory fetch catches HTTP/protocol/JSON errors and returns an empty list without logging. Since this is a security-relevant vulnerability scan, a failed fetch silently degrades to "no advisories", potentially hiding known vulnerable versions. Log a warning/error and mark the scan as degraded. A09:
@ -17,0 +32,4 @@"""minima: list[Version] = []for advisory in advisories:parsed = [parse_version(v) for v in advisory.fixed_in]code [MEDIUM]
parse_versionmay raisepackaging.version.InvalidVersionfor malformed/non-PEP-440 entries infixed_in. Sincesuggest_flooris called insidecheck_dependency's try block, that ValueError can turn a best-effort range advisory scan into a package-check error instead of simply ignoring the unfixable advisory. CatchInvalidVersionper entry and skip invalid versions.@ -88,0 +127,4 @@"""if not result.range_vulns:returnresult.range_vulns = _filter_vulns(code [MEDIUM]
After
_filter_vulnsfilters out ignored range advisories,suggested_flooris not recomputed. It was previously set from the unfiltered list incheck_dependency; if the ignored advisory had the highest minimum fixed version, the suggested floor is stale/higher than necessary, and if all are ignored asuggested_floorcan remain in JSON whilerange_vulnerabilitiesis absent. Recompute viasuggest_floor(result.range_vulns)or setNonewhen empty.082eb7bd6a0148cdfed6src/zaojun/pypi.py✅ Fixed in
0148cdf— a failed range fetch now prints a warning to stderr (zaojun: range-advisory fetch for <package> <floor> failed (…) — scan degraded), so a silently-degraded scan is visible. Covered bytest_http_error_warns_on_stderr.src/zaojun/vulns.py✅ Fixed in
0148cdf—_apply_range_vuln_filternow recomputessuggested_floorviasuggest_floor(result.range_vulns)after suppression: a suppressed max-fix advisory lowers the floor, and full suppression clears it toNone(so it cannot linger in JSON withoutrange_vulnerabilities).src/zaojun/vulns.py✅ Fixed in
0148cdf—suggest_floornow skips non-PEP-440fixed_inentries (contextlib.suppress(InvalidVersion)per entry) instead of bubbling a ValueError into a package-check error.src/zaojun/checker.py✅ Fixed in
0148cdf— the membership check now also drops range advisories whose aliases intersect the latest-advisory ID/alias set.README.md✅ Fixed in
0148cdf— labelled New in v1.8.0 (features ship in minor bumps under this repo's semver history — v1.5.0, v1.6.0; if the release lands as a different number the label is a one-word fix) and added the exit-code-1 / never-silenced parity sentence.Release-Notes.md✅ Fixed in
0148cdf— reworded to "still admitting versions with six known advisories", matching the README/PR wording.docs/usage/command-reference.md✅ Fixed in
0148cdf— the section now states explicitly that only constraints defining a concrete allowed floor are scanned (>=X.Y, exact==X.Y.Z,~=X.Y), and that>2.3,==2.3.*,<2.0are not scanned (bound either not allowed by the constraint or not a concrete release). That matches the implementation:_spec_lower_boundrecognises>=,==,~=only and skips wildcard/exclusive bounds.docs/usage/command-reference.md✅ Fixed in
0148cdf— replaced with explicit rules: theAdvisories in declared rangeblock is hidden by--short(likeVulnerabilities); the 🚨 inline indicator is always shown, even in--short; neither is affected by--libraryor--compat-ok.docs/usage/command-reference.md✅ Fixed in
0148cdf— the JSON section now documentsrange_vulnerabilitiesas an array of advisory objects with the same keys asvulnerabilities(id,aliases,fixed_in,details,link),suggested_flooras a version string, and the exact presence conditions for each — including recomputation aftervuln-ignoresuppression.WuMing
Found 3 issue(s). See inline comments below.
@ -15,6 +15,8 @@`zaojun` is a command-line (CLI) tool to check versions of your dependencies as defined in `pyproject.toml` against the latest versions published on [PyPI](https://pypi.org). Full documentation is available at [zaojun.marvin8.zone](https://zaojun.marvin8.zone/latest/).**New in v1.8.0**: Constraint-range advisory scanning — zaojun checks the oldest version your constraint allows, not just the newest release. When that floor has known security advisories, the dependency line gains a 🚨 inline indicator and an `Advisories in declared range` block lists them with a suggested floor that clears every fixable advisory. One extra cached HTTP request per constrained package; `vuln-ignore` rules apply. Range advisories set exit code 1 and are never silenced by `--library` or `--compat-ok`.docs [HIGH]
Version mismatch: this entry claims "New in v1.8.0", but the corresponding Release-Notes entry is published under the heading
zaojun 1.7.5(Release-Notes.md line 5). The README changelog also now jumps straight from v1.8.0 to v1.6.0, so no v1.7.x entry exists. Decide which version actually ships this feature and make the README heading and the Release-Notes version heading agree (and insert the missing v1.7.x entry if one was released).@ -160,0 +168,4 @@lower bound and reports advisories that affect it. Only constraints that define aconcrete allowed floor are scanned — `>=X.Y`, exact `==X.Y.Z`, and `~=X.Y` — because thebound version must itself be allowed by the constraint to be a meaningful floor.Constraints without one (e.g. `>2.3`, `==2.3.*`, `<2.0`) are not scanned.docs [LOW]
The stated rationale for excluding constraints is inconsistent with the example. The text says only constraints whose bound version "must itself be allowed by the constraint" are scanned, yet
==2.3.*is listed as excluded even though 2.3.x (and therefore a concrete floor such as 2.3.0) is allowed by that constraint. Either fix the rationale or state the real reason (e.g. wildcard==specifiers are not resolvable to a single floor version).@ -160,0 +189,4 @@suggested floor is the highest per-advisory minimum `fixed_in` version; advisorieswithout any listed fix cannot be cleared by a floor raise.A failed range fetch degrades to no advisories for that package and prints a warning todocs [HIGH]
Self-contradictory and contradicts the PR description. The sentence says a failed range fetch "degrades to no advisories" and prints a warning to stderr, then calls the result a "silently-degraded scan" — a scan that prints a warning is not silent. The PR description states the opposite behaviour: "failing fetches degrade silently". Confirm the actual implementation and document one behaviour: either the failure is silent (per the PR description) or it emits a stderr warning (per this sentence), not both.
docs/usage/command-reference.md✅ Fixed in commit
e4f7553— reworded to: "A failed range fetch produces no advisories for that package and prints a warning to stderr — the degradation is never silent." Also patched the PR description, which still said "degrade silently" from before the wave-1 fix.docs/usage/command-reference.md✅ Fixed in commit
e4f7553— the text now states the scan requires a single concrete allowed version and gives each exclusion its real reason:>2.3excludes the bound itself (scanning it would produce false positives);==2.3.*and<2.0do not resolve to a single concrete version to query.README.md🔴 No change — this re-raises the substance of the wave-1 finding (already answered ✅ above: new sha256, same substance — duplicate), but the three factual claims deserve direct answers:
zaojun 1.7.5heading inRelease-Notes.mdis the reset-template state, not a published mapping:bump-my-versionrewriteszaojun {current_version}in that file when the release is cut. Afterjust release minorit readszaojun 1.8.0and the README heading and Release-Notes agree.docs/usage/command-reference.md), so there is no missing v1.7.x entry to insert.just releasetime;v1.8.0is the semver-consistent label for a feature and the assumption was disclosed in the wave-1 reply — a one-word fix if the release lands differently.WuMing
Found 2 issue(s). See inline comments below.
@ -4,10 +4,16 @@ visibility: publiczaojun 1.7.5docs [MEDIUM]
Version mismatch: README.md line 18 announces this feature as "New in v1.8.0", but the new Release-Notes entry is added under the
zaojun 1.7.5heading. If the release notes are generated byjust reset-release-notestemplate substitution, either the heading must be updated to the version being shipped (1.8.0) or the README version must be corrected, so the two documents agree.@ -160,0 +166,4 @@constraint like `~=2.3` can look fully up to date while still admitting known-vulnerablereleases. zaojun therefore also fetches the version-level PyPI JSON response for thelower bound and reports advisories that affect it. Only constraints that resolve to asingle concrete allowed version are scanned — `>=X.Y`, exact `==X.Y.Z`, and `~=X.Y`.docs [MEDIUM]
Inaccurate description of which constraints are scanned.
>=2.3and~=2.3each allow many versions, so they do not "resolve to a single concrete allowed version". The actual criterion (as the following sentence about>2.3,==2.3.*and<2.0implies) is that the constraint has a single concrete inclusive lower bound. Reword to something like: "Only constraints with a single concrete lower bound are scanned —>=X.Y, exact==X.Y.Z, and~=X.Y." and align the exclusion sentence with that criterion.docs/usage/command-reference.md✅ Fixed in commit
3438968— reworded to "Only constraints with a single concrete inclusive lower bound are scanned", with the exclusion sentence aligned to that criterion.Release-Notes.md🔴 Duplicate — third re-raise of the same substance (wave-1 ✅ reply, wave-2 🔴 reply). The one decisive fact: the
zaojun 1.7.5heading is not generated by the reset template substitution —[tool.bumpversion]rewrites thezaojun {current_version}line inRelease-Notes.mdwhen the release is cut. Manually setting it to 1.8.0 now would (a) claim an unreleased version and (b) break bump-my-version's search match at release time. Afterjust release minorthe heading readszaojun 1.8.0and both documents agree. No further action.WuMing
Found 4 issue(s). See inline comments below.
@ -4,10 +4,16 @@ visibility: publiczaojun 1.7.5- Constraint-range advisory scanning: zaojun now also checks the oldest version each constraint allows — 🚨 warns when your declared range still admits known-vulnerable releases, and suggests a floor that clears themdocs [HIGH]
Version mismatch: this new bullet is inserted directly under the existing heading
zaojun 1.7.5(line 5), so the constraint-range feature is announced as part of 1.7.5, while README line 18 announces the same feature as "New in v1.8.0". Users and the fediverse post will be attributed to the wrong release. Update the heading on line 5 (and any other version placeholders in this file) to the actual release version, or correct the README if 1.7.5 is correct, so both files agree.@ -160,0 +193,4 @@A failed range fetch produces no advisories for that package and prints a warning tostderr — the degradation is never silent.Known limitation (documented): advisories introduced *after* the floor (affecting onlydocs [LOW]
The parenthetical "(documented)" is internal meta-commentary about the docs themselves and reads oddly in user-facing documentation — it also duplicates the "Known limitation" framing that already appeared in the PR description. Drop the parenthetical and keep only the limitation text, e.g. "Known limitation: advisories introduced after the floor …".
@ -124,0 +174,4 @@return _rebuild_advisories(cached.get("vulns") or [])try:response = await client.get(f"{index_url.rstrip('/')}/{package_name}/{lower_bound}/json")security [MEDIUM]
A10: The user-controllable index_url is concatenated directly into the request URL and passed to the HTTP client with no allow-list or scheme validation. In library/CI usage an attacker-influenced index_url could target internal endpoints or cloud metadata services. Restrict index_url to an allow-list of trusted HTTPS PyPI mirrors and parse/validate it before use.
@ -124,0 +179,4 @@payload = response.json()except (httpx2.HTTPStatusError, httpx2.RequestError, json.JSONDecodeError) as exc:print(f"zaojun: range-advisory fetch for {package_name} {lower_bound} failed ({exc!r}) — scan degraded",security [MEDIUM]
A09: The exception is logged with {exc!r}, which can expose the full request URL in stderr. If index_url contains embedded credentials (userinfo or query tokens), those secrets can leak into logs. Sanitise/redact the URL and exception before logging, and avoid interpolating raw dynamic values into log messages.
docs/usage/command-reference.md✅ Fixed in commit
f6f684f— dropped the parenthetical; the limitation text stands alone.src/zaojun/pypi.py✅ Fixed in commit
f6f684f— the warning now logs only the exception type name (HTTPStatusError/RequestError/JSONDecodeError), never the exception repr, so no URL or credentials can reach stderr. Test assertshttps://is absent from the warning.src/zaojun/pypi.py🔴 Declined — by design, not by omission.
--index-urlis a documented core zaojun feature (Artifactory, devpi, any PyPI JSON-compatible index); the value comes from the operator's own CLI args or[tool.zaojun]config. zaojun is a developer CLI, not a network service: an "attacker-influenced index_url" already implies local config compromise, at which point the attacker controls the entire check anyway. An HTTPS allow-list of "trusted mirrors" would break every private-index user and contradict the feature contract introduced in v1.5.0. The range probe reuses exactly the trust model of the pre-existing package-level fetch one line above ({index_url}/{package}/json) — no new exposure is introduced by this PR. Documenting "prefer HTTPS for token-bearing indexes" is a reasonable docs follow-up; a code-level gate is not.Release-Notes.md🔴 Duplicate — fourth re-raise of the same substance (wave-1, wave-2, wave-3). The answer is unchanged: the
zaojun 1.7.5heading is the current-version line that[tool.bumpversion]rewrites at release (search = "zaojun {current_version}"inpyproject.toml); setting it to 1.8.0 by hand would claim an unreleased version and break the release automation. Afterjust release minor, heading and README agree. Escalated to Marvin8 for a ruling on this loop.WuMing
Found 1 issue(s). See inline comments below.
@ -8,3 +10,4 @@---zaojun's vulnerability feed only looked at the newest release on PyPI, so a constraint like `~=2.3` could look fully up to date while still admitting versions with six known advisories. zaojun now fetches the advisory list for each constraint's lower bound as well. Anything your range still allows shows up as a 🚨 warning inline plus an "Advisories in declared range" section, with a suggested floor that clears every fixable advisory — the exact gap that hid the httpx2 advisories from library-mode runs. Range warnings respect `vuln-ignore`, always set exit code 1, and are never silenced by `--library` or `--compat-ok`.docs [LOW]
"Anything your range still allows shows up as a 🚨 warning" overstates the feature and contradicts the limitation documented in docs/usage/command-reference.md lines 196-198 (and the PR description): only advisories affecting the constraint's lower bound are fetched, so advisories introduced after the floor are missed. Soften this to something like "anything your lower bound is known to be affected by shows up" so the published note does not promise full-range coverage.
Release-Notes.md✅ Fixed in commit
3cbac0e— reworded to "Anything the floor is known to be affected by shows up as a 🚨 warning…", with the after-the-floor limitation now stated explicitly in the same paragraph. Also tightened the intro bullet ("warns when that floor is known to be affected by vulnerabilities") under the same no-overpromise rationale — flagged here since it goes one sentence beyond the quoted one.