274 lines
7.5 KiB
Go
274 lines
7.5 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// ── Module Categories ───────────────────────────────────────────────
|
|
|
|
var moduleCategories = map[string]string{
|
|
"defender.py": "Defense",
|
|
"defender_monitor.py": "Defense",
|
|
"defender_windows.py": "Defense",
|
|
"container_sec.py": "Defense",
|
|
"msf.py": "Offense",
|
|
"exploit_dev.py": "Offense",
|
|
"loadtest.py": "Offense",
|
|
"phishmail.py": "Offense",
|
|
"deauth.py": "Offense",
|
|
"mitm_proxy.py": "Offense",
|
|
"c2_framework.py": "Offense",
|
|
"api_fuzzer.py": "Offense",
|
|
"webapp_scanner.py": "Offense",
|
|
"cloud_scan.py": "Offense",
|
|
"starlink_hack.py": "Offense",
|
|
"rcs_tools.py": "Offense",
|
|
"sms_forge.py": "Offense",
|
|
"pineapple.py": "Offense",
|
|
"password_toolkit.py": "Offense",
|
|
"counter.py": "Counter",
|
|
"anti_forensics.py": "Counter",
|
|
"analyze.py": "Analysis",
|
|
"forensics.py": "Analysis",
|
|
"llm_trainer.py": "Analysis",
|
|
"report_engine.py": "Analysis",
|
|
"threat_intel.py": "Analysis",
|
|
"ble_scanner.py": "Analysis",
|
|
"rfid_tools.py": "Analysis",
|
|
"reverse_eng.py": "Analysis",
|
|
"steganography.py": "Analysis",
|
|
"incident_resp.py": "Analysis",
|
|
"net_mapper.py": "Analysis",
|
|
"log_correlator.py": "Analysis",
|
|
"malware_sandbox.py": "Analysis",
|
|
"email_sec.py": "Analysis",
|
|
"vulnerab_scanner.py": "Analysis",
|
|
"recon.py": "OSINT",
|
|
"dossier.py": "OSINT",
|
|
"geoip.py": "OSINT",
|
|
"adultscan.py": "OSINT",
|
|
"yandex_osint.py": "OSINT",
|
|
"social_eng.py": "OSINT",
|
|
"ipcapture.py": "OSINT",
|
|
"snoop_decoder.py": "OSINT",
|
|
"simulate.py": "Simulate",
|
|
"android_apps.py": "Android",
|
|
"android_advanced.py": "Android",
|
|
"android_boot.py": "Android",
|
|
"android_payload.py": "Android",
|
|
"android_protect.py": "Android",
|
|
"android_recon.py": "Android",
|
|
"android_root.py": "Android",
|
|
"android_screen.py": "Android",
|
|
"android_sms.py": "Android",
|
|
"hardware_local.py": "Hardware",
|
|
"hardware_remote.py": "Hardware",
|
|
"iphone_local.py": "Hardware",
|
|
"wireshark.py": "Hardware",
|
|
"sdr_tools.py": "Hardware",
|
|
"upnp_manager.py": "System",
|
|
"wireguard_manager.py": "System",
|
|
"revshell.py": "System",
|
|
"hack_hijack.py": "System",
|
|
"chat.py": "Core",
|
|
"agent.py": "Core",
|
|
"agent_hal.py": "Core",
|
|
"mysystem.py": "Core",
|
|
"setup.py": "Core",
|
|
"workflow.py": "Core",
|
|
"nettest.py": "Core",
|
|
"rsf.py": "Core",
|
|
"ad_audit.py": "Offense",
|
|
"router_sploit.py": "Offense",
|
|
"wifi_audit.py": "Offense",
|
|
}
|
|
|
|
// ── Rendering ───────────────────────────────────────────────────────
|
|
|
|
func (a App) renderModulesList() string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(styleTitle.Render("MODULES"))
|
|
b.WriteString("\n")
|
|
b.WriteString(a.renderHR())
|
|
b.WriteString("\n")
|
|
|
|
if len(a.listItems) == 0 {
|
|
b.WriteString(styleDim.Render(" Loading..."))
|
|
b.WriteString("\n")
|
|
return b.String()
|
|
}
|
|
|
|
// Group by category
|
|
groups := make(map[string][]int)
|
|
for i, item := range a.listItems {
|
|
groups[item.Extra] = append(groups[item.Extra], i)
|
|
}
|
|
|
|
// Sort category names
|
|
var cats []string
|
|
for c := range groups {
|
|
cats = append(cats, c)
|
|
}
|
|
sort.Strings(cats)
|
|
|
|
for _, cat := range cats {
|
|
b.WriteString(styleKey.Render(fmt.Sprintf(" ── %s ", cat)))
|
|
b.WriteString(styleDim.Render(fmt.Sprintf("(%d)", len(groups[cat]))))
|
|
b.WriteString("\n")
|
|
|
|
for _, idx := range groups[cat] {
|
|
item := a.listItems[idx]
|
|
|
|
cursor := " "
|
|
if idx == a.cursor {
|
|
cursor = styleSelected.Render(" ▸") + " "
|
|
}
|
|
|
|
status := styleStatusOK.Render("●")
|
|
if !item.Enabled {
|
|
status = styleStatusBad.Render("○")
|
|
}
|
|
|
|
name := item.Name
|
|
if idx == a.cursor {
|
|
name = lipgloss.NewStyle().Foreground(colorWhite).Bold(true).Render(name)
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf("%s%s %s\n", cursor, status, name))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
b.WriteString(a.renderHR())
|
|
b.WriteString(styleKey.Render(" [enter]") + " Toggle enabled/disabled ")
|
|
b.WriteString(styleKey.Render("[r]") + " Refresh\n")
|
|
b.WriteString(styleDim.Render(" esc back"))
|
|
b.WriteString("\n")
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// ── Key Handling ────────────────────────────────────────────────────
|
|
|
|
func (a App) handleModulesMenu(key string) (tea.Model, tea.Cmd) {
|
|
switch key {
|
|
case "enter":
|
|
if a.cursor >= 0 && a.cursor < len(a.listItems) {
|
|
return a.toggleModule(a.cursor)
|
|
}
|
|
case "r":
|
|
return a, a.loadModules()
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (a App) handleModuleToggle(key string) (tea.Model, tea.Cmd) {
|
|
return a.handleModulesMenu(key)
|
|
}
|
|
|
|
// ── Commands ────────────────────────────────────────────────────────
|
|
|
|
func (a App) loadModules() tea.Cmd {
|
|
return func() tea.Msg {
|
|
dir := findAutarchDir()
|
|
modulesDir := filepath.Join(dir, "modules")
|
|
|
|
entries, err := os.ReadDir(modulesDir)
|
|
if err != nil {
|
|
return ResultMsg{
|
|
Title: "Error",
|
|
Lines: []string{"Cannot read modules directory: " + err.Error()},
|
|
IsError: true,
|
|
}
|
|
}
|
|
|
|
var items []ListItem
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if !strings.HasSuffix(name, ".py") || name == "__init__.py" {
|
|
continue
|
|
}
|
|
|
|
cat := "Other"
|
|
if c, ok := moduleCategories[name]; ok {
|
|
cat = c
|
|
}
|
|
|
|
// Check if module has a run() function (basic check)
|
|
content, _ := os.ReadFile(filepath.Join(modulesDir, name))
|
|
hasRun := strings.Contains(string(content), "def run(")
|
|
|
|
items = append(items, ListItem{
|
|
Name: strings.TrimSuffix(name, ".py"),
|
|
Enabled: hasRun,
|
|
Extra: cat,
|
|
Status: name,
|
|
})
|
|
}
|
|
|
|
// Sort by category then name
|
|
sort.Slice(items, func(i, j int) bool {
|
|
if items[i].Extra != items[j].Extra {
|
|
return items[i].Extra < items[j].Extra
|
|
}
|
|
return items[i].Name < items[j].Name
|
|
})
|
|
|
|
return modulesLoadedMsg{items: items}
|
|
}
|
|
}
|
|
|
|
type modulesLoadedMsg struct{ items []ListItem }
|
|
|
|
func (a App) toggleModule(idx int) (App, tea.Cmd) {
|
|
if idx < 0 || idx >= len(a.listItems) {
|
|
return a, nil
|
|
}
|
|
|
|
item := a.listItems[idx]
|
|
dir := findAutarchDir()
|
|
modulesDir := filepath.Join(dir, "modules")
|
|
disabledDir := filepath.Join(modulesDir, "disabled")
|
|
|
|
srcFile := filepath.Join(modulesDir, item.Status)
|
|
dstFile := filepath.Join(disabledDir, item.Status)
|
|
|
|
if item.Enabled {
|
|
// Disable: move to disabled/
|
|
os.MkdirAll(disabledDir, 0755)
|
|
if err := os.Rename(srcFile, dstFile); err != nil {
|
|
return a, func() tea.Msg {
|
|
return ResultMsg{
|
|
Title: "Error",
|
|
Lines: []string{"Cannot disable module: " + err.Error()},
|
|
IsError: true,
|
|
}
|
|
}
|
|
}
|
|
a.listItems[idx].Enabled = false
|
|
} else {
|
|
// Enable: move from disabled/ back
|
|
if err := os.Rename(dstFile, srcFile); err != nil {
|
|
// It might just be a module without run()
|
|
return a, func() tea.Msg {
|
|
return ResultMsg{
|
|
Title: "Note",
|
|
Lines: []string{"Module " + item.Name + " is present but has no run() entry point."},
|
|
IsError: false,
|
|
}
|
|
}
|
|
}
|
|
a.listItems[idx].Enabled = true
|
|
}
|
|
|
|
return a, nil
|
|
}
|