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.
- {pydoctor_cli-2.0.0/pydoctor_cli.egg-info → pydoctor_cli-2.0.1}/PKG-INFO +5 -1
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/README.md +4 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/__init__.py +1 -1
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cli/main.py +1 -1
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/vulnerability_scanner.py +71 -3
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/security/osv_client.py +12 -2
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1/pydoctor_cli.egg-info}/PKG-INFO +5 -1
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pyproject.toml +1 -1
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_vulnerability_scanner.py +75 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/LICENSE +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/analysis/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/analysis/health_score.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cache/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cache/cache_manager.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/cli/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/config/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/config/settings.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/analyzer.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/project.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/core/report.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/performance/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/performance/executor.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/performance/parallel.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/json_formatter.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/table_formatter.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/reports/terminal_colors.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/ci_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/dependency_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/env_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/outdated_package_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/scanners/unused_package_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/security/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/__init__.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/file_utils.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/parser_utils.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/pip_utils.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor/utils/subprocess_utils.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/SOURCES.txt +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/dependency_links.txt +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/entry_points.txt +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/requires.txt +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/pydoctor_cli.egg-info/top_level.txt +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/setup.cfg +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/conftest.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_ci_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_cli_ux.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_dependency_scanner.py +0 -0
- {pydoctor_cli-2.0.0 → pydoctor_cli-2.0.1}/tests/test_env_scanner.py +0 -0
- {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.
|
|
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.
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|