python-qv 0.1.0__py3-none-any.whl
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.
- python_qv-0.1.0.dist-info/METADATA +281 -0
- python_qv-0.1.0.dist-info/RECORD +33 -0
- python_qv-0.1.0.dist-info/WHEEL +4 -0
- python_qv-0.1.0.dist-info/entry_points.txt +4 -0
- qv/__init__.py +3 -0
- qv/analyzers/__init__.py +1 -0
- qv/analyzers/ci/__init__.py +1 -0
- qv/analyzers/dependencies/__init__.py +5 -0
- qv/analyzers/dependencies/analyzer.py +347 -0
- qv/analyzers/docker/__init__.py +1 -0
- qv/analyzers/environment/__init__.py +5 -0
- qv/analyzers/environment/drift.py +164 -0
- qv/analyzers/imports/__init__.py +5 -0
- qv/analyzers/imports/analyzer.py +160 -0
- qv/analyzers/packaging/__init__.py +5 -0
- qv/analyzers/packaging/analyzer.py +117 -0
- qv/analyzers/security/__init__.py +1 -0
- qv/cli/__init__.py +5 -0
- qv/cli/main.py +253 -0
- qv/core/analyzer.py +21 -0
- qv/core/config.py +122 -0
- qv/core/context.py +105 -0
- qv/core/engine.py +116 -0
- qv/core/models.py +132 -0
- qv/core/project.py +330 -0
- qv/frameworks/__init__.py +1 -0
- qv/integrations/__init__.py +1 -0
- qv/reporters/__init__.py +13 -0
- qv/reporters/base.py +15 -0
- qv/reporters/json_reporter.py +16 -0
- qv/reporters/sarif.py +85 -0
- qv/reporters/terminal.py +126 -0
- qv/rules/registry.py +151 -0
qv/reporters/terminal.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Terminal reporter using Rich for structured, human-readable diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from qv.core.models import ScanResult, Severity
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TerminalReporter:
|
|
15
|
+
"""Renders diagnostics to rich terminal output."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, console: Console | None = None) -> None:
|
|
18
|
+
self.console = console or Console(color_system="auto")
|
|
19
|
+
|
|
20
|
+
def render(self, result: ScanResult) -> str:
|
|
21
|
+
"""Render scan result to string for display."""
|
|
22
|
+
string_io = io.StringIO()
|
|
23
|
+
out_console = Console(file=string_io, force_terminal=True, width=90)
|
|
24
|
+
self.print_result(result, console=out_console)
|
|
25
|
+
return string_io.getvalue()
|
|
26
|
+
|
|
27
|
+
def print_result(self, result: ScanResult, console: Console | None = None) -> None:
|
|
28
|
+
"""Print the scan result directly to console."""
|
|
29
|
+
con = console or self.console
|
|
30
|
+
|
|
31
|
+
# Header
|
|
32
|
+
con.print()
|
|
33
|
+
con.print("[bold cyan]đ qv[/bold cyan]")
|
|
34
|
+
con.print(f"[dim]Project:[/dim] [bold]{result.project_name}[/bold]")
|
|
35
|
+
con.print(f"[dim]Python:[/dim] [green]{result.python_version}[/green]")
|
|
36
|
+
con.print(f"[dim]Package Manager:[/dim] [blue]{result.package_manager}[/blue]")
|
|
37
|
+
con.print()
|
|
38
|
+
|
|
39
|
+
# Summary Badges
|
|
40
|
+
summary = result.summary
|
|
41
|
+
err_badge = (
|
|
42
|
+
f"[bold red]đ´ {summary.errors_count} Errors[/bold red]"
|
|
43
|
+
if summary.errors_count
|
|
44
|
+
else "[dim]0 Errors[/dim]"
|
|
45
|
+
)
|
|
46
|
+
warn_badge = (
|
|
47
|
+
f"[bold yellow]đĄ {summary.warnings_count} Warnings[/bold yellow]"
|
|
48
|
+
if summary.warnings_count
|
|
49
|
+
else "[dim]0 Warnings[/dim]"
|
|
50
|
+
)
|
|
51
|
+
pass_badge = f"[bold green]đĸ {summary.checks_passed} Checks Passed[/bold green]"
|
|
52
|
+
|
|
53
|
+
con.print(f"{err_badge} {warn_badge} {pass_badge}")
|
|
54
|
+
con.print()
|
|
55
|
+
|
|
56
|
+
# Diagnostics listing
|
|
57
|
+
if not result.diagnostics:
|
|
58
|
+
con.print(
|
|
59
|
+
"[bold green]⨠Everything looks healthy! No diagnostic issues found.[/bold green]"
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
for diag in result.diagnostics:
|
|
63
|
+
color = (
|
|
64
|
+
"red"
|
|
65
|
+
if diag.severity == Severity.ERROR
|
|
66
|
+
else "yellow"
|
|
67
|
+
if diag.severity == Severity.WARNING
|
|
68
|
+
else "blue"
|
|
69
|
+
)
|
|
70
|
+
icon = (
|
|
71
|
+
"đ´"
|
|
72
|
+
if diag.severity == Severity.ERROR
|
|
73
|
+
else "đĄ"
|
|
74
|
+
if diag.severity == Severity.WARNING
|
|
75
|
+
else "âšī¸"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
title_text = Text()
|
|
79
|
+
title_text.append(f"{icon} {diag.id} ", style=f"bold {color}")
|
|
80
|
+
title_text.append(f"{diag.title}", style="bold white")
|
|
81
|
+
|
|
82
|
+
body = []
|
|
83
|
+
body.append(f"[bold]{diag.message}[/bold]")
|
|
84
|
+
|
|
85
|
+
if diag.file:
|
|
86
|
+
loc = f"[dim]Location:[/dim] {diag.file}"
|
|
87
|
+
if diag.line:
|
|
88
|
+
loc += f":{diag.line}"
|
|
89
|
+
body.append(loc)
|
|
90
|
+
|
|
91
|
+
if diag.evidence:
|
|
92
|
+
body.append("\n[bold dim]Evidence:[/bold dim]")
|
|
93
|
+
for ev in diag.evidence:
|
|
94
|
+
body.append(f" âĸ {ev.fact}")
|
|
95
|
+
|
|
96
|
+
if diag.suggestions:
|
|
97
|
+
body.append("\n[bold cyan]Suggested fix:[/bold cyan]")
|
|
98
|
+
for sug in diag.suggestions:
|
|
99
|
+
body.append(f" {sug.description}")
|
|
100
|
+
if sug.command:
|
|
101
|
+
body.append(f" [bold green]$ {sug.command}[/bold green]")
|
|
102
|
+
if sug.code_snippet:
|
|
103
|
+
body.append(
|
|
104
|
+
f" [dim]```[/dim]\n [white]{sug.code_snippet}[/white]\n [dim]```[/dim]"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
panel = Panel(
|
|
108
|
+
"\n".join(body),
|
|
109
|
+
title=title_text,
|
|
110
|
+
border_style=color,
|
|
111
|
+
padding=(0, 1),
|
|
112
|
+
)
|
|
113
|
+
con.print(panel)
|
|
114
|
+
|
|
115
|
+
# Health score footer
|
|
116
|
+
score_color = (
|
|
117
|
+
"green"
|
|
118
|
+
if summary.health_score >= 85
|
|
119
|
+
else "yellow"
|
|
120
|
+
if summary.health_score >= 60
|
|
121
|
+
else "red"
|
|
122
|
+
)
|
|
123
|
+
con.print(
|
|
124
|
+
f"[bold]Health Score:[/bold] [{score_color}]{summary.health_score}/100[/{score_color}]"
|
|
125
|
+
)
|
|
126
|
+
con.print()
|
qv/rules/registry.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Central registry of all supported diagnostic rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from qv.core.models import Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class RuleDefinition:
|
|
12
|
+
"""Metadata for a diagnostic rule."""
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
category: str
|
|
16
|
+
title: str
|
|
17
|
+
description: str
|
|
18
|
+
default_severity: Severity
|
|
19
|
+
remediation_hint: str
|
|
20
|
+
doc_url: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Stable Registry of rules as defined in docs/rules.md
|
|
24
|
+
RULES_CATALOG: dict[str, RuleDefinition] = {
|
|
25
|
+
# Dependency rules
|
|
26
|
+
"DEP-001": RuleDefinition(
|
|
27
|
+
id="DEP-001",
|
|
28
|
+
category="dependency",
|
|
29
|
+
title="Dependency constraint conflict",
|
|
30
|
+
description="Two or more declared or transitive dependencies have incompatible version constraints.",
|
|
31
|
+
default_severity=Severity.ERROR,
|
|
32
|
+
remediation_hint="Upgrade the conflicting package or loosen the version pin to satisfy all requirements.",
|
|
33
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-001--dependency-constraint-conflict",
|
|
34
|
+
),
|
|
35
|
+
"DEP-002": RuleDefinition(
|
|
36
|
+
id="DEP-002",
|
|
37
|
+
category="dependency",
|
|
38
|
+
title="Missing dependency declaration",
|
|
39
|
+
description="A third-party package is imported in source code but is not declared in project dependencies.",
|
|
40
|
+
default_severity=Severity.ERROR,
|
|
41
|
+
remediation_hint="Add the missing package to your pyproject.toml or requirements.txt.",
|
|
42
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-002--missing-dependency-declaration",
|
|
43
|
+
),
|
|
44
|
+
"DEP-003": RuleDefinition(
|
|
45
|
+
id="DEP-003",
|
|
46
|
+
category="dependency",
|
|
47
|
+
title="Unused declared dependency",
|
|
48
|
+
description="A package is declared as a direct dependency but no imports were detected across project source files.",
|
|
49
|
+
default_severity=Severity.WARNING,
|
|
50
|
+
remediation_hint="Verify if this dependency is needed at runtime or remove it to keep dependencies lean.",
|
|
51
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-003--unused-declared-dependency",
|
|
52
|
+
),
|
|
53
|
+
"DEP-004": RuleDefinition(
|
|
54
|
+
id="DEP-004",
|
|
55
|
+
category="dependency",
|
|
56
|
+
title="Python compatibility mismatch",
|
|
57
|
+
description="A package requires a Python version incompatible with the target project runtime.",
|
|
58
|
+
default_severity=Severity.WARNING,
|
|
59
|
+
remediation_hint="Update your target Python version or install a version of the package compatible with your Python runtime.",
|
|
60
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-004--python-compatibility-mismatch",
|
|
61
|
+
),
|
|
62
|
+
"DEP-005": RuleDefinition(
|
|
63
|
+
id="DEP-005",
|
|
64
|
+
category="dependency",
|
|
65
|
+
title="Installed/declaration mismatch",
|
|
66
|
+
description="The package version installed in the virtualenv does not match the pinned requirement in project declaration.",
|
|
67
|
+
default_severity=Severity.WARNING,
|
|
68
|
+
remediation_hint="Sync your virtual environment using your package manager (e.g. `uv sync` or `poetry install`).",
|
|
69
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-005--installeddeclaration-mismatch",
|
|
70
|
+
),
|
|
71
|
+
"DEP-006": RuleDefinition(
|
|
72
|
+
id="DEP-006",
|
|
73
|
+
category="dependency",
|
|
74
|
+
title="Vulnerable transitive dependency",
|
|
75
|
+
description="A security vulnerability has been identified in a direct or transitive dependency.",
|
|
76
|
+
default_severity=Severity.ERROR,
|
|
77
|
+
remediation_hint="Update the vulnerable dependency or apply vendor security patches.",
|
|
78
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-006--vulnerable-transitive-dependency",
|
|
79
|
+
),
|
|
80
|
+
# Environment rules
|
|
81
|
+
"ENV-001": RuleDefinition(
|
|
82
|
+
id="ENV-001",
|
|
83
|
+
category="environment",
|
|
84
|
+
title="Python version drift",
|
|
85
|
+
description="The active Python interpreter version differs from the project's target runtime specification.",
|
|
86
|
+
default_severity=Severity.WARNING,
|
|
87
|
+
remediation_hint="Ensure the virtual environment is built using the configured target Python version.",
|
|
88
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#env-001--python-version-drift",
|
|
89
|
+
),
|
|
90
|
+
"ENV-002": RuleDefinition(
|
|
91
|
+
id="ENV-002",
|
|
92
|
+
category="environment",
|
|
93
|
+
title="Docker runtime drift",
|
|
94
|
+
description="The Python version in Dockerfile/docker-compose differs from the project's target runtime.",
|
|
95
|
+
default_severity=Severity.WARNING,
|
|
96
|
+
remediation_hint="Update the base image tag in your Dockerfile to match your project's target Python version.",
|
|
97
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#env-002--docker-runtime-drift",
|
|
98
|
+
),
|
|
99
|
+
"ENV-003": RuleDefinition(
|
|
100
|
+
id="ENV-003",
|
|
101
|
+
category="environment",
|
|
102
|
+
title="CI runtime drift",
|
|
103
|
+
description="CI workflow test matrix does not cover or differs from declared project Python versions.",
|
|
104
|
+
default_severity=Severity.WARNING,
|
|
105
|
+
remediation_hint="Align your CI matrix with the Python versions specified in pyproject.toml.",
|
|
106
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#env-003--ci-runtime-drift",
|
|
107
|
+
),
|
|
108
|
+
# Import / Architecture rules
|
|
109
|
+
"IMP-001": RuleDefinition(
|
|
110
|
+
id="IMP-001",
|
|
111
|
+
category="architecture",
|
|
112
|
+
title="Circular import detected",
|
|
113
|
+
description="An import cycle exists between two or more modules, which may cause runtime initialization errors.",
|
|
114
|
+
default_severity=Severity.ERROR,
|
|
115
|
+
remediation_hint="Refactor shared dependencies into a separate module or use deferred/lazy imports.",
|
|
116
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#imp-001--circular-import-detected",
|
|
117
|
+
),
|
|
118
|
+
"IMP-002": RuleDefinition(
|
|
119
|
+
id="IMP-002",
|
|
120
|
+
category="architecture",
|
|
121
|
+
title="Unresolved local import",
|
|
122
|
+
description="A local module imported in source code could not be resolved on the project's Python path.",
|
|
123
|
+
default_severity=Severity.ERROR,
|
|
124
|
+
remediation_hint="Verify the module name and ensure source directory is marked on the PYTHONPATH or package root.",
|
|
125
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#imp-002--unresolved-local-import",
|
|
126
|
+
),
|
|
127
|
+
# Packaging rules
|
|
128
|
+
"PKG-001": RuleDefinition(
|
|
129
|
+
id="PKG-001",
|
|
130
|
+
category="packaging",
|
|
131
|
+
title="Missing package metadata",
|
|
132
|
+
description="Essential packaging metadata (such as project name, version, or description) is missing.",
|
|
133
|
+
default_severity=Severity.WARNING,
|
|
134
|
+
remediation_hint="Provide project name and version in pyproject.toml [project] table.",
|
|
135
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#pkg-001--missing-package-metadata",
|
|
136
|
+
),
|
|
137
|
+
"PKG-002": RuleDefinition(
|
|
138
|
+
id="PKG-002",
|
|
139
|
+
category="packaging",
|
|
140
|
+
title="Invalid project configuration",
|
|
141
|
+
description="The project configuration file contains syntax errors or invalid keys.",
|
|
142
|
+
default_severity=Severity.ERROR,
|
|
143
|
+
remediation_hint="Fix configuration syntax to conform to PEP 621 / tool specifications.",
|
|
144
|
+
doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#pkg-002--invalid-project-configuration",
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_rule_definition(rule_id: str) -> RuleDefinition | None:
|
|
150
|
+
"""Retrieve rule definition by ID."""
|
|
151
|
+
return RULES_CATALOG.get(rule_id.upper())
|