mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
345 lines
11 KiB
Python
345 lines
11 KiB
Python
"""Dependency discovery — detect Java, Ghidra, PyGhidra with status and remediation.
|
|
|
|
Precedence order:
|
|
1. Environment variables (JAVA_HOME, GHIDRA_INSTALL_DIR)
|
|
2. Common installation paths
|
|
3. PATH-based discovery
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Dependency:
|
|
"""A discovered dependency with its status and remediation hint.
|
|
|
|
Attributes:
|
|
name: Component name ("java", "ghidra", "pyghidra").
|
|
status: "present", "missing", or "error".
|
|
version: Detected version string, or None if not found.
|
|
path: Resolved path to the component, or None.
|
|
message: Human-readable diagnostic message.
|
|
remediation: Human-readable instruction for fixing the issue.
|
|
"""
|
|
|
|
name: str
|
|
status: str
|
|
version: str | None = None
|
|
path: str | None = None
|
|
message: str = ""
|
|
remediation: str = ""
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"name": self.name,
|
|
"status": self.status,
|
|
"version": self.version,
|
|
"path": self.path,
|
|
"message": self.message,
|
|
"remediation": self.remediation,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Java discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _find_java() -> Dependency:
|
|
"""Discover Java JDK installation.
|
|
|
|
Checks JAVA_HOME first, then scans common macOS/Linux paths,
|
|
then falls back to PATH-based discovery.
|
|
"""
|
|
java_home = os.environ.get("JAVA_HOME", "")
|
|
|
|
# 1. JAVA_HOME env var
|
|
if java_home:
|
|
java_bin = Path(java_home) / "bin" / "java"
|
|
if java_bin.exists():
|
|
version = _run_version([str(java_bin), "-version"])
|
|
if version:
|
|
return Dependency(
|
|
name="java",
|
|
status="present",
|
|
version=version,
|
|
path=str(java_bin),
|
|
message=f"Java found at {java_bin} (version: {version})",
|
|
remediation="",
|
|
)
|
|
|
|
# 2. Common macOS homebrew paths
|
|
if sys.platform == "darwin":
|
|
candidate_dirs = [
|
|
Path("/opt/homebrew/opt/openjdk@21"),
|
|
Path("/opt/homebrew/opt/openjdk@17"),
|
|
Path("/opt/homebrew/opt/openjdk"),
|
|
Path("/usr/local/opt/openjdk@21"),
|
|
Path("/usr/local/opt/openjdk@17"),
|
|
Path("/usr/local/opt/openjdk"),
|
|
]
|
|
for d in candidate_dirs:
|
|
java_bin = d / "bin" / "java"
|
|
if java_bin.exists():
|
|
version = _run_version([str(java_bin), "-version"])
|
|
if version:
|
|
return Dependency(
|
|
name="java",
|
|
status="present",
|
|
version=version,
|
|
path=str(java_bin),
|
|
message=f"Java found at {java_bin} (version: {version})",
|
|
remediation="",
|
|
)
|
|
|
|
# 3. Common Linux paths
|
|
if sys.platform == "linux":
|
|
for d in [
|
|
Path("/usr/lib/jvm/java-21-openjdk"),
|
|
Path("/usr/lib/jvm/java-17-openjdk"),
|
|
Path("/usr/lib/jvm/default-java"),
|
|
]:
|
|
java_bin = d / "bin" / "java"
|
|
if java_bin.exists():
|
|
version = _run_version([str(java_bin), "-version"])
|
|
if version:
|
|
return Dependency(
|
|
name="java",
|
|
status="present",
|
|
version=version,
|
|
path=str(java_bin),
|
|
message=f"Java found at {java_bin} (version: {version})",
|
|
remediation="",
|
|
)
|
|
|
|
# 4. PATH-based fallback
|
|
java_path = shutil.which("java")
|
|
if java_path:
|
|
version = _run_version(["java", "-version"])
|
|
if version:
|
|
return Dependency(
|
|
name="java",
|
|
status="present",
|
|
version=version,
|
|
path=java_path,
|
|
message=f"Java found on PATH at {java_path} (version: {version})",
|
|
remediation="",
|
|
)
|
|
|
|
# Java not found
|
|
return Dependency(
|
|
name="java",
|
|
status="missing",
|
|
version=None,
|
|
path=None,
|
|
message="Java JDK 17 or later is not installed.",
|
|
remediation=(
|
|
"Install Java JDK 17+ (recommended: OpenJDK 21). "
|
|
"On macOS: brew install openjdk@21. "
|
|
"On Linux: apt install openjdk-21-jdk or yum install java-21-openjdk-devel. "
|
|
"Set JAVA_HOME to the JDK root directory."
|
|
),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ghidra discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _find_ghidra() -> Dependency:
|
|
"""Discover Ghidra installation.
|
|
|
|
Checks GHIDRA_INSTALL_DIR first, then scans common macOS/Linux paths.
|
|
"""
|
|
ghidra_dir = os.environ.get("GHIDRA_INSTALL_DIR", "")
|
|
|
|
# 1. GHIDRA_INSTALL_DIR env var
|
|
if ghidra_dir:
|
|
ghidra_path = Path(ghidra_dir)
|
|
if ghidra_path.exists():
|
|
version = _detect_ghidra_version(ghidra_path)
|
|
if version:
|
|
return Dependency(
|
|
name="ghidra",
|
|
status="present",
|
|
version=version,
|
|
path=str(ghidra_path),
|
|
message=f"Ghidra found at {ghidra_path} (version: {version})",
|
|
remediation="",
|
|
)
|
|
|
|
# 2. Common macOS paths
|
|
candidate_dirs: list[Path] = [
|
|
Path.home() / ".local" / "opt" / "ghidra",
|
|
Path("/opt/ghidra"),
|
|
Path("/usr/local/ghidra"),
|
|
]
|
|
|
|
for base in candidate_dirs:
|
|
if base.exists():
|
|
# Look for versioned subdirs like ghidra_12.1.2_PUBLIC
|
|
for entry in sorted(base.iterdir(), reverse=True):
|
|
if entry.is_dir() and "ghidra" in entry.name.lower():
|
|
version = _detect_ghidra_version(entry)
|
|
if version:
|
|
return Dependency(
|
|
name="ghidra",
|
|
status="present",
|
|
version=version,
|
|
path=str(entry),
|
|
message=f"Ghidra found at {entry} (version: {version})",
|
|
remediation="",
|
|
)
|
|
|
|
# Ghidra not found
|
|
return Dependency(
|
|
name="ghidra",
|
|
status="missing",
|
|
version=None,
|
|
path=None,
|
|
message="Ghidra is not installed.",
|
|
remediation=(
|
|
"Download Ghidra from https://ghidra-sre.org/. "
|
|
"Extract to ~/.local/opt/ghidra/ghidra_<version>_PUBLIC. "
|
|
"Set GHIDRA_INSTALL_DIR to the extracted directory. "
|
|
"Requires Java JDK 17+."
|
|
),
|
|
)
|
|
|
|
|
|
def _detect_ghidra_version(ghidra_path: Path) -> str | None:
|
|
"""Try to detect Ghidra version from the directory name or application.properties."""
|
|
# Method 1: directory name pattern (ghidra_12.1.2_PUBLIC)
|
|
dir_name = ghidra_path.name
|
|
import re
|
|
|
|
m = re.match(r"ghidra[_-](\d+\.\d+(?:\.\d+)?)", dir_name, re.IGNORECASE)
|
|
if m:
|
|
return m.group(1)
|
|
|
|
# Method 2: look for application.properties
|
|
props = ghidra_path / "Ghidra" / "application.properties"
|
|
if props.exists():
|
|
try:
|
|
content = props.read_text()
|
|
m = re.search(r"application\.version\s*=\s*(\S+)", content)
|
|
if m:
|
|
return m.group(1)
|
|
except Exception:
|
|
pass
|
|
|
|
# Method 3: support/analyzeHeadless (Ghidra's headless launcher exists)
|
|
headless = ghidra_path / "support" / "analyzeHeadless"
|
|
if headless.exists():
|
|
return "unknown"
|
|
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PyGhidra discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _find_pyghidra() -> Dependency:
|
|
"""Discover PyGhidra Python package.
|
|
|
|
Tries to import pyghidra. If it fails, checks if it can be installed via pip.
|
|
"""
|
|
try:
|
|
import pyghidra # type: ignore[import-not-found,unused-ignore]
|
|
|
|
version = getattr(pyghidra, "__version__", "unknown")
|
|
pyghidra_path = getattr(pyghidra, "__file__", None)
|
|
return Dependency(
|
|
name="pyghidra",
|
|
status="present",
|
|
version=str(version),
|
|
path=str(pyghidra_path),
|
|
message=f"PyGhidra {version} is installed.",
|
|
remediation="",
|
|
)
|
|
except ImportError:
|
|
pass
|
|
|
|
# Check if pip is available for installation
|
|
pip_cmd = _find_pip()
|
|
pip_msg = ""
|
|
if pip_cmd:
|
|
pip_msg = f" Run: {pip_cmd} install pyghidra"
|
|
|
|
return Dependency(
|
|
name="pyghidra",
|
|
status="missing",
|
|
version=None,
|
|
path=None,
|
|
message="PyGhidra Python package is not installed.",
|
|
remediation=f"Install PyGhidra via pip.{pip_msg}",
|
|
)
|
|
|
|
|
|
def _find_pip() -> str | None:
|
|
"""Find a usable pip command."""
|
|
candidates = ["pip3", "pip", f"{sys.executable} -m pip"]
|
|
for cmd in candidates:
|
|
pip_path = shutil.which(cmd.split()[0])
|
|
if pip_path:
|
|
return cmd
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Utility
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _run_version(cmd: list[str]) -> str | None:
|
|
"""Run a command and extract a version string from its combined output.
|
|
|
|
For 'java -version' which prints to stderr, we capture all output.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
output = (result.stdout + result.stderr).strip()
|
|
if output:
|
|
# Take the first non-empty line as the version info
|
|
for line in output.splitlines():
|
|
line = line.strip()
|
|
if line:
|
|
return line
|
|
return None
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, PermissionError):
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def discover_dependencies() -> list[Dependency]:
|
|
"""Discover all external dependencies and return their current status.
|
|
|
|
Returns:
|
|
List of Dependency objects, one per component (java, ghidra, pyghidra).
|
|
"""
|
|
return [
|
|
_find_java(),
|
|
_find_ghidra(),
|
|
_find_pyghidra(),
|
|
]
|