Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions GenerateReport.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
GetPyPiInfo
)
from utils.VersionSuggester import (
suggest_safe_minor_upgrade
suggest_safe_minor_upgrade,
find_latest_safe_version_for_major
)
from utils.VulnChecker import (
check_cv_uv
Expand Down Expand Up @@ -216,6 +217,59 @@ def main() -> None:
else:
instruction = generate_upgrade_instruction(pkg, suggested)

outdated_instruction = None
outdated_target_version = None

try:
cur_ver_obj = version.parse(cur_ver)
except InvalidVersion:
cur_ver_obj = None

if cur_ver_obj and all_vs:
available_majors = sorted({version.parse(v).major for v in all_vs})
if available_majors:
latest_major_available = available_majors[-1]
if cur_ver_obj.major == latest_major_available:
target_major = cur_ver_obj.major
else:
lower_majors = [m for m in available_majors if m < latest_major_available]
target_major = lower_majors[-1] if lower_majors else cur_ver_obj.major
if target_major < cur_ver_obj.major:
target_major = cur_ver_obj.major

if (
target_major in available_majors
and not (target_major == cur_ver_obj.major and not newer)
):
try:
outdated_target_version = asyncio.run(
find_latest_safe_version_for_major(
pkg, cur_ver, all_vs, target_major
)
)
except Exception as e:
logger.warning(
f"Failed to compute outdated-oriented upgrade for {pkg}: {e}"
)

if (
outdated_target_version
and outdated_target_version != cur_ver
):
try:
outdated_instruction = generate_upgrade_instruction(
pkg, outdated_target_version
)
except Exception as e:
logger.warning(
"Failed to generate outdated-oriented instruction for "
f"{pkg}=={outdated_target_version}: {e}"
)
outdated_instruction = {
"base_package": f"{pkg}=={outdated_target_version}",
"dependencies": []
}

# Current version dependency JSON (only for base packages)
if pkg.lower() in base_packages:
current_json = generate_current_dependency_json(pkg, cur_ver, cur_ver_deps)
Expand Down Expand Up @@ -253,6 +307,7 @@ def main() -> None:
'Upgrade Vulnerability Details': upgrade_vuln_details,
'Suggested Upgrade': suggested,
'Upgrade Instruction': instruction,
'Outdated-oriented Upgrade Instruction': outdated_instruction,
'Remarks': Remarks
})
logger.debug(f"Custodian for {pkg}: {custodian}")
Expand Down Expand Up @@ -319,7 +374,7 @@ def main() -> None:
'Dependencies for Current', 'Newer Versions', 'Dependencies for Latest',
'Latest Version', 'Current Version Vulnerable?', 'Current Version Vulnerability Details',
'Upgrade Version Vulnerable?', 'Upgrade Vulnerability Details',
'Suggested Upgrade', 'Remarks'
'Suggested Upgrade', 'Outdated-oriented Upgrade Instruction', 'Remarks'
]]
# Overview Sheet
total_packages = len(monthly_df)
Expand Down
76 changes: 76 additions & 0 deletions utils/VersionSuggester.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,82 @@ async def suggest_safe_minor_upgrade(
return "unknown"


async def find_latest_safe_version_for_major(
pkg: str,
current_version: str,
all_versions: list[str],
target_major: int,
) -> str | None:
"""Return the newest vulnerability-free version within ``target_major``.

The candidate list is restricted to the specified major version. When the
target major matches the current version's major, only versions greater
than or equal to the current version are evaluated to avoid unnecessary
downgrades. The function iterates from the newest candidate backwards and
returns the first version confirmed to be free of known vulnerabilities.

Parameters
----------
pkg:
Package name on PyPI.
current_version:
Currently installed version string. Used to filter out downgrades
when ``target_major`` equals the current major release.
all_versions:
Collection of available release versions (string representation).
target_major:
Major version number that should be evaluated.

Returns
-------
str | None
The newest vulnerability-free version string within the selected
major. ``None`` if no suitable release could be found or validated.
"""

try:
cur_ver = version.parse(current_version)
except InvalidVersion:
cur_ver = None

candidates: list[tuple[version.Version, str]] = []
for ver_str in all_versions:
try:
parsed = version.parse(ver_str)
except InvalidVersion:
continue

if parsed.major != target_major:
continue

if cur_ver and parsed.major == cur_ver.major and parsed < cur_ver:
# Skip downgrades within the same major release
continue

candidates.append((parsed, ver_str))

if not candidates:
return None

candidates.sort(reverse=True, key=lambda item: item[0])

async with aiohttp.ClientSession() as session:
sem = asyncio.Semaphore(5)
for _, ver_str in candidates:
try:
_, status, _ = await fetch_osv(session, pkg, ver_str, sem)
except Exception as exc: # pragma: no cover - network safety
logger.warning(
f"Failed to verify vulnerabilities for {pkg}=={ver_str}: {exc}"
)
continue

if status == "No":
return ver_str

return None


def main():
"""
Parses command-line arguments and suggests upgrade versions for a specified Python package.
Expand Down
Loading