sae8d commited on
Commit
55e2289
·
verified ·
1 Parent(s): 72a4577

Upload 16 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .env
5
+ .git/
6
+ .vscode/
7
+ .idea/
8
+ *.pkt
9
+ *.pka
10
+ test_*.py
11
+ Dockerfile
12
+ .dockerignore
13
+ requirements.txt
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GHAYMAH_BASE_URL=https://genai.ghaymah.systems
2
+ GHAYMAH_GENERATE_AI_KEY=sk-iKzhUlwdPKlGDhKDAi53Wg
Decipher/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Decipher package
Decipher/cisco_pwd.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def decrypt_cisco_type7(encrypted_hex):
2
+ """
3
+ Decrypts Cisco Type 7 passwords.
4
+ """
5
+ if not encrypted_hex or len(encrypted_hex) < 4:
6
+ return None
7
+
8
+ # Cisco Type 7 key
9
+ key = [
10
+ 0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f,
11
+ 0x41, 0x2c, 0x2e, 0x69, 0x79, 0x65, 0x77, 0x72,
12
+ 0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, 0x48, 0x53,
13
+ 0x55, 0x42
14
+ ]
15
+
16
+ try:
17
+ # First two digits are the index into the key
18
+ index = int(encrypted_hex[:2])
19
+ encrypted_data = encrypted_hex[2:]
20
+
21
+ decrypted = ""
22
+ for i in range(0, len(encrypted_data), 2):
23
+ # Get the next byte from the hex string
24
+ char_hex = encrypted_data[i:i+2]
25
+ char_code = int(char_hex, 16)
26
+
27
+ # XOR with the key and advance index
28
+ decrypted_char = chr(char_code ^ key[index % len(key)])
29
+ decrypted += decrypted_char
30
+ index += 1
31
+
32
+ return decrypted
33
+ except:
34
+ return None
Decipher/cmac.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+
3
+ BLOCK_SIZE = 16 # 128 bit for Twofish
4
+
5
+ def xor_bytes(a: bytes, b: bytes) -> bytes:
6
+ return bytes(x ^ y for x, y in zip(a, b))
7
+
8
+ def left_shift_one(bitstring: bytes) -> bytes:
9
+ out = bytearray(len(bitstring))
10
+ carry = 0
11
+ for i in reversed(range(len(bitstring))):
12
+ new = (bitstring[i] << 1) & 0xFF
13
+ out[i] = new | carry
14
+ carry = (bitstring[i] & 0x80) >> 7
15
+ return bytes(out)
16
+
17
+ def generate_subkeys(encrypt_block: Callable[[bytes], bytes]):
18
+ const_rb = 0x87
19
+ zero = bytes(BLOCK_SIZE)
20
+ L = encrypt_block(zero)
21
+
22
+ K1 = left_shift_one(L)
23
+ if L[0] & 0x80:
24
+ K1 = xor_bytes(K1, b'\x00' * 15 + bytes([const_rb]))
25
+
26
+ K2 = left_shift_one(K1)
27
+ if K1[0] & 0x80:
28
+ K2 = xor_bytes(K2, b'\x00' * 15 + bytes([const_rb]))
29
+
30
+ return K1, K2
31
+
32
+ def pad(block: bytes) -> bytes:
33
+ padded = block + b'\x80'
34
+ return padded + b'\x00' * (BLOCK_SIZE - len(padded))
35
+
36
+ class CMAC:
37
+ def __init__(self, encrypt_block: Callable[[bytes], bytes]):
38
+ self.encrypt_block = encrypt_block
39
+ self.K1, self.K2 = generate_subkeys(encrypt_block)
40
+
41
+ def digest(self, data: bytes) -> bytes:
42
+ if len(data) == 0:
43
+ last = xor_bytes(pad(b''), self.K2)
44
+ blocks = []
45
+ else:
46
+ blocks = [data[i:i+BLOCK_SIZE] for i in range(0, len(data), BLOCK_SIZE)]
47
+ if len(blocks[-1]) == BLOCK_SIZE:
48
+ last = xor_bytes(blocks[-1], self.K1)
49
+ blocks = blocks[:-1]
50
+ else:
51
+ last = xor_bytes(pad(blocks[-1]), self.K2)
52
+ blocks = blocks[:-1]
53
+
54
+ X = bytes(BLOCK_SIZE)
55
+ for block in blocks:
56
+ X = self.encrypt_block(xor_bytes(X, block))
57
+
58
+ return self.encrypt_block(xor_bytes(X, last))
Decipher/ctr.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+
3
+ BLOCK_SIZE = 16 # 128 bit
4
+
5
+
6
+ def inc_counter_be(counter: bytearray):
7
+ """Incrementa un contatore big-endian a 128 bit (come Crypto++)."""
8
+ for i in range(BLOCK_SIZE - 1, -1, -1):
9
+ counter[i] = (counter[i] + 1) & 0xFF
10
+ if counter[i] != 0:
11
+ break
12
+
13
+
14
+ class CTR:
15
+ def __init__(self, encrypt_block: Callable[[bytes], bytes], initial_counter: bytes):
16
+ assert len(initial_counter) == BLOCK_SIZE
17
+ self.encrypt_block = encrypt_block
18
+ self.counter = bytearray(initial_counter)
19
+
20
+ def process(self, data: bytes) -> bytes:
21
+ out = bytearray()
22
+ offset = 0
23
+
24
+ while offset < len(data):
25
+ keystream = self.encrypt_block(bytes(self.counter))
26
+ inc_counter_be(self.counter)
27
+
28
+ block = data[offset:offset + BLOCK_SIZE]
29
+ ks = keystream[:len(block)]
30
+ out.extend(b ^ k for b, k in zip(block, ks))
31
+ offset += BLOCK_SIZE
32
+
33
+ return bytes(out)
Decipher/eax.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+ from Decipher.cmac import CMAC, xor_bytes, BLOCK_SIZE
3
+ from Decipher.ctr import CTR
4
+
5
+
6
+ def _omac_with_prefix(cmac: CMAC, prefix: int, data: bytes) -> bytes:
7
+ # Prefisso di 16 byte: [0, 0, ..., prefix]
8
+ P = b'\x00' * (BLOCK_SIZE - 1) + bytes([prefix])
9
+ return cmac.digest(P + data)
10
+
11
+
12
+ class EAX:
13
+ def __init__(self, encrypt_block: Callable[[bytes], bytes]):
14
+ self.encrypt_block = encrypt_block
15
+ self.cmac = CMAC(encrypt_block)
16
+
17
+ def encrypt(self, nonce: bytes, plaintext: bytes, aad: bytes = b''):
18
+ # OMAC_0 = CMAC(0x00 || nonce)
19
+ n_tag = _omac_with_prefix(self.cmac, 0x00, nonce)
20
+
21
+ # OMAC_1 = CMAC(0x01 || aad)
22
+ h_tag = _omac_with_prefix(self.cmac, 0x01, aad)
23
+
24
+ # CTR parte da n_tag
25
+ ctr = CTR(self.encrypt_block, n_tag)
26
+ ciphertext = ctr.process(plaintext)
27
+
28
+ # OMAC_2 = CMAC(0x02 || ciphertext)
29
+ c_tag = _omac_with_prefix(self.cmac, 0x02, ciphertext)
30
+
31
+ # TAG finale = n_tag ⊕ h_tag ⊕ c_tag
32
+ tag = xor_bytes(xor_bytes(n_tag, h_tag), c_tag)
33
+
34
+ return ciphertext, tag
35
+
36
+ def decrypt(self, nonce: bytes, ciphertext: bytes, tag: bytes, aad: bytes = b''):
37
+ # Ricalcolo OMAC_0
38
+ n_tag = _omac_with_prefix(self.cmac, 0x00, nonce)
39
+
40
+ # CTR
41
+ ctr = CTR(self.encrypt_block, n_tag)
42
+ plaintext = ctr.process(ciphertext)
43
+
44
+ # Ricalcolo OMAC_1 e OMAC_2
45
+ h_tag = _omac_with_prefix(self.cmac, 0x01, aad)
46
+ c_tag = _omac_with_prefix(self.cmac, 0x02, ciphertext)
47
+
48
+ # Verifica TAG
49
+ expected_tag = xor_bytes(xor_bytes(n_tag, h_tag), c_tag)
50
+ if expected_tag != tag:
51
+ raise ValueError("EAX authentication failed")
52
+
53
+ return plaintext
Decipher/extract.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xml.etree.ElementTree as ET
2
+ import re
3
+ import base64
4
+ from ciscoconfparse import CiscoConfParse
5
+ from Decipher.cisco_pwd import decrypt_cisco_type7
6
+
7
+ def sanitize_xml_content(xml_bytes):
8
+ """
9
+ Sanitizes XML content by properly handling problematic binary data in attributes
10
+ instead of just deleting them.
11
+ """
12
+ # Robust decoding
13
+ xml_content = xml_bytes.decode('utf-8', errors='ignore')
14
+
15
+ def encode_binary_attr(match):
16
+ attr_name = match.group(1)
17
+ attr_value = match.group(2)
18
+ # Check if value contains non-XML friendly characters
19
+ if any(ord(c) < 32 and c not in '\n\r\t' for c in attr_value):
20
+ encoded = base64.b64encode(attr_value.encode('utf-8', errors='ignore')).decode('utf-8')
21
+ return f'{attr_name}="base64:{encoded}"'
22
+ return match.group(0)
23
+
24
+ # Find attributes that might contain binary data (like VALUE="...")
25
+ xml_content = re.sub(r'(\b\w+)="([^"]*)"', encode_binary_attr, xml_content)
26
+
27
+ # Ensure root tag is closed if it's a snippet
28
+ if "</PACKETTRACER5>" not in xml_content and "<PACKETTRACER5>" in xml_content:
29
+ xml_content += "</PACKETTRACER5>"
30
+ elif "</PACKETTRACER5_ACTIVITY>" not in xml_content and "<PACKETTRACER5_ACTIVITY>" in xml_content:
31
+ xml_content += "</PACKETTRACER5_ACTIVITY>"
32
+
33
+ return xml_content
34
+
35
+ def extract_structured_data(xml_path):
36
+ try:
37
+ with open(xml_path, 'rb') as f:
38
+ xml_bytes = f.read()
39
+
40
+ xml_content = sanitize_xml_content(xml_bytes)
41
+ root = ET.fromstring(xml_content)
42
+ except Exception as e:
43
+ print(f"[-] Error parsing {xml_path}: {e}")
44
+ return None
45
+
46
+ data = {
47
+ "version": root.findtext(".//VERSION"),
48
+ "devices": [],
49
+ "physical_links": [],
50
+ "wireless_connections": [],
51
+ "vlans": {},
52
+ "instructions": ""
53
+ }
54
+
55
+ # 1. Extract Devices and their properties
56
+ devices_by_id = {}
57
+ ssids_servers = {} # ssid -> device_name
58
+
59
+ for device in root.findall(".//DEVICE"):
60
+ engine = device.find("ENGINE")
61
+ if engine is None:
62
+ continue
63
+
64
+ name = engine.findtext("NAME")
65
+ dev_type = engine.findtext("TYPE")
66
+ model = engine.find("TYPE").get("model") if engine.find("TYPE") is not None else ""
67
+ ref_id = engine.findtext("SAVE_REF_ID")
68
+
69
+ dev_info = {
70
+ "name": name,
71
+ "type": dev_type,
72
+ "model": model,
73
+ "interfaces": [],
74
+ "wireless_info": None,
75
+ "vlans": []
76
+ }
77
+
78
+ if ref_id:
79
+ devices_by_id[ref_id] = name
80
+
81
+ # Extract Interfaces & IP info
82
+ for port in engine.findall(".//PORT"):
83
+ port_name = port.findtext("PORT_NAME") or port.findtext("NAME")
84
+ if not port_name:
85
+ # Try to find in parent SLOT/MODULE
86
+ parent_module = port.find("..")
87
+ if parent_module is not None:
88
+ port_type = port.findtext("TYPE")
89
+ if port_type:
90
+ port_name = port_type
91
+
92
+ if port_name:
93
+ ip = port.findtext("IP")
94
+ subnet = port.findtext("SUBNET")
95
+ ipv6 = port.findtext("IPV6_LINK_LOCAL")
96
+
97
+ if ip or ipv6 or port.findtext("PORT_DHCP_ENABLE") == "true":
98
+ dev_info["interfaces"].append({
99
+ "name": port_name,
100
+ "ip": ip if ip else "DHCP" if port.findtext("PORT_DHCP_ENABLE") == "true" else None,
101
+ "subnet": subnet,
102
+ "ipv6": ipv6
103
+ })
104
+
105
+ # Extract Wireless Server info (Access Points)
106
+ wireless_server = engine.find("WIRELESS_SERVER")
107
+ if wireless_server is not None:
108
+ common = wireless_server.find("WIRELESS_COMMON")
109
+ if common is not None:
110
+ ssid = common.findtext("SSID")
111
+ dev_info["wireless_info"] = {
112
+ "role": "server",
113
+ "ssid": ssid,
114
+ "encryption": common.findtext("ENCRYPT_TYPE"),
115
+ "key": common.find(".//KEY").text if common.find(".//KEY") is not None else None
116
+ }
117
+ if ssid:
118
+ ssids_servers[ssid] = name
119
+
120
+ # Extract Wireless Client info (PC/Laptop/Phone)
121
+ wireless_client = engine.find("WIRELESS_CLIENT")
122
+ if wireless_client is not None:
123
+ common = wireless_client.find("WIRELESS_COMMON")
124
+ if common is not None:
125
+ ssid = common.findtext("SSID")
126
+ dev_info["wireless_info"] = {
127
+ "role": "client",
128
+ "ssid": ssid,
129
+ "encryption": common.findtext("ENCRYPT_TYPE")
130
+ }
131
+
132
+ # Extract VLANs from device-specific storage (Switches)
133
+ vlan_storage = engine.findall(".//VLANS/VLAN")
134
+ for v in vlan_storage:
135
+ v_num = v.get("number")
136
+ v_name = v.get("name")
137
+ if v_num and v_num not in data["vlans"]:
138
+ data["vlans"][v_num] = v_name
139
+ dev_info["vlans"].append({"id": v_num, "name": v_name})
140
+
141
+ # Parse RUNNINGCONFIG using CiscoConfParse for deeper analysis
142
+ config = engine.findtext("RUNNINGCONFIG")
143
+ if config:
144
+ parse = CiscoConfParse(config.splitlines(), factory=True)
145
+
146
+ # 1. Improved VLAN assignments
147
+ for intf_obj in parse.find_objects(r'^interface'):
148
+ intf_name = intf_obj.text.replace('interface ', '').strip()
149
+ vlan_id = None
150
+
151
+ # Check for access vlan
152
+ access_vlan = intf_obj.re_search_children(r'switchport access vlan (\d+)')
153
+ if access_vlan:
154
+ vlan_id = access_vlan[0].re_match_typed(r'switchport access vlan (\d+)')
155
+
156
+ if vlan_id:
157
+ # Update or add interface info
158
+ found = False
159
+ for i in dev_info["interfaces"]:
160
+ if i["name"] == intf_name:
161
+ i["vlan"] = vlan_id
162
+ found = True
163
+ break
164
+ if not found:
165
+ dev_info["interfaces"].append({"name": intf_name, "vlan": vlan_id})
166
+
167
+ # 2. L3/L4 Support: Routing Protocols (Static, OSPF)
168
+ dev_info["routing"] = {
169
+ "static_routes": [],
170
+ "ospf": [],
171
+ "nat": [],
172
+ "dhcp_pools": []
173
+ }
174
+
175
+ # Static Routes
176
+ static_routes = parse.find_objects(r'^ip route')
177
+ for route in static_routes:
178
+ dev_info["routing"]["static_routes"].append(route.text)
179
+
180
+ # OSPF
181
+ ospf_processes = parse.find_objects(r'^router ospf')
182
+ for ospf in ospf_processes:
183
+ ospf_info = {"process_id": ospf.text.split()[-1], "networks": []}
184
+ for network in ospf.re_search_children(r'network'):
185
+ ospf_info["networks"].append(network.text.strip())
186
+ dev_info["routing"]["ospf"].append(ospf_info)
187
+
188
+ # NAT
189
+ nat_rules = parse.find_objects(r'^ip nat')
190
+ for rule in nat_rules:
191
+ dev_info["routing"]["nat"].append(rule.text)
192
+
193
+ # DHCP Pools
194
+ dhcp_pools = parse.find_objects(r'^ip dhcp pool')
195
+ for pool in dhcp_pools:
196
+ pool_info = {
197
+ "name": pool.text.replace('ip dhcp pool ', '').strip(),
198
+ "details": [child.text.strip() for child in pool.children]
199
+ }
200
+ dev_info["routing"]["dhcp_pools"].append(pool_info)
201
+
202
+ # 3. Security Audit & ACLs
203
+ dev_info["security"] = {
204
+ "acls": [],
205
+ "vulnerabilities": [],
206
+ "decrypted_passwords": []
207
+ }
208
+
209
+ # Decrypt Type 7 passwords
210
+ type7_pwds = re.findall(r'password 7 ([0-9A-Fa-f]+)|enable password 7 ([0-9A-Fa-f]+)', config)
211
+ for pwd_match in type7_pwds:
212
+ encrypted = pwd_match[0] or pwd_match[1]
213
+ decrypted = decrypt_cisco_type7(encrypted)
214
+ if decrypted:
215
+ dev_info["security"]["decrypted_passwords"].append({
216
+ "encrypted": encrypted,
217
+ "decrypted": decrypted,
218
+ "type": "Cisco Type 7"
219
+ })
220
+ dev_info["security"]["vulnerabilities"].append(f"Weak encryption found: Decrypted password '{decrypted}'")
221
+
222
+ # Standard and Extended ACLs
223
+ acls = parse.find_objects(r'^access-list|ip access-list')
224
+ for acl in acls:
225
+ acl_info = {"name": acl.text.strip(), "rules": []}
226
+ if acl.children:
227
+ acl_info["rules"] = [child.text.strip() for child in acl.children]
228
+ dev_info["security"]["acls"].append(acl_info)
229
+
230
+ # Security Audit: Check for plaintext passwords or weak services
231
+ if parse.find_objects(r'^no service password-encryption'):
232
+ dev_info["security"]["vulnerabilities"].append("Plaintext password storage enabled (no service password-encryption)")
233
+
234
+ if parse.find_objects(r'^line vty'):
235
+ vty_lines = parse.find_objects(r'^line vty')
236
+ for vty in vty_lines:
237
+ if not vty.re_search_children(r'transport input ssh'):
238
+ dev_info["security"]["vulnerabilities"].append(f"Insecure remote access (Telnet allowed on {vty.text.strip()})")
239
+
240
+ # 4. Server Services (if applicable)
241
+ dev_info["services"] = []
242
+ services_node = engine.find("SERVICES")
243
+ if services_node is not None:
244
+ # HTTP Service
245
+ http = services_node.find("HTTP")
246
+ if http is not None and http.get("enabled") == "true":
247
+ dev_info["services"].append({"type": "HTTP", "port": 80})
248
+
249
+ # DNS Service
250
+ dns = services_node.find("DNS")
251
+ if dns is not None and dns.get("enabled") == "true":
252
+ dns_info = {"type": "DNS", "records": []}
253
+ for record in dns.findall("RECORD"):
254
+ dns_info["records"].append({
255
+ "name": record.get("name"),
256
+ "type": record.get("type"),
257
+ "value": record.get("value")
258
+ })
259
+ dev_info["services"].append(dns_info)
260
+
261
+ data["devices"].append(dev_info)
262
+
263
+ # 2. Extract Physical Links
264
+ for link in root.findall(".//LINK"):
265
+ cable = link.find("CABLE")
266
+ if cable is not None:
267
+ ports = cable.findall("PORT")
268
+ from_id = cable.findtext("FROM")
269
+ to_id = cable.findtext("TO")
270
+
271
+ link_info = {
272
+ "type": link.findtext("TYPE"),
273
+ "from_device": devices_by_id.get(from_id, from_id),
274
+ "to_device": devices_by_id.get(to_id, to_id),
275
+ }
276
+ if len(ports) >= 2:
277
+ link_info["from_port"] = ports[0].text
278
+ link_info["to_port"] = ports[1].text
279
+
280
+ data["physical_links"].append(link_info)
281
+
282
+ # 3. Establish Wireless Connections
283
+ for dev in data["devices"]:
284
+ if dev["wireless_info"] and dev["wireless_info"]["role"] == "client":
285
+ ssid = dev["wireless_info"]["ssid"]
286
+ if ssid in ssids_servers:
287
+ data["wireless_connections"].append({
288
+ "from_device": dev["name"],
289
+ "to_device": ssids_servers[ssid],
290
+ "ssid": ssid,
291
+ "type": "wireless"
292
+ })
293
+
294
+ # 4. Extract Instructions
295
+ instr_page = root.find(".//INSTRUCTIONS/PAGE")
296
+ if instr_page is not None:
297
+ data["instructions"] = instr_page.text
298
+
299
+ return data
Decipher/generator.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from openai import OpenAI
4
+ from dotenv import load_dotenv
5
+ from Decipher.models import NetworkTopology
6
+ from Decipher.validator import validate_topology
7
+
8
+ load_dotenv()
9
+
10
+ client = OpenAI(
11
+ api_key=os.getenv("GHAYMAH_GENERATE_AI_KEY"),
12
+ base_url=os.getenv("GHAYMAH_BASE_URL"),
13
+ timeout=60.0 # Increase timeout to 60 seconds
14
+ )
15
+ SYSTEM_PROMPT = """
16
+ You are a Senior Network Architect. Your task is to generate a highly accurate and valid network topology JSON based on a user's description.
17
+ The output MUST strictly follow the provided JSON schema.
18
+
19
+ Principles for accuracy:
20
+ 1. IP Addressing: Ensure devices in the same segment have unique IPs in the same subnet.
21
+ 2. Connectivity: Define physical links correctly between existing devices and ports.
22
+ 3. Routing: If OSPF or static routes are requested, ensure the configuration is logically sound.
23
+ 4. VLANs: Ensure ports are assigned to correct VLANs if specified.
24
+ 5. Device Types: Use realistic device types (Router, Switch, PC, Server, etc.) and models (e.g., 2911, 2960).
25
+
26
+ You must respond ONLY with the valid JSON object. No markdown, no conversational text.
27
+ Ensure all relevant arrays (devices, physical_links) are populated based on the user's request. Do not return empty arrays if the user asked for specific components.
28
+
29
+ Example Output Structure:
30
+ {
31
+ "version": "1.0",
32
+ "devices": [
33
+ {
34
+ "name": "Router1",
35
+ "type": "Router",
36
+ "model": "2911",
37
+ "interfaces": [{"name": "GigabitEthernet0/0", "ip": "192.168.1.1", "subnet": "255.255.255.0"}],
38
+ "routing": {"static_routes": [], "ospf": [{"process_id": "1", "networks": ["192.168.1.0 0.0.0.255 area 0"]}]}
39
+ }
40
+ ],
41
+ "physical_links": [
42
+ {"type": "Copper", "from_device": "Router1", "to_device": "Switch1", "from_port": "GigabitEthernet0/0", "to_port": "FastEthernet0/1"}
43
+ ],
44
+ "vlans": {"10": "Sales", "20": "HR"},
45
+ "instructions": "Brief summary of the network..."
46
+ }
47
+ """
48
+
49
+
50
+ def generate_network(prompt: str) -> NetworkTopology:
51
+ try:
52
+ response = client.chat.completions.create(
53
+ model="DeepSeek-V3-0324", # Using available model
54
+ messages=[
55
+ {"role": "system", "content": SYSTEM_PROMPT},
56
+ {"role": "user", "content": f"Generate a network topology for: {prompt}"}
57
+ ],
58
+ response_format={"type": "json_object"}
59
+ )
60
+
61
+ content = response.choices[0].message.content
62
+ print(f"--- RAW LLM RESPONSE ---\n{content}\n-----------------------")
63
+ data = json.loads(content)
64
+
65
+ if not data.get("devices") and not data.get("physical_links"):
66
+ # If AI returned an empty-ish object, it might have failed to understand.
67
+ # We can try to raise an error so the API doesn't return an empty success.
68
+ raise ValueError("AI generated an empty network topology. Please try a more descriptive prompt.")
69
+
70
+ # Validate and parse into Pydantic model
71
+ topology = NetworkTopology(**data)
72
+
73
+ # Intelligent Validation Layer
74
+ validation_issues = validate_topology(topology)
75
+ if validation_issues:
76
+ # Append issues to instructions so user is aware of logical warnings
77
+ issue_text = "\n\n[Accuracy Layer Warnings]:\n- " + "\n- ".join(validation_issues)
78
+ topology.instructions += issue_text
79
+
80
+ return topology
81
+ except Exception as e:
82
+ print(f"Error generating network: {e}")
83
+ raise e
Decipher/models.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional, Dict, Any
3
+
4
+ class Interface(BaseModel):
5
+ name: str
6
+ ip: Optional[str] = None
7
+ subnet: Optional[str] = None
8
+ ipv6: Optional[str] = None
9
+ vlan: Optional[str] = None
10
+
11
+ class WirelessInfo(BaseModel):
12
+ role: str # "server" or "client"
13
+ ssid: Optional[str] = None
14
+ encryption: Optional[str] = None
15
+ key: Optional[str] = None
16
+
17
+ class OSPFNetwork(BaseModel):
18
+ network: str
19
+
20
+ class OSPFProcess(BaseModel):
21
+ process_id: str
22
+ networks: List[str]
23
+
24
+ class Routing(BaseModel):
25
+ static_routes: List[str] = []
26
+ ospf: List[OSPFProcess] = []
27
+ nat: List[str] = []
28
+ dhcp_pools: List[Dict[str, Any]] = []
29
+
30
+ class Security(BaseModel):
31
+ acls: List[Dict[str, Any]] = []
32
+ vulnerabilities: List[str] = []
33
+ decrypted_passwords: List[Dict[str, Any]] = []
34
+
35
+ class Device(BaseModel):
36
+ name: str
37
+ type: str
38
+ model: Optional[str] = ""
39
+ interfaces: List[Interface] = []
40
+ wireless_info: Optional[WirelessInfo] = None
41
+ vlans: List[Dict[str, str]] = []
42
+ routing: Optional[Routing] = None
43
+ security: Optional[Security] = None
44
+ services: List[Dict[str, Any]] = []
45
+
46
+ class PhysicalLink(BaseModel):
47
+ type: str
48
+ from_device: str
49
+ to_device: str
50
+ from_port: Optional[str] = None
51
+ to_port: Optional[str] = None
52
+
53
+ class WirelessConnection(BaseModel):
54
+ from_device: str
55
+ to_device: str
56
+ ssid: str
57
+ type: str = "wireless"
58
+
59
+ class NetworkTopology(BaseModel):
60
+ version: Optional[str] = "1.0"
61
+ devices: List[Device] = []
62
+ physical_links: List[PhysicalLink] = []
63
+ wireless_connections: List[WirelessConnection] = []
64
+ vlans: Dict[str, str] = {}
65
+ instructions: str = ""
66
+
67
+ model_config = {
68
+ "json_schema_extra": {
69
+ "example": {
70
+ "version": "1.0",
71
+ "devices": [
72
+ {
73
+ "name": "Router-ISP",
74
+ "type": "Router",
75
+ "model": "2911",
76
+ "interfaces": [
77
+ {"name": "GigabitEthernet0/0", "ip": "10.0.0.1", "subnet": "255.255.255.0"}
78
+ ]
79
+ }
80
+ ],
81
+ "physical_links": [],
82
+ "instructions": "This is an example topology."
83
+ }
84
+ }
85
+ }
Decipher/pt_crypto.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from Decipher.eax import EAX
2
+ from Decipher.twofish import Twofish
3
+ import zlib
4
+ import struct
5
+
6
+ def deobf_stage1(data: bytes) -> bytes:
7
+ L = len(data)
8
+ res = bytearray(L)
9
+ for i in range(L):
10
+ res[i] = data[L-1-i] ^ (L - i*L & 0xFF)
11
+ return bytes(res)
12
+
13
+ def deobf_stage2(data: bytes) -> bytes:
14
+ L = len(data)
15
+ res = bytearray(L)
16
+ for i, b in enumerate(data):
17
+ res[i] = b ^ (L - i & 0xFF)
18
+ return bytes(res)
19
+
20
+ def uncompress_qt(blob: bytes) -> bytes:
21
+ size = struct.unpack(">I", blob[:4])[0]
22
+ return zlib.decompress(blob[4:])[:size]
23
+
24
+ def decrypt_pkt(pkt: bytes) -> bytes:
25
+ # Stage 1 deobfuscation
26
+ stage1 = deobf_stage1(pkt)
27
+
28
+ # Chiave e IV per i file .pkt
29
+ key = bytes([137])*16
30
+ iv = bytes([16])*16
31
+
32
+ # Twofish block cipher
33
+ tf = Twofish(key)
34
+ encrypt_block = tf.encrypt
35
+
36
+ # EAX con nonce = iv
37
+ eax = EAX(encrypt_block)
38
+
39
+ # Supponiamo che negli .pkt il tag sia alla fine
40
+ ciphertext = stage1[:-16]
41
+ tag = stage1[-16:]
42
+
43
+ # Decrypt usando nonce fisso
44
+ decrypted = eax.decrypt(nonce=iv, ciphertext=ciphertext, tag=tag)
45
+
46
+ # Stage 2 deobfuscation
47
+ stage2 = deobf_stage2(decrypted)
48
+
49
+ # Decompressione
50
+ xml = uncompress_qt(stage2)
51
+
52
+ return xml
Decipher/twofish.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## twofish.py - pure Python implementation of the Twofish algorithm.
2
+ ## Bjorn Edstrom <be@bjrn.se> 13 december 2007.
3
+ ##
4
+ ## Copyrights
5
+ ## ==========
6
+ ##
7
+ ## This code is a derived from an implementation by Dr Brian Gladman
8
+ ## (gladman@seven77.demon.co.uk) which is subject to the following license.
9
+ ## This Python implementation is not subject to any other license.
10
+ ##
11
+ ##/* This is an independent implementation of the encryption algorithm: */
12
+ ##/* */
13
+ ##/* Twofish by Bruce Schneier and colleagues */
14
+ ##/* */
15
+ ##/* which is a candidate algorithm in the Advanced Encryption Standard */
16
+ ##/* programme of the US National Institute of Standards and Technology. */
17
+ ##/* */
18
+ ##/* Copyright in this implementation is held by Dr B R Gladman but I */
19
+ ##/* hereby give permission for its free direct or derivative use subject */
20
+ ##/* to acknowledgment of its origin and compliance with any conditions */
21
+ ##/* that the originators of t he algorithm place on its exploitation. */
22
+ ##/* */
23
+ ##/* My thanks to Doug Whiting and Niels Ferguson for comments that led */
24
+ ##/* to improvements in this implementation. */
25
+ ##/* */
26
+ ##/* Dr Brian Gladman (gladman@seven77.demon.co.uk) 14th January 1999 */
27
+ ##
28
+ ## The above copyright notice must not be removed.
29
+ ##
30
+ ## Information
31
+ ## ===========
32
+ ##
33
+ ## Anyone thinking of using this code should reconsider. It's slow.
34
+ ## Try python-mcrypt instead. In case a faster library is not installed
35
+ ## on the target system, this code can be used as a portable fallback.
36
+
37
+ # pylint: disable-all
38
+
39
+ block_size = 16
40
+ key_size = 32
41
+
42
+ class Twofish:
43
+
44
+ def __init__(self, key=None):
45
+ """Twofish."""
46
+
47
+ if key:
48
+ self.set_key(key)
49
+
50
+
51
+ def set_key(self, key):
52
+ """Init."""
53
+
54
+ key_len = len(key)
55
+ if key_len not in [16, 24, 32]:
56
+ # XXX: add padding?
57
+ raise KeyError("key must be 16, 24 or 32 bytes")
58
+ if key_len % 4:
59
+ # XXX: add padding?
60
+ raise KeyError("key not a multiple of 4")
61
+ if key_len > 32:
62
+ # XXX: prune?
63
+ raise KeyError("key_len > 32")
64
+
65
+ self.context = TWI()
66
+
67
+ key_word32 = [0] * 32
68
+ i = 0
69
+ while key:
70
+ key_word32[i] = struct.unpack("<L", key[0:4])[0]
71
+ key = key[4:]
72
+ i += 1
73
+
74
+ set_key(self.context, key_word32, key_len)
75
+
76
+
77
+ def decrypt(self, block):
78
+ """Decrypt blocks."""
79
+
80
+ if len(block) % 16:
81
+ raise ValueError("block size must be a multiple of 16")
82
+
83
+ plaintext = b''
84
+
85
+ while block:
86
+ a, b, c, d = struct.unpack("<4L", block[:16])
87
+ temp = [a, b, c, d]
88
+ decrypt(self.context, temp)
89
+ plaintext += struct.pack("<4L", *temp)
90
+ block = block[16:]
91
+
92
+ return plaintext
93
+
94
+
95
+ def encrypt(self, block):
96
+ """Encrypt blocks."""
97
+
98
+ if len(block) % 16:
99
+ raise ValueError("block size must be a multiple of 16")
100
+
101
+ ciphertext = b''
102
+
103
+ while block:
104
+ a, b, c, d = struct.unpack("<4L", block[0:16])
105
+ temp = [a, b, c, d]
106
+ encrypt(self.context, temp)
107
+ ciphertext += struct.pack("<4L", *temp)
108
+ block = block[16:]
109
+
110
+ return ciphertext
111
+
112
+
113
+ def get_name(self):
114
+ """Return the name of the cipher."""
115
+
116
+ return "Twofish"
117
+
118
+
119
+ def get_block_size(self):
120
+ """Get cipher block size in bytes."""
121
+
122
+ return 16
123
+
124
+
125
+ def get_key_size(self):
126
+ """Get cipher key size in bytes."""
127
+
128
+ return 32
129
+
130
+
131
+ #
132
+ # Private.
133
+ #
134
+
135
+ import struct
136
+ import sys
137
+
138
+ WORD_BIGENDIAN = 0
139
+ if sys.byteorder == 'big':
140
+ WORD_BIGENDIAN = 1
141
+
142
+ def rotr32(x, n):
143
+ return (x >> n) | ((x << (32 - n)) & 0xFFFFFFFF)
144
+
145
+ def rotl32(x, n):
146
+ return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))
147
+
148
+ def byteswap32(x):
149
+ return ((x & 0xff) << 24) | (((x >> 8) & 0xff) << 16) | \
150
+ (((x >> 16) & 0xff) << 8) | ((x >> 24) & 0xff)
151
+
152
+ class TWI:
153
+ def __init__(self):
154
+ self.k_len = 0 # word32
155
+ self.l_key = [0]*40 # word32
156
+ self.s_key = [0]*4 # word32
157
+ self.qt_gen = 0 # word32
158
+ self.q_tab = [[0]*256, [0]*256] # byte
159
+ self.mt_gen = 0 # word32
160
+ self.m_tab = [[0]*256, [0]*256, [0]*256, [0]*256] # word32
161
+ self.mk_tab = [[0]*256, [0]*256, [0]*256, [0]*256] # word32
162
+
163
+ def byte(x, n):
164
+ return (x >> (8 * n)) & 0xff
165
+
166
+ tab_5b = [0, 90, 180, 238]
167
+ tab_ef = [0, 238, 180, 90]
168
+ ror4 = [0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15]
169
+ ashx = [0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 5, 14, 7]
170
+ qt0 = [[8, 1, 7, 13, 6, 15, 3, 2, 0, 11, 5, 9, 14, 12, 10, 4],
171
+ [2, 8, 11, 13, 15, 7, 6, 14, 3, 1, 9, 4, 0, 10, 12, 5]]
172
+ qt1 = [[14, 12, 11, 8, 1, 2, 3, 5, 15, 4, 10, 6, 7, 0, 9, 13],
173
+ [1, 14, 2, 11, 4, 12, 3, 7, 6, 13, 10, 5, 15, 9, 0, 8]]
174
+ qt2 = [[11, 10, 5, 14, 6, 13, 9, 0, 12, 8, 15, 3, 2, 4, 7, 1],
175
+ [4, 12, 7, 5, 1, 6, 9, 10, 0, 14, 13, 8, 2, 11, 3, 15]]
176
+ qt3 = [[13, 7, 15, 4, 1, 2, 6, 14, 9, 11, 3, 0, 8, 5, 12, 10],
177
+ [11, 9, 5, 1, 12, 3, 13, 14, 6, 4, 7, 15, 2, 0, 8, 10]]
178
+
179
+ def qp(n, x): # word32, byte
180
+ n %= 0x100000000
181
+ x %= 0x100
182
+ a0 = x >> 4;
183
+ b0 = x & 15;
184
+ a1 = a0 ^ b0;
185
+ b1 = ror4[b0] ^ ashx[a0];
186
+ a2 = qt0[n][a1];
187
+ b2 = qt1[n][b1];
188
+ a3 = a2 ^ b2;
189
+ b3 = ror4[b2] ^ ashx[a2];
190
+ a4 = qt2[n][a3];
191
+ b4 = qt3[n][b3];
192
+ return (b4 << 4) | a4;
193
+
194
+ def gen_qtab(pkey):
195
+ for i in range(256):
196
+ pkey.q_tab[0][i] = qp(0, i)
197
+ pkey.q_tab[1][i] = qp(1, i)
198
+
199
+ def gen_mtab(pkey):
200
+ for i in range(256):
201
+ f01 = pkey.q_tab[1][i]
202
+ f01 = pkey.q_tab[1][i];
203
+ f5b = ((f01) ^ ((f01) >> 2) ^ tab_5b[(f01) & 3]);
204
+ fef = ((f01) ^ ((f01) >> 1) ^ ((f01) >> 2) ^ tab_ef[(f01) & 3]);
205
+ pkey.m_tab[0][i] = f01 + (f5b << 8) + (fef << 16) + (fef << 24);
206
+ pkey.m_tab[2][i] = f5b + (fef << 8) + (f01 << 16) + (fef << 24);
207
+
208
+ f01 = pkey.q_tab[0][i];
209
+ f5b = ((f01) ^ ((f01) >> 2) ^ tab_5b[(f01) & 3]);
210
+ fef = ((f01) ^ ((f01) >> 1) ^ ((f01) >> 2) ^ tab_ef[(f01) & 3]);
211
+ pkey.m_tab[1][i] = fef + (fef << 8) + (f5b << 16) + (f01 << 24);
212
+ pkey.m_tab[3][i] = f5b + (f01 << 8) + (fef << 16) + (f5b << 24);
213
+
214
+ def gen_mk_tab(pkey, key):
215
+ if pkey.k_len == 2:
216
+ for i in range(256):
217
+ by = i % 0x100
218
+ pkey.mk_tab[0][i] = pkey.m_tab[0][pkey.q_tab[0][pkey.q_tab[0][by] ^ byte(key[1],0)] ^ byte(key[0],0)];
219
+ pkey.mk_tab[1][i] = pkey.m_tab[1][pkey.q_tab[0][pkey.q_tab[1][by] ^ byte(key[1],1)] ^ byte(key[0],1)];
220
+ pkey.mk_tab[2][i] = pkey.m_tab[2][pkey.q_tab[1][pkey.q_tab[0][by] ^ byte(key[1],2)] ^ byte(key[0],2)];
221
+ pkey.mk_tab[3][i] = pkey.m_tab[3][pkey.q_tab[1][pkey.q_tab[1][by] ^ byte(key[1],3)] ^ byte(key[0],3)];
222
+ if pkey.k_len == 3:
223
+ for i in range(256):
224
+ by = i % 0x100
225
+ pkey.mk_tab[0][i] = pkey.m_tab[0][pkey.q_tab[0][pkey.q_tab[0][pkey.q_tab[1][by] ^ byte(key[2], 0)] ^ byte(key[1], 0)] ^ byte(key[0], 0)];
226
+ pkey.mk_tab[1][i] = pkey.m_tab[1][pkey.q_tab[0][pkey.q_tab[1][pkey.q_tab[1][by] ^ byte(key[2], 1)] ^ byte(key[1], 1)] ^ byte(key[0], 1)];
227
+ pkey.mk_tab[2][i] = pkey.m_tab[2][pkey.q_tab[1][pkey.q_tab[0][pkey.q_tab[0][by] ^ byte(key[2], 2)] ^ byte(key[1], 2)] ^ byte(key[0], 2)];
228
+ pkey.mk_tab[3][i] = pkey.m_tab[3][pkey.q_tab[1][pkey.q_tab[1][pkey.q_tab[0][by] ^ byte(key[2], 3)] ^ byte(key[1], 3)] ^ byte(key[0], 3)];
229
+ if pkey.k_len == 4:
230
+ for i in range(256):
231
+ by = i % 0x100
232
+ pkey.mk_tab[0][i] = pkey.m_tab[0][pkey.q_tab[0][pkey.q_tab[0][pkey.q_tab[1][pkey.q_tab[1][by] ^ byte(key[3], 0)] ^ byte(key[2], 0)] ^ byte(key[1], 0)] ^ byte(key[0], 0)];
233
+ pkey.mk_tab[1][i] = pkey.m_tab[1][pkey.q_tab[0][pkey.q_tab[1][pkey.q_tab[1][pkey.q_tab[0][by] ^ byte(key[3], 1)] ^ byte(key[2], 1)] ^ byte(key[1], 1)] ^ byte(key[0], 1)];
234
+ pkey.mk_tab[2][i] = pkey.m_tab[2][pkey.q_tab[1][pkey.q_tab[0][pkey.q_tab[0][pkey.q_tab[0][by] ^ byte(key[3], 2)] ^ byte(key[2], 2)] ^ byte(key[1], 2)] ^ byte(key[0], 2)];
235
+ pkey.mk_tab[3][i] = pkey.m_tab[3][pkey.q_tab[1][pkey.q_tab[1][pkey.q_tab[0][pkey.q_tab[1][by] ^ byte(key[3], 3)] ^ byte(key[2], 3)] ^ byte(key[1], 3)] ^ byte(key[0], 3)];
236
+
237
+ def h_fun(pkey, x, key):
238
+ b0 = byte(x, 0);
239
+ b1 = byte(x, 1);
240
+ b2 = byte(x, 2);
241
+ b3 = byte(x, 3);
242
+ if pkey.k_len >= 4:
243
+ b0 = pkey.q_tab[1][b0] ^ byte(key[3], 0);
244
+ b1 = pkey.q_tab[0][b1] ^ byte(key[3], 1);
245
+ b2 = pkey.q_tab[0][b2] ^ byte(key[3], 2);
246
+ b3 = pkey.q_tab[1][b3] ^ byte(key[3], 3);
247
+ if pkey.k_len >= 3:
248
+ b0 = pkey.q_tab[1][b0] ^ byte(key[2], 0);
249
+ b1 = pkey.q_tab[1][b1] ^ byte(key[2], 1);
250
+ b2 = pkey.q_tab[0][b2] ^ byte(key[2], 2);
251
+ b3 = pkey.q_tab[0][b3] ^ byte(key[2], 3);
252
+ if pkey.k_len >= 2:
253
+ b0 = pkey.q_tab[0][pkey.q_tab[0][b0] ^ byte(key[1], 0)] ^ byte(key[0], 0);
254
+ b1 = pkey.q_tab[0][pkey.q_tab[1][b1] ^ byte(key[1], 1)] ^ byte(key[0], 1);
255
+ b2 = pkey.q_tab[1][pkey.q_tab[0][b2] ^ byte(key[1], 2)] ^ byte(key[0], 2);
256
+ b3 = pkey.q_tab[1][pkey.q_tab[1][b3] ^ byte(key[1], 3)] ^ byte(key[0], 3);
257
+ return pkey.m_tab[0][b0] ^ pkey.m_tab[1][b1] ^ pkey.m_tab[2][b2] ^ pkey.m_tab[3][b3];
258
+
259
+ def mds_rem(p0, p1):
260
+ i, t, u = 0, 0, 0
261
+ for i in range(8):
262
+ t = p1 >> 24
263
+ p1 = ((p1 << 8) & 0xffffffff) | (p0 >> 24)
264
+ p0 = (p0 << 8) & 0xffffffff
265
+ u = (t << 1) & 0xffffffff
266
+ if t & 0x80:
267
+ u ^= 0x0000014d
268
+ p1 ^= t ^ ((u << 16) & 0xffffffff)
269
+ u ^= (t >> 1)
270
+ if t & 0x01:
271
+ u ^= 0x0000014d >> 1
272
+ p1 ^= ((u << 24) & 0xffffffff) | ((u << 8) & 0xffffffff)
273
+ return p1
274
+
275
+ def set_key(pkey, in_key, key_len):
276
+ pkey.qt_gen = 0
277
+ if not pkey.qt_gen:
278
+ gen_qtab(pkey)
279
+ pkey.qt_gen = 1
280
+ pkey.mt_gen = 0
281
+ if not pkey.mt_gen:
282
+ gen_mtab(pkey)
283
+ pkey.mt_gen = 1
284
+ pkey.k_len = (key_len * 8) // 64
285
+
286
+ a = 0
287
+ b = 0
288
+ me_key = [0,0,0,0]
289
+ mo_key = [0,0,0,0]
290
+ for i in range(pkey.k_len):
291
+ if WORD_BIGENDIAN:
292
+ a = byteswap32(in_key[i + 1])
293
+ me_key[i] = a
294
+ b = byteswap32(in_key[i + i + 1])
295
+ else:
296
+ a = in_key[i + i]
297
+ me_key[i] = a
298
+ b = in_key[i + i + 1]
299
+ mo_key[i] = b
300
+ pkey.s_key[pkey.k_len - i - 1] = mds_rem(a, b);
301
+ for i in range(0, 40, 2):
302
+ a = (0x01010101 * i) % 0x100000000;
303
+ b = (a + 0x01010101) % 0x100000000;
304
+ a = h_fun(pkey, a, me_key);
305
+ b = rotl32(h_fun(pkey, b, mo_key), 8);
306
+ pkey.l_key[i] = (a + b) % 0x100000000;
307
+ pkey.l_key[i + 1] = rotl32((a + 2 * b) % 0x100000000, 9);
308
+ gen_mk_tab(pkey, pkey.s_key)
309
+
310
+ def encrypt(pkey, in_blk):
311
+ blk = [0, 0, 0, 0]
312
+
313
+ if WORD_BIGENDIAN:
314
+ blk[0] = byteswap32(in_blk[0]) ^ pkey.l_key[0];
315
+ blk[1] = byteswap32(in_blk[1]) ^ pkey.l_key[1];
316
+ blk[2] = byteswap32(in_blk[2]) ^ pkey.l_key[2];
317
+ blk[3] = byteswap32(in_blk[3]) ^ pkey.l_key[3];
318
+ else:
319
+ blk[0] = in_blk[0] ^ pkey.l_key[0];
320
+ blk[1] = in_blk[1] ^ pkey.l_key[1];
321
+ blk[2] = in_blk[2] ^ pkey.l_key[2];
322
+ blk[3] = in_blk[3] ^ pkey.l_key[3];
323
+
324
+ for i in range(8):
325
+ t1 = ( pkey.mk_tab[0][byte(blk[1],3)] ^ pkey.mk_tab[1][byte(blk[1],0)] ^ pkey.mk_tab[2][byte(blk[1],1)] ^ pkey.mk_tab[3][byte(blk[1],2)] );
326
+ t0 = ( pkey.mk_tab[0][byte(blk[0],0)] ^ pkey.mk_tab[1][byte(blk[0],1)] ^ pkey.mk_tab[2][byte(blk[0],2)] ^ pkey.mk_tab[3][byte(blk[0],3)] );
327
+
328
+ blk[2] = rotr32(blk[2] ^ ((t0 + t1 + pkey.l_key[4 * (i) + 8]) % 0x100000000), 1);
329
+ blk[3] = rotl32(blk[3], 1) ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 9]) % 0x100000000);
330
+
331
+ t1 = ( pkey.mk_tab[0][byte(blk[3],3)] ^ pkey.mk_tab[1][byte(blk[3],0)] ^ pkey.mk_tab[2][byte(blk[3],1)] ^ pkey.mk_tab[3][byte(blk[3],2)] );
332
+ t0 = ( pkey.mk_tab[0][byte(blk[2],0)] ^ pkey.mk_tab[1][byte(blk[2],1)] ^ pkey.mk_tab[2][byte(blk[2],2)] ^ pkey.mk_tab[3][byte(blk[2],3)] );
333
+
334
+ blk[0] = rotr32(blk[0] ^ ((t0 + t1 + pkey.l_key[4 * (i) + 10]) % 0x100000000), 1);
335
+ blk[1] = rotl32(blk[1], 1) ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 11]) % 0x100000000);
336
+
337
+ if WORD_BIGENDIAN:
338
+ in_blk[0] = byteswap32(blk[2] ^ pkey.l_key[4]);
339
+ in_blk[1] = byteswap32(blk[3] ^ pkey.l_key[5]);
340
+ in_blk[2] = byteswap32(blk[0] ^ pkey.l_key[6]);
341
+ in_blk[3] = byteswap32(blk[1] ^ pkey.l_key[7]);
342
+ else:
343
+ in_blk[0] = blk[2] ^ pkey.l_key[4];
344
+ in_blk[1] = blk[3] ^ pkey.l_key[5];
345
+ in_blk[2] = blk[0] ^ pkey.l_key[6];
346
+ in_blk[3] = blk[1] ^ pkey.l_key[7];
347
+
348
+ return
349
+
350
+ def decrypt(pkey, in_blk):
351
+ blk = [0, 0, 0, 0]
352
+
353
+ if WORD_BIGENDIAN:
354
+ blk[0] = byteswap32(in_blk[0]) ^ pkey.l_key[4];
355
+ blk[1] = byteswap32(in_blk[1]) ^ pkey.l_key[5];
356
+ blk[2] = byteswap32(in_blk[2]) ^ pkey.l_key[6];
357
+ blk[3] = byteswap32(in_blk[3]) ^ pkey.l_key[7];
358
+ else:
359
+ blk[0] = in_blk[0] ^ pkey.l_key[4];
360
+ blk[1] = in_blk[1] ^ pkey.l_key[5];
361
+ blk[2] = in_blk[2] ^ pkey.l_key[6];
362
+ blk[3] = in_blk[3] ^ pkey.l_key[7];
363
+
364
+ for i in range(7, -1, -1):
365
+ t1 = ( pkey.mk_tab[0][byte(blk[1],3)] ^ pkey.mk_tab[1][byte(blk[1],0)] ^ pkey.mk_tab[2][byte(blk[1],1)] ^ pkey.mk_tab[3][byte(blk[1],2)] )
366
+ t0 = ( pkey.mk_tab[0][byte(blk[0],0)] ^ pkey.mk_tab[1][byte(blk[0],1)] ^ pkey.mk_tab[2][byte(blk[0],2)] ^ pkey.mk_tab[3][byte(blk[0],3)] )
367
+
368
+ blk[2] = rotl32(blk[2], 1) ^ ((t0 + t1 + pkey.l_key[4 * (i) + 10]) % 0x100000000)
369
+ blk[3] = rotr32(blk[3] ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 11]) % 0x100000000), 1)
370
+
371
+ t1 = ( pkey.mk_tab[0][byte(blk[3],3)] ^ pkey.mk_tab[1][byte(blk[3],0)] ^ pkey.mk_tab[2][byte(blk[3],1)] ^ pkey.mk_tab[3][byte(blk[3],2)] )
372
+ t0 = ( pkey.mk_tab[0][byte(blk[2],0)] ^ pkey.mk_tab[1][byte(blk[2],1)] ^ pkey.mk_tab[2][byte(blk[2],2)] ^ pkey.mk_tab[3][byte(blk[2],3)] )
373
+
374
+ blk[0] = rotl32(blk[0], 1) ^ ((t0 + t1 + pkey.l_key[4 * (i) + 8]) % 0x100000000)
375
+ blk[1] = rotr32(blk[1] ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 9]) % 0x100000000), 1)
376
+
377
+ if WORD_BIGENDIAN:
378
+ in_blk[0] = byteswap32(blk[2] ^ pkey.l_key[0]);
379
+ in_blk[1] = byteswap32(blk[3] ^ pkey.l_key[1]);
380
+ in_blk[2] = byteswap32(blk[0] ^ pkey.l_key[2]);
381
+ in_blk[3] = byteswap32(blk[1] ^ pkey.l_key[3]);
382
+ else:
383
+ in_blk[0] = blk[2] ^ pkey.l_key[0];
384
+ in_blk[1] = blk[3] ^ pkey.l_key[1];
385
+ in_blk[2] = blk[0] ^ pkey.l_key[2];
386
+ in_blk[3] = blk[1] ^ pkey.l_key[3];
387
+ return
388
+
389
+ __testkey = b'\xD4\x3B\xB7\x55\x6E\xA3\x2E\x46\xF2\xA2\x82\xB7\xD4\x5B\x4E\x0D\x57\xFF\x73\x9D\x4D\xC9\x2C\x1B\xD7\xFC\x01\x70\x0C\xC8\x21\x6F'
390
+ __testdat = b'\x90\xAF\xE9\x1B\xB2\x88\x54\x4F\x2C\x32\xDC\x23\x9B\x26\x35\xE6'
391
+ assert b'l\xb4V\x1c@\xbf\n\x97\x05\x93\x1c\xb6\xd4\x08\xe7\xfa' == Twofish(__testkey).encrypt(__testdat)
392
+ assert __testdat == Twofish(__testkey).decrypt(b'l\xb4V\x1c@\xbf\n\x97\x05\x93\x1c\xb6\xd4\x08\xe7\xfa')
393
+
Decipher/validator.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ipaddress
2
+ from typing import List, Dict, Set
3
+ from Decipher.models import NetworkTopology
4
+
5
+ def validate_topology(topology: NetworkTopology) -> List[str]:
6
+ """
7
+ Performs logical validation on the network topology and returns a list of warnings/errors.
8
+ """
9
+ issues = []
10
+ device_names = {d.name for d in topology.devices}
11
+ ip_to_interface = {} # ip -> (device, interface)
12
+
13
+ # 1. Check for IP Conflicts and basic interface sanity
14
+ for device in topology.devices:
15
+ for intf in device.interfaces:
16
+ if intf.ip:
17
+ # Check for duplicate IPs
18
+ if intf.ip in ip_to_interface:
19
+ other_dev, other_intf = ip_to_interface[intf.ip]
20
+ issues.append(f"IP Conflict: {intf.ip} is assigned to both {device.name} ({intf.name}) and {other_dev} ({other_intf})")
21
+ else:
22
+ ip_to_interface[intf.ip] = (device.name, intf.name)
23
+
24
+ # Basic IP format validation
25
+ try:
26
+ if "/" in intf.ip:
27
+ ipaddress.ip_interface(intf.ip)
28
+ else:
29
+ ipaddress.ip_address(intf.ip)
30
+ except ValueError:
31
+ issues.append(f"Invalid IP Format: '{intf.ip}' on {device.name} ({intf.name})")
32
+
33
+ # 2. Ensure Link Endpoints Exist
34
+ for link in topology.physical_links:
35
+ if link.from_device not in device_names:
36
+ issues.append(f"Broken Link: Source device '{link.from_device}' does not exist.")
37
+ if link.to_device not in device_names:
38
+ issues.append(f"Broken Link: Destination device '{link.to_device}' does not exist.")
39
+
40
+ # 3. Verify Routing Protocols (Basic Sanity)
41
+ for device in topology.devices:
42
+ if device.routing and device.routing.ospf:
43
+ device_ips = [intf.ip for intf in device.interfaces if intf.ip]
44
+ for ospf in device.routing.ospf:
45
+ for net_statement in ospf.networks:
46
+ # Check if the OSPF network statement matches at least one interface IP segment
47
+ try:
48
+ # network statement is usually "network 10.0.0.0 0.0.0.255 area 0"
49
+ # we extract the network and wildcard mask
50
+ parts = net_statement.split()
51
+ if "network" in parts:
52
+ net_addr = parts[parts.index("network") + 1]
53
+ # Simple check: does any device IP start with the same prefix?
54
+ # This is a heuristic check
55
+ found_match = False
56
+ for ip in device_ips:
57
+ if ip.split('.')[0:2] == net_addr.split('.')[0:2]:
58
+ found_match = True
59
+ break
60
+ if not found_match:
61
+ issues.append(f"OSPF Warning: Network statement '{net_statement}' on {device.name} doesn't seem to match any local interface IPs.")
62
+ except Exception:
63
+ pass
64
+
65
+ return issues
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.13-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies if any (none needed for now)
8
+ # RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ # build-essential \
10
+ # && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy the requirements file into the container
13
+ COPY requirements.txt .
14
+
15
+ # Install any needed packages specified in requirements.txt
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy the current directory contents into the container at /app
19
+ COPY . .
20
+
21
+ # Hugging Face Spaces uses port 7860 by default
22
+ ENV PORT=7860
23
+
24
+ # Run the application
25
+ CMD ["python", "main.py"]
main.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ import xml.etree.ElementTree as ET
7
+ from Decipher.pt_crypto import decrypt_pkt
8
+ from Decipher.extract import extract_structured_data
9
+ from Decipher.generator import generate_network
10
+ from Decipher.models import NetworkTopology
11
+ from pydantic import Field, BaseModel
12
+
13
+ class PromptRequest(BaseModel):
14
+ prompt: str = Field(..., description="The description of the network to generate", json_schema_extra={"example": "A network with 1 router and 2 PCs"})
15
+
16
+ app = FastAPI(title="TopoScan API", description="API for decrypting and extracting data from Cisco Packet Tracer files")
17
+
18
+ # Enable CORS
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"],
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ @app.get("/")
28
+ async def root():
29
+ return {"message": "TopoScan API is running"}
30
+
31
+ @app.post("/analyze-pkt")
32
+ async def analyze_pkt(file: UploadFile = File(...)):
33
+ if not file.filename.endswith(".pkt") and not file.filename.endswith(".pka"):
34
+ raise HTTPException(status_code=400, detail="Only .pkt or .pka files are supported")
35
+
36
+ try:
37
+ # Read the uploaded file content
38
+ content = await file.read()
39
+
40
+ # Decrypt the file
41
+ try:
42
+ xml_data = decrypt_pkt(content)
43
+ except Exception as e:
44
+ raise HTTPException(status_code=500, detail=f"Decryption failed: {str(e)}")
45
+
46
+ # Create a temporary file to store the XML for extraction
47
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xml") as temp_xml:
48
+ temp_xml.write(xml_data)
49
+ temp_xml_path = temp_xml.name
50
+
51
+ try:
52
+ # Extract structured data using existing logic
53
+ structured_data = extract_structured_data(temp_xml_path)
54
+
55
+ if structured_data is None:
56
+ raise HTTPException(status_code=500, detail="Failed to extract structured data from decrypted XML")
57
+
58
+ return structured_data
59
+
60
+ finally:
61
+ # Clean up the temporary file
62
+ if os.path.exists(temp_xml_path):
63
+ os.remove(temp_xml_path)
64
+
65
+ except Exception as e:
66
+ raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
67
+
68
+ @app.post("/generate-network", response_model=NetworkTopology)
69
+ async def api_generate_network(request: PromptRequest):
70
+ try:
71
+ topology = generate_network(request.prompt)
72
+ return topology
73
+ except Exception as e:
74
+ raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
75
+
76
+ if __name__ == "__main__":
77
+ import uvicorn
78
+ # Hugging Face Spaces and other cloud providers often use the PORT environment variable
79
+ port = int(os.environ.get("PORT", 8000))
80
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.128.3
2
+ uvicorn==0.40.0
3
+ pydantic==2.12.5
4
+ python-dotenv==1.2.1
5
+ openai==2.36.0
6
+ ciscoconfparse==1.9.52
7
+ python-multipart==0.0.22
8
+ requests==2.32.5