Files
Bjorn/actions/wpasec_potfiles.py

318 lines
13 KiB
Python

# wpasec_potfiles.py
# WPAsec Potfile Manager - Download, clean, import, or erase WiFi credentials
import os
import json
import glob
import argparse
import requests
import subprocess
from datetime import datetime
import logging
# ── METADATA / UI FOR NEO LAUNCHER ────────────────────────────────────────────
b_class = "WPAsecPotfileManager"
b_module = "wpasec_potfiles"
b_enabled = 1
b_action = "normal" # normal | aggressive | stealth
b_category = "wifi"
b_name = "WPAsec Potfile Manager"
b_description = (
"Download, clean, import, or erase Wi-Fi networks from WPAsec potfiles. "
"Options: download (default if API key is set), clean, import, erase."
)
b_author = "Infinition"
b_version = "1.0.0"
b_icon = f"/actions_icons/{b_class}.png"
b_docs_url = "https://wpa-sec.stanev.org/?api"
b_args = {
"key": {
"type": "text",
"label": "API key (WPAsec)",
"placeholder": "wpa-sec api key",
"secret": True,
"help": "API key used to download the potfile. If empty, the saved key is reused."
},
"directory": {
"type": "text",
"label": "Potfiles directory",
"default": "/home/bjorn/Bjorn/data/input/potfiles",
"placeholder": "/path/to/potfiles",
"help": "Directory containing/receiving .pot / .potfile files."
},
"clean": {
"type": "checkbox",
"label": "Clean potfiles directory",
"default": False,
"help": "Delete all files in the potfiles directory."
},
"import_potfiles": {
"type": "checkbox",
"label": "Import potfiles into NetworkManager",
"default": False,
"help": "Add Wi-Fi networks found in potfiles via nmcli (avoiding duplicates)."
},
"erase": {
"type": "checkbox",
"label": "Erase Wi-Fi connections from potfiles",
"default": False,
"help": "Delete via nmcli the Wi-Fi networks listed in potfiles (avoiding duplicates)."
}
}
b_examples = [
{"directory": "/home/bjorn/Bjorn/data/input/potfiles"},
{"key": "YOUR_API_KEY_HERE", "directory": "/home/bjorn/Bjorn/data/input/potfiles"},
{"directory": "/home/bjorn/Bjorn/data/input/potfiles", "clean": True},
{"directory": "/home/bjorn/Bjorn/data/input/potfiles", "import_potfiles": True},
{"directory": "/home/bjorn/Bjorn/data/input/potfiles", "erase": True},
{"directory": "/home/bjorn/Bjorn/data/input/potfiles", "clean": True, "import_potfiles": True},
]
def compute_dynamic_b_args(base: dict) -> dict:
"""
Enrich dynamic UI arguments:
- Pre-fill the API key if previously saved.
- Show info about the number of potfiles in the chosen directory.
"""
d = dict(base or {})
try:
settings_path = os.path.join(
os.path.expanduser("~"), ".settings_bjorn", "wpasec_settings.json"
)
if os.path.exists(settings_path):
with open(settings_path, "r", encoding="utf-8") as f:
saved = json.load(f)
saved_key = (saved or {}).get("api_key")
if saved_key and not d.get("key", {}).get("default"):
d.setdefault("key", {}).setdefault("default", saved_key)
d["key"]["help"] = (d["key"].get("help") or "") + " (auto-detected)"
except Exception:
pass
try:
directory = d.get("directory", {}).get("default") or "/home/bjorn/Bjorn/data/input/potfiles"
exists = os.path.isdir(directory)
count = 0
if exists:
count = len(glob.glob(os.path.join(directory, "*.pot"))) + \
len(glob.glob(os.path.join(directory, "*.potfile")))
extra = f" | Found: {count} potfile(s)" if exists else " | (directory does not exist yet)"
d["directory"]["help"] = (d["directory"].get("help") or "") + extra
except Exception:
pass
return d
# ── CLASS IMPLEMENTATION ─────────────────────────────────────────────────────
class WPAsecPotfileManager:
DEFAULT_SAVE_DIR = "/home/bjorn/Bjorn/data/input/potfiles"
DEFAULT_SETTINGS_DIR = "/home/bjorn/.settings_bjorn"
SETTINGS_FILE = os.path.join(DEFAULT_SETTINGS_DIR, "wpasec_settings.json")
DOWNLOAD_URL = "https://wpa-sec.stanev.org/?api&dl=1"
def __init__(self, shared_data):
"""
Orchestrator always passes shared_data.
Even if unused here, we store it for compatibility.
"""
self.shared_data = shared_data
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# --- Orchestrator entry point ---
def execute(self, ip=None, port=None, row=None, status_key=None):
"""
Entry point for orchestrator.
By default: download latest potfile if API key is available.
"""
self.shared_data.bjorn_orch_status = "WPAsecPotfileManager"
self.shared_data.comment_params = {"ip": ip, "port": port}
api_key = self.load_api_key()
if api_key:
logging.info("WPAsecPotfileManager: downloading latest potfile (orchestrator trigger).")
self.download_potfile(self.DEFAULT_SAVE_DIR, api_key)
return "success"
else:
logging.warning("WPAsecPotfileManager: no API key found, nothing done.")
return "failed"
# --- API Key Handling ---
def save_api_key(self, api_key: str):
"""Save the API key locally."""
try:
os.makedirs(self.DEFAULT_SETTINGS_DIR, exist_ok=True)
settings = {"api_key": api_key}
with open(self.SETTINGS_FILE, "w") as file:
json.dump(settings, file)
logging.info(f"API key saved to {self.SETTINGS_FILE}")
except Exception as e:
logging.error(f"Failed to save API key: {e}")
def load_api_key(self):
"""Load the API key from local storage."""
if os.path.exists(self.SETTINGS_FILE):
try:
with open(self.SETTINGS_FILE, "r") as file:
settings = json.load(file)
return settings.get("api_key")
except Exception as e:
logging.error(f"Failed to load API key: {e}")
return None
# --- Actions ---
def download_potfile(self, save_dir, api_key):
"""Download the potfile from WPAsec."""
try:
cookies = {"key": api_key}
logging.info(f"Downloading potfile from: {self.DOWNLOAD_URL}")
response = requests.get(self.DOWNLOAD_URL, cookies=cookies, stream=True)
response.raise_for_status()
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = os.path.join(save_dir, f"potfile_{timestamp}.pot")
os.makedirs(save_dir, exist_ok=True)
with open(filename, "wb") as file:
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)
logging.info(f"Potfile saved to: {filename}")
except requests.exceptions.RequestException as e:
logging.error(f"Failed to download potfile: {e}")
except Exception as e:
logging.error(f"Unexpected error: {e}")
def clean_directory(self, directory):
"""Delete all potfiles in the given directory."""
try:
if os.path.exists(directory):
logging.info(f"Cleaning directory: {directory}")
for file in os.listdir(directory):
file_path = os.path.join(directory, file)
if os.path.isfile(file_path):
os.remove(file_path)
logging.info(f"Deleted: {file_path}")
else:
logging.info(f"Directory does not exist: {directory}")
except Exception as e:
logging.error(f"Failed to clean directory {directory}: {e}")
def import_potfiles(self, directory):
"""Import potfiles into NetworkManager using nmcli."""
try:
potfile_paths = glob.glob(os.path.join(directory, "*.pot")) + glob.glob(os.path.join(directory, "*.potfile"))
processed_ssids = set()
networks_added = []
DEFAULT_PRIORITY = 5
for path in potfile_paths:
with open(path, "r") as potfile:
for line in potfile:
line = line.strip()
if ":" not in line:
continue
ssid, password = self._parse_potfile_line(line)
if not ssid or not password or ssid in processed_ssids:
continue
try:
subprocess.run(
["sudo", "nmcli", "connection", "add", "type", "wifi",
"con-name", ssid, "ifname", "*", "ssid", ssid,
"wifi-sec.key-mgmt", "wpa-psk", "wifi-sec.psk", password,
"connection.autoconnect", "yes",
"connection.autoconnect-priority", str(DEFAULT_PRIORITY)],
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
processed_ssids.add(ssid)
networks_added.append(ssid)
logging.info(f"Imported network {ssid}")
except subprocess.CalledProcessError as e:
logging.error(f"Failed to import {ssid}: {e.stderr.strip()}")
logging.info(f"Total imported: {networks_added}")
except Exception as e:
logging.error(f"Unexpected error while importing: {e}")
def erase_networks(self, directory):
"""Erase Wi-Fi connections listed in potfiles using nmcli."""
try:
potfile_paths = glob.glob(os.path.join(directory, "*.pot")) + glob.glob(os.path.join(directory, "*.potfile"))
processed_ssids = set()
networks_removed = []
for path in potfile_paths:
with open(path, "r") as potfile:
for line in potfile:
line = line.strip()
if ":" not in line:
continue
ssid, _ = self._parse_potfile_line(line)
if not ssid or ssid in processed_ssids:
continue
try:
subprocess.run(
["sudo", "nmcli", "connection", "delete", "id", ssid],
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
processed_ssids.add(ssid)
networks_removed.append(ssid)
logging.info(f"Deleted network {ssid}")
except subprocess.CalledProcessError as e:
logging.warning(f"Failed to delete {ssid}: {e.stderr.strip()}")
logging.info(f"Total deleted: {networks_removed}")
except Exception as e:
logging.error(f"Unexpected error while erasing: {e}")
# --- Helpers ---
def _parse_potfile_line(self, line: str):
"""Parse a potfile line into (ssid, password)."""
ssid, password = None, None
if line.startswith("$WPAPSK$") and "#" in line:
try:
ssid_hash, password = line.split(":", 1)
ssid = ssid_hash.split("#")[0].replace("$WPAPSK$", "")
except ValueError:
return None, None
elif len(line.split(":")) == 4:
try:
_, _, ssid, password = line.split(":")
except ValueError:
return None, None
return ssid, password
# --- CLI ---
def run(self, argv=None):
parser = argparse.ArgumentParser(description="Manage WPAsec potfiles (download, clean, import, erase).")
parser.add_argument("-k", "--key", help="API key for WPAsec (saved locally after first use).")
parser.add_argument("-d", "--directory", default=self.DEFAULT_SAVE_DIR, help="Directory for potfiles.")
parser.add_argument("-c", "--clean", action="store_true", help="Clean the potfiles directory.")
parser.add_argument("-a", "--import-potfiles", action="store_true", help="Import potfiles into NetworkManager.")
parser.add_argument("-e", "--erase", action="store_true", help="Erase Wi-Fi connections from potfiles.")
args = parser.parse_args(argv)
api_key = args.key
if api_key:
self.save_api_key(api_key)
else:
api_key = self.load_api_key()
if args.clean:
self.clean_directory(args.directory)
if args.import_potfiles:
self.import_potfiles(args.directory)
if args.erase:
self.erase_networks(args.directory)
if api_key and not args.clean and not args.import_potfiles and not args.erase:
self.download_potfile(args.directory, api_key)
if __name__ == "__main__":
WPAsecPotfileManager(shared_data=None).run()