pydoctor-cli 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {pydoctor_cli-2.0.0/pydoctor_cli.egg-info → pydoctor_cli-2.0.1}/PKG-INFO +5 -1
  2. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/README.md +4 -0
  3. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/__init__.py +1 -1
  4. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cli/main.py +1 -1
  5. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/vulnerability_scanner.py +71 -3
  6. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/security/osv_client.py +12 -2
  7. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1/pydoctor_cli.egg-info}/PKG-INFO +5 -1
  8. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pyproject.toml +1 -1
  9. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_vulnerability_scanner.py +75 -0
  10. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/LICENSE +0 -0
  11. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/analysis/__init__.py +0 -0
  12. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/analysis/health_score.py +0 -0
  13. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cache/__init__.py +0 -0
  14. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cache/cache_manager.py +0 -0
  15. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cli/__init__.py +0 -0
  16. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/config/__init__.py +0 -0
  17. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/config/settings.py +0 -0
  18. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/__init__.py +0 -0
  19. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/analyzer.py +0 -0
  20. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/project.py +0 -0
  21. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/report.py +0 -0
  22. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/performance/__init__.py +0 -0
  23. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/performance/executor.py +0 -0
  24. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/performance/parallel.py +0 -0
  25. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/__init__.py +0 -0
  26. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/json_formatter.py +0 -0
  27. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/table_formatter.py +0 -0
  28. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/terminal_colors.py +0 -0
  29. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/__init__.py +0 -0
  30. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/ci_scanner.py +0 -0
  31. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/dependency_scanner.py +0 -0
  32. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/env_scanner.py +0 -0
  33. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/outdated_package_scanner.py +0 -0
  34. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/unused_package_scanner.py +0 -0
  35. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/security/__init__.py +0 -0
  36. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/__init__.py +0 -0
  37. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/file_utils.py +0 -0
  38. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/parser_utils.py +0 -0
  39. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/pip_utils.py +0 -0
  40. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/subprocess_utils.py +0 -0
  41. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/SOURCES.txt +0 -0
  42. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/dependency_links.txt +0 -0
  43. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/entry_points.txt +0 -0
  44. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/requires.txt +0 -0
  45. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/top_level.txt +0 -0
  46. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/setup.cfg +0 -0
  47. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/conftest.py +0 -0
  48. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_ci_scanner.py +0 -0
  49. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_cli_ux.py +0 -0
  50. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_dependency_scanner.py +0 -0
  51. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_env_scanner.py +0 -0
  52. {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_unused_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydoctor-cli
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Premium Python environment diagnostic CLI — your project's personal physician.
5
5
  Author-email: Favour Bamgboye <demiladebamgboye@gmail.com>
6
6
  Maintainer-email: Favour Bamgboye <demiladebamgboye@gmail.com>
@@ -323,6 +323,10 @@ pydoctor fix
323
323
 
324
324
  ## Release History
325
325
 
326
+ ### v2.0.1
327
+ - **Security scans fail closed:** if the OSV database can't be reached, pydoctor now reports `SEC_SCAN_INCOMPLETE` (error, exit 1) instead of a clean result.
328
+ - **No more double-counted advisories:** advisories listed under several IDs (e.g. GHSA and PYSEC) are reported and scored once.
329
+
326
330
  ### v2.0.0
327
331
 
328
332
  - **🚀 Major UI/UX Revamp**: Introduced a premium "kernel/hacker" aesthetic with boxed reports, dynamic progress animations, and improved readability.
@@ -74,6 +74,10 @@ pydoctor fix
74
74
 
75
75
  ## Release History
76
76
 
77
+ ### v2.0.1
78
+ - **Security scans fail closed:** if the OSV database can't be reached, pydoctor now reports `SEC_SCAN_INCOMPLETE` (error, exit 1) instead of a clean result.
79
+ - **No more double-counted advisories:** advisories listed under several IDs (e.g. GHSA and PYSEC) are reported and scored once.
80
+
77
81
  ### v2.0.0
78
82
 
79
83
  - **🚀 Major UI/UX Revamp**: Introduced a premium "kernel/hacker" aesthetic with boxed reports, dynamic progress animations, and improved readability.
@@ -9,6 +9,6 @@ Author: Favour Bamgboye
9
9
  License: MIT
10
10
  """
11
11
 
12
- __version__ = "2.0.0"
12
+ __version__ = "2.0.1"
13
13
  __author__ = "Favour Bamgboye"
14
14
  __description__ = "Python environment diagnostic CLI — your project's doctor."
@@ -82,7 +82,7 @@ _HELP_EPILOG = """
82
82
  [section]🏥 COMMUNITY & SUPPORT[/]
83
83
 
84
84
  [label]Docs:[/][pkg] https://pydoctor.vercel.app/docs[/]
85
- [label]Version:[/][info] v2.0.0 (Premium Kernel Edition)[/]
85
+ [label]Version:[/][info] v2.0.1 (Premium Kernel Edition)[/]
86
86
 
87
87
  [kernel]Healthy code leads to healthy deployments. Keep your environment sterile.[/]
88
88
  """
@@ -50,9 +50,19 @@ def scan(ctx: ProjectContext) -> list[Issue]:
50
50
  client = OSVClient(cache=cache)
51
51
 
52
52
  # Query OSV for all installed packages
53
- records = client.query_packages(ctx.installed_packages)
53
+ records = _dedupe_records(client.query_packages(ctx.installed_packages))
54
+ failed = list(getattr(client, "failed_packages", []) or [])
55
+
56
+ issues: list[Issue] = []
57
+
58
+ if failed:
59
+ # Fail closed: a security scan that could not reach the vulnerability
60
+ # database must never report the project as healthy.
61
+ issues.append(_incomplete_scan_issue(failed, len(ctx.installed_packages)))
54
62
 
55
63
  if not records:
64
+ if failed:
65
+ return issues
56
66
  return [
57
67
  Issue(
58
68
  category=CATEGORY,
@@ -67,8 +77,6 @@ def scan(ctx: ProjectContext) -> list[Issue]:
67
77
  )
68
78
  ]
69
79
 
70
- issues: list[Issue] = []
71
-
72
80
  # Add environment context info
73
81
  issues.append(
74
82
  Issue(
@@ -90,6 +98,66 @@ def scan(ctx: ProjectContext) -> list[Issue]:
90
98
  return issues
91
99
 
92
100
 
101
+ _SEVERITY_RANK = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "UNKNOWN": 0}
102
+
103
+
104
+ def _dedupe_records(records: list[VulnerabilityRecord]) -> list[VulnerabilityRecord]:
105
+ """
106
+ Collapse records that describe the same advisory.
107
+
108
+ OSV often returns one advisory under several databases (for example a
109
+ GHSA entry and a PYSEC entry that list each other as aliases). Without
110
+ this step each copy is reported and penalised separately, so a single
111
+ vulnerability can cost the health score twice. Records for the same
112
+ package and version are merged when their IDs or aliases overlap; the
113
+ highest severity among the duplicates is kept.
114
+ """
115
+ kept: list[VulnerabilityRecord] = []
116
+ kept_ids: list[set[str]] = []
117
+ for rec in records:
118
+ ids = {rec.vuln_id, *rec.aliases}
119
+ for idx, existing in enumerate(kept):
120
+ if (
121
+ existing.package == rec.package
122
+ and existing.version == rec.version
123
+ and kept_ids[idx] & ids
124
+ ):
125
+ kept_ids[idx] |= ids
126
+ if _SEVERITY_RANK.get(rec.severity.upper(), 0) > _SEVERITY_RANK.get(
127
+ existing.severity.upper(), 0
128
+ ):
129
+ existing.severity = rec.severity
130
+ existing.aliases = sorted(kept_ids[idx] - {existing.vuln_id})
131
+ break
132
+ else:
133
+ kept.append(rec)
134
+ kept_ids.append(ids)
135
+ return kept
136
+
137
+
138
+ def _incomplete_scan_issue(failed: list[str], total: int) -> Issue:
139
+ """Build the ERROR issue reported when OSV lookups did not complete."""
140
+ preview = ", ".join(sorted(failed)[:5])
141
+ if len(failed) > 5:
142
+ preview += f", and {len(failed) - 5} more"
143
+ return Issue(
144
+ category=CATEGORY,
145
+ code="SEC_SCAN_INCOMPLETE",
146
+ severity=Severity.ERROR,
147
+ title=f"Security scan incomplete: {len(failed)} of {total} packages not checked",
148
+ description=(
149
+ "pydoctor could not reach the OSV vulnerability database "
150
+ f"(https://api.osv.dev) for: {preview}. These packages were not "
151
+ "checked, so the absence of findings is not evidence that they are safe."
152
+ ),
153
+ recommendation=(
154
+ "Check network or proxy access to api.osv.dev, or raise "
155
+ "PYDOCTOR_REQUEST_TIMEOUT, then run `pydoctor scan-security` again."
156
+ ),
157
+ extra={"unchecked_packages": sorted(failed)},
158
+ )
159
+
160
+
93
161
  def _record_to_issue(rec: VulnerabilityRecord) -> Issue:
94
162
  """
95
163
  Convert a VulnerabilityRecord into a structured Issue.
@@ -99,6 +99,11 @@ class OSVClient:
99
99
  self._cache = cache or CacheManager()
100
100
  self._timeout = timeout
101
101
  self._session = requests.Session()
102
+ # Packages whose OSV lookup could not be completed (network error,
103
+ # timeout, or a non-2xx response after all retries). Callers must
104
+ # check this: an empty result list does NOT mean "no vulnerabilities"
105
+ # when this list is non-empty.
106
+ self.failed_packages: list[str] = []
102
107
  self._session.headers.update(
103
108
  {
104
109
  "Content-Type": "application/json",
@@ -123,8 +128,10 @@ class OSVClient:
123
128
  Returns
124
129
  -------
125
130
  list[VulnerabilityRecord]
126
- All vulnerabilities found. Empty list if none / network error.
131
+ All vulnerabilities found. Packages that could not be checked
132
+ are recorded in ``self.failed_packages``.
127
133
  """
134
+ self.failed_packages = []
128
135
  if not packages:
129
136
  return []
130
137
 
@@ -196,7 +203,10 @@ class OSVClient:
196
203
  break
197
204
  except (requests.RequestException, ValueError):
198
205
  if attempt == retries - 1:
199
- # Final attempt failed — return whatever we got from cache
206
+ # Final attempt failed. Record the packages we could not
207
+ # check so the scanner can report an incomplete scan
208
+ # instead of a clean bill of health.
209
+ self.failed_packages.extend(name for name, _ in to_fetch)
200
210
  return records
201
211
  # Simple exponential backoff: 1s, 2s, 4s...
202
212
  time.sleep(2**attempt)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydoctor-cli
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Premium Python environment diagnostic CLI — your project's personal physician.
5
5
  Author-email: Favour Bamgboye <demiladebamgboye@gmail.com>
6
6
  Maintainer-email: Favour Bamgboye <demiladebamgboye@gmail.com>
@@ -323,6 +323,10 @@ pydoctor fix
323
323
 
324
324
  ## Release History
325
325
 
326
+ ### v2.0.1
327
+ - **Security scans fail closed:** if the OSV database can't be reached, pydoctor now reports `SEC_SCAN_INCOMPLETE` (error, exit 1) instead of a clean result.
328
+ - **No more double-counted advisories:** advisories listed under several IDs (e.g. GHSA and PYSEC) are reported and scored once.
329
+
326
330
  ### v2.0.0
327
331
 
328
332
  - **🚀 Major UI/UX Revamp**: Introduced a premium "kernel/hacker" aesthetic with boxed reports, dynamic progress animations, and improved readability.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pydoctor-cli"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "Premium Python environment diagnostic CLI — your project's personal physician."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -80,3 +80,78 @@ def test_vulns_detected_and_mapped(basic_ctx, monkeypatch):
80
80
  assert critical_issue.package == "requests"
81
81
  assert critical_issue.code == "SEC_VULNERABILITY"
82
82
  assert warning_issue.package == "flask"
83
+
84
+
85
+ def test_unreachable_osv_fails_closed(basic_ctx, monkeypatch):
86
+ """When OSV cannot be reached, report an ERROR instead of 'no vulnerabilities'."""
87
+
88
+ class FakeClient:
89
+ def __init__(self, **kwargs):
90
+ self.failed_packages = []
91
+
92
+ def query_packages(self, pkgs):
93
+ self.failed_packages = list(pkgs)
94
+ return []
95
+
96
+ monkeypatch.setattr("pydoctor.scanners.vulnerability_scanner.OSVClient", FakeClient)
97
+
98
+ issues = vulnerability_scanner.scan(basic_ctx)
99
+ codes = [i.code for i in issues]
100
+ assert "SEC_SCAN_INCOMPLETE" in codes
101
+ assert "SEC_NO_VULNS" not in codes
102
+ incomplete = next(i for i in issues if i.code == "SEC_SCAN_INCOMPLETE")
103
+ assert incomplete.severity == Severity.ERROR
104
+
105
+
106
+ def test_osv_client_records_failed_packages(monkeypatch, tmp_path):
107
+ """OSVClient records unchecked packages when every retry fails."""
108
+ import requests
109
+
110
+ from pydoctor.cache.cache_manager import CacheManager
111
+ from pydoctor.security import osv_client as osv_module
112
+
113
+ monkeypatch.setattr(osv_module.time, "sleep", lambda _s: None)
114
+ client = osv_module.OSVClient(cache=CacheManager(cache_file=tmp_path / "c.json"))
115
+
116
+ def boom(*args, **kwargs):
117
+ raise requests.ConnectionError("offline")
118
+
119
+ monkeypatch.setattr(client._session, "post", boom)
120
+ records = client.query_packages({"requests": "2.31.0"})
121
+ assert records == []
122
+ assert client.failed_packages == ["requests"]
123
+
124
+
125
+ def test_aliased_advisories_reported_once(basic_ctx, monkeypatch):
126
+ """A GHSA and a PYSEC record that alias each other count as one vulnerability."""
127
+
128
+ class FakeClient:
129
+ def __init__(self, **kwargs):
130
+ self.failed_packages = []
131
+
132
+ def query_packages(self, pkgs):
133
+ return [
134
+ VulnerabilityRecord(
135
+ vuln_id="GHSA-6q48-6c7w-9gw2",
136
+ package="certifi",
137
+ version="2026.7.22",
138
+ summary="certifi bundles emSign roots with compromised keys",
139
+ severity="HIGH",
140
+ aliases=["PYSEC-2026-4320"],
141
+ ),
142
+ VulnerabilityRecord(
143
+ vuln_id="PYSEC-2026-4320",
144
+ package="certifi",
145
+ version="2026.7.22",
146
+ summary="certifi bundles emSign roots with compromised keys",
147
+ severity="CRITICAL",
148
+ aliases=["GHSA-6q48-6c7w-9gw2"],
149
+ ),
150
+ ]
151
+
152
+ monkeypatch.setattr("pydoctor.scanners.vulnerability_scanner.OSVClient", FakeClient)
153
+
154
+ issues = vulnerability_scanner.scan(basic_ctx)
155
+ vulns = [i for i in issues if i.code == "SEC_VULNERABILITY"]
156
+ assert len(vulns) == 1
157
+ assert vulns[0].severity == Severity.CRITICAL
File without changes
File without changes