264 lines
7.8 KiB
Python
264 lines
7.8 KiB
Python
|
|
"""
|
||
|
|
AUTARCH Path Resolution
|
||
|
|
Centralized path management for cross-platform portability.
|
||
|
|
|
||
|
|
All paths resolve relative to the application root directory.
|
||
|
|
Tool lookup checks project directories first, then system PATH.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import platform
|
||
|
|
import shutil
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional, List
|
||
|
|
|
||
|
|
|
||
|
|
# ── Application Root ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
# Computed once: the autarch project root (parent of core/)
|
||
|
|
_APP_DIR = Path(__file__).resolve().parent.parent
|
||
|
|
|
||
|
|
|
||
|
|
def get_app_dir() -> Path:
|
||
|
|
"""Return the AUTARCH application root directory."""
|
||
|
|
return _APP_DIR
|
||
|
|
|
||
|
|
|
||
|
|
def get_core_dir() -> Path:
|
||
|
|
return _APP_DIR / 'core'
|
||
|
|
|
||
|
|
|
||
|
|
def get_modules_dir() -> Path:
|
||
|
|
return _APP_DIR / 'modules'
|
||
|
|
|
||
|
|
|
||
|
|
def get_data_dir() -> Path:
|
||
|
|
d = _APP_DIR / 'data'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
def get_config_path() -> Path:
|
||
|
|
return _APP_DIR / 'autarch_settings.conf'
|
||
|
|
|
||
|
|
|
||
|
|
def get_results_dir() -> Path:
|
||
|
|
d = _APP_DIR / 'results'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
def get_reports_dir() -> Path:
|
||
|
|
d = get_results_dir() / 'reports'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
def get_dossiers_dir() -> Path:
|
||
|
|
d = _APP_DIR / 'dossiers'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
def get_uploads_dir() -> Path:
|
||
|
|
d = get_data_dir() / 'uploads'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
def get_backups_dir() -> Path:
|
||
|
|
d = _APP_DIR / 'backups'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
def get_templates_dir() -> Path:
|
||
|
|
return _APP_DIR / '.config'
|
||
|
|
|
||
|
|
|
||
|
|
def get_custom_configs_dir() -> Path:
|
||
|
|
d = _APP_DIR / '.config' / 'custom'
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
return d
|
||
|
|
|
||
|
|
|
||
|
|
# ── Platform Detection ──────────────────────────────────────────────
|
||
|
|
|
||
|
|
def _get_arch() -> str:
|
||
|
|
"""Return architecture string: 'x86_64', 'arm64', etc."""
|
||
|
|
machine = platform.machine().lower()
|
||
|
|
if machine in ('aarch64', 'arm64'):
|
||
|
|
return 'arm64'
|
||
|
|
elif machine in ('x86_64', 'amd64'):
|
||
|
|
return 'x86_64'
|
||
|
|
return machine
|
||
|
|
|
||
|
|
|
||
|
|
def get_platform() -> str:
|
||
|
|
"""Return platform: 'linux', 'windows', or 'darwin'."""
|
||
|
|
return platform.system().lower()
|
||
|
|
|
||
|
|
|
||
|
|
def get_platform_tag() -> str:
|
||
|
|
"""Return platform-arch tag like 'linux-arm64', 'windows-x86_64'."""
|
||
|
|
return f"{get_platform()}-{_get_arch()}"
|
||
|
|
|
||
|
|
|
||
|
|
def is_windows() -> bool:
|
||
|
|
return platform.system() == 'Windows'
|
||
|
|
|
||
|
|
|
||
|
|
def is_linux() -> bool:
|
||
|
|
return platform.system() == 'Linux'
|
||
|
|
|
||
|
|
|
||
|
|
def is_mac() -> bool:
|
||
|
|
return platform.system() == 'Darwin'
|
||
|
|
|
||
|
|
|
||
|
|
# ── Tool / Binary Lookup ───────────────────────────────────────────
|
||
|
|
#
|
||
|
|
# Priority order:
|
||
|
|
# 1. System PATH (shutil.which — native binaries, correct arch)
|
||
|
|
# 2. Platform-specific well-known install locations
|
||
|
|
# 3. Platform-specific project tools (tools/linux-arm64/, etc.)
|
||
|
|
# 4. Generic project directories (android/, tools/, bin/)
|
||
|
|
# 5. Extra paths passed by caller
|
||
|
|
#
|
||
|
|
|
||
|
|
# Well-known install locations by platform (last resort)
|
||
|
|
_PLATFORM_SEARCH_PATHS = {
|
||
|
|
'windows': [
|
||
|
|
Path(os.environ.get('LOCALAPPDATA', '')) / 'Android' / 'Sdk' / 'platform-tools',
|
||
|
|
Path(os.environ.get('USERPROFILE', '')) / 'Android' / 'Sdk' / 'platform-tools',
|
||
|
|
Path('C:/Program Files (x86)/Nmap'),
|
||
|
|
Path('C:/Program Files/Nmap'),
|
||
|
|
Path('C:/Program Files/Wireshark'),
|
||
|
|
Path('C:/Program Files (x86)/Wireshark'),
|
||
|
|
Path('C:/metasploit-framework/bin'),
|
||
|
|
],
|
||
|
|
'darwin': [
|
||
|
|
Path('/opt/homebrew/bin'),
|
||
|
|
Path('/usr/local/bin'),
|
||
|
|
],
|
||
|
|
'linux': [
|
||
|
|
Path('/usr/local/bin'),
|
||
|
|
Path('/snap/bin'),
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
# Tools that need extra environment setup when run from bundled copies
|
||
|
|
_TOOL_ENV_SETUP = {
|
||
|
|
'nmap': '_setup_nmap_env',
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _setup_nmap_env(tool_path: str):
|
||
|
|
"""Set NMAPDIR so bundled nmap finds its data files."""
|
||
|
|
tool_dir = Path(tool_path).parent
|
||
|
|
nmap_data = tool_dir / 'nmap-data'
|
||
|
|
if nmap_data.is_dir():
|
||
|
|
os.environ['NMAPDIR'] = str(nmap_data)
|
||
|
|
|
||
|
|
|
||
|
|
def _is_native_binary(path: str) -> bool:
|
||
|
|
"""Check if an ELF binary matches the host architecture."""
|
||
|
|
try:
|
||
|
|
with open(path, 'rb') as f:
|
||
|
|
magic = f.read(20)
|
||
|
|
if magic[:4] != b'\x7fELF':
|
||
|
|
return True # Not ELF (script, etc.) — assume OK
|
||
|
|
# ELF e_machine at offset 18 (2 bytes, little-endian)
|
||
|
|
e_machine = int.from_bytes(magic[18:20], 'little')
|
||
|
|
arch = _get_arch()
|
||
|
|
if arch == 'arm64' and e_machine == 183: # EM_AARCH64
|
||
|
|
return True
|
||
|
|
if arch == 'x86_64' and e_machine == 62: # EM_X86_64
|
||
|
|
return True
|
||
|
|
if arch == 'arm64' and e_machine == 62: # x86-64 on arm64 host
|
||
|
|
return False
|
||
|
|
if arch == 'x86_64' and e_machine == 183: # arm64 on x86-64 host
|
||
|
|
return False
|
||
|
|
return True # Unknown arch combo — let it try
|
||
|
|
except Exception:
|
||
|
|
return True # Can't read — assume OK
|
||
|
|
|
||
|
|
|
||
|
|
def find_tool(name: str, extra_paths: Optional[List[str]] = None) -> Optional[str]:
|
||
|
|
"""
|
||
|
|
Find an executable binary by name.
|
||
|
|
|
||
|
|
Search order:
|
||
|
|
1. System PATH (native binaries, correct architecture)
|
||
|
|
2. Platform-specific well-known install locations
|
||
|
|
3. Platform-specific project tools (tools/linux-arm64/ etc.)
|
||
|
|
4. Generic project directories (android/, tools/, bin/)
|
||
|
|
5. Extra paths provided by caller
|
||
|
|
|
||
|
|
Skips binaries that don't match the host architecture (e.g. x86-64
|
||
|
|
binaries on ARM64 hosts) to avoid FEX/emulation issues with root.
|
||
|
|
|
||
|
|
Returns absolute path string, or None if not found.
|
||
|
|
"""
|
||
|
|
# On Windows, append .exe if no extension
|
||
|
|
names = [name]
|
||
|
|
if is_windows() and '.' not in name:
|
||
|
|
names.append(name + '.exe')
|
||
|
|
|
||
|
|
# 1. System PATH (most reliable — native packages)
|
||
|
|
found = shutil.which(name)
|
||
|
|
if found and _is_native_binary(found):
|
||
|
|
return found
|
||
|
|
|
||
|
|
# 2. Platform-specific well-known locations
|
||
|
|
plat = get_platform()
|
||
|
|
for search_dir in _PLATFORM_SEARCH_PATHS.get(plat, []):
|
||
|
|
if search_dir.is_dir():
|
||
|
|
for n in names:
|
||
|
|
full = search_dir / n
|
||
|
|
if full.is_file() and os.access(str(full), os.X_OK) and _is_native_binary(str(full)):
|
||
|
|
return str(full)
|
||
|
|
|
||
|
|
# 3-4. Bundled project directories
|
||
|
|
plat_tag = get_platform_tag()
|
||
|
|
search_dirs = [
|
||
|
|
_APP_DIR / 'tools' / plat_tag, # Platform-specific (tools/linux-arm64/)
|
||
|
|
_APP_DIR / 'android', # Android tools
|
||
|
|
_APP_DIR / 'tools', # Generic tools/
|
||
|
|
_APP_DIR / 'bin', # Generic bin/
|
||
|
|
]
|
||
|
|
|
||
|
|
for tool_dir in search_dirs:
|
||
|
|
if tool_dir.is_dir():
|
||
|
|
for n in names:
|
||
|
|
full = tool_dir / n
|
||
|
|
if full.is_file() and os.access(str(full), os.X_OK):
|
||
|
|
found = str(full)
|
||
|
|
if not _is_native_binary(found):
|
||
|
|
continue # Wrong arch — skip
|
||
|
|
# Apply environment setup for bundled tools
|
||
|
|
env_fn = _TOOL_ENV_SETUP.get(name)
|
||
|
|
if env_fn:
|
||
|
|
globals()[env_fn](found)
|
||
|
|
return found
|
||
|
|
|
||
|
|
# 5. Extra paths from caller
|
||
|
|
if extra_paths:
|
||
|
|
for p in extra_paths:
|
||
|
|
for n in names:
|
||
|
|
full = os.path.join(p, n)
|
||
|
|
if os.path.isfile(full) and os.access(full, os.X_OK) and _is_native_binary(full):
|
||
|
|
return full
|
||
|
|
|
||
|
|
# Last resort: return system PATH result even if wrong arch (FEX may work for user)
|
||
|
|
found = shutil.which(name)
|
||
|
|
if found:
|
||
|
|
return found
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def tool_available(name: str) -> bool:
|
||
|
|
"""Check if a tool is available anywhere."""
|
||
|
|
return find_tool(name) is not None
|