@trac3er/oh-my-god 2.0.0 → 2.0.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/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Package manifest detector — scans project directories for dependency manifests.
|
|
2
|
+
|
|
3
|
+
Supports: package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, pyproject.toml.
|
|
4
|
+
Returns a unified DependencyList with all discovered packages.
|
|
5
|
+
|
|
6
|
+
stdlib only — no external dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ─── Data Classes ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ManifestFile:
|
|
25
|
+
"""Represents a discovered manifest file."""
|
|
26
|
+
|
|
27
|
+
path: str
|
|
28
|
+
format: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Package:
|
|
33
|
+
"""Represents a single dependency package."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
version: str
|
|
37
|
+
dev: bool
|
|
38
|
+
source_manifest: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DependencyList:
|
|
43
|
+
"""Unified result of manifest detection."""
|
|
44
|
+
|
|
45
|
+
manifests: list[ManifestFile] = field(default_factory=list)
|
|
46
|
+
packages: list[Package] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ─── Known manifest filenames ────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
_MANIFEST_FILES: list[tuple[str, str]] = [
|
|
52
|
+
("package.json", "package.json"),
|
|
53
|
+
("requirements.txt", "requirements.txt"),
|
|
54
|
+
("Cargo.toml", "Cargo.toml"),
|
|
55
|
+
("go.mod", "go.mod"),
|
|
56
|
+
("Gemfile", "Gemfile"),
|
|
57
|
+
("pyproject.toml", "pyproject.toml"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ─── Parsers ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_package_json(file_path: str) -> list[Package]:
|
|
65
|
+
"""Parse package.json for dependencies and devDependencies."""
|
|
66
|
+
packages: list[Package] = []
|
|
67
|
+
try:
|
|
68
|
+
with open(file_path, "r") as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
except (json.JSONDecodeError, OSError):
|
|
71
|
+
return packages
|
|
72
|
+
|
|
73
|
+
deps = data.get("dependencies", {})
|
|
74
|
+
if isinstance(deps, dict):
|
|
75
|
+
for name, version in deps.items():
|
|
76
|
+
packages.append(Package(
|
|
77
|
+
name=name,
|
|
78
|
+
version=str(version),
|
|
79
|
+
dev=False,
|
|
80
|
+
source_manifest=file_path,
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
dev_deps = data.get("devDependencies", {})
|
|
84
|
+
if isinstance(dev_deps, dict):
|
|
85
|
+
for name, version in dev_deps.items():
|
|
86
|
+
packages.append(Package(
|
|
87
|
+
name=name,
|
|
88
|
+
version=str(version),
|
|
89
|
+
dev=True,
|
|
90
|
+
source_manifest=file_path,
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
return packages
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_requirements_txt(file_path: str) -> list[Package]:
|
|
97
|
+
"""Parse requirements.txt (name==version, name>=version, or bare name)."""
|
|
98
|
+
packages: list[Package] = []
|
|
99
|
+
try:
|
|
100
|
+
with open(file_path, "r") as f:
|
|
101
|
+
lines = f.readlines()
|
|
102
|
+
except OSError:
|
|
103
|
+
return packages
|
|
104
|
+
|
|
105
|
+
# Matches: name==version, name>=version, name<=version, name~=version, name!=version
|
|
106
|
+
req_re = re.compile(r"^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(?:[><=!~]+\s*(.+))?$")
|
|
107
|
+
|
|
108
|
+
for line in lines:
|
|
109
|
+
line = line.strip()
|
|
110
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
111
|
+
continue
|
|
112
|
+
m = req_re.match(line)
|
|
113
|
+
if m:
|
|
114
|
+
name = m.group(1)
|
|
115
|
+
version = m.group(2) or ""
|
|
116
|
+
packages.append(Package(
|
|
117
|
+
name=name,
|
|
118
|
+
version=version.strip(),
|
|
119
|
+
dev=False,
|
|
120
|
+
source_manifest=file_path,
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
return packages
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_cargo_toml(file_path: str) -> list[Package]:
|
|
127
|
+
"""Parse Cargo.toml [dependencies] and [dev-dependencies] via regex."""
|
|
128
|
+
packages: list[Package] = []
|
|
129
|
+
try:
|
|
130
|
+
with open(file_path, "r") as f:
|
|
131
|
+
content = f.read()
|
|
132
|
+
except OSError:
|
|
133
|
+
return packages
|
|
134
|
+
|
|
135
|
+
# Split into sections by [header]
|
|
136
|
+
section_re = re.compile(r"^\[([^\]]+)\]\s*$", re.MULTILINE)
|
|
137
|
+
sections: dict[str, str] = {}
|
|
138
|
+
positions = [(m.group(1).strip(), m.end()) for m in section_re.finditer(content)]
|
|
139
|
+
|
|
140
|
+
for i, (name, start) in enumerate(positions):
|
|
141
|
+
end = positions[i + 1][1] if i + 1 < len(positions) else len(content)
|
|
142
|
+
# Adjust end to be the start of the next section header line
|
|
143
|
+
if i + 1 < len(positions):
|
|
144
|
+
# Find the start of the next header line
|
|
145
|
+
next_header_start = content.rfind("[", start, end)
|
|
146
|
+
if next_header_start >= 0:
|
|
147
|
+
end = next_header_start
|
|
148
|
+
sections[name] = content[start:end]
|
|
149
|
+
|
|
150
|
+
# Parse dependency lines: name = "version" or name = { version = "ver", ... }
|
|
151
|
+
dep_line_re = re.compile(
|
|
152
|
+
r'^([A-Za-z0-9_][A-Za-z0-9_-]*)\s*=\s*(?:"([^"]+)"|'
|
|
153
|
+
r'\{[^}]*version\s*=\s*"([^"]+)"[^}]*\})',
|
|
154
|
+
re.MULTILINE,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
for section_name, section_body in sections.items():
|
|
158
|
+
is_dev = "dev-dependencies" in section_name.lower()
|
|
159
|
+
is_dep = "dependencies" in section_name.lower()
|
|
160
|
+
if not is_dep:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
for m in dep_line_re.finditer(section_body):
|
|
164
|
+
name = m.group(1)
|
|
165
|
+
version = m.group(2) or m.group(3) or ""
|
|
166
|
+
packages.append(Package(
|
|
167
|
+
name=name,
|
|
168
|
+
version=version,
|
|
169
|
+
dev=is_dev,
|
|
170
|
+
source_manifest=file_path,
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
return packages
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _parse_go_mod(file_path: str) -> list[Package]:
|
|
177
|
+
"""Parse go.mod require block."""
|
|
178
|
+
packages: list[Package] = []
|
|
179
|
+
try:
|
|
180
|
+
with open(file_path, "r") as f:
|
|
181
|
+
content = f.read()
|
|
182
|
+
except OSError:
|
|
183
|
+
return packages
|
|
184
|
+
|
|
185
|
+
# Match require ( ... ) block
|
|
186
|
+
require_block_re = re.compile(r"require\s*\(\s*(.*?)\s*\)", re.DOTALL)
|
|
187
|
+
for block_m in require_block_re.finditer(content):
|
|
188
|
+
block = block_m.group(1)
|
|
189
|
+
for line in block.strip().splitlines():
|
|
190
|
+
line = line.strip()
|
|
191
|
+
if not line or line.startswith("//"):
|
|
192
|
+
continue
|
|
193
|
+
parts = line.split()
|
|
194
|
+
if len(parts) >= 2:
|
|
195
|
+
packages.append(Package(
|
|
196
|
+
name=parts[0],
|
|
197
|
+
version=parts[1],
|
|
198
|
+
dev=False,
|
|
199
|
+
source_manifest=file_path,
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
# Also match single-line require: require github.com/foo/bar v1.0.0
|
|
203
|
+
single_re = re.compile(r"^require\s+(\S+)\s+(\S+)", re.MULTILINE)
|
|
204
|
+
for m in single_re.finditer(content):
|
|
205
|
+
packages.append(Package(
|
|
206
|
+
name=m.group(1),
|
|
207
|
+
version=m.group(2),
|
|
208
|
+
dev=False,
|
|
209
|
+
source_manifest=file_path,
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
return packages
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _parse_gemfile(file_path: str) -> list[Package]:
|
|
216
|
+
"""Parse Gemfile gem declarations."""
|
|
217
|
+
packages: list[Package] = []
|
|
218
|
+
try:
|
|
219
|
+
with open(file_path, "r") as f:
|
|
220
|
+
lines = f.readlines()
|
|
221
|
+
except OSError:
|
|
222
|
+
return packages
|
|
223
|
+
|
|
224
|
+
# gem "name", "version" or gem 'name', 'version' or gem "name"
|
|
225
|
+
gem_re = re.compile(
|
|
226
|
+
r"""gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?"""
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
for line in lines:
|
|
230
|
+
line = line.strip()
|
|
231
|
+
if not line or line.startswith("#"):
|
|
232
|
+
continue
|
|
233
|
+
m = gem_re.search(line)
|
|
234
|
+
if m:
|
|
235
|
+
name = m.group(1)
|
|
236
|
+
version = m.group(2) or ""
|
|
237
|
+
packages.append(Package(
|
|
238
|
+
name=name,
|
|
239
|
+
version=version,
|
|
240
|
+
dev=False,
|
|
241
|
+
source_manifest=file_path,
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
return packages
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _parse_pyproject_toml(file_path: str) -> list[Package]:
|
|
248
|
+
"""Parse pyproject.toml for [project.dependencies] and optional-dependencies."""
|
|
249
|
+
packages: list[Package] = []
|
|
250
|
+
try:
|
|
251
|
+
with open(file_path, "r") as f:
|
|
252
|
+
content = f.read()
|
|
253
|
+
except OSError:
|
|
254
|
+
return packages
|
|
255
|
+
|
|
256
|
+
# Extract PEP 508 name and version from dependency string like "fastapi>=0.100.0"
|
|
257
|
+
pep508_re = re.compile(r"^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(.*)$")
|
|
258
|
+
|
|
259
|
+
def _extract_deps_from_array(text: str, is_dev: bool) -> list[Package]:
|
|
260
|
+
"""Extract packages from a TOML array literal."""
|
|
261
|
+
result: list[Package] = []
|
|
262
|
+
# Match quoted strings inside brackets
|
|
263
|
+
str_re = re.compile(r'["\']([^"\']+)["\']')
|
|
264
|
+
for m in str_re.finditer(text):
|
|
265
|
+
dep_str = m.group(1).strip()
|
|
266
|
+
pm = pep508_re.match(dep_str)
|
|
267
|
+
if pm:
|
|
268
|
+
name = pm.group(1)
|
|
269
|
+
version = pm.group(2).strip()
|
|
270
|
+
result.append(Package(
|
|
271
|
+
name=name,
|
|
272
|
+
version=version,
|
|
273
|
+
dev=is_dev,
|
|
274
|
+
source_manifest=file_path,
|
|
275
|
+
))
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
# Find dependencies = [...] under [project]
|
|
279
|
+
# Simple approach: find "dependencies = [" after [project] section
|
|
280
|
+
project_deps_re = re.compile(
|
|
281
|
+
r"\[project\].*?^dependencies\s*=\s*\[(.*?)\]",
|
|
282
|
+
re.MULTILINE | re.DOTALL,
|
|
283
|
+
)
|
|
284
|
+
pm = project_deps_re.search(content)
|
|
285
|
+
if pm:
|
|
286
|
+
packages.extend(_extract_deps_from_array(pm.group(1), is_dev=False))
|
|
287
|
+
|
|
288
|
+
# Find [project.optional-dependencies] sections
|
|
289
|
+
opt_deps_re = re.compile(
|
|
290
|
+
r"\[project\.optional-dependencies\].*?$",
|
|
291
|
+
re.MULTILINE,
|
|
292
|
+
)
|
|
293
|
+
om = opt_deps_re.search(content)
|
|
294
|
+
if om:
|
|
295
|
+
# Extract the section body until next [section] or end of file
|
|
296
|
+
start = om.end()
|
|
297
|
+
next_section = re.search(r"^\[", content[start:], re.MULTILINE)
|
|
298
|
+
end = start + next_section.start() if next_section else len(content)
|
|
299
|
+
section_body = content[start:end]
|
|
300
|
+
|
|
301
|
+
# Find key = [...] arrays (e.g., dev = ["pytest>=7.4.0"])
|
|
302
|
+
array_re = re.compile(r"^\w+\s*=\s*\[(.*?)\]", re.MULTILINE | re.DOTALL)
|
|
303
|
+
for am in array_re.finditer(section_body):
|
|
304
|
+
packages.extend(_extract_deps_from_array(am.group(1), is_dev=True))
|
|
305
|
+
|
|
306
|
+
# Also support [tool.poetry.dependencies] pattern
|
|
307
|
+
poetry_deps_re = re.compile(
|
|
308
|
+
r"\[tool\.poetry\.dependencies\](.*?)(?=\[|$)",
|
|
309
|
+
re.DOTALL,
|
|
310
|
+
)
|
|
311
|
+
pm = poetry_deps_re.search(content)
|
|
312
|
+
if pm:
|
|
313
|
+
section_body = pm.group(1)
|
|
314
|
+
# Poetry deps: name = "version" or name = {version = "ver"}
|
|
315
|
+
dep_line_re = re.compile(
|
|
316
|
+
r'^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*=\s*(?:"([^"]+)"|'
|
|
317
|
+
r"\{[^}]*version\s*=\s*\"([^\"]+)\"[^}]*\})",
|
|
318
|
+
re.MULTILINE,
|
|
319
|
+
)
|
|
320
|
+
for dm in dep_line_re.finditer(section_body):
|
|
321
|
+
name = dm.group(1)
|
|
322
|
+
if name.lower() == "python":
|
|
323
|
+
continue # Skip python version constraint
|
|
324
|
+
version = dm.group(2) or dm.group(3) or ""
|
|
325
|
+
packages.append(Package(
|
|
326
|
+
name=name,
|
|
327
|
+
version=version,
|
|
328
|
+
dev=False,
|
|
329
|
+
source_manifest=file_path,
|
|
330
|
+
))
|
|
331
|
+
|
|
332
|
+
# [tool.poetry.dev-dependencies]
|
|
333
|
+
poetry_dev_re = re.compile(
|
|
334
|
+
r"\[tool\.poetry\.dev-dependencies\](.*?)(?=\[|$)",
|
|
335
|
+
re.DOTALL,
|
|
336
|
+
)
|
|
337
|
+
pm = poetry_dev_re.search(content)
|
|
338
|
+
if pm:
|
|
339
|
+
section_body = pm.group(1)
|
|
340
|
+
dep_line_re = re.compile(
|
|
341
|
+
r'^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*=\s*(?:"([^"]+)"|'
|
|
342
|
+
r"\{[^}]*version\s*=\s*\"([^\"]+)\"[^}]*\})",
|
|
343
|
+
re.MULTILINE,
|
|
344
|
+
)
|
|
345
|
+
for dm in dep_line_re.finditer(section_body):
|
|
346
|
+
name = dm.group(1)
|
|
347
|
+
version = dm.group(2) or dm.group(3) or ""
|
|
348
|
+
packages.append(Package(
|
|
349
|
+
name=name,
|
|
350
|
+
version=version,
|
|
351
|
+
dev=True,
|
|
352
|
+
source_manifest=file_path,
|
|
353
|
+
))
|
|
354
|
+
|
|
355
|
+
return packages
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ─── Parser dispatch ─────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
_PARSERS: dict[str, Any] = {
|
|
361
|
+
"package.json": _parse_package_json,
|
|
362
|
+
"requirements.txt": _parse_requirements_txt,
|
|
363
|
+
"Cargo.toml": _parse_cargo_toml,
|
|
364
|
+
"go.mod": _parse_go_mod,
|
|
365
|
+
"Gemfile": _parse_gemfile,
|
|
366
|
+
"pyproject.toml": _parse_pyproject_toml,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ─── Public API ──────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _dep_health_enabled() -> bool:
|
|
374
|
+
env_val = os.environ.get("OMG_DEP_HEALTH_ENABLED", "").lower()
|
|
375
|
+
if env_val in ("1", "true", "yes"):
|
|
376
|
+
return True
|
|
377
|
+
if env_val in ("0", "false", "no"):
|
|
378
|
+
return False
|
|
379
|
+
try:
|
|
380
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
381
|
+
from hooks._common import get_feature_flag
|
|
382
|
+
return get_feature_flag("DEP_HEALTH", default=False)
|
|
383
|
+
except Exception:
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def detect_manifests(project_dir: str) -> DependencyList:
|
|
388
|
+
"""Scan project_dir for manifest files and return a unified DependencyList.
|
|
389
|
+
|
|
390
|
+
Supports: package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, pyproject.toml.
|
|
391
|
+
Gracefully handles missing/malformed files (skips, no crash).
|
|
392
|
+
"""
|
|
393
|
+
if not _dep_health_enabled():
|
|
394
|
+
return DependencyList()
|
|
395
|
+
|
|
396
|
+
result = DependencyList()
|
|
397
|
+
project_path = Path(project_dir)
|
|
398
|
+
|
|
399
|
+
if not project_path.is_dir():
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
for filename, fmt in _MANIFEST_FILES:
|
|
403
|
+
file_path = project_path / filename
|
|
404
|
+
if not file_path.is_file():
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
parser = _PARSERS.get(fmt)
|
|
408
|
+
if not parser:
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
packages = parser(str(file_path))
|
|
413
|
+
if packages:
|
|
414
|
+
result.manifests.append(ManifestFile(
|
|
415
|
+
path=str(file_path),
|
|
416
|
+
format=fmt,
|
|
417
|
+
))
|
|
418
|
+
result.packages.extend(packages)
|
|
419
|
+
except Exception:
|
|
420
|
+
# Graceful degradation: skip malformed files
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
return result
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_CRITICAL_PATTERN = re.compile(r"\bcritical\b", re.IGNORECASE)
|
|
8
|
+
_HIGH_PATTERN = re.compile(r"\bhigh\b", re.IGNORECASE)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def analyze_reachability(cve_result: dict[str, Any], project_dir: str) -> dict[str, Any]:
|
|
12
|
+
package = str(cve_result.get("package", "")).strip()
|
|
13
|
+
cve_id = str(cve_result.get("id", "")).strip()
|
|
14
|
+
summary = str(cve_result.get("summary", "")).strip()
|
|
15
|
+
fixed_version = str(cve_result.get("fixed_version", "")).strip()
|
|
16
|
+
|
|
17
|
+
imported_files: list[str] = []
|
|
18
|
+
usage_found = False
|
|
19
|
+
|
|
20
|
+
for py_file in Path(project_dir).rglob("*.py"):
|
|
21
|
+
try:
|
|
22
|
+
source = py_file.read_text(encoding="utf-8")
|
|
23
|
+
except Exception:
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
imported, used = _inspect_python_file(source, package)
|
|
27
|
+
if imported:
|
|
28
|
+
imported_files.append(py_file.relative_to(project_dir).as_posix())
|
|
29
|
+
if used:
|
|
30
|
+
usage_found = True
|
|
31
|
+
|
|
32
|
+
reachability = _classify_reachability(imported_files, usage_found)
|
|
33
|
+
risk_level = _classify_risk(reachability, summary)
|
|
34
|
+
recommendation = _build_recommendation(package, fixed_version, reachability)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"package": package,
|
|
38
|
+
"cve_id": cve_id,
|
|
39
|
+
"reachability": reachability,
|
|
40
|
+
"import_locations": sorted(imported_files),
|
|
41
|
+
"risk_level": risk_level,
|
|
42
|
+
"recommendation": recommendation,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _inspect_python_file(source: str, package: str) -> tuple[bool, bool]:
|
|
47
|
+
imported, module_aliases, imported_symbols = _grep_like_import_scan(source, package)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
tree = ast.parse(source)
|
|
51
|
+
except SyntaxError:
|
|
52
|
+
return imported, False
|
|
53
|
+
|
|
54
|
+
ast_imported, ast_module_aliases, ast_imported_symbols = _ast_import_scan(tree, package)
|
|
55
|
+
imported = imported or ast_imported
|
|
56
|
+
module_aliases.update(ast_module_aliases)
|
|
57
|
+
imported_symbols.update(ast_imported_symbols)
|
|
58
|
+
|
|
59
|
+
used = _ast_usage_scan(tree, module_aliases, imported_symbols)
|
|
60
|
+
return imported, used
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _grep_like_import_scan(source: str, package: str) -> tuple[bool, set[str], set[str]]:
|
|
64
|
+
imported = False
|
|
65
|
+
module_aliases: set[str] = set()
|
|
66
|
+
imported_symbols: set[str] = set()
|
|
67
|
+
|
|
68
|
+
if not package:
|
|
69
|
+
return imported, module_aliases, imported_symbols
|
|
70
|
+
|
|
71
|
+
pkg = re.escape(package)
|
|
72
|
+
import_re = re.compile(rf"^\s*import\s+{pkg}(?:\.|\s|,|$)", re.MULTILINE)
|
|
73
|
+
from_re = re.compile(rf"^\s*from\s+{pkg}(?:\.|\s)", re.MULTILINE)
|
|
74
|
+
alias_re = re.compile(rf"^\s*import\s+{pkg}\s+as\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE)
|
|
75
|
+
from_names_re = re.compile(rf"^\s*from\s+{pkg}(?:\.[A-Za-z0-9_\.]+)?\s+import\s+(.+)$", re.MULTILINE)
|
|
76
|
+
|
|
77
|
+
if import_re.search(source) or from_re.search(source):
|
|
78
|
+
imported = True
|
|
79
|
+
|
|
80
|
+
module_aliases.add(package.split(".")[0])
|
|
81
|
+
for match in alias_re.findall(source):
|
|
82
|
+
module_aliases.add(match)
|
|
83
|
+
|
|
84
|
+
for line in from_names_re.findall(source):
|
|
85
|
+
for item in line.split(","):
|
|
86
|
+
part = item.strip()
|
|
87
|
+
if not part:
|
|
88
|
+
continue
|
|
89
|
+
if " as " in part:
|
|
90
|
+
imported_symbols.add(part.rsplit(" as ", 1)[1].strip())
|
|
91
|
+
else:
|
|
92
|
+
imported_symbols.add(part)
|
|
93
|
+
|
|
94
|
+
return imported, module_aliases, imported_symbols
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ast_import_scan(tree: ast.AST, package: str) -> tuple[bool, set[str], set[str]]:
|
|
98
|
+
imported = False
|
|
99
|
+
module_aliases: set[str] = set()
|
|
100
|
+
imported_symbols: set[str] = set()
|
|
101
|
+
|
|
102
|
+
if not package:
|
|
103
|
+
return imported, module_aliases, imported_symbols
|
|
104
|
+
|
|
105
|
+
root = package.split(".")[0]
|
|
106
|
+
|
|
107
|
+
for node in ast.walk(tree):
|
|
108
|
+
if isinstance(node, ast.Import):
|
|
109
|
+
for alias in node.names:
|
|
110
|
+
if alias.name == package or alias.name.startswith(f"{package}."):
|
|
111
|
+
imported = True
|
|
112
|
+
module_aliases.add(alias.asname or alias.name.split(".")[0])
|
|
113
|
+
elif alias.name.split(".")[0] == root:
|
|
114
|
+
imported = True
|
|
115
|
+
module_aliases.add(alias.asname or alias.name.split(".")[0])
|
|
116
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
117
|
+
if node.module == package or node.module.startswith(f"{package}."):
|
|
118
|
+
imported = True
|
|
119
|
+
for alias in node.names:
|
|
120
|
+
imported_symbols.add(alias.asname or alias.name)
|
|
121
|
+
elif node.module.split(".")[0] == root:
|
|
122
|
+
imported = True
|
|
123
|
+
for alias in node.names:
|
|
124
|
+
imported_symbols.add(alias.asname or alias.name)
|
|
125
|
+
|
|
126
|
+
return imported, module_aliases, imported_symbols
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _ast_usage_scan(tree: ast.AST, module_aliases: set[str], imported_symbols: set[str]) -> bool:
|
|
130
|
+
for node in ast.walk(tree):
|
|
131
|
+
if not isinstance(node, ast.Call):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
func = node.func
|
|
135
|
+
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
136
|
+
if func.value.id in module_aliases or func.value.id in imported_symbols:
|
|
137
|
+
return True
|
|
138
|
+
elif isinstance(func, ast.Name) and func.id in imported_symbols:
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _classify_reachability(imported_files: list[str], usage_found: bool) -> str:
|
|
145
|
+
if usage_found:
|
|
146
|
+
return "REACHABLE"
|
|
147
|
+
if imported_files:
|
|
148
|
+
return "POTENTIALLY_REACHABLE"
|
|
149
|
+
return "UNREACHABLE"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _classify_risk(reachability: str, summary: str) -> str:
|
|
153
|
+
if reachability == "UNREACHABLE":
|
|
154
|
+
return "LOW"
|
|
155
|
+
if reachability == "POTENTIALLY_REACHABLE":
|
|
156
|
+
return "MEDIUM"
|
|
157
|
+
if _CRITICAL_PATTERN.search(summary):
|
|
158
|
+
return "CRITICAL"
|
|
159
|
+
if _HIGH_PATTERN.search(summary):
|
|
160
|
+
return "HIGH"
|
|
161
|
+
return "HIGH"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _build_recommendation(package: str, fixed_version: str, reachability: str) -> str:
|
|
165
|
+
if reachability == "UNREACHABLE":
|
|
166
|
+
return "No action needed (unreachable)"
|
|
167
|
+
if fixed_version:
|
|
168
|
+
return f"Upgrade {package} to {fixed_version}"
|
|
169
|
+
return f"Upgrade {package} to a fixed version"
|
|
File without changes
|