mxready 1.0.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.
Files changed (71) hide show
  1. mxready/__init__.py +1 -0
  2. mxready/api/__init__.py +4 -0
  3. mxready/api/middleware.py +149 -0
  4. mxready/api/rules.py +10 -0
  5. mxready/api/scans.py +164 -0
  6. mxready/api/workspace.py +156 -0
  7. mxready/app.py +179 -0
  8. mxready/cli/__init__.py +1 -0
  9. mxready/cli/build_verification_bundle.py +45 -0
  10. mxready/cli/generate_public_report.py +140 -0
  11. mxready/cli/scan_batch.py +368 -0
  12. mxready/cli/scan_repository.py +212 -0
  13. mxready/cli/serve.py +55 -0
  14. mxready/config.py +117 -0
  15. mxready/errors.py +11 -0
  16. mxready/logging_setup.py +88 -0
  17. mxready/mcp/__init__.py +5 -0
  18. mxready/mcp/server.py +433 -0
  19. mxready/models.py +382 -0
  20. mxready/persistence/__init__.py +1 -0
  21. mxready/persistence/sqlite.py +581 -0
  22. mxready/py.typed +0 -0
  23. mxready/reporting/__init__.py +4 -0
  24. mxready/reporting/badge.py +40 -0
  25. mxready/reporting/comparison.py +33 -0
  26. mxready/reporting/markdown.py +157 -0
  27. mxready/reporting/sarif.py +94 -0
  28. mxready/reporting/terminal.py +75 -0
  29. mxready/repository/__init__.py +14 -0
  30. mxready/repository/git_client.py +275 -0
  31. mxready/repository/github_archive.py +566 -0
  32. mxready/repository/identity.py +140 -0
  33. mxready/resources/__init__.py +19 -0
  34. mxready/resources/frontend/assets/index-BxJEwNUm.css +1 -0
  35. mxready/resources/frontend/assets/index-lzTCdWAt.js +9 -0
  36. mxready/resources/frontend/assets/mx-chip-C0HgFQyE.webp +0 -0
  37. mxready/resources/frontend/index.html +17 -0
  38. mxready/resources/rules/v1/core.yml +634 -0
  39. mxready/resources/rules/v1/manifest.yml +4 -0
  40. mxready/resources/schemas/verification-result-v1.json +259 -0
  41. mxready/scanning/__init__.py +17 -0
  42. mxready/scanning/analyzer.py +183 -0
  43. mxready/scanning/facts.py +596 -0
  44. mxready/scanning/indexer.py +299 -0
  45. mxready/scanning/path_context.py +174 -0
  46. mxready/scanning/python_source.py +67 -0
  47. mxready/scanning/rule_engine.py +595 -0
  48. mxready/scanning/rule_loader.py +282 -0
  49. mxready/scanning/toolchain.py +100 -0
  50. mxready/services/__init__.py +3 -0
  51. mxready/services/scans.py +239 -0
  52. mxready/services/worker.py +38 -0
  53. mxready/verification/__init__.py +11 -0
  54. mxready/verification/bundle.py +137 -0
  55. mxready/verification/validation.py +101 -0
  56. mxready-1.0.0.dist-info/METADATA +526 -0
  57. mxready-1.0.0.dist-info/RECORD +71 -0
  58. mxready-1.0.0.dist-info/WHEEL +5 -0
  59. mxready-1.0.0.dist-info/entry_points.txt +8 -0
  60. mxready-1.0.0.dist-info/licenses/LICENSE +201 -0
  61. mxready-1.0.0.dist-info/top_level.txt +2 -0
  62. mxready_runner/__init__.py +1 -0
  63. mxready_runner/__main__.py +3 -0
  64. mxready_runner/cli.py +88 -0
  65. mxready_runner/contracts.py +26 -0
  66. mxready_runner/execute.py +185 -0
  67. mxready_runner/inspect.py +79 -0
  68. mxready_runner/process.py +103 -0
  69. mxready_runner/py.typed +0 -0
  70. mxready_runner/redact.py +36 -0
  71. mxready_runner/schema.py +326 -0
@@ -0,0 +1,259 @@
1
+ {
2
+ "$defs": {
3
+ "VerificationCheck": {
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "id": {
7
+ "pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
8
+ "title": "Id",
9
+ "type": "string"
10
+ },
11
+ "command": {
12
+ "items": {
13
+ "type": "string"
14
+ },
15
+ "maxItems": 32,
16
+ "minItems": 1,
17
+ "title": "Command",
18
+ "type": "array"
19
+ },
20
+ "status": {
21
+ "enum": [
22
+ "passed",
23
+ "failed",
24
+ "unavailable"
25
+ ],
26
+ "title": "Status",
27
+ "type": "string"
28
+ },
29
+ "return_code": {
30
+ "anyOf": [
31
+ {
32
+ "type": "integer"
33
+ },
34
+ {
35
+ "type": "null"
36
+ }
37
+ ],
38
+ "default": null,
39
+ "title": "Return Code"
40
+ },
41
+ "stdout": {
42
+ "default": "",
43
+ "maxLength": 16384,
44
+ "title": "Stdout",
45
+ "type": "string"
46
+ },
47
+ "stderr": {
48
+ "default": "",
49
+ "maxLength": 16384,
50
+ "title": "Stderr",
51
+ "type": "string"
52
+ },
53
+ "duration_ms": {
54
+ "minimum": 0,
55
+ "title": "Duration Ms",
56
+ "type": "integer"
57
+ }
58
+ },
59
+ "required": [
60
+ "id",
61
+ "command",
62
+ "status",
63
+ "duration_ms"
64
+ ],
65
+ "title": "VerificationCheck",
66
+ "type": "object"
67
+ },
68
+ "VerificationCommand": {
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "id": {
72
+ "pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
73
+ "title": "Id",
74
+ "type": "string"
75
+ },
76
+ "command": {
77
+ "items": {
78
+ "type": "string"
79
+ },
80
+ "maxItems": 32,
81
+ "minItems": 1,
82
+ "title": "Command",
83
+ "type": "array"
84
+ },
85
+ "timeout_seconds": {
86
+ "maximum": 600,
87
+ "minimum": 1,
88
+ "title": "Timeout Seconds",
89
+ "type": "integer"
90
+ },
91
+ "status": {
92
+ "enum": [
93
+ "passed",
94
+ "failed",
95
+ "timeout",
96
+ "cancelled"
97
+ ],
98
+ "title": "Status",
99
+ "type": "string"
100
+ },
101
+ "return_code": {
102
+ "anyOf": [
103
+ {
104
+ "type": "integer"
105
+ },
106
+ {
107
+ "type": "null"
108
+ }
109
+ ],
110
+ "default": null,
111
+ "title": "Return Code"
112
+ },
113
+ "stdout": {
114
+ "default": "",
115
+ "maxLength": 16384,
116
+ "title": "Stdout",
117
+ "type": "string"
118
+ },
119
+ "stderr": {
120
+ "default": "",
121
+ "maxLength": 16384,
122
+ "title": "Stderr",
123
+ "type": "string"
124
+ },
125
+ "duration_ms": {
126
+ "minimum": 0,
127
+ "title": "Duration Ms",
128
+ "type": "integer"
129
+ }
130
+ },
131
+ "required": [
132
+ "id",
133
+ "command",
134
+ "timeout_seconds",
135
+ "status",
136
+ "duration_ms"
137
+ ],
138
+ "title": "VerificationCommand",
139
+ "type": "object"
140
+ },
141
+ "VerifiedRepositoryState": {
142
+ "additionalProperties": false,
143
+ "properties": {
144
+ "working_directory": {
145
+ "maxLength": 4096,
146
+ "minLength": 1,
147
+ "title": "Working Directory",
148
+ "type": "string"
149
+ },
150
+ "commit": {
151
+ "pattern": "^[0-9a-f]{40}$",
152
+ "title": "Commit",
153
+ "type": "string"
154
+ },
155
+ "dirty": {
156
+ "const": false,
157
+ "title": "Dirty",
158
+ "type": "boolean"
159
+ }
160
+ },
161
+ "required": [
162
+ "working_directory",
163
+ "commit",
164
+ "dirty"
165
+ ],
166
+ "title": "VerifiedRepositoryState",
167
+ "type": "object"
168
+ }
169
+ },
170
+ "additionalProperties": false,
171
+ "properties": {
172
+ "repository_state": {
173
+ "anyOf": [
174
+ {
175
+ "$ref": "#/$defs/VerifiedRepositoryState"
176
+ },
177
+ {
178
+ "type": "null"
179
+ }
180
+ ],
181
+ "default": null
182
+ },
183
+ "schema_version": {
184
+ "const": "1.0",
185
+ "default": "1.0",
186
+ "title": "Schema Version",
187
+ "type": "string"
188
+ },
189
+ "scan_id": {
190
+ "format": "uuid",
191
+ "title": "Scan Id",
192
+ "type": "string"
193
+ },
194
+ "repository_commit": {
195
+ "pattern": "^[0-9a-f]{40}$",
196
+ "title": "Repository Commit",
197
+ "type": "string"
198
+ },
199
+ "runner_version": {
200
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
201
+ "title": "Runner Version",
202
+ "type": "string"
203
+ },
204
+ "environment_fingerprint": {
205
+ "pattern": "^sha256:[0-9a-f]{64}$",
206
+ "title": "Environment Fingerprint",
207
+ "type": "string"
208
+ },
209
+ "checks": {
210
+ "items": {
211
+ "$ref": "#/$defs/VerificationCheck"
212
+ },
213
+ "maxItems": 64,
214
+ "title": "Checks",
215
+ "type": "array"
216
+ },
217
+ "commands": {
218
+ "items": {
219
+ "$ref": "#/$defs/VerificationCommand"
220
+ },
221
+ "maxItems": 64,
222
+ "title": "Commands",
223
+ "type": "array"
224
+ },
225
+ "started_at": {
226
+ "format": "date-time",
227
+ "title": "Started At",
228
+ "type": "string"
229
+ },
230
+ "finished_at": {
231
+ "format": "date-time",
232
+ "title": "Finished At",
233
+ "type": "string"
234
+ },
235
+ "overall_status": {
236
+ "enum": [
237
+ "passed",
238
+ "failed",
239
+ "cancelled"
240
+ ],
241
+ "title": "Overall Status",
242
+ "type": "string"
243
+ }
244
+ },
245
+ "required": [
246
+ "scan_id",
247
+ "repository_commit",
248
+ "runner_version",
249
+ "environment_fingerprint",
250
+ "checks",
251
+ "commands",
252
+ "started_at",
253
+ "finished_at",
254
+ "overall_status"
255
+ ],
256
+ "title": "VerificationRun",
257
+ "type": "object",
258
+ "$schema": "https://json-schema.org/draft/2020-12/schema"
259
+ }
@@ -0,0 +1,17 @@
1
+ from mxready.scanning.facts import ProjectFacts, SourceLocation, extract_project_facts
2
+ from mxready.scanning.indexer import (
3
+ FileIndex,
4
+ IndexedFile,
5
+ IndexWarning,
6
+ build_file_index,
7
+ )
8
+
9
+ __all__ = [
10
+ "FileIndex",
11
+ "IndexWarning",
12
+ "IndexedFile",
13
+ "ProjectFacts",
14
+ "SourceLocation",
15
+ "build_file_index",
16
+ "extract_project_facts",
17
+ ]
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from uuid import UUID
8
+
9
+ from mxready import __version__
10
+ from mxready.models import (
11
+ AnalysisWarning,
12
+ Finding,
13
+ FindingOccurrence,
14
+ MigrationChecklistItem,
15
+ RepositorySnapshot,
16
+ ScanReport,
17
+ ScanStatus,
18
+ Severity,
19
+ VerificationStatus,
20
+ calculate_static_status,
21
+ summarize_findings,
22
+ )
23
+ from mxready.repository.identity import parse_repository_url
24
+ from mxready.scanning.facts import extract_project_facts
25
+ from mxready.scanning.indexer import build_file_index
26
+ from mxready.scanning.path_context import apply_path_context
27
+ from mxready.scanning.rule_engine import evaluate_rules, prefilter_status
28
+ from mxready.scanning.rule_loader import RuleCatalog
29
+ from mxready.scanning.toolchain import apply_toolchain_profile
30
+
31
+
32
+ class ScanAnalyzer:
33
+ def __init__(
34
+ self,
35
+ catalog: RuleCatalog,
36
+ providers: Mapping[str, str] | None = None,
37
+ *,
38
+ max_findings: int = 10_000,
39
+ rule_timeout_seconds: float = 30,
40
+ enable_prefilter: bool = True,
41
+ ) -> None:
42
+ self.catalog = catalog
43
+ self.providers = providers
44
+ self.max_findings = max_findings
45
+ self.rule_timeout_seconds = rule_timeout_seconds
46
+ self.enable_prefilter = enable_prefilter
47
+
48
+ def analyze(
49
+ self,
50
+ repository_root: Path,
51
+ *,
52
+ repository_url: str,
53
+ commit: str,
54
+ scan_id: UUID,
55
+ stage_callback: Callable[[ScanStatus], None],
56
+ ) -> ScanReport:
57
+ stage_callback(ScanStatus.INDEXING)
58
+ index = build_file_index(repository_root)
59
+
60
+ stage_callback(ScanStatus.ANALYZING)
61
+ facts = extract_project_facts(index)
62
+ findings = _aggregate_findings(
63
+ evaluate_rules(
64
+ self.catalog,
65
+ index,
66
+ facts,
67
+ max_findings=self.max_findings,
68
+ timeout_seconds=self.rule_timeout_seconds,
69
+ enable_prefilter=self.enable_prefilter,
70
+ )
71
+ )
72
+ # 路径上下文先于工具链画像生效:测试/示例/文档路径上的命中先封顶为提示,
73
+ # 工具链画像随后只需处理交付路径,避免出现"从 blocker 降为 warning"
74
+ # 这种对一条早已是提示的 finding 的错误说明。
75
+ findings, path_warnings = apply_path_context(findings)
76
+ # 严重级别取决于"项目是否已接入 MXMACA 兼容层"这一前提;工具链画像要在
77
+ # 汇总之前生效,否则 summary 计数与 findings 的级别会对不上。
78
+ findings, toolchain_warnings = apply_toolchain_profile(findings, facts)
79
+ summary = summarize_findings(findings)
80
+ identity = parse_repository_url(repository_url, self.providers)
81
+
82
+ return ScanReport(
83
+ scan_id=scan_id,
84
+ repository=RepositorySnapshot(
85
+ provider=identity.provider,
86
+ owner=identity.owner,
87
+ name=identity.name,
88
+ url=identity.clone_url.removesuffix(".git"),
89
+ commit=commit,
90
+ ),
91
+ tool_version=__version__,
92
+ ruleset_version=self.catalog.version,
93
+ ruleset_digest=self.catalog.digest,
94
+ extra_ruleset_version=self.catalog.extra_version,
95
+ analysis_config={
96
+ "max_findings": self.max_findings,
97
+ "rule_timeout_seconds": self.rule_timeout_seconds,
98
+ "index_max_files": 10_000,
99
+ "index_max_file_bytes": 1_048_576,
100
+ # 记录实际生效状态而非配置意图:解析器探测失败时为 "unavailable"。
101
+ "prefilter": prefilter_status() if self.enable_prefilter else "off",
102
+ },
103
+ scanned_at=datetime.now(UTC),
104
+ summary=summary,
105
+ findings=findings,
106
+ coverage=index.coverage,
107
+ migration_checklist=_build_migration_checklist(findings),
108
+ analysis_warnings=[
109
+ AnalysisWarning(
110
+ code=warning.code,
111
+ relative_path=warning.relative_path,
112
+ message=warning.message,
113
+ )
114
+ for warning in index.warnings
115
+ ]
116
+ + (
117
+ [
118
+ AnalysisWarning(
119
+ code="NO_ANALYZABLE_SOURCE",
120
+ message="没有可分析的源码或配置文件;零命中不代表兼容性通过。",
121
+ )
122
+ ]
123
+ if not index.files
124
+ else []
125
+ )
126
+ + path_warnings
127
+ + toolchain_warnings,
128
+ static_status=calculate_static_status(summary),
129
+ verification_status=VerificationStatus.NOT_RUN,
130
+ )
131
+
132
+
133
+ def _aggregate_findings(findings: list[Finding]) -> list[Finding]:
134
+ """同一规则在同一文件多次命中时合并为一条并累计命中数,降低报告噪音。"""
135
+ grouped: dict[tuple[str, str], Finding] = {}
136
+ for finding in findings:
137
+ occurrences = finding.occurrences or [
138
+ FindingOccurrence(
139
+ line_start=finding.line_start,
140
+ line_end=finding.line_end,
141
+ evidence=finding.evidence,
142
+ )
143
+ ]
144
+ key = (finding.rule_id, finding.relative_path)
145
+ existing = grouped.get(key)
146
+ if existing is None:
147
+ grouped[key] = finding.model_copy(update={"occurrences": occurrences})
148
+ continue
149
+ grouped[key] = existing.model_copy(
150
+ update={
151
+ "count": existing.count + finding.count,
152
+ "occurrences": existing.occurrences + occurrences,
153
+ "line_start": min(existing.line_start, finding.line_start),
154
+ "line_end": max(existing.line_end, finding.line_end),
155
+ }
156
+ )
157
+ return list(grouped.values())
158
+
159
+
160
+ def _build_migration_checklist(findings) -> list[MigrationChecklistItem]:
161
+ grouped: dict[str, dict[str, Any]] = {}
162
+ for finding in findings:
163
+ if finding.severity not in {Severity.BLOCKER, Severity.WARNING}:
164
+ continue
165
+ item = grouped.setdefault(
166
+ finding.rule_id,
167
+ {
168
+ "title": finding.title,
169
+ "action": finding.recommendation,
170
+ "files": set(),
171
+ },
172
+ )
173
+ item["files"].add(finding.relative_path)
174
+
175
+ return [
176
+ MigrationChecklistItem(
177
+ rule_id=rule_id,
178
+ title=item["title"],
179
+ action=item["action"],
180
+ affected_files=sorted(item["files"]),
181
+ )
182
+ for rule_id, item in grouped.items()
183
+ ]