pydoctor-cli 2.2.0__tar.gz → 2.3.0__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.2.0 → pydoctor_cli-2.3.0}/PKG-INFO +48 -1
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/README.md +47 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/__init__.py +1 -1
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/cli/main.py +122 -15
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/core/analyzer.py +1 -0
- pydoctor_cli-2.3.0/pydoctor/core/baseline.py +153 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/core/report.py +8 -0
- pydoctor_cli-2.3.0/pydoctor/reports/docs_links.py +52 -0
- pydoctor_cli-2.3.0/pydoctor/reports/fingerprint.py +57 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/reports/json_formatter.py +1 -0
- pydoctor_cli-2.3.0/pydoctor/reports/sarif_formatter.py +187 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/reports/table_formatter.py +21 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/PKG-INFO +48 -1
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/SOURCES.txt +7 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/top_level.txt +1 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pyproject.toml +1 -1
- pydoctor_cli-2.3.0/tests/test_baseline.py +165 -0
- pydoctor_cli-2.3.0/tests/test_github_action.py +86 -0
- pydoctor_cli-2.3.0/tests/test_sarif.py +117 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/LICENSE +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/analysis/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/analysis/health_score.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/cache/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/cache/cache_manager.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/cli/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/config/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/config/settings.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/core/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/core/project.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/reports/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/reports/terminal_colors.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/ci_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/dependency_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/env_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/outdated_package_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/unused_package_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/vulnerability_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/security/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/security/cvss.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/security/osv_client.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/security/secrets.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/utils/__init__.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/utils/env_probe.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/utils/file_utils.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/utils/parser_utils.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/utils/pip_utils.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor/utils/subprocess_utils.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/dependency_links.txt +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/entry_points.txt +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/requires.txt +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/setup.cfg +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/conftest.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_ci_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_cli_ux.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_dependency_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_env_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_environment.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_fix_command.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_osv_parsing.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_report_output.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_unused_scanner.py +0 -0
- {pydoctor_cli-2.2.0 → pydoctor_cli-2.3.0}/tests/test_vulnerability_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoctor-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
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>
|
|
@@ -322,6 +322,44 @@ Run a comprehensive check-up on your project.
|
|
|
322
322
|
pydoctor diagnose
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
+
### In CI: GitHub Action
|
|
326
|
+
|
|
327
|
+
Findings appear as code-scanning alerts on pull requests, and the job fails on new problems:
|
|
328
|
+
|
|
329
|
+
```yaml
|
|
330
|
+
permissions:
|
|
331
|
+
contents: read
|
|
332
|
+
security-events: write
|
|
333
|
+
|
|
334
|
+
steps:
|
|
335
|
+
- uses: actions/checkout@v4
|
|
336
|
+
- uses: actions/setup-python@v5
|
|
337
|
+
with: { python-version: "3.12" }
|
|
338
|
+
- run: pip install -r requirements.txt
|
|
339
|
+
- uses: iamAgbaCoder/pydoctor-cli@v2
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Set `ci-only: "true"` to run only the secrets and workflow checks. Any CI system can use `pydoctor check --sarif pydoctor.sarif` directly.
|
|
343
|
+
|
|
344
|
+
### Adopting it in an existing project: baselines
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
pydoctor baseline # accept today's issues; commit .pydoctor-baseline.json
|
|
348
|
+
pydoctor check # now fails only on new issues
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Problems with the scan itself (unreadable environment, unreachable vulnerability database) are never baselined.
|
|
352
|
+
|
|
353
|
+
### pre-commit
|
|
354
|
+
|
|
355
|
+
```yaml
|
|
356
|
+
repos:
|
|
357
|
+
- repo: https://github.com/iamAgbaCoder/pydoctor-cli
|
|
358
|
+
rev: v2.3.0
|
|
359
|
+
hooks:
|
|
360
|
+
- id: pydoctor-ci # blocks commits that add secrets or risky workflows
|
|
361
|
+
```
|
|
362
|
+
|
|
325
363
|
### Automated Fixes
|
|
326
364
|
|
|
327
365
|
Let PyDoctor perform the heavy lifting. It will offer to install missing packages, upgrade outdated ones, and clean up your environment.
|
|
@@ -334,6 +372,15 @@ pydoctor fix
|
|
|
334
372
|
|
|
335
373
|
## Release History
|
|
336
374
|
|
|
375
|
+
### v2.3.0
|
|
376
|
+
Built for CI: adopt pydoctor in an existing project without a wall of failures, and see findings where you review code.
|
|
377
|
+
|
|
378
|
+
- **Baselines:** `pydoctor baseline` records today's issues in `.pydoctor-baseline.json`; scans then fail only on new ones and tell you when baselined issues are fixed. Fingerprints survive unrelated edits (a secret moved down by new lines stays known) and package upgrades. `--ci` updates only the CI-guard entries. `--baseline FILE` / `--no-baseline` on every scan command. Broken scans are never baselined.
|
|
379
|
+
- **SARIF output:** `--sarif FILE` on every scan command (SARIF 2.1.0, schema-validated). Vulnerabilities carry their CVSS score as security severity and point at the line that declares the package; secrets and workflow findings point at their file and line.
|
|
380
|
+
- **GitHub Action:** `uses: iamAgbaCoder/pydoctor-cli@v2` installs pydoctor in isolation, uploads findings to code scanning, and fails the job on new problems.
|
|
381
|
+
- **pre-commit hooks:** `pydoctor-ci` blocks commits that add secrets, `.env` files or risky workflows; `pydoctor-check` runs the full scan at pre-push.
|
|
382
|
+
- **Docs links:** every issue links to its page on pydoctor-docs.thally.app (`docs_url` in JSON; shown in the terminal for errors).
|
|
383
|
+
|
|
337
384
|
### v2.2.0
|
|
338
385
|
Results you can trust: every fix in this release removes a case where PyDoctor reported a problem as fine, or got a severity wrong.
|
|
339
386
|
|
|
@@ -73,6 +73,44 @@ Run a comprehensive check-up on your project.
|
|
|
73
73
|
pydoctor diagnose
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### In CI: GitHub Action
|
|
77
|
+
|
|
78
|
+
Findings appear as code-scanning alerts on pull requests, and the job fails on new problems:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
permissions:
|
|
82
|
+
contents: read
|
|
83
|
+
security-events: write
|
|
84
|
+
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: actions/setup-python@v5
|
|
88
|
+
with: { python-version: "3.12" }
|
|
89
|
+
- run: pip install -r requirements.txt
|
|
90
|
+
- uses: iamAgbaCoder/pydoctor-cli@v2
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Set `ci-only: "true"` to run only the secrets and workflow checks. Any CI system can use `pydoctor check --sarif pydoctor.sarif` directly.
|
|
94
|
+
|
|
95
|
+
### Adopting it in an existing project: baselines
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pydoctor baseline # accept today's issues; commit .pydoctor-baseline.json
|
|
99
|
+
pydoctor check # now fails only on new issues
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Problems with the scan itself (unreadable environment, unreachable vulnerability database) are never baselined.
|
|
103
|
+
|
|
104
|
+
### pre-commit
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
repos:
|
|
108
|
+
- repo: https://github.com/iamAgbaCoder/pydoctor-cli
|
|
109
|
+
rev: v2.3.0
|
|
110
|
+
hooks:
|
|
111
|
+
- id: pydoctor-ci # blocks commits that add secrets or risky workflows
|
|
112
|
+
```
|
|
113
|
+
|
|
76
114
|
### Automated Fixes
|
|
77
115
|
|
|
78
116
|
Let PyDoctor perform the heavy lifting. It will offer to install missing packages, upgrade outdated ones, and clean up your environment.
|
|
@@ -85,6 +123,15 @@ pydoctor fix
|
|
|
85
123
|
|
|
86
124
|
## Release History
|
|
87
125
|
|
|
126
|
+
### v2.3.0
|
|
127
|
+
Built for CI: adopt pydoctor in an existing project without a wall of failures, and see findings where you review code.
|
|
128
|
+
|
|
129
|
+
- **Baselines:** `pydoctor baseline` records today's issues in `.pydoctor-baseline.json`; scans then fail only on new ones and tell you when baselined issues are fixed. Fingerprints survive unrelated edits (a secret moved down by new lines stays known) and package upgrades. `--ci` updates only the CI-guard entries. `--baseline FILE` / `--no-baseline` on every scan command. Broken scans are never baselined.
|
|
130
|
+
- **SARIF output:** `--sarif FILE` on every scan command (SARIF 2.1.0, schema-validated). Vulnerabilities carry their CVSS score as security severity and point at the line that declares the package; secrets and workflow findings point at their file and line.
|
|
131
|
+
- **GitHub Action:** `uses: iamAgbaCoder/pydoctor-cli@v2` installs pydoctor in isolation, uploads findings to code scanning, and fails the job on new problems.
|
|
132
|
+
- **pre-commit hooks:** `pydoctor-ci` blocks commits that add secrets, `.env` files or risky workflows; `pydoctor-check` runs the full scan at pre-push.
|
|
133
|
+
- **Docs links:** every issue links to its page on pydoctor-docs.thally.app (`docs_url` in JSON; shown in the terminal for errors).
|
|
134
|
+
|
|
88
135
|
### v2.2.0
|
|
89
136
|
Results you can trust: every fix in this release removes a case where PyDoctor reported a problem as fine, or got a severity wrong.
|
|
90
137
|
|
|
@@ -85,7 +85,7 @@ _HELP_EPILOG = """
|
|
|
85
85
|
[section]🏥 COMMUNITY & SUPPORT[/]
|
|
86
86
|
|
|
87
87
|
[label]Docs:[/][pkg] https://pydoctor-docs.thally.app[/]
|
|
88
|
-
[label]Version:[/][info] v2.
|
|
88
|
+
[label]Version:[/][info] v2.3.0 (Premium Kernel Edition)[/]
|
|
89
89
|
|
|
90
90
|
[kernel]Healthy code leads to healthy deployments. Keep your environment sterile.[/]
|
|
91
91
|
"""
|
|
@@ -122,6 +122,19 @@ _PATH_ARG = typer.Option(..., "--path", "-p", help="Project directory to scan.")
|
|
|
122
122
|
_JSON_ARG = typer.Option(..., "--json", "-j", help="Output results as JSON.")
|
|
123
123
|
_VERBOSE_ARG = typer.Option(..., "--verbose", "-v", help="Show detailed output and timing.")
|
|
124
124
|
_NO_CACHE_ARG = typer.Option(..., "--no-cache", help="Bypass the local vulnerability cache.")
|
|
125
|
+
_SARIF_ARG = typer.Option(
|
|
126
|
+
...,
|
|
127
|
+
"--sarif",
|
|
128
|
+
help="Also write findings to this file in SARIF format (for GitHub code scanning).",
|
|
129
|
+
)
|
|
130
|
+
_BASELINE_ARG = typer.Option(
|
|
131
|
+
...,
|
|
132
|
+
"--baseline",
|
|
133
|
+
help="Baseline file of accepted issues (default: .pydoctor-baseline.json if present).",
|
|
134
|
+
)
|
|
135
|
+
_NO_BASELINE_ARG = typer.Option(
|
|
136
|
+
..., "--no-baseline", help="Report every issue, ignoring the baseline."
|
|
137
|
+
)
|
|
125
138
|
_PYTHON_ARG = typer.Option(
|
|
126
139
|
...,
|
|
127
140
|
"--python",
|
|
@@ -212,6 +225,42 @@ def _output(report: DiagnosisReport, as_json: bool, verbose: bool = False) -> No
|
|
|
212
225
|
render_report(report, verbose=verbose)
|
|
213
226
|
|
|
214
227
|
|
|
228
|
+
def _finish(
|
|
229
|
+
report: DiagnosisReport,
|
|
230
|
+
path: str,
|
|
231
|
+
as_json: bool,
|
|
232
|
+
verbose: bool,
|
|
233
|
+
sarif: Path | None,
|
|
234
|
+
baseline: Path | None,
|
|
235
|
+
no_baseline: bool,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Apply the baseline, print the report, write SARIF, and exit with the right code."""
|
|
238
|
+
from pydoctor.core.baseline import BaselineError, apply_baseline, default_path
|
|
239
|
+
|
|
240
|
+
if not no_baseline:
|
|
241
|
+
path_to_use = baseline or default_path(report)
|
|
242
|
+
if baseline is not None and not path_to_use.is_file():
|
|
243
|
+
err_console.print(f"[error]✖ Baseline file not found: {path_to_use}[/]")
|
|
244
|
+
raise typer.Exit(code=2)
|
|
245
|
+
if path_to_use.is_file():
|
|
246
|
+
try:
|
|
247
|
+
apply_baseline(report, path_to_use, set(report.scanners))
|
|
248
|
+
except BaselineError as exc:
|
|
249
|
+
err_console.print(f"[error]✖ {exc}[/]")
|
|
250
|
+
raise typer.Exit(code=2) from exc
|
|
251
|
+
|
|
252
|
+
_output(report, as_json=as_json, verbose=verbose)
|
|
253
|
+
|
|
254
|
+
if sarif is not None:
|
|
255
|
+
from pydoctor.reports.sarif_formatter import write_sarif
|
|
256
|
+
|
|
257
|
+
write_sarif(report, sarif)
|
|
258
|
+
if not as_json:
|
|
259
|
+
console.print(f"[dim_text]SARIF report written to {sarif}[/]")
|
|
260
|
+
|
|
261
|
+
raise typer.Exit(code=_exit_code(report, path))
|
|
262
|
+
|
|
263
|
+
|
|
215
264
|
def _exit_code(report: DiagnosisReport, path: str = ".") -> int:
|
|
216
265
|
"""
|
|
217
266
|
Return a non-zero exit code if:
|
|
@@ -237,6 +286,9 @@ def _exit_code(report: DiagnosisReport, path: str = ".") -> int:
|
|
|
237
286
|
def diagnose(
|
|
238
287
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
239
288
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
289
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
290
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
291
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
240
292
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
241
293
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
242
294
|
no_cache: Annotated[bool, _NO_CACHE_ARG] = False,
|
|
@@ -268,14 +320,16 @@ def diagnose(
|
|
|
268
320
|
python=python,
|
|
269
321
|
ctx=ctx,
|
|
270
322
|
)
|
|
271
|
-
|
|
272
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
323
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
273
324
|
|
|
274
325
|
|
|
275
326
|
@app.command(rich_help_panel="CORE COMMANDS")
|
|
276
327
|
def check(
|
|
277
328
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
278
329
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
330
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
331
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
332
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
279
333
|
ci: bool = typer.Option(False, "--ci", help="Run in CI/CD guard mode."),
|
|
280
334
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
281
335
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
@@ -284,8 +338,7 @@ def check(
|
|
|
284
338
|
report = _run_scan(
|
|
285
339
|
path=path, ci_mode=ci, verbose=verbose, as_json=json, command="check", python=python
|
|
286
340
|
)
|
|
287
|
-
|
|
288
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
341
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
289
342
|
|
|
290
343
|
|
|
291
344
|
@app.command(rich_help_panel="SECURITY & CI/CD")
|
|
@@ -319,6 +372,9 @@ def github(
|
|
|
319
372
|
repo: Annotated[str | None, typer.Argument(help="GitHub repository URL (optional).")] = None,
|
|
320
373
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
321
374
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
375
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
376
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
377
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
322
378
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
323
379
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
324
380
|
) -> None:
|
|
@@ -347,8 +403,7 @@ def github(
|
|
|
347
403
|
report = _run_scan(
|
|
348
404
|
path=path, ci_mode=True, verbose=verbose, as_json=json, command="github", python=python
|
|
349
405
|
)
|
|
350
|
-
|
|
351
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
406
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
352
407
|
|
|
353
408
|
|
|
354
409
|
# --- Individual Targeted Scans ---
|
|
@@ -358,6 +413,9 @@ def github(
|
|
|
358
413
|
def scan_security(
|
|
359
414
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
360
415
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
416
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
417
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
418
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
361
419
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
362
420
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
363
421
|
) -> None:
|
|
@@ -370,8 +428,7 @@ def scan_security(
|
|
|
370
428
|
command="scan-security",
|
|
371
429
|
python=python,
|
|
372
430
|
)
|
|
373
|
-
|
|
374
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
431
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
375
432
|
|
|
376
433
|
|
|
377
434
|
# ──────────────────────────────────────────────────────────────
|
|
@@ -383,6 +440,9 @@ def scan_security(
|
|
|
383
440
|
def scan_unused(
|
|
384
441
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
385
442
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
443
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
444
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
445
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
386
446
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
387
447
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
388
448
|
) -> None:
|
|
@@ -395,14 +455,16 @@ def scan_unused(
|
|
|
395
455
|
command="scan-unused",
|
|
396
456
|
python=python,
|
|
397
457
|
)
|
|
398
|
-
|
|
399
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
458
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
400
459
|
|
|
401
460
|
|
|
402
461
|
@app.command(name="scan-deps", rich_help_panel="RESOURCE HYGIENE")
|
|
403
462
|
def scan_deps(
|
|
404
463
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
405
464
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
465
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
466
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
467
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
406
468
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
407
469
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
408
470
|
) -> None:
|
|
@@ -415,14 +477,16 @@ def scan_deps(
|
|
|
415
477
|
command="scan-deps",
|
|
416
478
|
python=python,
|
|
417
479
|
)
|
|
418
|
-
|
|
419
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
480
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
420
481
|
|
|
421
482
|
|
|
422
483
|
@app.command(name="check-env", rich_help_panel="RESOURCE HYGIENE")
|
|
423
484
|
def check_env(
|
|
424
485
|
path: Annotated[str, _PATH_ARG] = ".",
|
|
425
486
|
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
487
|
+
sarif: Annotated[Path | None, _SARIF_ARG] = None,
|
|
488
|
+
baseline: Annotated[Path | None, _BASELINE_ARG] = None,
|
|
489
|
+
no_baseline: Annotated[bool, _NO_BASELINE_ARG] = False,
|
|
426
490
|
json: Annotated[bool, _JSON_ARG] = False,
|
|
427
491
|
verbose: Annotated[bool, _VERBOSE_ARG] = False,
|
|
428
492
|
) -> None:
|
|
@@ -435,13 +499,56 @@ def check_env(
|
|
|
435
499
|
command="check-env",
|
|
436
500
|
python=python,
|
|
437
501
|
)
|
|
438
|
-
|
|
439
|
-
raise typer.Exit(code=_exit_code(report, path))
|
|
502
|
+
_finish(report, path, json, verbose, sarif, baseline, no_baseline)
|
|
440
503
|
|
|
441
504
|
|
|
442
505
|
# --- Re-adding fix and other commands ---
|
|
443
506
|
|
|
444
507
|
|
|
508
|
+
@app.command(rich_help_panel="CORE COMMANDS")
|
|
509
|
+
def baseline(
|
|
510
|
+
path: Annotated[str, _PATH_ARG] = ".",
|
|
511
|
+
python: Annotated[str | None, _PYTHON_ARG] = None,
|
|
512
|
+
ci: bool = typer.Option(
|
|
513
|
+
False, "--ci", help="Only record CI/CD guard findings (keeps the rest of the file)."
|
|
514
|
+
),
|
|
515
|
+
output: Annotated[
|
|
516
|
+
Path | None,
|
|
517
|
+
typer.Option(
|
|
518
|
+
"--output", "-o", help="Where to write it (default: .pydoctor-baseline.json)."
|
|
519
|
+
),
|
|
520
|
+
] = None,
|
|
521
|
+
) -> None:
|
|
522
|
+
"""📌 Accept today's issues so CI only fails on new ones."""
|
|
523
|
+
from pydoctor.core.baseline import NEVER_BASELINE, default_path, write_baseline
|
|
524
|
+
|
|
525
|
+
report = _run_scan(
|
|
526
|
+
scanners=["ci"] if ci else None, path=path, python=python, command="baseline"
|
|
527
|
+
)
|
|
528
|
+
target = output or default_path(report)
|
|
529
|
+
count = write_baseline(report, target, __version__, set(report.scanners) if ci else None)
|
|
530
|
+
console.print(
|
|
531
|
+
Panel(
|
|
532
|
+
f"[ok]✔ {count} issue{'s' if count != 1 else ''} recorded in {target}[/]\n\n"
|
|
533
|
+
"Commit this file. Scans now hide these issues and fail only on new ones.\n"
|
|
534
|
+
"Run `pydoctor baseline` again after fixing issues to shrink it, or scan with "
|
|
535
|
+
"--no-baseline to see everything.",
|
|
536
|
+
border_style="ok",
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
incomplete = [i for i in report.issues if i.code in NEVER_BASELINE]
|
|
541
|
+
if incomplete:
|
|
542
|
+
for issue in incomplete:
|
|
543
|
+
err_console.print(f"[error]✖ {issue.title}[/]\n {issue.description}")
|
|
544
|
+
err_console.print(
|
|
545
|
+
"\n[warning]⚠ Part of the scan didn't run, so its issues aren't in the baseline. "
|
|
546
|
+
"They will be reported as new once the scan can run. Fix the problems above and "
|
|
547
|
+
"run `pydoctor baseline` again.[/]"
|
|
548
|
+
)
|
|
549
|
+
raise typer.Exit(code=1)
|
|
550
|
+
|
|
551
|
+
|
|
445
552
|
@app.command(rich_help_panel="CORE COMMANDS")
|
|
446
553
|
def fix(
|
|
447
554
|
packages: Annotated[
|
|
@@ -159,6 +159,7 @@ class Analyzer:
|
|
|
159
159
|
ctx = self._ctx or ProjectContext.from_path(self._project_path, python=self._python)
|
|
160
160
|
ctx.no_cache = self._no_cache
|
|
161
161
|
report = DiagnosisReport(scan_path=str(ctx.root), ctx=ctx)
|
|
162
|
+
report.scanners = [k for k in self._scanner_keys if k in SCANNER_REGISTRY]
|
|
162
163
|
scanner_timings: dict[str, float] = {}
|
|
163
164
|
report.extend(context_issues(ctx, self._scanner_keys))
|
|
164
165
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pydoctor/core/baseline.py
|
|
3
|
+
──────────────────────────
|
|
4
|
+
Baselines: accept today's issues so CI only fails on new ones.
|
|
5
|
+
|
|
6
|
+
A project with dozens of existing findings can't turn on a pydoctor CI gate
|
|
7
|
+
without it failing on every run. ``pydoctor baseline`` records the current
|
|
8
|
+
issues in ``.pydoctor-baseline.json``; later scans hide issues whose
|
|
9
|
+
fingerprint is in that file and report how many were hidden. Fixed issues
|
|
10
|
+
drop out naturally, and the file can be refreshed at any time.
|
|
11
|
+
|
|
12
|
+
Problems with the scan itself (an unreadable environment, an unreachable
|
|
13
|
+
vulnerability database, a crashed scanner) are never baselined: accepting
|
|
14
|
+
them would turn a broken scan into a passing one.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import datetime
|
|
20
|
+
import json
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from pydoctor.config.settings import Severity
|
|
25
|
+
from pydoctor.core.report import DiagnosisReport, Issue
|
|
26
|
+
from pydoctor.reports.fingerprint import fingerprint
|
|
27
|
+
|
|
28
|
+
BASELINE_FILE = ".pydoctor-baseline.json"
|
|
29
|
+
BASELINE_FORMAT = 1
|
|
30
|
+
|
|
31
|
+
# Never accepted into a baseline.
|
|
32
|
+
NEVER_BASELINE = {"ENV_PACKAGES_UNAVAILABLE", "SEC_SCAN_INCOMPLETE", "SCANNER_ERROR"}
|
|
33
|
+
_IGNORED_SEVERITIES = {Severity.OK, Severity.INFO}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaselineError(Exception):
|
|
37
|
+
"""The baseline file exists but can't be used."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class BaselineResult:
|
|
42
|
+
path: Path
|
|
43
|
+
suppressed: int = 0
|
|
44
|
+
stale: int = 0
|
|
45
|
+
suppressed_codes: dict[str, int] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _root(report: DiagnosisReport) -> Path:
|
|
49
|
+
return Path(report.ctx.root) if report.ctx is not None else Path(report.scan_path)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def baselinable(issue: Issue) -> bool:
|
|
53
|
+
return issue.severity not in _IGNORED_SEVERITIES and issue.code not in NEVER_BASELINE
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def default_path(report: DiagnosisReport) -> Path:
|
|
57
|
+
return _root(report) / BASELINE_FILE
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_baseline(
|
|
61
|
+
report: DiagnosisReport, path: Path, version: str, scanned_categories: set[str] | None = None
|
|
62
|
+
) -> int:
|
|
63
|
+
"""
|
|
64
|
+
Record every baselinable issue in ``report``. Returns how many are in the file.
|
|
65
|
+
|
|
66
|
+
When ``scanned_categories`` is given (a partial scan such as
|
|
67
|
+
`pydoctor baseline --ci`), entries for categories that weren't scanned are
|
|
68
|
+
kept from the existing file instead of being dropped.
|
|
69
|
+
"""
|
|
70
|
+
root = _root(report)
|
|
71
|
+
entries: dict[str, dict] = {}
|
|
72
|
+
if scanned_categories is not None and path.is_file():
|
|
73
|
+
try:
|
|
74
|
+
previous = json.loads(path.read_text(encoding="utf-8")).get("issues", [])
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
previous = []
|
|
77
|
+
for e in previous:
|
|
78
|
+
if e.get("category") not in scanned_categories and "fingerprint" in e:
|
|
79
|
+
entries[e["fingerprint"]] = e
|
|
80
|
+
for issue in report.issues:
|
|
81
|
+
if not baselinable(issue):
|
|
82
|
+
continue
|
|
83
|
+
fp = fingerprint(issue, root)
|
|
84
|
+
entries[fp] = {
|
|
85
|
+
"fingerprint": fp,
|
|
86
|
+
"code": issue.code,
|
|
87
|
+
"category": issue.category,
|
|
88
|
+
"severity": issue.severity,
|
|
89
|
+
"title": issue.title,
|
|
90
|
+
}
|
|
91
|
+
data = {
|
|
92
|
+
"format": BASELINE_FORMAT,
|
|
93
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
|
|
94
|
+
"pydoctor_version": version,
|
|
95
|
+
"issues": sorted(entries.values(), key=lambda e: (e["code"], e["title"])),
|
|
96
|
+
}
|
|
97
|
+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
98
|
+
return len(entries)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_fingerprints(path: Path) -> set[str]:
|
|
102
|
+
try:
|
|
103
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
104
|
+
except (OSError, ValueError) as exc:
|
|
105
|
+
raise BaselineError(f"{path} couldn't be read: {exc}") from exc
|
|
106
|
+
if not isinstance(data, dict) or data.get("format") != BASELINE_FORMAT:
|
|
107
|
+
raise BaselineError(
|
|
108
|
+
f"{path} isn't a pydoctor baseline (format {BASELINE_FORMAT}). "
|
|
109
|
+
"Recreate it with `pydoctor baseline`."
|
|
110
|
+
)
|
|
111
|
+
return {e["fingerprint"] for e in data.get("issues", []) if "fingerprint" in e}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def apply_baseline(
|
|
115
|
+
report: DiagnosisReport, path: Path, scanned_categories: set[str]
|
|
116
|
+
) -> BaselineResult:
|
|
117
|
+
"""
|
|
118
|
+
Remove issues recorded in the baseline from ``report``.
|
|
119
|
+
|
|
120
|
+
``scanned_categories`` limits stale-entry counting to what this scan
|
|
121
|
+
actually checked: `scan-security` can't tell whether a baselined secret
|
|
122
|
+
still exists.
|
|
123
|
+
"""
|
|
124
|
+
known = load_fingerprints(path)
|
|
125
|
+
root = _root(report)
|
|
126
|
+
result = BaselineResult(path=path)
|
|
127
|
+
seen: set[str] = set()
|
|
128
|
+
kept: list[Issue] = []
|
|
129
|
+
for issue in report.issues:
|
|
130
|
+
fp = fingerprint(issue, root) if baselinable(issue) else None
|
|
131
|
+
if fp is not None and fp in known:
|
|
132
|
+
seen.add(fp)
|
|
133
|
+
result.suppressed += 1
|
|
134
|
+
result.suppressed_codes[issue.code] = result.suppressed_codes.get(issue.code, 0) + 1
|
|
135
|
+
continue
|
|
136
|
+
kept.append(issue)
|
|
137
|
+
report.issues = kept
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
entries = json.loads(path.read_text(encoding="utf-8")).get("issues", [])
|
|
141
|
+
except (OSError, ValueError):
|
|
142
|
+
entries = []
|
|
143
|
+
result.stale = sum(
|
|
144
|
+
1
|
|
145
|
+
for e in entries
|
|
146
|
+
if e.get("fingerprint") not in seen and e.get("category") in scanned_categories
|
|
147
|
+
)
|
|
148
|
+
report.scanner_meta["baseline"] = {
|
|
149
|
+
"file": str(path),
|
|
150
|
+
"suppressed": result.suppressed,
|
|
151
|
+
"stale": result.stale,
|
|
152
|
+
}
|
|
153
|
+
return result
|
|
@@ -60,8 +60,15 @@ class Issue:
|
|
|
60
60
|
"recommendation": self.recommendation,
|
|
61
61
|
"package": self.package,
|
|
62
62
|
"extra": self.extra,
|
|
63
|
+
"docs_url": self.docs_url,
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
@property
|
|
67
|
+
def docs_url(self) -> str:
|
|
68
|
+
from pydoctor.reports.docs_links import docs_url
|
|
69
|
+
|
|
70
|
+
return docs_url(self)
|
|
71
|
+
|
|
65
72
|
|
|
66
73
|
# ──────────────────────────────────────────────────────────────
|
|
67
74
|
# DiagnosisReport — aggregated result of all scans
|
|
@@ -89,6 +96,7 @@ class DiagnosisReport:
|
|
|
89
96
|
scanner_meta: dict = field(default_factory=dict)
|
|
90
97
|
ctx: Any = None
|
|
91
98
|
command: str | None = None
|
|
99
|
+
scanners: list[str] = field(default_factory=list) # scanner keys that ran
|
|
92
100
|
|
|
93
101
|
# ── Convenience helpers ────────────────────────────────────
|
|
94
102
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pydoctor/reports/docs_links.py
|
|
3
|
+
───────────────────────────────
|
|
4
|
+
Link every issue to the page of the documentation that explains it.
|
|
5
|
+
|
|
6
|
+
Anchors here must exist on https://pydoctor-docs.thally.app. Specific
|
|
7
|
+
troubleshooting entries win; otherwise the issue links to the section of
|
|
8
|
+
the scanner reference that produced it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pydoctor.core.report import Issue
|
|
14
|
+
|
|
15
|
+
DOCS_URL = "https://pydoctor-docs.thally.app"
|
|
16
|
+
|
|
17
|
+
_BY_CODE: dict[str, str] = {
|
|
18
|
+
"ENV_PACKAGES_UNAVAILABLE": "/guides/troubleshooting#packages-unavailable",
|
|
19
|
+
"ENV_DEPS_NOT_INSTALLED": "/guides/troubleshooting#wrong-python",
|
|
20
|
+
"SEC_SCAN_INCOMPLETE": "/guides/troubleshooting#scan-incomplete",
|
|
21
|
+
"CI_ENV_FILE_COMMITTED": "/guides/troubleshooting#env-file-committed",
|
|
22
|
+
"CI_ENV_FILE_NOT_IGNORED": "/guides/troubleshooting#env-file-committed",
|
|
23
|
+
"CI_PULL_REQUEST_TARGET_CHECKOUT": "/scanners#workflow-checks",
|
|
24
|
+
"CI_SCRIPT_INJECTION": "/scanners#workflow-checks",
|
|
25
|
+
"CI_BROAD_PERMISSIONS": "/scanners#workflow-checks",
|
|
26
|
+
"CI_UNPINNED_ACTION": "/scanners#workflow-checks",
|
|
27
|
+
"CI_CLEARTEXT_ENV": "/scanners#workflow-checks",
|
|
28
|
+
"SEC_VULNERABILITY": "/scanners#severity",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_BY_PREFIX: dict[str, str] = {
|
|
32
|
+
"SECRET_EXPOSED_": "/scanners#secret-rules",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_BY_CATEGORY: dict[str, str] = {
|
|
36
|
+
"environment": "/scanners#environment",
|
|
37
|
+
"dependencies": "/scanners#dependencies",
|
|
38
|
+
"outdated": "/scanners#outdated-packages",
|
|
39
|
+
"security": "/scanners#security",
|
|
40
|
+
"unused": "/scanners#unused-packages",
|
|
41
|
+
"ci": "/scanners#ci-cd",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def docs_url(issue: Issue) -> str:
|
|
46
|
+
"""The documentation page for ``issue`` (falls back to the docs home)."""
|
|
47
|
+
path = _BY_CODE.get(issue.code)
|
|
48
|
+
if path is None:
|
|
49
|
+
path = next((p for prefix, p in _BY_PREFIX.items() if issue.code.startswith(prefix)), None)
|
|
50
|
+
if path is None:
|
|
51
|
+
path = _BY_CATEGORY.get(issue.category, "")
|
|
52
|
+
return DOCS_URL + path
|