Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions web/backend/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import os
import sys
from pathlib import Path


BACKEND_DIR = Path(__file__).resolve().parent
ROOT_DIR = BACKEND_DIR.parent.parent

if str(ROOT_DIR) not in sys.path:
sys.path.append(str(ROOT_DIR))

from config import get_tools_dir
from core import HackingTool as ODKTool, HackingToolsCollection as ODKToolsCollection
from hackingtool import all_tools, tool_definitions
from os_detect import CURRENT_OS


def _walk_collection(items: list, leaf_title: str = "", top_level_title: str = "") -> list[tuple[ODKTool, str, str]]:
flattened: list[tuple[ODKTool, str, str]] = []
for item in items:
if isinstance(item, ODKToolsCollection):
next_leaf = item.TITLE or leaf_title
next_top_level = top_level_title or next_leaf
flattened.extend(_walk_collection(item.TOOLS, next_leaf, next_top_level))
elif isinstance(item, ODKTool):
flattened.append((item, leaf_title or top_level_title, top_level_title or leaf_title))
return flattened


def _tool_support_reason(tool: ODKTool) -> str | None:
if getattr(tool, "ARCHIVED", False):
return getattr(tool, "ARCHIVED_REASON", "") or "Archived"
supported_os = getattr(tool, "SUPPORTED_OS", ["linux", "macos"])
if CURRENT_OS.system not in supported_os:
supported = ", ".join(supported_os)
return f"Unsupported on {CURRENT_OS.system}. Supported: {supported}."
return None


def _tool_local_path(tool: ODKTool) -> str | None:
original_cwd = os.getcwd()
try:
os.chdir(str(get_tools_dir()))
return tool._get_tool_dir()
finally:
os.chdir(original_cwd)


def _option_dict(tool: ODKTool, index: int, option: tuple[str, object]) -> dict:
label, callback = option
callback_name = getattr(callback, "__name__", "")

web_supported = False
kind = "custom"

if label == "Install":
kind = "install"
web_supported = True
elif label == "Update":
kind = "update"
web_supported = True
elif label == "Uninstall":
kind = "uninstall"
web_supported = tool.__class__.uninstall is ODKTool.uninstall and bool(getattr(tool, "UNINSTALL_COMMANDS", []))
elif label == "Run":
kind = "run"
web_supported = tool.__class__.run is ODKTool.run and bool(getattr(tool, "RUN_COMMANDS", []))
elif label == "Open Folder":
kind = "open-folder"
elif label == "Update System":
kind = "update-system"
web_supported = True
elif label == "Update Hacking Tool":
kind = "update-hackingtool"
web_supported = True
elif callback_name == "open":
kind = "open"

return {
"index": index,
"label": label,
"kind": kind,
"webSupported": web_supported,
}


def _tool_to_dict(
tool_id: int,
tool: ODKTool,
category_id: int,
category_title: str,
category_label: str,
category_icon: str,
top_level_category_title: str,
) -> dict:
local_path = _tool_local_path(tool)
support_reason = _tool_support_reason(tool)
compatible = support_reason is None
options = [_option_dict(tool, index, option) for index, option in enumerate(tool.OPTIONS)]

return {
"id": tool_id,
"title": tool.TITLE,
"description": getattr(tool, "DESCRIPTION", "") or "",
"categoryId": category_id,
"category": category_title,
"categoryLabel": category_label,
"categoryIcon": category_icon,
"topLevelCategory": top_level_category_title,
"tags": getattr(tool, "TAGS", []),
"installed": tool.is_installed if hasattr(tool, "is_installed") else False,
"projectUrl": getattr(tool, "PROJECT_URL", "") or "",
"localPath": local_path,
"supportedOs": getattr(tool, "SUPPORTED_OS", ["linux", "macos"]),
"compatible": compatible,
"supportReason": support_reason,
"archived": getattr(tool, "ARCHIVED", False),
"archivedReason": getattr(tool, "ARCHIVED_REASON", "") or "",
"requires": {
"root": getattr(tool, "REQUIRES_ROOT", False),
"wifi": getattr(tool, "REQUIRES_WIFI", False),
"go": getattr(tool, "REQUIRES_GO", False),
"ruby": getattr(tool, "REQUIRES_RUBY", False),
"java": getattr(tool, "REQUIRES_JAVA", False),
"docker": getattr(tool, "REQUIRES_DOCKER", False),
},
"commands": {
"install": getattr(tool, "INSTALL_COMMANDS", []),
"uninstall": getattr(tool, "UNINSTALL_COMMANDS", []),
"run": getattr(tool, "RUN_COMMANDS", []),
},
"options": options,
"actions": {
"canInstall": any(option["kind"] == "install" and option["webSupported"] for option in options),
"canUpdate": any(option["kind"] == "update" and option["webSupported"] for option in options),
"canUninstall": any(option["kind"] == "uninstall" and option["webSupported"] for option in options),
"canRun": any(option["kind"] == "run" and option["webSupported"] for option in options),
},
}


def get_category_collections() -> list[dict]:
categories = []
for index, ((full_title, icon, menu_label), collection) in enumerate(zip(tool_definitions, all_tools), start=1):
flattened_tools = _walk_collection(collection.TOOLS, full_title, full_title)
active_tools = [tool for tool, _leaf, _top in flattened_tools if _tool_support_reason(tool) is None]
archived_tools = [tool for tool, _leaf, _top in flattened_tools if getattr(tool, "ARCHIVED", False)]
incompatible_tools = [
tool for tool, _leaf, _top in flattened_tools
if not getattr(tool, "ARCHIVED", False)
and CURRENT_OS.system not in getattr(tool, "SUPPORTED_OS", ["linux", "macos"])
]
categories.append({
"id": index,
"title": full_title,
"label": menu_label,
"icon": icon,
"description": getattr(collection, "DESCRIPTION", "") or "",
"counts": {
"active": len(active_tools),
"archived": len(archived_tools),
"incompatible": len(incompatible_tools),
"installed": len([tool for tool in active_tools if getattr(tool, "is_installed", False)]),
"total": len(flattened_tools),
},
"supportsInstallAll": any(hasattr(tool, "is_installed") and not getattr(tool, "ARCHIVED", False) for tool in active_tools),
"collection": collection,
"flattenedTools": flattened_tools,
})
return categories


def get_categories_payload() -> list[dict]:
categories = []
for category in get_category_collections():
categories.append({key: value for key, value in category.items() if key not in {"collection", "flattenedTools"}})
return categories


def get_tools_payload() -> list[dict]:
tools = []
tool_id = 1

for category in get_category_collections():
for tool, leaf_title, top_level_title in category["flattenedTools"]:
tools.append(_tool_to_dict(
tool_id=tool_id,
tool=tool,
category_id=category["id"],
category_title=leaf_title,
category_label=category["label"],
category_icon=category["icon"],
top_level_category_title=top_level_title,
))
tool_id += 1

return tools


def get_tool_by_id(tool_id: int) -> dict | None:
for tool in get_tools_payload():
if tool["id"] == tool_id:
return tool
return None


def get_category_by_id(category_id: int) -> dict | None:
for category in get_categories_payload():
if category["id"] == category_id:
return category
return None


def resolve_tool_instance(tool_id: int) -> tuple[ODKTool, dict] | tuple[None, None]:
current_id = 1
for category in get_category_collections():
for tool, _leaf_title, _top_level_title in category["flattenedTools"]:
if current_id == tool_id:
return tool, category
current_id += 1
return None, None


def resolve_category_collection(category_id: int) -> tuple[ODKToolsCollection, dict] | tuple[None, None]:
for category in get_category_collections():
if category["id"] == category_id:
return category["collection"], category
return None, None
145 changes: 145 additions & 0 deletions web/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from datetime import datetime, timezone
from pathlib import Path
import os
import subprocess
import sys

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware


BACKEND_DIR = Path(__file__).resolve().parent
ROOT_DIR = BACKEND_DIR.parent.parent

if str(BACKEND_DIR) not in sys.path:
sys.path.append(str(BACKEND_DIR))
if str(ROOT_DIR) not in sys.path:
sys.path.append(str(ROOT_DIR))

from catalog import get_categories_payload, get_category_by_id, get_tool_by_id, get_tools_payload
from config import get_tools_dir
from os_detect import CURRENT_OS


app = FastAPI(title="Hacking Tool API")

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


def _run_backend_action(*args: str) -> dict:
runner_path = BACKEND_DIR / "runner.py"
started_at = datetime.now(timezone.utc)
result = subprocess.run(
[sys.executable, str(runner_path), *args],
cwd=str(get_tools_dir()),
capture_output=True,
text=True,
errors="replace",
)
finished_at = datetime.now(timezone.utc)

return {
"success": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"startedAt": started_at.isoformat(),
"finishedAt": finished_at.isoformat(),
}


@app.get("/")
def read_root():
return {"message": "Welcome to Hacking Tool API"}


@app.get("/api/system")
def get_system():
return {
"os": {
"system": CURRENT_OS.system,
"distroId": CURRENT_OS.distro_id,
"distroLike": CURRENT_OS.distro_like,
"version": CURRENT_OS.distro_version,
"packageManager": CURRENT_OS.pkg_manager,
"isRoot": CURRENT_OS.is_root,
"arch": CURRENT_OS.arch,
"isWsl": CURRENT_OS.is_wsl,
},
"paths": {
"toolsDir": str(get_tools_dir()),
"backendDir": str(BACKEND_DIR),
"repoRoot": str(ROOT_DIR),
},
"user": {
"home": str(Path.home()),
"name": os.environ.get("USER", os.environ.get("LOGNAME", "")),
},
}


@app.get("/api/categories")
def get_categories():
return get_categories_payload()


@app.get("/api/categories/{category_id}")
def get_category(category_id: int):
category = get_category_by_id(category_id)
if category is None:
raise HTTPException(status_code=404, detail="Category not found")
return category


@app.post("/api/categories/{category_id}/actions/install-missing")
def install_missing_in_category(category_id: int):
category = get_category_by_id(category_id)
if category is None:
raise HTTPException(status_code=404, detail="Category not found")
return _run_backend_action("--category-id", str(category_id), "--action", "install-missing")


@app.get("/api/tools")
def get_tools():
return get_tools_payload()


@app.get("/api/tools/{tool_id}")
def get_tool(tool_id: int):
tool = get_tool_by_id(tool_id)
if tool is None:
raise HTTPException(status_code=404, detail="Tool not found")
return tool


@app.post("/api/tools/{tool_id}/actions/{action_name}")
def run_tool_action(tool_id: int, action_name: str):
tool = get_tool_by_id(tool_id)
if tool is None:
raise HTTPException(status_code=404, detail="Tool not found")

if action_name not in {"install", "update", "uninstall", "run"}:
raise HTTPException(status_code=400, detail="Unsupported action")

return _run_backend_action("--tool-id", str(tool_id), "--action", action_name)


@app.post("/api/tools/{tool_id}/options/{option_index}")
def run_tool_option(tool_id: int, option_index: int):
tool = get_tool_by_id(tool_id)
if tool is None:
raise HTTPException(status_code=404, detail="Tool not found")

return _run_backend_action("--tool-id", str(tool_id), "--option-index", str(option_index))


if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=8000)
3 changes: 3 additions & 0 deletions web/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fastapi==0.115.6
uvicorn==0.34.0
pydantic==2.10.4
Loading