packet-tracer-skill 0.1.0

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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/LICENSE.Twofish-BSD-3-Clause.txt +29 -0
  3. package/README.md +687 -0
  4. package/SKILL.md +221 -0
  5. package/bin/packet-tracer-skill.js +635 -0
  6. package/examples/blueprint_minimal.json +46 -0
  7. package/package.json +42 -0
  8. package/references/packettracer-sample-catalog.json +21410 -0
  9. package/references/packettracer-sample-catalog.md +1124 -0
  10. package/references/pkt-format.md +57 -0
  11. package/references/xml-skeleton-notes.md +44 -0
  12. package/requirements-dev.txt +1 -0
  13. package/requirements.txt +6 -0
  14. package/scripts/build_sample_catalog.py +65 -0
  15. package/scripts/donor_diagnostics.py +35 -0
  16. package/scripts/generate_pkt.py +1264 -0
  17. package/scripts/install_skill.py +71 -0
  18. package/scripts/intent_parser.py +712 -0
  19. package/scripts/packet_tracer_env.py +278 -0
  20. package/scripts/pkt_builder.py +15 -0
  21. package/scripts/pkt_codec.py +181 -0
  22. package/scripts/pkt_editor.py +752 -0
  23. package/scripts/pkt_transformer.py +541 -0
  24. package/scripts/sample_catalog.py +385 -0
  25. package/scripts/sample_selector.py +156 -0
  26. package/scripts/setup.ps1 +26 -0
  27. package/scripts/twofish_diagnostics.py +91 -0
  28. package/scripts/vendor/README.md +46 -0
  29. package/scripts/vendor/twofish.py +81 -0
  30. package/scripts/workspace_repair.py +441 -0
  31. package/templates/pt900/base_empty.xml +21 -0
  32. package/templates/pt900/device_library/pc.xml +20 -0
  33. package/templates/pt900/device_library/printer.xml +432 -0
  34. package/templates/pt900/device_library/router.xml +16 -0
  35. package/templates/pt900/device_library/switch.xml +38 -0
@@ -0,0 +1,278 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ import xml.etree.ElementTree as ET
6
+ from dataclasses import dataclass
7
+
8
+
9
+ DEFAULT_INSTALL_CANDIDATES = [
10
+ Path(r"C:\Program Files\Cisco Packet Tracer 9.0.0"),
11
+ Path(r"C:\Program Files\Cisco Packet Tracer"),
12
+ Path(r"C:\Program Files (x86)\Cisco Packet Tracer 9.0.0"),
13
+ Path(r"C:\Program Files (x86)\Cisco Packet Tracer"),
14
+ ]
15
+ DEFAULT_PACKET_TRACER_TARGET_VERSION = "9.0.0.0810"
16
+ DEFAULT_DONOR_FALLBACKS = [
17
+ Path.home() / "Downloads",
18
+ Path.home() / "Documents",
19
+ Path.home() / "Desktop",
20
+ ]
21
+ DEFAULT_SAMPLE_DONOR_FILES = [
22
+ Path(r"01 Networking\FTP\FTP.pkt"),
23
+ Path(r"01 Networking\HTTPS\HTTPS.pkt"),
24
+ Path(r"01 Networking\DNS\Multilevel_DNS.pkt"),
25
+ Path(r"01 Networking\DHCP\dhcp_snooping_trusted_untrusted_gigabit_ports.pkt"),
26
+ ]
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class CompatibilityDonorDetails:
31
+ target_version: str
32
+ resolved_path: Path | None
33
+ donor_version: str | None
34
+ donor_source: str | None
35
+ status: str
36
+ blocking_reason: str
37
+ candidate_paths: list[tuple[str, Path]]
38
+
39
+
40
+ def _existing_path(raw: str | None) -> Path | None:
41
+ if not raw:
42
+ return None
43
+ path = Path(raw).expanduser()
44
+ return path if path.exists() else None
45
+
46
+
47
+ def get_packet_tracer_root() -> Path | None:
48
+ env_root = _existing_path(os.getenv("PACKET_TRACER_ROOT"))
49
+ if env_root is not None:
50
+ return env_root
51
+ for candidate in DEFAULT_INSTALL_CANDIDATES:
52
+ if candidate.exists():
53
+ return candidate
54
+ return None
55
+
56
+
57
+ def get_packet_tracer_saves_root() -> Path | None:
58
+ env_saves = _existing_path(os.getenv("PACKET_TRACER_SAVES_ROOT"))
59
+ if env_saves is not None:
60
+ return env_saves
61
+ root = get_packet_tracer_root()
62
+ if root is None:
63
+ return None
64
+ saves = root / "saves"
65
+ return saves if saves.exists() else None
66
+
67
+
68
+ def get_packet_tracer_exe() -> Path | None:
69
+ env_exe = _existing_path(os.getenv("PACKET_TRACER_EXE"))
70
+ if env_exe is not None:
71
+ return env_exe
72
+ root = get_packet_tracer_root()
73
+ if root is None:
74
+ return None
75
+ for candidate in [root / "bin" / "PacketTracer.exe", root / "PacketTracer.exe"]:
76
+ if candidate.exists():
77
+ return candidate
78
+ return None
79
+
80
+
81
+ def require_packet_tracer_saves_root() -> Path:
82
+ saves = get_packet_tracer_saves_root()
83
+ if saves is None:
84
+ raise FileNotFoundError(
85
+ "Packet Tracer sample saves were not found. Set PACKET_TRACER_SAVES_ROOT or PACKET_TRACER_ROOT."
86
+ )
87
+ return saves
88
+
89
+
90
+ def require_packet_tracer_exe() -> Path:
91
+ exe = get_packet_tracer_exe()
92
+ if exe is None:
93
+ raise FileNotFoundError(
94
+ "Packet Tracer executable was not found. Set PACKET_TRACER_EXE or PACKET_TRACER_ROOT."
95
+ )
96
+ return exe
97
+
98
+
99
+ def resolve_sample_path(relative_path: str) -> Path:
100
+ return require_packet_tracer_saves_root() / relative_path
101
+
102
+
103
+ def get_packet_tracer_target_version() -> str:
104
+ return os.getenv("PACKET_TRACER_TARGET_VERSION", DEFAULT_PACKET_TRACER_TARGET_VERSION)
105
+
106
+
107
+ def _pkt_version(pkt_path: Path) -> str | None:
108
+ try:
109
+ from pkt_codec import decode_pkt_modern
110
+
111
+ root = ET.fromstring(decode_pkt_modern(pkt_path.read_bytes()))
112
+ except Exception:
113
+ return None
114
+ return root.findtext("./VERSION")
115
+
116
+
117
+ def _candidate_pkt_files(directory: Path, source: str) -> list[tuple[str, Path]]:
118
+ if not directory.exists() or not directory.is_dir():
119
+ return []
120
+ candidates = sorted(
121
+ (path for path in directory.glob("*.pkt") if path.is_file()),
122
+ key=lambda path: (path.stat().st_mtime, path.name.lower()),
123
+ reverse=True,
124
+ )
125
+ return [(source, candidate) for candidate in candidates]
126
+
127
+
128
+ def list_packet_tracer_compatibility_donor_candidates() -> list[tuple[str, Path]]:
129
+ candidates: list[tuple[str, Path]] = []
130
+ seen: set[str] = set()
131
+
132
+ env_donor = os.getenv("PACKET_TRACER_COMPAT_DONOR")
133
+ if env_donor:
134
+ env_path = Path(env_donor).expanduser()
135
+ seen.add(str(env_path).lower())
136
+ candidates.append(("env", env_path))
137
+
138
+ for directory in DEFAULT_DONOR_FALLBACKS:
139
+ for source, candidate in _candidate_pkt_files(directory, f"auto:{directory.name.lower()}"):
140
+ key = str(candidate).lower()
141
+ if key in seen:
142
+ continue
143
+ seen.add(key)
144
+ candidates.append((source, candidate))
145
+
146
+ saves_root = get_packet_tracer_saves_root()
147
+ if saves_root is not None:
148
+ for relative_path in DEFAULT_SAMPLE_DONOR_FILES:
149
+ candidate = saves_root / relative_path
150
+ key = str(candidate).lower()
151
+ if key in seen:
152
+ continue
153
+ seen.add(key)
154
+ candidates.append(("auto:packet-tracer-saves", candidate))
155
+
156
+ return candidates
157
+
158
+
159
+ def inspect_packet_tracer_compatibility_donor() -> CompatibilityDonorDetails:
160
+ target_version = get_packet_tracer_target_version()
161
+ candidates = list_packet_tracer_compatibility_donor_candidates()
162
+ env_donor = os.getenv("PACKET_TRACER_COMPAT_DONOR")
163
+
164
+ if env_donor:
165
+ env_path = Path(env_donor).expanduser()
166
+ if not env_path.exists():
167
+ return CompatibilityDonorDetails(
168
+ target_version=target_version,
169
+ resolved_path=None,
170
+ donor_version=None,
171
+ donor_source="env",
172
+ status="missing",
173
+ blocking_reason=f"set but missing: {env_path}",
174
+ candidate_paths=candidates,
175
+ )
176
+ if env_path.suffix.lower() != ".pkt":
177
+ return CompatibilityDonorDetails(
178
+ target_version=target_version,
179
+ resolved_path=None,
180
+ donor_version=None,
181
+ donor_source="env",
182
+ status="invalid_extension",
183
+ blocking_reason=f"compatibility donor must be a .pkt file: {env_path}",
184
+ candidate_paths=candidates,
185
+ )
186
+ donor_version = _pkt_version(env_path)
187
+ if donor_version is None:
188
+ return CompatibilityDonorDetails(
189
+ target_version=target_version,
190
+ resolved_path=None,
191
+ donor_version=None,
192
+ donor_source="env",
193
+ status="decode_error",
194
+ blocking_reason=f"could not decode donor version: {env_path}",
195
+ candidate_paths=candidates,
196
+ )
197
+ if donor_version != target_version:
198
+ return CompatibilityDonorDetails(
199
+ target_version=target_version,
200
+ resolved_path=None,
201
+ donor_version=donor_version,
202
+ donor_source="env",
203
+ status="version_mismatch",
204
+ blocking_reason=f"{env_path} is version {donor_version}; expected {target_version}",
205
+ candidate_paths=candidates,
206
+ )
207
+ return CompatibilityDonorDetails(
208
+ target_version=target_version,
209
+ resolved_path=env_path,
210
+ donor_version=donor_version,
211
+ donor_source="env",
212
+ status="ok",
213
+ blocking_reason="",
214
+ candidate_paths=candidates,
215
+ )
216
+
217
+ decode_failures = 0
218
+ wrong_version_count = 0
219
+ for source, candidate in candidates:
220
+ if not candidate.exists() or candidate.suffix.lower() != ".pkt":
221
+ continue
222
+ donor_version = _pkt_version(candidate)
223
+ if donor_version is None:
224
+ decode_failures += 1
225
+ continue
226
+ if donor_version != target_version:
227
+ wrong_version_count += 1
228
+ continue
229
+ return CompatibilityDonorDetails(
230
+ target_version=target_version,
231
+ resolved_path=candidate,
232
+ donor_version=donor_version,
233
+ donor_source=source,
234
+ status="ok",
235
+ blocking_reason="",
236
+ candidate_paths=candidates,
237
+ )
238
+
239
+ if candidates:
240
+ if decode_failures == len(candidates):
241
+ reason = (
242
+ "donor candidates were found, but none could be decoded. "
243
+ "Check the local Twofish bridge and Python 3.14 runtime."
244
+ )
245
+ elif wrong_version_count > 0:
246
+ reason = f"no Packet Tracer {target_version} donor was found among the discovered local candidates"
247
+ else:
248
+ reason = f"no compatible Packet Tracer {target_version} donor was found"
249
+ else:
250
+ reason = (
251
+ "no donor candidates were discovered. Set PACKET_TRACER_COMPAT_DONOR "
252
+ "or place a working 9.0 donor lab in Downloads, Documents, Desktop, or Packet Tracer saves."
253
+ )
254
+
255
+ return CompatibilityDonorDetails(
256
+ target_version=target_version,
257
+ resolved_path=None,
258
+ donor_version=None,
259
+ donor_source=None,
260
+ status="missing",
261
+ blocking_reason=reason,
262
+ candidate_paths=candidates,
263
+ )
264
+
265
+
266
+ def get_packet_tracer_compatibility_donor() -> Path | None:
267
+ details = inspect_packet_tracer_compatibility_donor()
268
+ return details.resolved_path if details.status == "ok" else None
269
+
270
+
271
+ def require_packet_tracer_compatibility_donor() -> Path:
272
+ details = inspect_packet_tracer_compatibility_donor()
273
+ if details.status != "ok" or details.resolved_path is None:
274
+ raise FileNotFoundError(
275
+ "Packet Tracer 9.0 compatibility donor was not found. "
276
+ f"{details.blocking_reason}"
277
+ )
278
+ return details.resolved_path
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from pkt_transformer import transform_from_blueprint
4
+ from sample_catalog import load_catalog
5
+ from sample_selector import select_best_sample
6
+
7
+
8
+ def build_packet_tracer_xml(blueprint: dict) -> bytes:
9
+ samples = load_catalog()
10
+ device_requirements: dict[str, int] = {}
11
+ for device in blueprint.get("devices", []):
12
+ device_type = str(device["type"])
13
+ device_requirements[device_type] = device_requirements.get(device_type, 0) + 1
14
+ selected = select_best_sample(samples, list(blueprint.get("capabilities", [])), device_requirements)
15
+ return transform_from_blueprint(blueprint, selected)
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import struct
4
+ import zlib
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from vendor.twofish import Twofish
10
+
11
+
12
+ BLOCK_SIZE = 16
13
+ TAG_LEN = 16
14
+ NEW_KEY = bytes([0x89]) * 16
15
+ NEW_IV = bytes([0x10]) * 16
16
+
17
+
18
+ def _twofish_cls() -> type["Twofish"]:
19
+ try:
20
+ from vendor.twofish import Twofish
21
+ except ImportError as exc:
22
+ raise ImportError(
23
+ "Packet Tracer modern codec requires a local Twofish bridge. "
24
+ "Set PKT_TWOFISH_LIBRARY or place a local _twofish binary next to scripts/vendor/twofish.py."
25
+ ) from exc
26
+ return Twofish
27
+
28
+
29
+ def qcompress(xml_bytes: bytes) -> bytes:
30
+ if not isinstance(xml_bytes, bytes):
31
+ raise TypeError("xml_bytes must be bytes")
32
+ return struct.pack(">I", len(xml_bytes)) + zlib.compress(xml_bytes, 9)
33
+
34
+
35
+ def quncompress(blob: bytes) -> bytes:
36
+ if len(blob) < 4:
37
+ raise ValueError("qCompress blob is too short")
38
+ size = struct.unpack(">I", blob[:4])[0]
39
+ out = zlib.decompress(blob[4:])
40
+ return out[:size]
41
+
42
+
43
+ def stage2_xor(data: bytes) -> bytes:
44
+ length = len(data)
45
+ return bytes(byte ^ ((length - index) & 0xFF) for index, byte in enumerate(data))
46
+
47
+
48
+ def stage1_obfuscate(clear: bytes) -> bytes:
49
+ length = len(clear)
50
+ out = bytearray(length)
51
+ for index, byte in enumerate(clear):
52
+ out[length - 1 - index] = byte ^ ((length - index * length) & 0xFF)
53
+ return bytes(out)
54
+
55
+
56
+ def stage1_deobfuscate(obfuscated: bytes) -> bytes:
57
+ length = len(obfuscated)
58
+ return bytes(
59
+ obfuscated[length - 1 - index] ^ ((length - index * length) & 0xFF)
60
+ for index in range(length)
61
+ )
62
+
63
+
64
+ def _xor_bytes(left: bytes, right: bytes) -> bytes:
65
+ return bytes(a ^ b for a, b in zip(left, right))
66
+
67
+
68
+ def _gf_double(block: bytes) -> bytes:
69
+ value = int.from_bytes(block, "big")
70
+ carry = (value >> 127) & 1
71
+ value = ((value << 1) & ((1 << 128) - 1))
72
+ if carry:
73
+ value ^= 0x87
74
+ return value.to_bytes(16, "big")
75
+
76
+
77
+ def _pad_cmac(block: bytes) -> bytes:
78
+ return block + b"\x80" + b"\x00" * (BLOCK_SIZE - len(block) - 1)
79
+
80
+
81
+ def _iterate_blocks(data: bytes) -> list[bytes]:
82
+ return [data[index : index + BLOCK_SIZE] for index in range(0, len(data), BLOCK_SIZE)]
83
+
84
+
85
+ def _cmac(cipher: "Twofish", data: bytes) -> bytes:
86
+ zero = b"\x00" * BLOCK_SIZE
87
+ l_val = cipher.encrypt(zero)
88
+ k1 = _gf_double(l_val)
89
+ k2 = _gf_double(k1)
90
+
91
+ blocks = _iterate_blocks(data)
92
+ if not blocks:
93
+ blocks = [b""]
94
+
95
+ if len(blocks[-1]) == BLOCK_SIZE:
96
+ last = _xor_bytes(blocks[-1], k1)
97
+ else:
98
+ last = _xor_bytes(_pad_cmac(blocks[-1]), k2)
99
+ blocks[-1] = last
100
+
101
+ state = zero
102
+ for block in blocks:
103
+ if len(block) != BLOCK_SIZE:
104
+ raise ValueError("CMAC internal block must be 16 bytes")
105
+ state = cipher.encrypt(_xor_bytes(state, block))
106
+ return state
107
+
108
+
109
+ def _omac(cipher: "Twofish", domain: int, data: bytes) -> bytes:
110
+ prefix = b"\x00" * 15 + bytes([domain & 0xFF])
111
+ return _cmac(cipher, prefix + data)
112
+
113
+
114
+ def _ctr_crypt(cipher: "Twofish", initial_counter: bytes, data: bytes) -> bytes:
115
+ counter = int.from_bytes(initial_counter, "big")
116
+ out = bytearray()
117
+ for offset in range(0, len(data), BLOCK_SIZE):
118
+ block = data[offset : offset + BLOCK_SIZE]
119
+ keystream = cipher.encrypt(counter.to_bytes(16, "big"))
120
+ out.extend(bytes(a ^ b for a, b in zip(block, keystream)))
121
+ counter = (counter + 1) % (1 << 128)
122
+ return bytes(out)
123
+
124
+
125
+ def eax_twofish_encrypt(plaintext: bytes, nonce: bytes = NEW_IV, header: bytes = b"") -> tuple[bytes, bytes]:
126
+ cipher = _twofish_cls()(NEW_KEY)
127
+ nonce_mac = _omac(cipher, 0, nonce)
128
+ header_mac = _omac(cipher, 1, header)
129
+ ciphertext = _ctr_crypt(cipher, nonce_mac, plaintext)
130
+ body_mac = _omac(cipher, 2, ciphertext)
131
+ tag = _xor_bytes(_xor_bytes(nonce_mac, header_mac), body_mac)
132
+ return ciphertext, tag
133
+
134
+
135
+ def eax_twofish_decrypt(ciphertext: bytes, tag: bytes, nonce: bytes = NEW_IV, header: bytes = b"") -> bytes:
136
+ if len(tag) != TAG_LEN:
137
+ raise ValueError("invalid EAX tag length")
138
+ cipher = _twofish_cls()(NEW_KEY)
139
+ nonce_mac = _omac(cipher, 0, nonce)
140
+ header_mac = _omac(cipher, 1, header)
141
+ body_mac = _omac(cipher, 2, ciphertext)
142
+ expected_tag = _xor_bytes(_xor_bytes(nonce_mac, header_mac), body_mac)
143
+ if expected_tag != tag:
144
+ raise ValueError("EAX authentication tag verification failed")
145
+ return _ctr_crypt(cipher, nonce_mac, ciphertext)
146
+
147
+
148
+ def encode_pkt_modern(xml_bytes: bytes) -> bytes:
149
+ payload = qcompress(xml_bytes)
150
+ stage2 = stage2_xor(payload)
151
+ ciphertext, tag = eax_twofish_encrypt(stage2)
152
+ return stage1_obfuscate(ciphertext + tag)
153
+
154
+
155
+ def decode_pkt_modern(pkt_bytes: bytes) -> bytes:
156
+ if len(pkt_bytes) < TAG_LEN:
157
+ raise ValueError("pkt blob is too short")
158
+ stage1 = stage1_deobfuscate(pkt_bytes)
159
+ ciphertext = stage1[:-TAG_LEN]
160
+ tag = stage1[-TAG_LEN:]
161
+ stage2 = eax_twofish_decrypt(ciphertext, tag)
162
+ payload = stage2_xor(stage2)
163
+ return quncompress(payload)
164
+
165
+
166
+ def encode_xml_file(xml_path: str | Path, output_path: str | Path) -> Path:
167
+ xml_path = Path(xml_path)
168
+ output_path = Path(output_path)
169
+ pkt_bytes = encode_pkt_modern(xml_path.read_bytes())
170
+ output_path.parent.mkdir(parents=True, exist_ok=True)
171
+ output_path.write_bytes(pkt_bytes)
172
+ return output_path
173
+
174
+
175
+ def decode_pkt_file(pkt_path: str | Path, xml_out_path: str | Path) -> Path:
176
+ pkt_path = Path(pkt_path)
177
+ xml_out_path = Path(xml_out_path)
178
+ xml_bytes = decode_pkt_modern(pkt_path.read_bytes())
179
+ xml_out_path.parent.mkdir(parents=True, exist_ok=True)
180
+ xml_out_path.write_bytes(xml_bytes)
181
+ return xml_out_path