tzero-mcp 10.7.0 → 10.7.2
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.
- package/CHANGELOG.md +589 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +25 -17
- package/bin/postinstall.js +34 -0
- package/bin/tzero-mcp.js +16 -19
- package/bin/tzero.js +14 -19
- package/index.d.ts +24 -0
- package/index.js +7 -3
- package/package.json +24 -2
- package/src/addmcp.py +1194 -0
- package/src/main.py +60 -0
- package/src/siber_akademi_icon.py +77 -0
- package/src/tzero/jarvis/semantic_compressor.py +373 -0
- package/src/tzero.py +58 -0
- package/src/tzero_benchmark.py +475 -0
- package/src/tzero_bridge.py +362 -0
- package/src/tzero_ci.py +153 -0
- package/src/tzero_commit.py +192 -0
- package/src/tzero_cost.py +163 -0
- package/src/tzero_dashboard.py +1448 -0
- package/src/tzero_deps.py +287 -0
- package/src/tzero_drift.py +208 -0
- package/src/tzero_explain.py +240 -0
- package/src/tzero_features.py +3022 -0
- package/src/tzero_fix.py +228 -0
- package/src/tzero_fixer/__init__.py +63 -0
- package/src/tzero_fixer/amalgamation.py +334 -0
- package/src/tzero_fixer/ast_syntax_healer.py +464 -0
- package/src/tzero_fixer/ast_type_modernizer.py +288 -0
- package/src/tzero_fixer/async_io_modernizer.py +127 -0
- package/src/tzero_fixer/automated_regression_suite.py +600 -0
- package/src/tzero_fixer/benchmarks_and_diagnostics.py +174 -0
- package/src/tzero_fixer/ci_cd_self_healing_gate.py +246 -0
- package/src/tzero_fixer/code_duplicity_and_refactorer.py +151 -0
- package/src/tzero_fixer/code_smells_healer.py +260 -0
- package/src/tzero_fixer/cwe_remediator.py +171 -0
- package/src/tzero_fixer/dead_code_eliminator.py +150 -0
- package/src/tzero_fixer/dependency_vulnerability_auditor.py +309 -0
- package/src/tzero_fixer/git_advanced_surgeon.py +280 -0
- package/src/tzero_fixer/hardware_and_disk_doctor.py +325 -0
- package/src/tzero_fixer/import_graph_healer.py +412 -0
- package/src/tzero_fixer/interactive_tui.py +91 -0
- package/src/tzero_fixer/knowledge_base.py +295 -0
- package/src/tzero_fixer/knowledge_base_extended.py +940 -0
- package/src/tzero_fixer/linters_and_static_analyzer.py +307 -0
- package/src/tzero_fixer/mcp_server.py +447 -0
- package/src/tzero_fixer/models.py +385 -0
- package/src/tzero_fixer/orchestrator.py +375 -0
- package/src/tzero_fixer/os_doctor.py +502 -0
- package/src/tzero_fixer/packaging_abi_surgeon.py +323 -0
- package/src/tzero_fixer/pc_fixer_doctor.py +599 -0
- package/src/tzero_fixer/performance_and_memory_profiler.py +274 -0
- package/src/tzero_fixer/polyglot_ast_parsers.py +313 -0
- package/src/tzero_fixer/polyglot_build_healer.py +364 -0
- package/src/tzero_fixer/polyglot_config_healer.py +318 -0
- package/src/tzero_fixer/polyglot_doctor.py +372 -0
- package/src/tzero_fixer/polyglot_linters.py +619 -0
- package/src/tzero_fixer/polyglot_schema_validator.py +305 -0
- package/src/tzero_fixer/security_cwe_database.py +444 -0
- package/src/tzero_fixer/security_deep_rules.py +114 -0
- package/src/tzero_fixer/security_rules_catalog.py +110 -0
- package/src/tzero_fixer/security_zero_leak.py +326 -0
- package/src/tzero_fixer/smart_patch_recommender.py +223 -0
- package/src/tzero_fixer/synthetic_workspace_generator.py +220 -0
- package/src/tzero_fixer/system_diagnostics_extended.py +493 -0
- package/src/tzero_fixer/system_repair_recipes.py +447 -0
- package/src/tzero_fixer/venv_doctor.py +309 -0
- package/src/tzero_gateway.py +885 -0
- package/src/tzero_guard.py +243 -0
- package/src/tzero_infra.py +274 -0
- package/src/tzero_jarvis/__init__.py +124 -0
- package/src/tzero_jarvis/causal_graph.py +310 -0
- package/src/tzero_jarvis/graph_visualizer.py +395 -0
- package/src/tzero_jarvis/hybrid_search.py +214 -0
- package/src/tzero_jarvis/latent_press.py +137 -0
- package/src/tzero_jarvis/memory_consolidation.py +212 -0
- package/src/tzero_jarvis/minhash_lsh.py +184 -0
- package/src/tzero_jarvis/orchestrator.py +335 -0
- package/src/tzero_jarvis/self_healing.py +308 -0
- package/src/tzero_jarvis/semantic_compressor.py +887 -0
- package/src/tzero_jarvis/swarm.py +261 -0
- package/src/tzero_jarvis/swarm_bft.py +144 -0
- package/src/tzero_jarvis/workspace_orchestrator.py +1167 -0
- package/src/tzero_jarvis/zero_key_router.py +173 -0
- package/src/tzero_mcp.py +4858 -0
- package/src/tzero_neural_cortex.py +1425 -0
- package/src/tzero_packer.py +370 -0
- package/src/tzero_patch.py +624 -0
- package/src/tzero_pc_fixer.py +12657 -0
- package/src/tzero_pipe.py +95 -0
- package/src/tzero_polyglot.py +422 -0
- package/src/tzero_reachability.py +272 -0
- package/src/tzero_replay.py +141 -0
- package/src/tzero_security.py +570 -0
- package/src/tzero_snapshot.py +258 -0
- package/src/tzero_stubs.py +313 -0
- package/src/tzero_superdesign.py +774 -0
- package/src/tzero_sync.py +241 -0
- package/src/tzero_task_benchmark.py +363 -0
- package/src/tzero_tui.py +244 -0
- package/src/tzero_v3.py +9435 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""T-Zero Polyglot Ecosystem Doctor: Node, Rust, Go, Docker, & CI Workflows.
|
|
3
|
+
|
|
4
|
+
Performs forensic diagnostics across polyglot project architectures:
|
|
5
|
+
- Node.js / TypeScript: package.json validation, tsconfig.json integrity, lockfile conflicts, node_modules bloat.
|
|
6
|
+
- Rust / Cargo: Cargo.toml linting, target/ directory bloat, missing edition flags, cargo lock sync.
|
|
7
|
+
- Go Toolchain: go.mod & go.sum validation, GOPATH resolution, dangling vendor folders.
|
|
8
|
+
- Docker & Containers: Dockerfile linting (unpinned base images, root user execution, dangling cache).
|
|
9
|
+
- GitHub Actions CI/CD: .github/workflows YAML syntax, unpinned actions SHA, hardcoded secret leaks in workflows.
|
|
10
|
+
|
|
11
|
+
Author: Toprak Ahmet Aydogmus (Siber Akademi)
|
|
12
|
+
Version: 10.4.0
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
21
|
+
|
|
22
|
+
from .models import (
|
|
23
|
+
DiagnosticCategory,
|
|
24
|
+
DiagnosticIssue,
|
|
25
|
+
HealAction,
|
|
26
|
+
SeverityLevel,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PolyglotDoctor:
|
|
31
|
+
"""Specialized doctor for polyglot language toolchains and container configurations."""
|
|
32
|
+
|
|
33
|
+
IGNORED_DIRS = {
|
|
34
|
+
".git", ".svn", ".hg", "__pycache__", ".pytest_cache", ".mypy_cache",
|
|
35
|
+
"node_modules", ".venv", "venv", "env", "build", "dist", ".idea", ".vscode"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# -------------------------------------------------------------------------
|
|
39
|
+
# 1. NODE.JS & TYPESCRIPT DOCTOR
|
|
40
|
+
# -------------------------------------------------------------------------
|
|
41
|
+
@classmethod
|
|
42
|
+
def check_nodejs_project(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
43
|
+
"""Inspect package.json, package-lock.json, and tsconfig.json for common defects."""
|
|
44
|
+
issues: List[DiagnosticIssue] = []
|
|
45
|
+
root = Path(workspace_dir).resolve()
|
|
46
|
+
pkg_json = root / "package.json"
|
|
47
|
+
|
|
48
|
+
if not pkg_json.is_file():
|
|
49
|
+
return issues
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
with open(pkg_json, "r", encoding="utf-8") as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
except json.JSONDecodeError as jde:
|
|
55
|
+
issues.append(DiagnosticIssue(
|
|
56
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
57
|
+
severity=SeverityLevel.CRITICAL.value,
|
|
58
|
+
file_path=str(pkg_json),
|
|
59
|
+
line=jde.lineno,
|
|
60
|
+
column=jde.colno,
|
|
61
|
+
error_code="TZ-NODE-INVALID-PACKAGE-JSON",
|
|
62
|
+
message=f"package.json is corrupted or not valid JSON: {jde.msg}",
|
|
63
|
+
suggested_fix="Fix JSON syntax error in package.json",
|
|
64
|
+
auto_fixable=False
|
|
65
|
+
))
|
|
66
|
+
return issues
|
|
67
|
+
|
|
68
|
+
# Check required fields
|
|
69
|
+
if not data.get("name"):
|
|
70
|
+
issues.append(DiagnosticIssue(
|
|
71
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
72
|
+
severity=SeverityLevel.WARNING.value,
|
|
73
|
+
file_path=str(pkg_json),
|
|
74
|
+
line=1,
|
|
75
|
+
column=0,
|
|
76
|
+
error_code="TZ-NODE-MISSING-NAME",
|
|
77
|
+
message="package.json is missing top-level 'name' attribute",
|
|
78
|
+
suggested_fix="Add a unique 'name' attribute to package.json",
|
|
79
|
+
auto_fixable=False
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
# Check conflicting lockfiles (e.g. package-lock.json AND yarn.lock AND pnpm-lock.yaml)
|
|
83
|
+
lock_files = []
|
|
84
|
+
if (root / "package-lock.json").is_file():
|
|
85
|
+
lock_files.append("package-lock.json (npm)")
|
|
86
|
+
if (root / "yarn.lock").is_file():
|
|
87
|
+
lock_files.append("yarn.lock (yarn)")
|
|
88
|
+
if (root / "pnpm-lock.yaml").is_file():
|
|
89
|
+
lock_files.append("pnpm-lock.yaml (pnpm)")
|
|
90
|
+
if (root / "bun.lockb").is_file():
|
|
91
|
+
lock_files.append("bun.lockb (bun)")
|
|
92
|
+
|
|
93
|
+
if len(lock_files) > 1:
|
|
94
|
+
issues.append(DiagnosticIssue(
|
|
95
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
96
|
+
severity=SeverityLevel.WARNING.value,
|
|
97
|
+
file_path=str(pkg_json),
|
|
98
|
+
line=None,
|
|
99
|
+
column=None,
|
|
100
|
+
error_code="TZ-NODE-CONFLICTING-LOCKFILES",
|
|
101
|
+
message=f"Multiple conflicting package lockfiles detected: {', '.join(lock_files)}",
|
|
102
|
+
suggested_fix="Standardize on a single package manager (npm, yarn, pnpm, or bun) and remove unused lockfiles",
|
|
103
|
+
auto_fixable=False
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
# Check node_modules existence vs package.json
|
|
107
|
+
has_deps = bool(data.get("dependencies") or data.get("devDependencies"))
|
|
108
|
+
if has_deps and not (root / "node_modules").is_dir():
|
|
109
|
+
issues.append(DiagnosticIssue(
|
|
110
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
111
|
+
severity=SeverityLevel.ERROR.value,
|
|
112
|
+
file_path=str(pkg_json),
|
|
113
|
+
line=None,
|
|
114
|
+
column=None,
|
|
115
|
+
error_code="TZ-NODE-MODULES-MISSING",
|
|
116
|
+
message="package.json defines dependencies but 'node_modules' directory is missing",
|
|
117
|
+
suggested_fix="Run 'npm install' or 'pnpm install' to install dependencies",
|
|
118
|
+
auto_fixable=False
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
# Check tsconfig.json if TypeScript project
|
|
122
|
+
has_ts = any("typescript" in str(k) for k in list((data.get("devDependencies") or {}).keys()) + list((data.get("dependencies") or {}).keys()))
|
|
123
|
+
tsconfig = root / "tsconfig.json"
|
|
124
|
+
if has_ts and not tsconfig.is_file():
|
|
125
|
+
issues.append(DiagnosticIssue(
|
|
126
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
127
|
+
severity=SeverityLevel.WARNING.value,
|
|
128
|
+
file_path=str(root),
|
|
129
|
+
line=None,
|
|
130
|
+
column=None,
|
|
131
|
+
error_code="TZ-TS-MISSING-TSCONFIG",
|
|
132
|
+
message="TypeScript dependency declared but 'tsconfig.json' is missing in workspace root",
|
|
133
|
+
suggested_fix="Initialize TypeScript config via 'npx tsc --init'",
|
|
134
|
+
auto_fixable=False
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
return issues
|
|
138
|
+
|
|
139
|
+
# -------------------------------------------------------------------------
|
|
140
|
+
# 2. RUST & CARGO DOCTOR
|
|
141
|
+
# -------------------------------------------------------------------------
|
|
142
|
+
@classmethod
|
|
143
|
+
def check_rust_project(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
144
|
+
"""Inspect Cargo.toml and target directory for Rust projects."""
|
|
145
|
+
issues: List[DiagnosticIssue] = []
|
|
146
|
+
root = Path(workspace_dir).resolve()
|
|
147
|
+
cargo_toml = root / "Cargo.toml"
|
|
148
|
+
|
|
149
|
+
if not cargo_toml.is_file():
|
|
150
|
+
return issues
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with open(cargo_toml, "r", encoding="utf-8") as f:
|
|
154
|
+
content = f.read()
|
|
155
|
+
|
|
156
|
+
# Check [package] table
|
|
157
|
+
if "[package]" not in content:
|
|
158
|
+
issues.append(DiagnosticIssue(
|
|
159
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
160
|
+
severity=SeverityLevel.ERROR.value,
|
|
161
|
+
file_path=str(cargo_toml),
|
|
162
|
+
line=1,
|
|
163
|
+
column=0,
|
|
164
|
+
error_code="TZ-RUST-MISSING-PACKAGE",
|
|
165
|
+
message="Cargo.toml is missing '[package]' manifest header",
|
|
166
|
+
suggested_fix="Add '[package]' section with name, version, and edition",
|
|
167
|
+
auto_fixable=False
|
|
168
|
+
))
|
|
169
|
+
|
|
170
|
+
# Check edition flag (edition = "2021")
|
|
171
|
+
if "edition =" not in content:
|
|
172
|
+
issues.append(DiagnosticIssue(
|
|
173
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
174
|
+
severity=SeverityLevel.WARNING.value,
|
|
175
|
+
file_path=str(cargo_toml),
|
|
176
|
+
line=1,
|
|
177
|
+
column=0,
|
|
178
|
+
error_code="TZ-RUST-MISSING-EDITION",
|
|
179
|
+
message="Cargo.toml does not declare Rust edition (defaults to legacy 2015)",
|
|
180
|
+
suggested_fix="Specify modern edition: 'edition = \"2021\"'",
|
|
181
|
+
auto_fixable=True,
|
|
182
|
+
metadata={"action": "add_cargo_edition", "path": str(cargo_toml)}
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
# Check target/ directory size bloat (> 1GB)
|
|
186
|
+
target_dir = root / "target"
|
|
187
|
+
if target_dir.is_dir():
|
|
188
|
+
total_b = sum(f.stat().st_size for f in target_dir.glob("**/*") if f.is_file())
|
|
189
|
+
if total_b > 1024 * 1024 * 1024: # > 1GB
|
|
190
|
+
issues.append(DiagnosticIssue(
|
|
191
|
+
category=DiagnosticCategory.CACHE.value,
|
|
192
|
+
severity=SeverityLevel.INFO.value,
|
|
193
|
+
file_path=str(target_dir),
|
|
194
|
+
line=None,
|
|
195
|
+
column=None,
|
|
196
|
+
error_code="TZ-RUST-TARGET-BLOAT",
|
|
197
|
+
message=f"Rust build artifacts in 'target/' consume {round(total_b / (1024**3), 2)}GB",
|
|
198
|
+
suggested_fix="Run 'cargo clean' to reclaim disk space",
|
|
199
|
+
auto_fixable=False
|
|
200
|
+
))
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
return issues
|
|
205
|
+
|
|
206
|
+
# -------------------------------------------------------------------------
|
|
207
|
+
# 3. GO TOOLCHAIN DOCTOR
|
|
208
|
+
# -------------------------------------------------------------------------
|
|
209
|
+
@classmethod
|
|
210
|
+
def check_go_project(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
211
|
+
"""Inspect go.mod and go.sum files in Go workspaces."""
|
|
212
|
+
issues: List[DiagnosticIssue] = []
|
|
213
|
+
root = Path(workspace_dir).resolve()
|
|
214
|
+
go_mod = root / "go.mod"
|
|
215
|
+
|
|
216
|
+
if not go_mod.is_file():
|
|
217
|
+
return issues
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
with open(go_mod, "r", encoding="utf-8") as f:
|
|
221
|
+
content = f.read()
|
|
222
|
+
|
|
223
|
+
if not content.startswith("module ") and "\nmodule " not in content:
|
|
224
|
+
issues.append(DiagnosticIssue(
|
|
225
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
226
|
+
severity=SeverityLevel.ERROR.value,
|
|
227
|
+
file_path=str(go_mod),
|
|
228
|
+
line=1,
|
|
229
|
+
column=0,
|
|
230
|
+
error_code="TZ-GO-MISSING-MODULE",
|
|
231
|
+
message="go.mod file is missing required 'module <path>' declaration",
|
|
232
|
+
suggested_fix="Add 'module <github.com/org/repo>' at top of go.mod",
|
|
233
|
+
auto_fixable=False
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
if "go 1." not in content:
|
|
237
|
+
issues.append(DiagnosticIssue(
|
|
238
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
239
|
+
severity=SeverityLevel.WARNING.value,
|
|
240
|
+
file_path=str(go_mod),
|
|
241
|
+
line=1,
|
|
242
|
+
column=0,
|
|
243
|
+
error_code="TZ-GO-MISSING-VERSION",
|
|
244
|
+
message="go.mod does not specify Go language version directive (e.g. 'go 1.22')",
|
|
245
|
+
suggested_fix="Add 'go 1.22' directive to go.mod",
|
|
246
|
+
auto_fixable=False
|
|
247
|
+
))
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
return issues
|
|
252
|
+
|
|
253
|
+
# -------------------------------------------------------------------------
|
|
254
|
+
# 4. DOCKER & CONTAINER DOCTOR
|
|
255
|
+
# -------------------------------------------------------------------------
|
|
256
|
+
@classmethod
|
|
257
|
+
def check_dockerfiles(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
258
|
+
"""Inspect Dockerfile and compose manifests for security and performance smells."""
|
|
259
|
+
issues: List[DiagnosticIssue] = []
|
|
260
|
+
root = Path(workspace_dir).resolve()
|
|
261
|
+
|
|
262
|
+
dockerfiles = list(root.glob("**/Dockerfile*")) + list(root.glob("**/docker-compose*.yml"))
|
|
263
|
+
for df in dockerfiles:
|
|
264
|
+
if not df.is_file() or any(p in df.parts for p in cls.IGNORED_DIRS):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
with open(df, "r", encoding="utf-8", errors="replace") as f:
|
|
269
|
+
lines = f.readlines()
|
|
270
|
+
|
|
271
|
+
has_user = False
|
|
272
|
+
for idx, line in enumerate(lines, start=1):
|
|
273
|
+
stripped = line.strip()
|
|
274
|
+
|
|
275
|
+
# Unpinned :latest tag
|
|
276
|
+
if stripped.startswith("FROM ") and (":latest" in stripped or ":" not in stripped.split()[1]):
|
|
277
|
+
issues.append(DiagnosticIssue(
|
|
278
|
+
category=DiagnosticCategory.POLYGLOT.value,
|
|
279
|
+
severity=SeverityLevel.WARNING.value,
|
|
280
|
+
file_path=str(df),
|
|
281
|
+
line=idx,
|
|
282
|
+
column=0,
|
|
283
|
+
error_code="TZ-DOCKER-LATEST-TAG",
|
|
284
|
+
message=f"Unpinned Docker base image in '{stripped}' creates non-reproducible builds",
|
|
285
|
+
suggested_fix="Pin base image to specific semantic version tag or digest (e.g. python:3.12-slim)",
|
|
286
|
+
auto_fixable=False
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
if stripped.startswith("USER ") and "root" not in stripped:
|
|
290
|
+
has_user = True
|
|
291
|
+
|
|
292
|
+
# Check root user execution in Dockerfile
|
|
293
|
+
if df.name.startswith("Dockerfile") and not has_user and len(lines) > 5:
|
|
294
|
+
issues.append(DiagnosticIssue(
|
|
295
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
296
|
+
severity=SeverityLevel.WARNING.value,
|
|
297
|
+
file_path=str(df),
|
|
298
|
+
line=len(lines),
|
|
299
|
+
column=0,
|
|
300
|
+
error_code="TZ-DOCKER-ROOT-USER",
|
|
301
|
+
message=f"Dockerfile '{df.name}' runs as default 'root' user without non-root USER directive",
|
|
302
|
+
suggested_fix="Create and switch to a non-privileged user (e.g. 'USER appuser')",
|
|
303
|
+
auto_fixable=False
|
|
304
|
+
))
|
|
305
|
+
except Exception:
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
return issues
|
|
309
|
+
|
|
310
|
+
# -------------------------------------------------------------------------
|
|
311
|
+
# 5. GITHUB ACTIONS & CI WORKFLOW DOCTOR
|
|
312
|
+
# -------------------------------------------------------------------------
|
|
313
|
+
@classmethod
|
|
314
|
+
def check_ci_workflows(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
315
|
+
"""Inspect .github/workflows YAML manifests for syntax flaws and security issues."""
|
|
316
|
+
issues: List[DiagnosticIssue] = []
|
|
317
|
+
root = Path(workspace_dir).resolve()
|
|
318
|
+
workflows_dir = root / ".github" / "workflows"
|
|
319
|
+
|
|
320
|
+
if not workflows_dir.is_dir():
|
|
321
|
+
return issues
|
|
322
|
+
|
|
323
|
+
for wf in workflows_dir.glob("*.yml"):
|
|
324
|
+
try:
|
|
325
|
+
with open(wf, "r", encoding="utf-8", errors="replace") as f:
|
|
326
|
+
lines = f.readlines()
|
|
327
|
+
|
|
328
|
+
for idx, line in enumerate(lines, start=1):
|
|
329
|
+
stripped = line.strip()
|
|
330
|
+
|
|
331
|
+
# Unpinned action version (@master or @main)
|
|
332
|
+
if "uses:" in stripped and ("@master" in stripped or "@main" in stripped):
|
|
333
|
+
issues.append(DiagnosticIssue(
|
|
334
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
335
|
+
severity=SeverityLevel.WARNING.value,
|
|
336
|
+
file_path=str(wf),
|
|
337
|
+
line=idx,
|
|
338
|
+
column=0,
|
|
339
|
+
error_code="TZ-CI-UNPINNED-ACTION",
|
|
340
|
+
message=f"GitHub Action uses mutable branch reference in '{stripped}'",
|
|
341
|
+
suggested_fix="Pin action to full immutable commit SHA (e.g. actions/checkout@v4)",
|
|
342
|
+
auto_fixable=False
|
|
343
|
+
))
|
|
344
|
+
|
|
345
|
+
# Hardcoded secrets in workflow
|
|
346
|
+
if re.search(r"""(?i)(api[_-]?key|password|token)\s*:\s*['"][A-Za-z0-9_\-]{16,}['"]""", stripped):
|
|
347
|
+
issues.append(DiagnosticIssue(
|
|
348
|
+
category=DiagnosticCategory.SECURITY.value,
|
|
349
|
+
severity=SeverityLevel.CRITICAL.value,
|
|
350
|
+
file_path=str(wf),
|
|
351
|
+
line=idx,
|
|
352
|
+
column=0,
|
|
353
|
+
error_code="TZ-CI-HARDCODED-SECRET",
|
|
354
|
+
message="Hardcoded secret or credential token in GitHub Actions workflow file",
|
|
355
|
+
suggested_fix="Move secret to repository settings and reference via ${{ secrets.NAME }}",
|
|
356
|
+
auto_fixable=False
|
|
357
|
+
))
|
|
358
|
+
except Exception:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
return issues
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def diagnose_workspace(cls, workspace_dir: str = ".") -> List[DiagnosticIssue]:
|
|
365
|
+
"""Run all polyglot ecosystem diagnostic checks."""
|
|
366
|
+
issues: List[DiagnosticIssue] = []
|
|
367
|
+
issues.extend(cls.check_nodejs_project(workspace_dir))
|
|
368
|
+
issues.extend(cls.check_rust_project(workspace_dir))
|
|
369
|
+
issues.extend(cls.check_go_project(workspace_dir))
|
|
370
|
+
issues.extend(cls.check_dockerfiles(workspace_dir))
|
|
371
|
+
issues.extend(cls.check_ci_workflows(workspace_dir))
|
|
372
|
+
return issues
|