packet-tracer-skill 0.2.2 → 0.2.3
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.
- package/CHANGELOG.md +32 -1
- package/README.md +221 -51
- package/docs/automation-controller-proof.md +35 -0
- package/docs/curated-donor-registry.md +11 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/industrial-programming-proof.md +48 -0
- package/docs/ipv4-routing-management-proof.md +37 -0
- package/docs/l2-resiliency-bgp-proof.md +60 -0
- package/docs/l2-security-qos-proof.md +59 -0
- package/docs/packet-tracer-feature-gap-atlas.md +172 -15
- package/docs/release-checklist.md +13 -8
- package/docs/release-notes-0.2.2.md +1 -1
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/security-edge-deepening-proof.md +65 -0
- package/docs/voice-collaboration-proof.md +38 -0
- package/docs/wan-security-donor-proof.md +20 -3
- package/package.json +10 -1
- package/references/packettracer-feature-atlas.json +67 -17
- package/scripts/coverage_matrix.py +505 -12
- package/scripts/feature_atlas.py +65 -1
- package/scripts/generate_pkt.py +161 -3
- package/scripts/intent_parser.py +521 -2
- package/scripts/pkt_editor.py +477 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/sample_catalog.py +57 -2
package/scripts/feature_atlas.py
CHANGED
|
@@ -28,8 +28,62 @@ EDITOR_TEST_ALIASES = {
|
|
|
28
28
|
"netflow": ["set_netflow", "netflow destination"],
|
|
29
29
|
"span": ["set_span", "monitor session"],
|
|
30
30
|
"port_security": ["set_port_security", "port-security"],
|
|
31
|
+
"dot1x": ["set_dot1x", "dot1x system-auth-control", "authentication port-control"],
|
|
32
|
+
"qos": ["set_qos_policy", "class-map", "policy-map", "service-policy"],
|
|
33
|
+
"bgp": ["set_bgp_neighbor", "router bgp", "remote-as"],
|
|
34
|
+
"stp": ["set_stp", "spanning-tree mode"],
|
|
35
|
+
"rstp": ["set_stp", "rapid-pvst"],
|
|
36
|
+
"etherchannel": ["set_etherchannel", "channel-group", "Port-channel"],
|
|
37
|
+
"lacp": ["set_etherchannel", "mode active"],
|
|
38
|
+
"pagp": ["set_etherchannel", "mode desirable"],
|
|
39
|
+
"vtp": ["set_vtp", "vtp domain"],
|
|
40
|
+
"dtp": ["set_dtp", "dynamic desirable"],
|
|
31
41
|
"wep": ["security wep", "WEP_KEY"],
|
|
32
42
|
"wpa_enterprise": ["wpa-enterprise", "wpa2-enterprise", "802.1x", "radius_server"],
|
|
43
|
+
"gre": ["set_gre_tunnel", "gre tunnel", "tunnel mode gre ip"],
|
|
44
|
+
"ppp": ["set_ppp_interface", "encapsulation ppp"],
|
|
45
|
+
"ipsec": ["set_ipsec_transform_set", "crypto ipsec transform-set", "set_crypto_map"],
|
|
46
|
+
"vpn": ["set_crypto_map", "crypto map"],
|
|
47
|
+
"cbac": ["set_cbac_inspect", "ip inspect name"],
|
|
48
|
+
"zfw": ["set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy", "zone-pair security", "policy-map type inspect"],
|
|
49
|
+
"voip": ["set_telephony_service", "set_dial_peer_voice", "telephony-service", "dial-peer voice"],
|
|
50
|
+
"ip_phone": ["set_ephone_dn", "set_ephone", "ephone-dn", "ephone"],
|
|
51
|
+
"call_manager": ["set_telephony_service", "set_ephone_dn", "telephony-service"],
|
|
52
|
+
"real_http": ["set_script_file_content", "realhttp", "real http"],
|
|
53
|
+
"real_websocket": ["set_script_file_content", "realws", "websocket"],
|
|
54
|
+
"python_programming": ["set_script_file_content", "main.py", "python"],
|
|
55
|
+
"javascript_programming": ["set_script_file_content", "main.js", "javascript"],
|
|
56
|
+
"tcp_udp_app": ["set_script_file_content", "tcpServer.js", "udpSend.js", "tcp_udp_app"],
|
|
57
|
+
"ospfv2": ["set_ospfv2_network", "router ospf"],
|
|
58
|
+
"eigrp_ipv4": ["set_eigrp_ipv4_network", "router eigrp"],
|
|
59
|
+
"ripv2": ["set_ripv2_network", "router rip"],
|
|
60
|
+
"static_route": ["set_static_route", "ip route"],
|
|
61
|
+
"default_route": ["set_static_route", "0.0.0.0"],
|
|
62
|
+
"dhcp_relay": ["set_dhcp_relay", "ip helper-address"],
|
|
63
|
+
"nat_static": ["set_nat_static", "ip nat inside source static"],
|
|
64
|
+
"nat_dynamic": ["set_nat_interface", "ip nat inside"],
|
|
65
|
+
"pat": ["set_pat_overload", "overload"],
|
|
66
|
+
"ssh_ios": ["set_ssh_ios", "ip ssh"],
|
|
67
|
+
"ntp_ios": ["set_ntp_server", "ntp server"],
|
|
68
|
+
"syslog_ios": ["set_syslog_server", "logging host"],
|
|
69
|
+
}
|
|
70
|
+
DONOR_BACKED_READY_PROOF_CAPABILITIES = {
|
|
71
|
+
"ospfv3",
|
|
72
|
+
"eigrp_ipv6",
|
|
73
|
+
"ripng",
|
|
74
|
+
"hsrp",
|
|
75
|
+
"dot1x",
|
|
76
|
+
"qos",
|
|
77
|
+
"zfw",
|
|
78
|
+
"cbac",
|
|
79
|
+
"voip",
|
|
80
|
+
"ip_phone",
|
|
81
|
+
"call_manager",
|
|
82
|
+
"real_http",
|
|
83
|
+
"real_websocket",
|
|
84
|
+
"python_programming",
|
|
85
|
+
"javascript_programming",
|
|
86
|
+
"tcp_udp_app",
|
|
33
87
|
}
|
|
34
88
|
|
|
35
89
|
STATUS_ORDER = [
|
|
@@ -111,7 +165,9 @@ def _editor_test_mentions(capability: str) -> bool:
|
|
|
111
165
|
def _feature_status(feature: dict[str, Any], evidence: dict[str, Any]) -> str:
|
|
112
166
|
capability = str(feature.get("capability") or feature.get("id") or "")
|
|
113
167
|
ceiling = str(feature.get("support_ceiling") or "report_supported")
|
|
114
|
-
if evidence
|
|
168
|
+
if evidence.get("donor_backed_proof_ready"):
|
|
169
|
+
raw_status = "donor_backed_ready"
|
|
170
|
+
elif evidence["editor_roundtrip_test"]:
|
|
115
171
|
raw_status = "edit_proven"
|
|
116
172
|
elif evidence["parser_pattern"] and (evidence["sample_count"] or evidence["catalog_keyword"] or evidence["coverage_provider"]):
|
|
117
173
|
raw_status = "report_supported"
|
|
@@ -164,12 +220,20 @@ def build_feature_gap_report(
|
|
|
164
220
|
"catalog_keyword": capability in CAPABILITY_KEYWORDS,
|
|
165
221
|
"coverage_provider": capability in CAPABILITY_PROVIDER_FAMILIES,
|
|
166
222
|
"editor_roundtrip_test": _editor_test_mentions(capability),
|
|
223
|
+
"donor_backed_proof": bool(feature.get("donor_backed_proof"))
|
|
224
|
+
and capability in DONOR_BACKED_READY_PROOF_CAPABILITIES,
|
|
167
225
|
"sample_count": len(hits),
|
|
168
226
|
"sample_examples": hits[:5],
|
|
169
227
|
"sample_path_count": len(hits),
|
|
170
228
|
"decode_verified_sample_count": len(decoded_hits),
|
|
171
229
|
"decode_verified_examples": decoded_hits[:5],
|
|
172
230
|
}
|
|
231
|
+
evidence["donor_backed_proof_ready"] = bool(
|
|
232
|
+
evidence["donor_backed_proof"]
|
|
233
|
+
and evidence["editor_roundtrip_test"]
|
|
234
|
+
and evidence["sample_count"]
|
|
235
|
+
and evidence["decode_verified_sample_count"]
|
|
236
|
+
)
|
|
173
237
|
status = _feature_status(feature, evidence)
|
|
174
238
|
if status != "not_mapped":
|
|
175
239
|
mapped_features += 1
|
package/scripts/generate_pkt.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import copy
|
|
7
|
+
from collections import Counter, defaultdict
|
|
7
8
|
from dataclasses import asdict, dataclass, field
|
|
8
9
|
from functools import lru_cache
|
|
9
10
|
import json
|
|
@@ -37,7 +38,13 @@ from pkt_builder import build_packet_tracer_xml
|
|
|
37
38
|
from pkt_codec import decode_pkt_file, decode_pkt_modern, encode_pkt_modern
|
|
38
39
|
from pkt_editor import apply_plan_operations, decode_pkt_to_root, edit_pkt_file, inventory_devices, inventory_links, inventory_root
|
|
39
40
|
from pkt_transformer import transform_from_blueprint
|
|
40
|
-
from remote_search import
|
|
41
|
+
from remote_search import (
|
|
42
|
+
asdict_list as remote_asdict_list,
|
|
43
|
+
auto_import_remote_candidates,
|
|
44
|
+
candidate_is_curated_eligible,
|
|
45
|
+
search_remote_candidates,
|
|
46
|
+
write_remote_sample_audit,
|
|
47
|
+
)
|
|
41
48
|
from sample_catalog import ReferencePattern, SampleCandidate, SampleDescriptor, load_catalog, load_curated_donor_catalog, load_reference_catalog, summarize_pkt_descriptor
|
|
42
49
|
from sample_selector import rank_curated_donor_samples, rank_reference_samples, rank_samples, select_best_sample
|
|
43
50
|
from workspace_repair import inspect_donor_coherence, inspect_workspace_integrity, validate_donor_coherence, validate_workspace_integrity
|
|
@@ -83,6 +90,9 @@ DEVICE_FAMILY_MAP = {
|
|
|
83
90
|
"Tablet": "end devices",
|
|
84
91
|
"Smartphone": "end devices",
|
|
85
92
|
"Printer": "end devices",
|
|
93
|
+
"IpPhone": "end devices",
|
|
94
|
+
"HomeVoip": "end devices",
|
|
95
|
+
"AnalogPhone": "end devices",
|
|
86
96
|
"LightWeightAccessPoint": "access points",
|
|
87
97
|
"WirelessRouter": "home/wireless routers",
|
|
88
98
|
"WirelessRouterNewGeneration": "home/wireless routers",
|
|
@@ -251,6 +261,8 @@ def _resolve_remote_sources(
|
|
|
251
261
|
remote_provider: str = "github",
|
|
252
262
|
import_cache_root: Path | None = None,
|
|
253
263
|
max_remote_results: int = 10,
|
|
264
|
+
remote_dry_run: bool = False,
|
|
265
|
+
remote_audit_out: Path | None = None,
|
|
254
266
|
) -> tuple[list[Path], list[Path], list[dict[str, object]]]:
|
|
255
267
|
resolved_reference_roots = list(reference_roots or [])
|
|
256
268
|
resolved_donor_roots = list(donor_roots or [])
|
|
@@ -258,13 +270,19 @@ def _resolve_remote_sources(
|
|
|
258
270
|
return resolved_reference_roots, resolved_donor_roots, []
|
|
259
271
|
remote_candidates = search_remote_candidates(plan, provider=remote_provider, max_results=max_remote_results)
|
|
260
272
|
cache_root = import_cache_root or _default_import_cache_root()
|
|
261
|
-
imported_candidates = auto_import_remote_candidates(
|
|
273
|
+
imported_candidates = auto_import_remote_candidates(
|
|
274
|
+
remote_candidates,
|
|
275
|
+
cache_root,
|
|
276
|
+
max_results=max_remote_results,
|
|
277
|
+
dry_run=remote_dry_run,
|
|
278
|
+
)
|
|
279
|
+
write_remote_sample_audit(imported_candidates, cache_root, remote_audit_out)
|
|
262
280
|
for candidate in imported_candidates:
|
|
263
281
|
if candidate.path:
|
|
264
282
|
imported_root = Path(candidate.path)
|
|
265
283
|
if imported_root not in resolved_reference_roots:
|
|
266
284
|
resolved_reference_roots.append(imported_root)
|
|
267
|
-
if imported_root not in resolved_donor_roots:
|
|
285
|
+
if candidate_is_curated_eligible(candidate) and imported_root not in resolved_donor_roots:
|
|
268
286
|
resolved_donor_roots.append(imported_root)
|
|
269
287
|
return resolved_reference_roots, resolved_donor_roots, remote_asdict_list(imported_candidates)
|
|
270
288
|
|
|
@@ -364,6 +382,7 @@ def _preferred_donor_archetypes_for_plan(plan: IntentPlan, topology_tags: list[s
|
|
|
364
382
|
or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"})
|
|
365
383
|
)
|
|
366
384
|
ipv6_routing_signal = plan.network_style == "ipv6_routing" or bool(capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"})
|
|
385
|
+
ipv4_routing_management_signal = plan.network_style == "ipv4_routing_management" or bool(capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"})
|
|
367
386
|
l2_security_monitoring_signal = plan.network_style == "l2_security_monitoring" or bool(capabilities & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"})
|
|
368
387
|
wireless_advanced_signal = plan.network_style == "wireless_advanced" or bool(capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"})
|
|
369
388
|
automation_controller_signal = plan.network_style == "automation_controller" or bool(capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"})
|
|
@@ -393,6 +412,8 @@ def _preferred_donor_archetypes_for_plan(plan: IntentPlan, topology_tags: list[s
|
|
|
393
412
|
family_hints.append("L2 security/monitoring")
|
|
394
413
|
elif ipv6_routing_signal:
|
|
395
414
|
family_hints.append("IPv6/routing")
|
|
415
|
+
elif ipv4_routing_management_signal:
|
|
416
|
+
family_hints.append("IPv4 routing/management")
|
|
396
417
|
elif wan_signal:
|
|
397
418
|
family_hints.append("WAN/security edge")
|
|
398
419
|
if campus_signal and not bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge"}):
|
|
@@ -447,6 +468,8 @@ def _required_runtime_features_for_plan(plan: IntentPlan) -> list[str]:
|
|
|
447
468
|
features.add("multilayer_runtime")
|
|
448
469
|
if capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
449
470
|
features.add("ipv6_runtime")
|
|
471
|
+
if capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
|
|
472
|
+
features.add("ipv4_routing_management_runtime")
|
|
450
473
|
return sorted(features)
|
|
451
474
|
|
|
452
475
|
|
|
@@ -789,12 +812,23 @@ def _critical_capability_parity(coverage_gap: dict[str, object]) -> tuple[list[d
|
|
|
789
812
|
def _parity_counts(parity_entries: list[dict[str, object]]) -> dict[str, int]:
|
|
790
813
|
return {
|
|
791
814
|
"parity_supported_count": sum(1 for entry in parity_entries if bool(entry.get("inventory_supported"))),
|
|
815
|
+
"parity_donor_backed_ready_count": sum(1 for entry in parity_entries if bool(entry.get("donor_backed_ready"))),
|
|
792
816
|
"parity_generate_ready_count": sum(1 for entry in parity_entries if bool(entry.get("generate_supported"))),
|
|
793
817
|
"parity_acceptance_verified_count": sum(1 for entry in parity_entries if bool(entry.get("acceptance_verified"))),
|
|
794
818
|
"parity_mismatch_count": sum(1 for entry in parity_entries if entry.get("generate_mismatch_reason")),
|
|
795
819
|
}
|
|
796
820
|
|
|
797
821
|
|
|
822
|
+
def _critical_parity_counts(critical_entries: list[dict[str, object]]) -> dict[str, int]:
|
|
823
|
+
return {
|
|
824
|
+
"critical_parity_supported_count": sum(1 for entry in critical_entries if bool(entry.get("inventory_supported"))),
|
|
825
|
+
"critical_parity_donor_backed_ready_count": sum(1 for entry in critical_entries if bool(entry.get("donor_backed_ready"))),
|
|
826
|
+
"critical_parity_generate_ready_count": sum(1 for entry in critical_entries if bool(entry.get("generate_supported"))),
|
|
827
|
+
"critical_parity_acceptance_verified_count": sum(1 for entry in critical_entries if bool(entry.get("acceptance_verified"))),
|
|
828
|
+
"critical_parity_mismatch_count": sum(1 for entry in critical_entries if entry.get("generate_mismatch_reason")),
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
|
|
798
832
|
def _selected_donor_summary(
|
|
799
833
|
diagnostics: list[dict[str, object]],
|
|
800
834
|
donor_archetype: DonorArchetypePlan | None = None,
|
|
@@ -2988,6 +3022,7 @@ def _scenario_acceptance_summary(
|
|
|
2988
3022
|
"missing_critical_capabilities": [str(item) for item in list(readiness.get("missing_critical_capabilities", [])) if str(item).strip()],
|
|
2989
3023
|
"critical_capability_parity": critical_capability_parity,
|
|
2990
3024
|
"critical_parity_mismatches": critical_parity_mismatches,
|
|
3025
|
+
**_critical_parity_counts(critical_capability_parity),
|
|
2991
3026
|
"key_reasons": key_reasons[:3],
|
|
2992
3027
|
"next_best_action": next_best_action,
|
|
2993
3028
|
"remediation_steps": remediation_steps,
|
|
@@ -3216,6 +3251,8 @@ def generate_from_prompt(
|
|
|
3216
3251
|
remote_provider: str = "github",
|
|
3217
3252
|
import_cache_root: Path | None = None,
|
|
3218
3253
|
max_remote_results: int = 10,
|
|
3254
|
+
remote_dry_run: bool = False,
|
|
3255
|
+
remote_audit_out: Path | None = None,
|
|
3219
3256
|
blueprint_out_path: Path | None = None,
|
|
3220
3257
|
) -> None:
|
|
3221
3258
|
raw_plan = parse_intent(prompt)
|
|
@@ -3227,6 +3264,8 @@ def generate_from_prompt(
|
|
|
3227
3264
|
remote_provider=remote_provider,
|
|
3228
3265
|
import_cache_root=import_cache_root,
|
|
3229
3266
|
max_remote_results=max_remote_results,
|
|
3267
|
+
remote_dry_run=remote_dry_run,
|
|
3268
|
+
remote_audit_out=remote_audit_out,
|
|
3230
3269
|
)
|
|
3231
3270
|
raw_plan.remote_search_results = remote_results
|
|
3232
3271
|
if raw_plan.goal == "edit" and raw_plan.pkt_path:
|
|
@@ -3343,6 +3382,8 @@ def _explain_plan_payload(
|
|
|
3343
3382
|
remote_provider: str = "github",
|
|
3344
3383
|
import_cache_root: Path | None = None,
|
|
3345
3384
|
max_remote_results: int = 10,
|
|
3385
|
+
remote_dry_run: bool = False,
|
|
3386
|
+
remote_audit_out: Path | None = None,
|
|
3346
3387
|
) -> dict[str, object]:
|
|
3347
3388
|
raw_plan = parse_intent(prompt)
|
|
3348
3389
|
resolved_reference_roots, resolved_donor_roots, remote_results = _resolve_remote_sources(
|
|
@@ -3353,6 +3394,8 @@ def _explain_plan_payload(
|
|
|
3353
3394
|
remote_provider=remote_provider,
|
|
3354
3395
|
import_cache_root=import_cache_root,
|
|
3355
3396
|
max_remote_results=max_remote_results,
|
|
3397
|
+
remote_dry_run=remote_dry_run,
|
|
3398
|
+
remote_audit_out=remote_audit_out,
|
|
3356
3399
|
)
|
|
3357
3400
|
plan = _apply_prompt_compatibility_requirements(raw_plan, resolved_donor_roots)
|
|
3358
3401
|
plan.remote_search_results = remote_results
|
|
@@ -3627,6 +3670,8 @@ def _explain_plan_payload(
|
|
|
3627
3670
|
result["scenario_matrix_row"]["fixture_name"] = _scenario_fixture_name(result["scenario_matrix_row"].get("family"))
|
|
3628
3671
|
result["scenario_matrix_row"]["matrix_version"] = "2.1"
|
|
3629
3672
|
result["scenario_matrix_row"].update(_parity_counts(_capability_parity_entries(result.get("coverage_gaps", {}))))
|
|
3673
|
+
critical_capability_parity, _critical_parity_mismatches = _critical_capability_parity(dict(result.get("coverage_gaps") or {}))
|
|
3674
|
+
result["scenario_matrix_row"].update(_critical_parity_counts(critical_capability_parity))
|
|
3630
3675
|
fixture_status, fixture_gaps = _fixture_expectation_status(result)
|
|
3631
3676
|
result["fixture_registry_version"] = _load_fixture_corpus().get("fixture_registry_version", "unknown")
|
|
3632
3677
|
result["fixture_expectation_status"] = fixture_status
|
|
@@ -3643,6 +3688,8 @@ def explain_plan(
|
|
|
3643
3688
|
remote_provider: str = "github",
|
|
3644
3689
|
import_cache_root: Path | None = None,
|
|
3645
3690
|
max_remote_results: int = 10,
|
|
3691
|
+
remote_dry_run: bool = False,
|
|
3692
|
+
remote_audit_out: Path | None = None,
|
|
3646
3693
|
acceptance_json_out: Path | None = None,
|
|
3647
3694
|
) -> None:
|
|
3648
3695
|
result = _explain_plan_payload(
|
|
@@ -3653,6 +3700,8 @@ def explain_plan(
|
|
|
3653
3700
|
remote_provider=remote_provider,
|
|
3654
3701
|
import_cache_root=import_cache_root,
|
|
3655
3702
|
max_remote_results=max_remote_results,
|
|
3703
|
+
remote_dry_run=remote_dry_run,
|
|
3704
|
+
remote_audit_out=remote_audit_out,
|
|
3656
3705
|
)
|
|
3657
3706
|
_write_json_artifact(result, acceptance_json_out)
|
|
3658
3707
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -3843,6 +3892,8 @@ def parity_report(
|
|
|
3843
3892
|
remote_provider: str = "github",
|
|
3844
3893
|
import_cache_root: Path | None = None,
|
|
3845
3894
|
max_remote_results: int = 10,
|
|
3895
|
+
remote_dry_run: bool = False,
|
|
3896
|
+
remote_audit_out: Path | None = None,
|
|
3846
3897
|
acceptance_json_out: Path | None = None,
|
|
3847
3898
|
) -> None:
|
|
3848
3899
|
payload = _explain_plan_payload(
|
|
@@ -3853,6 +3904,8 @@ def parity_report(
|
|
|
3853
3904
|
remote_provider=remote_provider,
|
|
3854
3905
|
import_cache_root=import_cache_root,
|
|
3855
3906
|
max_remote_results=max_remote_results,
|
|
3907
|
+
remote_dry_run=remote_dry_run,
|
|
3908
|
+
remote_audit_out=remote_audit_out,
|
|
3856
3909
|
)
|
|
3857
3910
|
coverage_gap = dict(payload.get("coverage_gaps") or {})
|
|
3858
3911
|
if "capability_parity" not in coverage_gap:
|
|
@@ -3865,6 +3918,7 @@ def parity_report(
|
|
|
3865
3918
|
"critical_capability_parity": critical_capability_parity,
|
|
3866
3919
|
"critical_parity_mismatches": critical_parity_mismatches,
|
|
3867
3920
|
**_parity_counts(list(payload.get("capability_parity", []))),
|
|
3921
|
+
**_critical_parity_counts(critical_capability_parity),
|
|
3868
3922
|
}
|
|
3869
3923
|
_write_json_artifact(result, acceptance_json_out)
|
|
3870
3924
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -3879,6 +3933,8 @@ def compare_scenarios(
|
|
|
3879
3933
|
remote_provider: str = "github",
|
|
3880
3934
|
import_cache_root: Path | None = None,
|
|
3881
3935
|
max_remote_results: int = 10,
|
|
3936
|
+
remote_dry_run: bool = False,
|
|
3937
|
+
remote_audit_out: Path | None = None,
|
|
3882
3938
|
matrix_out: Path | None = None,
|
|
3883
3939
|
acceptance_json_out: Path | None = None,
|
|
3884
3940
|
) -> None:
|
|
@@ -3893,6 +3949,8 @@ def compare_scenarios(
|
|
|
3893
3949
|
remote_provider=remote_provider,
|
|
3894
3950
|
import_cache_root=import_cache_root,
|
|
3895
3951
|
max_remote_results=max_remote_results,
|
|
3952
|
+
remote_dry_run=remote_dry_run,
|
|
3953
|
+
remote_audit_out=remote_audit_out,
|
|
3896
3954
|
)
|
|
3897
3955
|
fixture_status, fixture_gaps = _fixture_expectation_status(payload)
|
|
3898
3956
|
payloads.append(
|
|
@@ -3930,6 +3988,90 @@ def feature_gap_report() -> None:
|
|
|
3930
3988
|
print(json.dumps(build_feature_gap_report(), indent=2, ensure_ascii=False))
|
|
3931
3989
|
|
|
3932
3990
|
|
|
3991
|
+
LOCAL_AUDIT_PATTERNS = {
|
|
3992
|
+
"ospfv2": r"(?mi)^\s*router\s+ospf\s+\d+",
|
|
3993
|
+
"eigrp_ipv4": r"(?mi)^\s*router\s+eigrp\s+\d+",
|
|
3994
|
+
"ripv2": r"(?mi)^\s*router\s+rip\s*$",
|
|
3995
|
+
"static_route": r"(?mi)^\s*ip\s+route\s+\S+\s+\S+\s+\S+",
|
|
3996
|
+
"default_route": r"(?mi)^\s*ip\s+route\s+0\.0\.0\.0\s+0\.0\.0\.0\s+\S+",
|
|
3997
|
+
"dhcp_relay": r"(?mi)^\s*ip\s+helper-address\s+\d+\.\d+\.\d+\.\d+",
|
|
3998
|
+
"nat_static": r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+static\b",
|
|
3999
|
+
"nat_dynamic": r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+(?:list|route-map)\b",
|
|
4000
|
+
"pat": r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+list\s+\S+\s+interface\s+\S+\s+overload\b",
|
|
4001
|
+
"ssh_ios": r"(?mi)^\s*(?:ip\s+ssh|crypto\s+key\s+generate|ip\s+domain-name)\b",
|
|
4002
|
+
"ntp_ios": r"(?mi)^\s*ntp\s+server\s+\d+\.\d+\.\d+\.\d+",
|
|
4003
|
+
"syslog_ios": r"(?mi)^\s*logging\s+host\s+\d+\.\d+\.\d+\.\d+",
|
|
4004
|
+
"bgp": r"(?mi)^\s*router\s+bgp\s+\d+",
|
|
4005
|
+
"stp": r"(?mi)^\s*spanning-tree\b",
|
|
4006
|
+
"etherchannel": r"(?mi)^\s*(?:channel-group\s+\d+|interface\s+Port-channel)",
|
|
4007
|
+
"hsrp": r"(?mi)^\s*standby\s+\d+",
|
|
4008
|
+
"acl": r"(?mi)^\s*(?:access-list\s+\d+|ip\s+access-list\s+(?:standard|extended))\b",
|
|
4009
|
+
"dhcp_pool": r"(?mi)^\s*ip\s+dhcp\s+pool\b",
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
|
|
4013
|
+
def build_local_sample_audit(root: Path) -> dict[str, object]:
|
|
4014
|
+
files = sorted([*root.rglob("*.pkt"), *root.rglob("*.pka")])
|
|
4015
|
+
capability_counts: Counter[str] = Counter()
|
|
4016
|
+
capability_examples: dict[str, list[str]] = defaultdict(list)
|
|
4017
|
+
device_counts: Counter[str] = Counter()
|
|
4018
|
+
decode_failures: list[dict[str, str]] = []
|
|
4019
|
+
for pkt_path in files:
|
|
4020
|
+
rel = str(pkt_path.relative_to(root))
|
|
4021
|
+
try:
|
|
4022
|
+
pkt_root = decode_pkt_to_root(pkt_path)
|
|
4023
|
+
except Exception as exc:
|
|
4024
|
+
decode_failures.append({"path": rel, "error_type": type(exc).__name__, "message": str(exc)[:200]})
|
|
4025
|
+
continue
|
|
4026
|
+
try:
|
|
4027
|
+
inventory = inventory_root(pkt_root)
|
|
4028
|
+
for device_type, count in inventory.get("topology_summary", {}).get("device_counts", {}).items():
|
|
4029
|
+
device_counts[str(device_type)] += int(count)
|
|
4030
|
+
except Exception:
|
|
4031
|
+
pass
|
|
4032
|
+
running = "\n".join(line.text or "" for line in pkt_root.findall(".//ENGINE/RUNNINGCONFIG/LINE"))
|
|
4033
|
+
for capability, pattern in LOCAL_AUDIT_PATTERNS.items():
|
|
4034
|
+
if re.search(pattern, running):
|
|
4035
|
+
capability_counts[capability] += 1
|
|
4036
|
+
if len(capability_examples[capability]) < 5:
|
|
4037
|
+
capability_examples[capability].append(rel)
|
|
4038
|
+
promotion_candidates = [
|
|
4039
|
+
{
|
|
4040
|
+
"capability": capability,
|
|
4041
|
+
"sample_count": count,
|
|
4042
|
+
"candidate_status": "local_evidence_only",
|
|
4043
|
+
"next_safe_action": "Add parser/inventory/editor roundtrip proof before public readiness promotion.",
|
|
4044
|
+
}
|
|
4045
|
+
for capability, count in capability_counts.most_common()
|
|
4046
|
+
]
|
|
4047
|
+
return {
|
|
4048
|
+
"audit_version": "1.0",
|
|
4049
|
+
"source_root": str(root),
|
|
4050
|
+
"policy": "Local user-supplied Packet Tracer files are evidence inputs only; raw .pkt/.pka files are not public package content.",
|
|
4051
|
+
"total_files": len(files),
|
|
4052
|
+
"decode_success_count": len(files) - len(decode_failures),
|
|
4053
|
+
"decode_fail_count": len(decode_failures),
|
|
4054
|
+
"decode_failures": decode_failures[:20],
|
|
4055
|
+
"detected_config_capabilities": {
|
|
4056
|
+
capability: {
|
|
4057
|
+
"sample_count": count,
|
|
4058
|
+
"examples": capability_examples.get(capability, []),
|
|
4059
|
+
}
|
|
4060
|
+
for capability, count in capability_counts.most_common()
|
|
4061
|
+
},
|
|
4062
|
+
"top_device_types": [{"device_type": device_type, "count": count} for device_type, count in device_counts.most_common(30)],
|
|
4063
|
+
"promotion_candidates": promotion_candidates,
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
|
|
4067
|
+
def local_sample_audit(root: Path, audit_out: Path | None = None) -> None:
|
|
4068
|
+
payload = build_local_sample_audit(root)
|
|
4069
|
+
target = audit_out or (Path("output") / "local-sample-audit.json")
|
|
4070
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
4071
|
+
target.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
4072
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
4073
|
+
|
|
4074
|
+
|
|
3933
4075
|
def main() -> None:
|
|
3934
4076
|
parser = argparse.ArgumentParser(description="Generate or inspect Cisco Packet Tracer 9.0 .pkt files")
|
|
3935
4077
|
parser.add_argument("--blueprint", help="Path to the topology blueprint JSON")
|
|
@@ -3951,9 +4093,13 @@ def main() -> None:
|
|
|
3951
4093
|
parser.add_argument("--remote-provider", default="github", help="Remote search provider name (default: github)")
|
|
3952
4094
|
parser.add_argument("--import-cache-root", help="Local cache folder used for remote search auto-imports")
|
|
3953
4095
|
parser.add_argument("--max-remote-results", type=int, default=10, help="Maximum number of remote search results to fetch/import")
|
|
4096
|
+
parser.add_argument("--remote-dry-run", action="store_true", help="Preview remote GitHub sample candidates and write an audit without downloading archives")
|
|
4097
|
+
parser.add_argument("--remote-audit-out", help="Optional JSON path for the local-only remote sample audit report")
|
|
3954
4098
|
parser.add_argument("--blueprint-out", help="Optional JSON output path for the generated blueprint plan or refusal blueprint")
|
|
3955
4099
|
parser.add_argument("--coverage-report", action="store_true", help="Print the aggregated capability coverage matrix")
|
|
3956
4100
|
parser.add_argument("--feature-gap-report", action="store_true", help="Print the Packet Tracer feature gap atlas report")
|
|
4101
|
+
parser.add_argument("--local-sample-audit-root", help="Local user-supplied Packet Tracer sample folder to audit without importing raw files")
|
|
4102
|
+
parser.add_argument("--local-sample-audit-out", help="Optional JSON path for --local-sample-audit-root output")
|
|
3957
4103
|
parser.add_argument("--inventory-capabilities", action="store_true", help="Include inferred capability inventory when using --inventory")
|
|
3958
4104
|
parser.add_argument("--inventory-out", help="Optional JSON output path when using --inventory")
|
|
3959
4105
|
parser.add_argument("--matrix-out", help="Optional JSON output path when using --compare-scenarios")
|
|
@@ -3965,6 +4111,7 @@ def main() -> None:
|
|
|
3965
4111
|
reference_roots = [Path(path) for path in (args.reference_root or [])]
|
|
3966
4112
|
donor_roots = [Path(path) for path in (args.donor_root or [])]
|
|
3967
4113
|
import_cache_root = Path(args.import_cache_root) if args.import_cache_root else None
|
|
4114
|
+
remote_audit_out = Path(args.remote_audit_out) if args.remote_audit_out else None
|
|
3968
4115
|
|
|
3969
4116
|
if args.explain_plan:
|
|
3970
4117
|
explain_plan(
|
|
@@ -3975,6 +4122,8 @@ def main() -> None:
|
|
|
3975
4122
|
remote_provider=args.remote_provider,
|
|
3976
4123
|
import_cache_root=import_cache_root,
|
|
3977
4124
|
max_remote_results=args.max_remote_results,
|
|
4125
|
+
remote_dry_run=args.remote_dry_run,
|
|
4126
|
+
remote_audit_out=remote_audit_out,
|
|
3978
4127
|
acceptance_json_out=Path(args.acceptance_json_out) if args.acceptance_json_out else None,
|
|
3979
4128
|
)
|
|
3980
4129
|
return
|
|
@@ -3987,6 +4136,8 @@ def main() -> None:
|
|
|
3987
4136
|
remote_provider=args.remote_provider,
|
|
3988
4137
|
import_cache_root=import_cache_root,
|
|
3989
4138
|
max_remote_results=args.max_remote_results,
|
|
4139
|
+
remote_dry_run=args.remote_dry_run,
|
|
4140
|
+
remote_audit_out=remote_audit_out,
|
|
3990
4141
|
matrix_out=Path(args.matrix_out) if args.matrix_out else None,
|
|
3991
4142
|
acceptance_json_out=Path(args.acceptance_json_out) if args.acceptance_json_out else None,
|
|
3992
4143
|
)
|
|
@@ -4000,6 +4151,8 @@ def main() -> None:
|
|
|
4000
4151
|
remote_provider=args.remote_provider,
|
|
4001
4152
|
import_cache_root=import_cache_root,
|
|
4002
4153
|
max_remote_results=args.max_remote_results,
|
|
4154
|
+
remote_dry_run=args.remote_dry_run,
|
|
4155
|
+
remote_audit_out=remote_audit_out,
|
|
4003
4156
|
acceptance_json_out=Path(args.acceptance_json_out) if args.acceptance_json_out else None,
|
|
4004
4157
|
)
|
|
4005
4158
|
return
|
|
@@ -4024,6 +4177,9 @@ def main() -> None:
|
|
|
4024
4177
|
if args.feature_gap_report:
|
|
4025
4178
|
feature_gap_report()
|
|
4026
4179
|
return
|
|
4180
|
+
if args.local_sample_audit_root:
|
|
4181
|
+
local_sample_audit(Path(args.local_sample_audit_root), Path(args.local_sample_audit_out) if args.local_sample_audit_out else None)
|
|
4182
|
+
return
|
|
4027
4183
|
if args.decode:
|
|
4028
4184
|
if not args.xml_out:
|
|
4029
4185
|
parser.error("--decode requires --xml-out")
|
|
@@ -4055,6 +4211,8 @@ def main() -> None:
|
|
|
4055
4211
|
remote_provider=args.remote_provider,
|
|
4056
4212
|
import_cache_root=import_cache_root,
|
|
4057
4213
|
max_remote_results=args.max_remote_results,
|
|
4214
|
+
remote_dry_run=args.remote_dry_run,
|
|
4215
|
+
remote_audit_out=remote_audit_out,
|
|
4058
4216
|
blueprint_out_path=Path(args.blueprint_out) if args.blueprint_out else None,
|
|
4059
4217
|
)
|
|
4060
4218
|
except PlanningError as exc:
|