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
mxready/models.py ADDED
@@ -0,0 +1,382 @@
1
+ from datetime import datetime
2
+ from enum import StrEnum
3
+ from typing import Literal
4
+ from uuid import UUID
5
+
6
+ from pydantic import (
7
+ BaseModel,
8
+ ConfigDict,
9
+ Field,
10
+ field_validator,
11
+ model_validator,
12
+ )
13
+
14
+
15
+ class StrictModel(BaseModel):
16
+ model_config = ConfigDict(extra="forbid", frozen=True)
17
+
18
+
19
+ class ScanStatus(StrEnum):
20
+ QUEUED = "queued"
21
+ CLONING = "cloning"
22
+ INDEXING = "indexing"
23
+ ANALYZING = "analyzing"
24
+ COMPLETED = "completed"
25
+ FAILED = "failed"
26
+
27
+
28
+ class Severity(StrEnum):
29
+ BLOCKER = "blocker"
30
+ WARNING = "warning"
31
+ INFO = "info"
32
+
33
+
34
+ class StaticStatus(StrEnum):
35
+ PASSED = "passed"
36
+ WARNINGS = "warnings"
37
+ BLOCKED = "blocked"
38
+ FAILED = "failed"
39
+
40
+
41
+ class VerificationStatus(StrEnum):
42
+ NOT_RUN = "not-run"
43
+ VERIFIED = "verified"
44
+ FAILED = "failed"
45
+ STALE = "stale"
46
+
47
+
48
+ class BadgeStatus(StrEnum):
49
+ STATIC_PASSED = "static-passed"
50
+ WARNINGS = "warnings"
51
+ BLOCKED = "blocked"
52
+ VERIFIED = "verified"
53
+ VERIFICATION_STALE = "verification-stale"
54
+ SCAN_FAILED = "scan-failed"
55
+
56
+
57
+ class ScanJob(StrictModel):
58
+ id: UUID
59
+ repo_url: str
60
+ requested_ref: str | None = None
61
+ resolved_commit: str | None = None
62
+ status: ScanStatus = ScanStatus.QUEUED
63
+ stage_message: str
64
+ created_at: datetime
65
+ updated_at: datetime
66
+ failure_code: str | None = None
67
+ failure_message: str | None = None
68
+
69
+
70
+ class ScanPage(StrictModel):
71
+ items: list[ScanJob]
72
+ total: int
73
+ limit: int
74
+ offset: int
75
+
76
+
77
+ class SourceReference(StrictModel):
78
+ title: str
79
+ url: str
80
+
81
+
82
+ class RepositorySnapshot(StrictModel):
83
+ provider: str
84
+ owner: str
85
+ name: str
86
+ url: str
87
+ commit: str = Field(pattern=r"^[0-9a-f]{40}$")
88
+
89
+
90
+ class FindingOccurrence(StrictModel):
91
+ line_start: int = Field(ge=1)
92
+ line_end: int = Field(ge=1)
93
+ evidence: str
94
+
95
+ @model_validator(mode="after")
96
+ def ordered(self) -> "FindingOccurrence":
97
+ if self.line_end < self.line_start:
98
+ raise ValueError("line_end must be greater than or equal to line_start")
99
+ return self
100
+
101
+
102
+ class ScanCoverage(StrictModel):
103
+ analyzed_files: int = Field(ge=0)
104
+ analyzed_bytes: int = Field(ge=0)
105
+ files_by_type: dict[str, int]
106
+ skipped_by_reason: dict[str, int]
107
+ excluded_directories: int = Field(ge=0)
108
+
109
+
110
+ class Finding(StrictModel):
111
+ rule_id: str
112
+ rule_version: int = Field(ge=1)
113
+ severity: Severity
114
+ category: str
115
+ title: str
116
+ relative_path: str
117
+ line_start: int = Field(ge=1)
118
+ line_end: int = Field(ge=1)
119
+ evidence: str
120
+ message: str
121
+ recommendation: str
122
+ references: list[SourceReference]
123
+ count: int = Field(default=1, ge=1)
124
+ occurrences: list[FindingOccurrence] = Field(default_factory=list)
125
+
126
+ @model_validator(mode="after")
127
+ def line_range_is_ordered(self) -> "Finding":
128
+ if self.line_end < self.line_start:
129
+ raise ValueError("line_end must be greater than or equal to line_start")
130
+ return self
131
+
132
+
133
+ class FindingReviewInput(StrictModel):
134
+ rule_id: str = Field(min_length=1, max_length=100)
135
+ relative_path: str = Field(min_length=1, max_length=4096)
136
+ status: Literal["confirmed", "false-positive", "deferred"]
137
+ reason: str = Field(min_length=1, max_length=1000)
138
+
139
+ @field_validator("reason")
140
+ @classmethod
141
+ def nonblank_reason(cls, value: str) -> str:
142
+ if not value.strip():
143
+ raise ValueError("A review reason is required")
144
+ return value.strip()
145
+
146
+
147
+ class FindingReview(FindingReviewInput):
148
+ id: int
149
+ created_at: datetime
150
+
151
+
152
+ class FindingReviewPage(StrictModel):
153
+ items: list[FindingReview]
154
+ total: int
155
+ limit: int
156
+ offset: int
157
+
158
+
159
+ class ScanSummary(StrictModel):
160
+ total_count: int = Field(ge=0)
161
+ blocker_count: int = Field(ge=0)
162
+ warning_count: int = Field(ge=0)
163
+ info_count: int = Field(ge=0)
164
+ top_blockers: list[str] = Field(default_factory=list)
165
+
166
+
167
+ class MigrationChecklistItem(StrictModel):
168
+ rule_id: str
169
+ title: str
170
+ action: str
171
+ affected_files: list[str]
172
+ completed: bool = False
173
+
174
+
175
+ class AnalysisWarning(StrictModel):
176
+ code: str
177
+ relative_path: str | None = None
178
+ message: str
179
+
180
+
181
+ class ScanReport(StrictModel):
182
+ schema_version: Literal["1.0"] = "1.0"
183
+ scan_id: UUID
184
+ repository: RepositorySnapshot
185
+ tool_version: str
186
+ ruleset_version: str
187
+ ruleset_digest: str | None = Field(default=None, pattern=r"^sha256:[0-9a-f]{64}$")
188
+ extra_ruleset_version: str | None = None
189
+ analysis_config: dict[str, int | float | str] = Field(default_factory=dict)
190
+ scanned_at: datetime
191
+ summary: ScanSummary
192
+ findings: list[Finding]
193
+ coverage: ScanCoverage | None = None
194
+ migration_checklist: list[MigrationChecklistItem]
195
+ analysis_warnings: list[AnalysisWarning]
196
+ static_status: StaticStatus
197
+ verification_status: VerificationStatus
198
+
199
+
200
+ class VerificationCheck(StrictModel):
201
+ id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,63}$")
202
+ command: list[str] = Field(min_length=1, max_length=32)
203
+ status: Literal["passed", "failed", "unavailable"]
204
+ return_code: int | None = None
205
+ stdout: str = Field(default="", max_length=16_384)
206
+ stderr: str = Field(default="", max_length=16_384)
207
+ duration_ms: int = Field(ge=0)
208
+
209
+ @field_validator("command")
210
+ @classmethod
211
+ def command_arguments_are_bounded(cls, value: list[str]) -> list[str]:
212
+ return _validate_result_command(value)
213
+
214
+ @model_validator(mode="after")
215
+ def status_matches_return_code(self) -> "VerificationCheck":
216
+ if self.status == "passed" and self.return_code != 0:
217
+ raise ValueError("passed checks must have return_code 0")
218
+ if self.status == "failed" and self.return_code == 0:
219
+ raise ValueError("failed checks cannot have return_code 0")
220
+ if self.status == "unavailable" and self.return_code is not None:
221
+ raise ValueError("unavailable checks cannot have a return code")
222
+ return self
223
+
224
+
225
+ class VerificationCommand(StrictModel):
226
+ id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,63}$")
227
+ command: list[str] = Field(min_length=1, max_length=32)
228
+ timeout_seconds: int = Field(ge=1, le=600)
229
+ status: Literal["passed", "failed", "timeout", "cancelled"]
230
+ return_code: int | None = None
231
+ stdout: str = Field(default="", max_length=16_384)
232
+ stderr: str = Field(default="", max_length=16_384)
233
+ duration_ms: int = Field(ge=0)
234
+
235
+ @field_validator("command")
236
+ @classmethod
237
+ def command_arguments_are_bounded(cls, value: list[str]) -> list[str]:
238
+ return _validate_result_command(value)
239
+
240
+ @model_validator(mode="after")
241
+ def status_matches_return_code(self) -> "VerificationCommand":
242
+ if self.status == "passed" and self.return_code != 0:
243
+ raise ValueError("passed commands must have return_code 0")
244
+ if self.status == "failed" and self.return_code == 0:
245
+ raise ValueError("failed commands cannot have return_code 0")
246
+ if self.status in {"timeout", "cancelled"} and self.return_code is not None:
247
+ raise ValueError("timed out or cancelled commands cannot have a return code")
248
+ return self
249
+
250
+
251
+ class VerifiedRepositoryState(StrictModel):
252
+ working_directory: str = Field(min_length=1, max_length=4096)
253
+ commit: str = Field(pattern=r"^[0-9a-f]{40}$")
254
+ dirty: Literal[False]
255
+
256
+
257
+ class VerificationRun(StrictModel):
258
+ repository_state: VerifiedRepositoryState | None = None
259
+
260
+ @model_validator(mode="after")
261
+ def repository_state_matches(self) -> "VerificationRun":
262
+ if self.repository_state and self.repository_state.commit != self.repository_commit:
263
+ raise ValueError("repository state commit must match repository_commit")
264
+ return self
265
+
266
+ schema_version: Literal["1.0"] = "1.0"
267
+ scan_id: UUID
268
+ repository_commit: str = Field(pattern=r"^[0-9a-f]{40}$")
269
+ runner_version: str = Field(pattern=r"^[0-9]+\.[0-9]+\.[0-9]+$")
270
+ environment_fingerprint: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
271
+ checks: list[VerificationCheck] = Field(max_length=64)
272
+ commands: list[VerificationCommand] = Field(max_length=64)
273
+ started_at: datetime
274
+ finished_at: datetime
275
+ overall_status: Literal["passed", "failed", "cancelled"]
276
+
277
+ @model_validator(mode="after")
278
+ def result_is_semantically_consistent(self) -> "VerificationRun":
279
+ identifiers = [item.id for item in self.checks] + [item.id for item in self.commands]
280
+ if len(identifiers) != len(set(identifiers)):
281
+ raise ValueError("verification result ids must be unique")
282
+ if self.overall_status == "passed" and any(item.status != "passed" for item in self.checks):
283
+ raise ValueError("passed results cannot contain failed environment checks")
284
+ if self.overall_status == "passed" and any(
285
+ item.status != "passed" for item in self.commands
286
+ ):
287
+ raise ValueError("passed results cannot contain failed commands")
288
+ return self
289
+
290
+
291
+ class VerificationSummary(StrictModel):
292
+ id: int
293
+ uploaded_at: datetime | None
294
+ started_at: datetime
295
+ finished_at: datetime
296
+ expires_at: datetime
297
+ status: VerificationStatus
298
+ overall_status: str
299
+ repository_commit: str
300
+ environment_fingerprint: str
301
+ runner_version: str
302
+ check_count: int
303
+ command_count: int
304
+ is_latest: bool
305
+
306
+
307
+ class VerificationPage(StrictModel):
308
+ items: list[VerificationSummary]
309
+ total: int
310
+ limit: int
311
+ offset: int
312
+
313
+
314
+ class VerificationOutput(StrictModel):
315
+ id: str
316
+ status: str
317
+ return_code: int | None
318
+ duration_ms: int
319
+ stdout: str
320
+ stderr: str
321
+
322
+
323
+ class VerificationDetail(StrictModel):
324
+ summary: VerificationSummary
325
+ checks: list[VerificationOutput]
326
+ commands: list[VerificationOutput]
327
+
328
+
329
+ def _validate_result_command(value: list[str]) -> list[str]:
330
+ if any(
331
+ not argument or len(argument) > 4_096 or any(ord(character) < 32 for character in argument)
332
+ for argument in value
333
+ ):
334
+ raise ValueError("command arguments must be bounded printable strings")
335
+ return value
336
+
337
+
338
+ def summarize_findings(findings: list[Finding]) -> ScanSummary:
339
+ return ScanSummary(
340
+ total_count=len(findings),
341
+ blocker_count=sum(item.severity is Severity.BLOCKER for item in findings),
342
+ warning_count=sum(item.severity is Severity.WARNING for item in findings),
343
+ info_count=sum(item.severity is Severity.INFO for item in findings),
344
+ top_blockers=_top_priority_files(findings),
345
+ )
346
+
347
+
348
+ def _top_priority_files(findings: list[Finding], limit: int = 10) -> list[str]:
349
+ """按受影响文件数给出优先复核清单:优先 blocker,其次 warning。"""
350
+ from collections import Counter
351
+
352
+ counter: Counter[str] = Counter()
353
+ for item in findings:
354
+ if item.severity is Severity.BLOCKER:
355
+ counter[item.relative_path] += 1
356
+ if not counter:
357
+ for item in findings:
358
+ if item.severity is Severity.WARNING:
359
+ counter[item.relative_path] += 1
360
+ return [path for path, _ in counter.most_common(limit)]
361
+
362
+
363
+ def calculate_static_status(summary: ScanSummary) -> StaticStatus:
364
+ if summary.blocker_count:
365
+ return StaticStatus.BLOCKED
366
+ if summary.warning_count:
367
+ return StaticStatus.WARNINGS
368
+ return StaticStatus.PASSED
369
+
370
+
371
+ def calculate_badge_status(report: ScanReport) -> BadgeStatus:
372
+ if report.static_status is StaticStatus.FAILED:
373
+ return BadgeStatus.SCAN_FAILED
374
+ if report.static_status is StaticStatus.BLOCKED:
375
+ return BadgeStatus.BLOCKED
376
+ if report.verification_status is VerificationStatus.VERIFIED:
377
+ return BadgeStatus.VERIFIED
378
+ if report.verification_status is VerificationStatus.STALE:
379
+ return BadgeStatus.VERIFICATION_STALE
380
+ if report.static_status is StaticStatus.WARNINGS:
381
+ return BadgeStatus.WARNINGS
382
+ return BadgeStatus.STATIC_PASSED
@@ -0,0 +1 @@
1
+ """Persistence adapters and schema migrations."""