ssh-tower 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ssh_tower-1.0.0/PKG-INFO +12 -0
- ssh_tower-1.0.0/pyproject.toml +26 -0
- ssh_tower-1.0.0/setup.cfg +4 -0
- ssh_tower-1.0.0/ssh_tower.egg-info/PKG-INFO +12 -0
- ssh_tower-1.0.0/ssh_tower.egg-info/SOURCES.txt +13 -0
- ssh_tower-1.0.0/ssh_tower.egg-info/dependency_links.txt +1 -0
- ssh_tower-1.0.0/ssh_tower.egg-info/entry_points.txt +2 -0
- ssh_tower-1.0.0/ssh_tower.egg-info/requires.txt +2 -0
- ssh_tower-1.0.0/ssh_tower.egg-info/top_level.txt +1 -0
- ssh_tower-1.0.0/tower/__init__.py +1 -0
- ssh_tower-1.0.0/tower/cli.py +183 -0
- ssh_tower-1.0.0/tower/pairing.py +118 -0
- ssh_tower-1.0.0/tower/scanner.py +72 -0
- ssh_tower-1.0.0/tower/security.py +64 -0
- ssh_tower-1.0.0/tower/ssh_manager.py +53 -0
ssh_tower-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssh-tower
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An advanced SSH connection manager with AirDrop-style UDP pairing and network scanning.
|
|
5
|
+
Author-email: JuniorSir <juniorsir.bot@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: paramiko>=3.0.0
|
|
12
|
+
Requires-Dist: cryptography>=43.0.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ssh-tower"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="JuniorSir", email="juniorsir.bot@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "An advanced SSH connection manager with AirDrop-style UDP pairing and network scanning."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"paramiko>=3.0.0",
|
|
21
|
+
"cryptography>=43.0.0"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
# This lets users type 'ssh-nexus' in their terminal to launch the app!
|
|
26
|
+
tower = "tower.cli:main"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssh-tower
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An advanced SSH connection manager with AirDrop-style UDP pairing and network scanning.
|
|
5
|
+
Author-email: JuniorSir <juniorsir.bot@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: paramiko>=3.0.0
|
|
12
|
+
Requires-Dist: cryptography>=43.0.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
ssh_tower.egg-info/PKG-INFO
|
|
3
|
+
ssh_tower.egg-info/SOURCES.txt
|
|
4
|
+
ssh_tower.egg-info/dependency_links.txt
|
|
5
|
+
ssh_tower.egg-info/entry_points.txt
|
|
6
|
+
ssh_tower.egg-info/requires.txt
|
|
7
|
+
ssh_tower.egg-info/top_level.txt
|
|
8
|
+
tower/__init__.py
|
|
9
|
+
tower/cli.py
|
|
10
|
+
tower/pairing.py
|
|
11
|
+
tower/scanner.py
|
|
12
|
+
tower/security.py
|
|
13
|
+
tower/ssh_manager.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tower
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import getpass
|
|
3
|
+
|
|
4
|
+
from tower.security import SecurityManager
|
|
5
|
+
from tower.scanner import NetworkScanner
|
|
6
|
+
from tower.ssh_manager import SSHSession
|
|
7
|
+
from tower.pairing import PairingSystem
|
|
8
|
+
|
|
9
|
+
# ANSI Colors
|
|
10
|
+
CYAN = '\033[96m'
|
|
11
|
+
MAGENTA = '\033[95m'
|
|
12
|
+
YELLOW = '\033[93m'
|
|
13
|
+
GREEN = '\033[92m'
|
|
14
|
+
RED = '\033[91m'
|
|
15
|
+
BLUE = '\033[94m'
|
|
16
|
+
BOLD = '\033[1m'
|
|
17
|
+
RESET = '\033[0m'
|
|
18
|
+
|
|
19
|
+
sec_mgr = SecurityManager()
|
|
20
|
+
session = SSHSession(sec_mgr)
|
|
21
|
+
pairing = PairingSystem(sec_mgr)
|
|
22
|
+
|
|
23
|
+
def print_banner():
|
|
24
|
+
print(f"\n{CYAN}{'='*50}{RESET}")
|
|
25
|
+
print(f"{BOLD}{CYAN} SSH tower{RESET} - Advanced Remote Management Tool")
|
|
26
|
+
print(f"{CYAN}{'='*50}{RESET}\n")
|
|
27
|
+
|
|
28
|
+
def scan_network_menu():
|
|
29
|
+
print(f"{YELLOW}Initiating high-speed network sweep (Port 22)...{RESET}")
|
|
30
|
+
hosts = NetworkScanner.scan_network()
|
|
31
|
+
|
|
32
|
+
if not hosts:
|
|
33
|
+
print(f"{RED}No SSH devices found on the local network.{RESET}\n")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
print(f"\n{BOLD}{CYAN}{'IP Address'.ljust(18)} {MAGENTA}Hostname{RESET}")
|
|
37
|
+
print("-" * 50)
|
|
38
|
+
for h in hosts:
|
|
39
|
+
print(f"{CYAN}{h['ip'].ljust(18)}{RESET} {MAGENTA}{h['hostname']}{RESET}")
|
|
40
|
+
print()
|
|
41
|
+
|
|
42
|
+
def connect_ssh_menu():
|
|
43
|
+
profiles = list(sec_mgr.db["profiles"].keys())
|
|
44
|
+
if not profiles:
|
|
45
|
+
print(f"{RED}No profiles found. Add one first.{RESET}\n")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
for idx, p in enumerate(profiles):
|
|
49
|
+
print(f"[{idx}] {p} ({sec_mgr.db['profiles'][p]['ip']})")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
choice = int(input(f"\nSelect profile index: "))
|
|
53
|
+
if 0 <= choice < len(profiles):
|
|
54
|
+
name = profiles[choice]
|
|
55
|
+
prof = sec_mgr.db["profiles"][name]
|
|
56
|
+
print(f"{YELLOW}Connecting to {name}...{RESET}")
|
|
57
|
+
success, msg = session.connect(name, prof)
|
|
58
|
+
|
|
59
|
+
if success:
|
|
60
|
+
print(f"{BOLD}{GREEN}Connection Established!{RESET}\n")
|
|
61
|
+
session_menu()
|
|
62
|
+
else:
|
|
63
|
+
print(f"{BOLD}{RED}Failed: {msg}{RESET}\n")
|
|
64
|
+
else:
|
|
65
|
+
print(f"{RED}Invalid index.{RESET}\n")
|
|
66
|
+
except ValueError:
|
|
67
|
+
print(f"{RED}Please enter a valid number.{RESET}\n")
|
|
68
|
+
|
|
69
|
+
def session_menu():
|
|
70
|
+
while session.connected:
|
|
71
|
+
print(f"\n{BOLD}{BLUE}--- Active Session Menu ---{RESET}")
|
|
72
|
+
print("1. Execute Command")
|
|
73
|
+
print("2. Freeze Device (Simulate)")
|
|
74
|
+
print("3. Disconnect")
|
|
75
|
+
|
|
76
|
+
choice = input(f"\nSelect action (1/2/3): ").strip()
|
|
77
|
+
|
|
78
|
+
if choice == "1":
|
|
79
|
+
cmd = input("Enter command: ")
|
|
80
|
+
out = session.execute(cmd)
|
|
81
|
+
print(f"\n{GREEN}--- Output ---{RESET}\n{out}\n{GREEN}--------------{RESET}")
|
|
82
|
+
elif choice == "2":
|
|
83
|
+
session.freeze_device()
|
|
84
|
+
print(f"{BOLD}{RED}Device Frozen (CPU spiked via dummy process).{RESET}")
|
|
85
|
+
elif choice == "3":
|
|
86
|
+
session.disconnect()
|
|
87
|
+
print(f"{YELLOW}Disconnected.{RESET}\n")
|
|
88
|
+
|
|
89
|
+
def add_profile_menu():
|
|
90
|
+
print(f"{BOLD}--- Add New SSH Profile ---{RESET}")
|
|
91
|
+
name = input("Profile Name: ")
|
|
92
|
+
ip = input("IP Address: ")
|
|
93
|
+
user = input("Username: ")
|
|
94
|
+
|
|
95
|
+
auth_type = ""
|
|
96
|
+
while auth_type not in ["1", "2"]:
|
|
97
|
+
auth_type = input("Auth Type [1=Password, 2=Key]: ").strip()
|
|
98
|
+
|
|
99
|
+
if auth_type == "1":
|
|
100
|
+
pwd = getpass.getpass("Password (hidden): ")
|
|
101
|
+
sec_mgr.add_profile(name, ip, 22, user, password=pwd)
|
|
102
|
+
else:
|
|
103
|
+
k_path = input("Private Key Path: ")
|
|
104
|
+
sec_mgr.add_profile(name, ip, 22, user, key_path=k_path)
|
|
105
|
+
|
|
106
|
+
print(f"{BOLD}{GREEN}Profile Saved!{RESET}\n")
|
|
107
|
+
|
|
108
|
+
def pairing_menu():
|
|
109
|
+
print(f"\n{BOLD}--- AirDrop Pairing System ---{RESET}")
|
|
110
|
+
print("1. Host a Pairing Session (Broadcast presence)")
|
|
111
|
+
print("2. Scan & Connect to a Host")
|
|
112
|
+
choice = input("\nSelect (1/2): ").strip()
|
|
113
|
+
|
|
114
|
+
if choice == "1":
|
|
115
|
+
pairing.start_host()
|
|
116
|
+
input(f"\n{MAGENTA}Press Enter at any time to stop hosting...{RESET}\n")
|
|
117
|
+
pairing.stop_host()
|
|
118
|
+
print(f"{YELLOW}Hosting stopped.{RESET}\n")
|
|
119
|
+
|
|
120
|
+
elif choice == "2":
|
|
121
|
+
print(f"{YELLOW}Scanning airwaves for tower Hosts (3 seconds)...{RESET}")
|
|
122
|
+
discovered = pairing.discover_hosts()
|
|
123
|
+
|
|
124
|
+
if not discovered:
|
|
125
|
+
print(f"{RED}No broadcasting hosts found on the network.{RESET}\n")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
print(f"\n{BOLD}{GREEN}Discovered Hosts:{RESET}")
|
|
129
|
+
host_list = list(discovered.items())
|
|
130
|
+
for idx, (ip, name) in enumerate(host_list):
|
|
131
|
+
print(f"[{idx}] {name} ({ip})")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
h_choice = int(input("\nSelect host index to pair with: "))
|
|
135
|
+
if 0 <= h_choice < len(host_list):
|
|
136
|
+
target_ip = host_list[h_choice][0]
|
|
137
|
+
pin = input(f"Enter the 6-digit PIN shown on {host_list[h_choice][1]}: ")
|
|
138
|
+
|
|
139
|
+
print(f"{YELLOW}Verifying PIN and establishing secure link...{RESET}")
|
|
140
|
+
success, data = pairing.connect_to_host(target_ip, pin)
|
|
141
|
+
|
|
142
|
+
if success:
|
|
143
|
+
print(f"{BOLD}{GREEN}Paired Successfully! Received Credentials.{RESET}")
|
|
144
|
+
sec_mgr.add_profile(f"Auto_{target_ip}", target_ip, 22, data["user"], password=data["password"])
|
|
145
|
+
else:
|
|
146
|
+
print(f"{BOLD}{RED}Pairing Failed: {data}{RESET}\n")
|
|
147
|
+
else:
|
|
148
|
+
print(f"{RED}Invalid selection.{RESET}\n")
|
|
149
|
+
except ValueError:
|
|
150
|
+
print(f"{RED}Please enter a valid number.{RESET}\n")
|
|
151
|
+
|
|
152
|
+
def main():
|
|
153
|
+
while True:
|
|
154
|
+
print_banner()
|
|
155
|
+
print("1. Connect to Profile")
|
|
156
|
+
print("2. Add Profile")
|
|
157
|
+
print(f"3. Scan Network {GREEN}(High-Speed){RESET}")
|
|
158
|
+
print(f"4. AirDrop Pairing {MAGENTA}(Auto-Discovery){RESET}")
|
|
159
|
+
print("5. Recent Connections History")
|
|
160
|
+
print("6. Exit")
|
|
161
|
+
|
|
162
|
+
choice = input(f"\n{BOLD}Choose an option:{RESET} ").strip()
|
|
163
|
+
print()
|
|
164
|
+
|
|
165
|
+
if choice == "1": connect_ssh_menu()
|
|
166
|
+
elif choice == "2": add_profile_menu()
|
|
167
|
+
elif choice == "3": scan_network_menu()
|
|
168
|
+
elif choice == "4": pairing_menu()
|
|
169
|
+
elif choice == "5":
|
|
170
|
+
print(f"{BOLD}--- Recent Connections ---{RESET}")
|
|
171
|
+
print(f"{CYAN}{'Name'.ljust(15)} {MAGENTA}{'IP'.ljust(15)} {GREEN}Duration{RESET}")
|
|
172
|
+
print("-" * 45)
|
|
173
|
+
for h in sec_mgr.db["history"]:
|
|
174
|
+
print(f"{CYAN}{h['name'].ljust(15)}{RESET} {MAGENTA}{h['ip'].ljust(15)}{RESET} {GREEN}{h['duration']}{RESET}")
|
|
175
|
+
print()
|
|
176
|
+
elif choice == "6":
|
|
177
|
+
print(f"{YELLOW}Exiting SSH tower...{RESET}")
|
|
178
|
+
sys.exit(0)
|
|
179
|
+
else:
|
|
180
|
+
print(f"{RED}Invalid option. Try again.{RESET}")
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
main()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import random
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
CYAN = '\033[96m'
|
|
8
|
+
YELLOW = '\033[93m'
|
|
9
|
+
GREEN = '\033[92m'
|
|
10
|
+
RED = '\033[91m'
|
|
11
|
+
BOLD = '\033[1m'
|
|
12
|
+
RESET = '\033[0m'
|
|
13
|
+
|
|
14
|
+
class PairingSystem:
|
|
15
|
+
def __init__(self, sec_mgr):
|
|
16
|
+
self.sec_mgr = sec_mgr
|
|
17
|
+
self.tcp_port = 55555
|
|
18
|
+
self.udp_port = 55556
|
|
19
|
+
self.active_pin = None
|
|
20
|
+
self.hosting = False
|
|
21
|
+
|
|
22
|
+
def start_host(self):
|
|
23
|
+
self.active_pin = str(random.randint(100000, 999999))
|
|
24
|
+
local_ip = socket.gethostbyname(socket.gethostname())
|
|
25
|
+
self.hosting = True
|
|
26
|
+
|
|
27
|
+
print(f"\n{BOLD}{CYAN}AirDrop Hosting Active!{RESET}")
|
|
28
|
+
print(f"Tell the other device to scan, then enter PIN: {BOLD}{YELLOW}{self.active_pin}{RESET}")
|
|
29
|
+
|
|
30
|
+
# Start TCP Listener for the actual PIN exchange
|
|
31
|
+
threading.Thread(target=self._listen_for_pairing, args=(local_ip,), daemon=True).start()
|
|
32
|
+
# Start UDP Broadcaster so clients can auto-discover this device
|
|
33
|
+
threading.Thread(target=self._broadcast_presence, args=(local_ip,), daemon=True).start()
|
|
34
|
+
|
|
35
|
+
def _broadcast_presence(self, local_ip):
|
|
36
|
+
"""Invisibly broadcasts presence to the local network."""
|
|
37
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
38
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
39
|
+
hostname = socket.gethostname()
|
|
40
|
+
|
|
41
|
+
while self.hosting:
|
|
42
|
+
msg = f"NEXUS:{hostname}:{local_ip}"
|
|
43
|
+
try:
|
|
44
|
+
s.sendto(msg.encode(), ('<broadcast>', self.udp_port))
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
time.sleep(1.5)
|
|
48
|
+
s.close()
|
|
49
|
+
|
|
50
|
+
def stop_host(self):
|
|
51
|
+
self.hosting = False
|
|
52
|
+
self.active_pin = None
|
|
53
|
+
|
|
54
|
+
def _listen_for_pairing(self, ip):
|
|
55
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
56
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
57
|
+
server.bind(("0.0.0.0", self.tcp_port))
|
|
58
|
+
server.listen(1)
|
|
59
|
+
server.settimeout(1) # Allows us to check if hosting was stopped
|
|
60
|
+
|
|
61
|
+
while self.hosting:
|
|
62
|
+
try:
|
|
63
|
+
client, addr = server.accept()
|
|
64
|
+
data = client.recv(1024).decode()
|
|
65
|
+
if data == self.active_pin:
|
|
66
|
+
print(f"\n{BOLD}{GREEN}[!] Device at {addr[0]} entered correct PIN!{RESET}")
|
|
67
|
+
print(f"{YELLOW}Securely transmitting SSH credentials...{RESET}\n")
|
|
68
|
+
# In a real app, prompt the host to ACCEPT/REJECT here.
|
|
69
|
+
creds = json.dumps({"user": "admin", "password": "dummy_password"})
|
|
70
|
+
client.send(self.sec_mgr.encrypt(creds).encode())
|
|
71
|
+
self.stop_host() # Stop hosting after success
|
|
72
|
+
else:
|
|
73
|
+
client.send(b"DENIED")
|
|
74
|
+
client.close()
|
|
75
|
+
except socket.timeout:
|
|
76
|
+
continue
|
|
77
|
+
except Exception:
|
|
78
|
+
break
|
|
79
|
+
server.close()
|
|
80
|
+
|
|
81
|
+
def discover_hosts(self):
|
|
82
|
+
"""Scans the network for active AirDrop hosts broadcasting UDP."""
|
|
83
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
84
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
85
|
+
s.bind(('', self.udp_port))
|
|
86
|
+
s.settimeout(3.0) # Listen for 3 seconds
|
|
87
|
+
|
|
88
|
+
hosts = {}
|
|
89
|
+
try:
|
|
90
|
+
while True:
|
|
91
|
+
data, addr = s.recvfrom(1024)
|
|
92
|
+
if data.startswith(b"NEXUS:"):
|
|
93
|
+
_, name, ip = data.decode().split(":")
|
|
94
|
+
if ip not in hosts:
|
|
95
|
+
hosts[ip] = name
|
|
96
|
+
except socket.timeout:
|
|
97
|
+
pass
|
|
98
|
+
finally:
|
|
99
|
+
s.close()
|
|
100
|
+
|
|
101
|
+
return hosts
|
|
102
|
+
|
|
103
|
+
def connect_to_host(self, ip, pin):
|
|
104
|
+
try:
|
|
105
|
+
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
106
|
+
client.settimeout(5)
|
|
107
|
+
client.connect((ip, self.tcp_port))
|
|
108
|
+
client.send(pin.encode())
|
|
109
|
+
response = client.recv(2048).decode()
|
|
110
|
+
client.close()
|
|
111
|
+
|
|
112
|
+
if response == "DENIED":
|
|
113
|
+
return False, "Pairing rejected or invalid PIN."
|
|
114
|
+
|
|
115
|
+
creds = json.loads(self.sec_mgr.decrypt(response))
|
|
116
|
+
return True, creds
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return False, f"Connection error: {str(e)}"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import subprocess
|
|
3
|
+
import platform
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
|
|
6
|
+
class NetworkScanner:
|
|
7
|
+
@staticmethod
|
|
8
|
+
def get_local_ip():
|
|
9
|
+
try:
|
|
10
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
11
|
+
s.connect(("8.8.8.8", 80))
|
|
12
|
+
ip = s.getsockname()[0]
|
|
13
|
+
s.close()
|
|
14
|
+
return ip
|
|
15
|
+
except Exception:
|
|
16
|
+
return "127.0.0.1"
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def get_arp_ips():
|
|
20
|
+
"""Reads the ARP table for instant local device discovery (Fastest)."""
|
|
21
|
+
ips = set()
|
|
22
|
+
try:
|
|
23
|
+
if platform.system() == "Linux" or "termux" in platform.release().lower():
|
|
24
|
+
with open('/proc/net/arp') as f:
|
|
25
|
+
next(f) # Skip header
|
|
26
|
+
for line in f:
|
|
27
|
+
parts = line.split()
|
|
28
|
+
if len(parts) >= 4 and parts[3] != "00:00:00:00:00:00":
|
|
29
|
+
ips.add(parts[0])
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return ips
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def scan_port(ip, port=22, timeout=0.2):
|
|
36
|
+
"""Checks if port 22 is open on a given IP."""
|
|
37
|
+
try:
|
|
38
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
39
|
+
s.settimeout(timeout)
|
|
40
|
+
result = s.connect_ex((ip, port))
|
|
41
|
+
s.close()
|
|
42
|
+
if result == 0:
|
|
43
|
+
try:
|
|
44
|
+
hostname = socket.gethostbyaddr(ip)[0]
|
|
45
|
+
except socket.herror:
|
|
46
|
+
hostname = "Unknown Device"
|
|
47
|
+
return {"ip": ip, "hostname": hostname}
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def scan_network():
|
|
54
|
+
"""Combines ARP checking with a threaded subnet sweep."""
|
|
55
|
+
local_ip = NetworkScanner.get_local_ip()
|
|
56
|
+
base_ip = ".".join(local_ip.split(".")[:-1]) + "."
|
|
57
|
+
|
|
58
|
+
# Build list of IPs to scan: 1-255 + any known ARP IPs
|
|
59
|
+
target_ips = set([f"{base_ip}{i}" for i in range(1, 255)])
|
|
60
|
+
target_ips.update(NetworkScanner.get_arp_ips())
|
|
61
|
+
|
|
62
|
+
active_hosts = []
|
|
63
|
+
|
|
64
|
+
# Aggressive 100-thread sweep for blazing fast discovery
|
|
65
|
+
with ThreadPoolExecutor(max_workers=100) as executor:
|
|
66
|
+
futures = [executor.submit(NetworkScanner.scan_port, ip) for ip in target_ips]
|
|
67
|
+
for future in futures:
|
|
68
|
+
result = future.result()
|
|
69
|
+
if result:
|
|
70
|
+
active_hosts.append(result)
|
|
71
|
+
|
|
72
|
+
return sorted(active_hosts, key=lambda x: x['ip'])
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from cryptography.fernet import Fernet
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
9
|
+
|
|
10
|
+
HOME_DIR = os.path.expanduser("~/.tower")
|
|
11
|
+
os.mkdir(HOME_DIR, exist_ok=True)
|
|
12
|
+
|
|
13
|
+
DB_FILE = os.path.join(HOME_DIR, "tower_data.json")
|
|
14
|
+
KEY_FILE = os.path.join(HOME_DIR, "tower.key")
|
|
15
|
+
LOG_FILE = os.path.join(HOME_DIR, "tower.log")
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(filename="nexus.log", level=logging.INFO,
|
|
18
|
+
format="%(asctime)s - %(levelname)s - %(message)s")
|
|
19
|
+
|
|
20
|
+
class SecurityManager:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.key = self._load_or_create_key()
|
|
23
|
+
self.cipher = Fernet(self.key)
|
|
24
|
+
self.db = self._load_db()
|
|
25
|
+
|
|
26
|
+
def _load_or_create_key(self):
|
|
27
|
+
if not os.path.exists(KEY_FILE):
|
|
28
|
+
key = Fernet.generate_key()
|
|
29
|
+
with open(KEY_FILE, "wb") as f:
|
|
30
|
+
f.write(key)
|
|
31
|
+
else:
|
|
32
|
+
with open(KEY_FILE, "rb") as f:
|
|
33
|
+
key = f.read()
|
|
34
|
+
return key
|
|
35
|
+
|
|
36
|
+
def _load_db(self):
|
|
37
|
+
if not os.path.exists(DB_FILE):
|
|
38
|
+
return {"profiles": {}, "history": [], "trusted": []}
|
|
39
|
+
with open(DB_FILE, "r") as f:
|
|
40
|
+
return json.load(f)
|
|
41
|
+
|
|
42
|
+
def save_db(self):
|
|
43
|
+
with open(DB_FILE, "w") as f:
|
|
44
|
+
json.dump(self.db, f, indent=4)
|
|
45
|
+
|
|
46
|
+
def encrypt(self, data: str) -> str:
|
|
47
|
+
return self.cipher.encrypt(data.encode()).decode()
|
|
48
|
+
|
|
49
|
+
def decrypt(self, data: str) -> str:
|
|
50
|
+
return self.cipher.decrypt(data.encode()).decode()
|
|
51
|
+
|
|
52
|
+
def add_profile(self, name, ip, port, user, password=None, key_path=None):
|
|
53
|
+
profile = {"ip": ip, "port": port, "user": user}
|
|
54
|
+
if password: profile["password"] = self.encrypt(password)
|
|
55
|
+
if key_path: profile["key_path"] = key_path
|
|
56
|
+
self.db["profiles"][name] = profile
|
|
57
|
+
self.save_db()
|
|
58
|
+
logging.info(f"Profile saved: {name}")
|
|
59
|
+
|
|
60
|
+
def log_connection(self, name, ip, duration):
|
|
61
|
+
entry = {"name": name, "ip": ip, "date": str(datetime.now()), "duration": duration}
|
|
62
|
+
self.db["history"].insert(0, entry)
|
|
63
|
+
self.db["history"] = self.db["history"][:10] # Keep last 10
|
|
64
|
+
self.save_db()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import paramiko
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
class SSHSession:
|
|
7
|
+
def __init__(self, sec_mgr):
|
|
8
|
+
self.client = paramiko.SSHClient()
|
|
9
|
+
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
10
|
+
self.sec_mgr = sec_mgr
|
|
11
|
+
self.connected = False
|
|
12
|
+
self.start_time = None
|
|
13
|
+
self.current_ip = None
|
|
14
|
+
self.current_name = None
|
|
15
|
+
|
|
16
|
+
def connect(self, name, profile):
|
|
17
|
+
try:
|
|
18
|
+
password = self.sec_mgr.decrypt(profile["password"]) if "password" in profile else None
|
|
19
|
+
key_path = profile.get("key_path")
|
|
20
|
+
|
|
21
|
+
if key_path:
|
|
22
|
+
self.client.connect(profile["ip"], port=profile["port"], username=profile["user"], key_filename=key_path)
|
|
23
|
+
else:
|
|
24
|
+
self.client.connect(profile["ip"], port=profile["port"], username=profile["user"], password=password)
|
|
25
|
+
|
|
26
|
+
self.connected = True
|
|
27
|
+
self.start_time = time.time()
|
|
28
|
+
self.current_ip = profile["ip"]
|
|
29
|
+
self.current_name = name
|
|
30
|
+
logging.info(f"Connected to {name} ({self.current_ip})")
|
|
31
|
+
return True, "Connected successfully"
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logging.error(f"Failed to connect to {name}: {e}")
|
|
34
|
+
return False, str(e)
|
|
35
|
+
|
|
36
|
+
def execute(self, command):
|
|
37
|
+
if not self.connected: return "Not connected."
|
|
38
|
+
stdin, stdout, stderr = self.client.exec_command(command)
|
|
39
|
+
return stdout.read().decode().strip() + stderr.read().decode().strip()
|
|
40
|
+
|
|
41
|
+
def freeze_device(self):
|
|
42
|
+
# Simulates freeze by running a dummy CPU max process in background
|
|
43
|
+
return self.execute('nohup python3 -c "while True: pass" > /dev/null 2>&1 & echo $!')
|
|
44
|
+
|
|
45
|
+
def shutdown_device(self):
|
|
46
|
+
return self.execute("sudo shutdown -h now")
|
|
47
|
+
|
|
48
|
+
def disconnect(self):
|
|
49
|
+
if self.connected:
|
|
50
|
+
duration = round(time.time() - self.start_time, 2)
|
|
51
|
+
self.client.close()
|
|
52
|
+
self.connected = False
|
|
53
|
+
self.sec_mgr.log_connection(self.current_name, self.current_ip, f"{duration}s")
|