packet-tracer-skill 0.2.3 → 0.3.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.
- package/CHANGELOG.md +424 -73
- package/README.md +557 -442
- package/SKILL.md +337 -262
- package/bin/packet-tracer-skill.js +29 -2
- package/docs/github-launch-ops-0.2.3.md +37 -0
- package/docs/github-metadata.md +6 -4
- package/docs/hero-demo-plan.md +1 -1
- package/docs/home-iot-donor-proof.md +4 -4
- package/docs/l2-security-qos-proof.md +1 -1
- package/docs/packet-tracer-feature-gap-atlas.md +4 -4
- package/docs/post-launch-follow-up.md +9 -5
- package/docs/proof-readiness-dashboard.md +69 -0
- package/docs/publish-preview-roadmap.md +6 -5
- package/docs/release-checklist.md +17 -8
- package/docs/release-notes-0.2.4.md +20 -0
- package/docs/runtime-truth.md +33 -8
- package/docs/security-edge-deepening-proof.md +1 -1
- package/examples/README.md +98 -69
- package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
- package/examples/gallery.md +94 -6
- package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
- package/examples/index.json +932 -4
- package/examples/local-sample-evidence.json +24 -0
- package/examples/proof-cards.json +117 -0
- package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
- package/package.json +60 -53
- package/pytest.ini +9 -0
- package/references/packettracer-sample-catalog.json +45287 -4525
- package/references/packettracer-sample-catalog.md +599 -259
- package/references/proof-readiness-candidates.json +352 -0
- package/scripts/build_examples_index.py +228 -35
- package/scripts/build_sample_catalog.py +24 -44
- package/scripts/corpus_runner.py +430 -0
- package/scripts/coverage_matrix.py +1842 -1812
- package/scripts/donor_cache.py +354 -0
- package/scripts/donor_diagnostics.py +3 -1
- package/scripts/generate_pkt.py +8762 -4228
- package/scripts/intent_parser.py +2242 -1657
- package/scripts/local_donors.py +340 -0
- package/scripts/packet_tracer_env.py +846 -391
- package/scripts/pkt_annotate.py +218 -0
- package/scripts/pkt_codec.py +420 -181
- package/scripts/pkt_editor.py +2405 -1703
- package/scripts/pkt_transformer.py +1072 -727
- package/scripts/pkt_verify.py +461 -0
- package/scripts/runtime_doctor.py +80 -29
- package/scripts/sample_catalog.py +1372 -1250
- package/scripts/twofish_diagnostics.py +48 -31
- package/scripts/usage_ledger.py +218 -0
- package/scripts/vendor/README.md +44 -37
- package/scripts/vendor/twofish_pure.py +321 -0
- package/scripts/workspace_repair.py +548 -508
- package/templates/pt900/donors/README.md +15 -0
|
@@ -1,1250 +1,1372 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if
|
|
509
|
-
|
|
510
|
-
if
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
"
|
|
521
|
-
|
|
522
|
-
"
|
|
523
|
-
|
|
524
|
-
"
|
|
525
|
-
|
|
526
|
-
"
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
"
|
|
542
|
-
|
|
543
|
-
"
|
|
544
|
-
|
|
545
|
-
"
|
|
546
|
-
|
|
547
|
-
"
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
"
|
|
582
|
-
|
|
583
|
-
"
|
|
584
|
-
|
|
585
|
-
"
|
|
586
|
-
|
|
587
|
-
"
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
"
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"
|
|
656
|
-
"
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
"
|
|
696
|
-
"
|
|
697
|
-
"
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
"
|
|
719
|
-
|
|
720
|
-
"
|
|
721
|
-
|
|
722
|
-
"
|
|
723
|
-
|
|
724
|
-
"
|
|
725
|
-
|
|
726
|
-
"
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if item.get("
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if "
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
"
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
]
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
)
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1071
|
-
"
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
"
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
"
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1095
|
-
"
|
|
1096
|
-
"
|
|
1097
|
-
"
|
|
1098
|
-
"
|
|
1099
|
-
"
|
|
1100
|
-
"
|
|
1101
|
-
"
|
|
1102
|
-
"
|
|
1103
|
-
"
|
|
1104
|
-
"
|
|
1105
|
-
"
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
(
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
|
|
11
|
+
from packet_tracer_env import (
|
|
12
|
+
donor_compatibility,
|
|
13
|
+
donor_tier_is_accepted,
|
|
14
|
+
get_packet_tracer_saves_root,
|
|
15
|
+
get_packet_tracer_target_version,
|
|
16
|
+
)
|
|
17
|
+
from pkt_codec import decode_pkt_auto, decode_pkt_modern, parse_pkt_xml
|
|
18
|
+
from workspace_repair import inspect_workspace_integrity
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
21
|
+
SKILL_ROOT = SCRIPT_DIR.parent
|
|
22
|
+
DEFAULT_CATALOG_JSON = SKILL_ROOT / "references" / "packettracer-sample-catalog.json"
|
|
23
|
+
DEFAULT_CATALOG_MD = SKILL_ROOT / "references" / "packettracer-sample-catalog.md"
|
|
24
|
+
DEFAULT_CURATED_DONOR_REGISTRY = SKILL_ROOT / "references" / "curated-donor-registry.json"
|
|
25
|
+
|
|
26
|
+
CAPABILITY_KEYWORDS = {
|
|
27
|
+
"vlan": ["vlan", "trunk", "access"],
|
|
28
|
+
"trunk": ["trunk"],
|
|
29
|
+
"access_port": ["access"],
|
|
30
|
+
"dhcp_pool": ["dhcp", "reservation", "apipa"],
|
|
31
|
+
"router_dhcp": ["dhcp", "reservation", "apipa"],
|
|
32
|
+
"server_dhcp": ["dhcp", "reservation", "apipa"],
|
|
33
|
+
"dhcpv6_stateful": ["dhcpv6", "stateful_dhcpv6"],
|
|
34
|
+
"dhcpv6_stateless": ["stateless_dhcpv6"],
|
|
35
|
+
"ipv6_slaac": ["slaac"],
|
|
36
|
+
"ipv6_prefix_delegation": ["prefix_delegation"],
|
|
37
|
+
"ipv6_dns_aaaa": ["aaaa"],
|
|
38
|
+
"ipv6_tunneling": ["ipv6ip", "ipv6 tunneling"],
|
|
39
|
+
"isatap": ["isatap"],
|
|
40
|
+
"dhcp_snooping": ["dhcp snooping", "option_82", "trusted_untrusted"],
|
|
41
|
+
"ospf": ["ospf"],
|
|
42
|
+
"ospfv2": ["ospf", "ospfv2", "single-area ospf"],
|
|
43
|
+
"ospfv3": ["ospfv3", "ipv6_ospf"],
|
|
44
|
+
"eigrp": ["eigrp"],
|
|
45
|
+
"eigrp_ipv4": ["eigrp", "ipv4 eigrp"],
|
|
46
|
+
"eigrp_ipv6": ["ipv6_eigrp"],
|
|
47
|
+
"rip": ["rip"],
|
|
48
|
+
"ripv2": ["ripv2", "rip"],
|
|
49
|
+
"ripng": ["ripng", "ipv6 rip"],
|
|
50
|
+
"hsrp": ["hsrp"],
|
|
51
|
+
"nat": ["nat"],
|
|
52
|
+
"nat_static": ["static nat"],
|
|
53
|
+
"nat_dynamic": ["dynamic nat"],
|
|
54
|
+
"pat": ["pat", "overload"],
|
|
55
|
+
"static_route": ["static route", "ip route"],
|
|
56
|
+
"default_route": ["default route"],
|
|
57
|
+
"dhcp_relay": ["dhcp relay", "helper-address"],
|
|
58
|
+
"ssh_ios": ["ssh"],
|
|
59
|
+
"ntp_ios": ["ntp"],
|
|
60
|
+
"syslog_ios": ["syslog"],
|
|
61
|
+
"acl": ["acl", "access-list"],
|
|
62
|
+
"dai": ["dai", "dynamic arp inspection"],
|
|
63
|
+
"dot1x": ["dot1x", "802.1x", "port-based nac"],
|
|
64
|
+
"lldp": ["lldp"],
|
|
65
|
+
"rep": ["rep_"],
|
|
66
|
+
"snmp": ["snmp"],
|
|
67
|
+
"netflow": ["netflow"],
|
|
68
|
+
"span": ["rspan", "monitor session", "span session"],
|
|
69
|
+
"qos": ["qos"],
|
|
70
|
+
"port_security": ["port security", "port-security"],
|
|
71
|
+
"bgp": ["bgp"],
|
|
72
|
+
"stp": ["stp", "spanning tree", "spanning-tree"],
|
|
73
|
+
"rstp": ["rstp", "rapid-pvst"],
|
|
74
|
+
"etherchannel": ["etherchannel", "port-channel", "channel-group"],
|
|
75
|
+
"lacp": ["lacp"],
|
|
76
|
+
"pagp": ["pagp"],
|
|
77
|
+
"vtp": ["vtp"],
|
|
78
|
+
"dtp": ["dtp"],
|
|
79
|
+
"vpn": ["vpn", "ipsec", "gre"],
|
|
80
|
+
"ipsec": ["ipsec", "ike"],
|
|
81
|
+
"gre": ["gre"],
|
|
82
|
+
"ppp": ["ppp", "chap", "pap"],
|
|
83
|
+
"wan": ["wan", "cloud", "serial", "pppoe", "ppp"],
|
|
84
|
+
"security_edge": ["asa", "firewall", "security appliance"],
|
|
85
|
+
"asa_acl_nat": ["asa_acl_nat"],
|
|
86
|
+
"asa_service_policy": ["asa_service_policy", "service policy"],
|
|
87
|
+
"clientless_vpn": ["clientless_vpn"],
|
|
88
|
+
"cbac": ["cbac"],
|
|
89
|
+
"zfw": ["zfw", "zone based firewall"],
|
|
90
|
+
"sniffer": ["sniffer"],
|
|
91
|
+
"multilayer_switching": ["multilayer", "layer 3", "3560", "svi"],
|
|
92
|
+
"wireless": ["wireless", "wlan", "wlc", "wifi", "ssid", "wpa", "wep", "5g", "bluetooth", "cellular"],
|
|
93
|
+
"wireless_ap": ["wireless", "wlan", "wlc", "wifi", "ssid", "wpa", "wep"],
|
|
94
|
+
"wireless_client": ["wireless", "wifi", "tablet", "smartphone", "laptop"],
|
|
95
|
+
"wlc": ["wlc", "wireless lan controller"],
|
|
96
|
+
"wpa_enterprise": ["wpa_radius", "wpa2_enterprise"],
|
|
97
|
+
"wep": ["wep"],
|
|
98
|
+
"guest_wifi": ["guest"],
|
|
99
|
+
"beamforming": ["beamforming"],
|
|
100
|
+
"meraki": ["meraki"],
|
|
101
|
+
"cellular_5g": ["5g", "cellular", "cell tower"],
|
|
102
|
+
"bluetooth": ["bluetooth"],
|
|
103
|
+
"network_controller": ["network controller", "netcon"],
|
|
104
|
+
"python_programming": ["python"],
|
|
105
|
+
"javascript_programming": ["javascript"],
|
|
106
|
+
"blockly_programming": ["blockly"],
|
|
107
|
+
"tcp_udp_app": ["tcp_test_app", "udp_test_app"],
|
|
108
|
+
"vm_iox": ["iox", "uploading_and_running_vm"],
|
|
109
|
+
"voip": ["voip"],
|
|
110
|
+
"ip_phone": ["ip phone", "phone"],
|
|
111
|
+
"call_manager": ["call manager", "telephony", "voip"],
|
|
112
|
+
"linksys_voice": ["voip linksys"],
|
|
113
|
+
"mqtt": ["mqtt"],
|
|
114
|
+
"real_http": ["real-http", "real http"],
|
|
115
|
+
"real_websocket": ["real-websocket", "real websocket"],
|
|
116
|
+
"visual_scripting": ["visual-scripting", "visual scripting"],
|
|
117
|
+
"ptp": ["ptp"],
|
|
118
|
+
"profinet": ["profinet"],
|
|
119
|
+
"l2nat": ["l2nat"],
|
|
120
|
+
"cyberobserver": ["cyberobserver"],
|
|
121
|
+
"industrial_firewall": ["isa3000", "industrial cybersecurity"],
|
|
122
|
+
"coaxial": ["coaxial"],
|
|
123
|
+
"cable_dsl": ["cable modem", "dsl modem"],
|
|
124
|
+
"central_office": ["centraloffice", "central office"],
|
|
125
|
+
"cell_tower": ["cell tower"],
|
|
126
|
+
"power_distribution": ["power distribution"],
|
|
127
|
+
"hot_swappable": ["hotswappable", "hot swappable"],
|
|
128
|
+
"ios_license": ["ios_15", "license"],
|
|
129
|
+
"ntp": ["ntp"],
|
|
130
|
+
"dns": ["dns"],
|
|
131
|
+
"server_dns": ["dns"],
|
|
132
|
+
"server_http": ["http", "https", "websocket"],
|
|
133
|
+
"server_ftp": ["ftp", "tftp"],
|
|
134
|
+
"ftp_http_https": ["ftp", "http", "https", "websocket"],
|
|
135
|
+
"iot": ["iot", "sensor", "led", "arduino", "mqtt", "environment"],
|
|
136
|
+
"switching": ["switch", "switching", "lldp", "rep"],
|
|
137
|
+
"router_on_a_stick": ["router-on-a-stick", "dot1q"],
|
|
138
|
+
"host_server": ["server", "pc", "client", "host"],
|
|
139
|
+
"management_vlan": ["management", "vlan99", "telnet"],
|
|
140
|
+
"telnet": ["telnet", "terminal server"],
|
|
141
|
+
"tablet": ["tablet", "pda"],
|
|
142
|
+
"printer": ["printer"],
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
REPORT_ONLY_CAPABILITIES = {
|
|
146
|
+
"ipv6_slaac",
|
|
147
|
+
"dhcpv6_stateful",
|
|
148
|
+
"dhcpv6_stateless",
|
|
149
|
+
"ipv6_prefix_delegation",
|
|
150
|
+
"ipv6_dns_aaaa",
|
|
151
|
+
"ipv6_tunneling",
|
|
152
|
+
"isatap",
|
|
153
|
+
"ospfv3",
|
|
154
|
+
"eigrp_ipv6",
|
|
155
|
+
"ripng",
|
|
156
|
+
"hsrp",
|
|
157
|
+
"dhcp_snooping",
|
|
158
|
+
"dai",
|
|
159
|
+
"dot1x",
|
|
160
|
+
"lldp",
|
|
161
|
+
"rep",
|
|
162
|
+
"snmp",
|
|
163
|
+
"netflow",
|
|
164
|
+
"span",
|
|
165
|
+
"qos",
|
|
166
|
+
"port_security",
|
|
167
|
+
"bgp",
|
|
168
|
+
"stp",
|
|
169
|
+
"rstp",
|
|
170
|
+
"etherchannel",
|
|
171
|
+
"lacp",
|
|
172
|
+
"pagp",
|
|
173
|
+
"vtp",
|
|
174
|
+
"dtp",
|
|
175
|
+
"ospfv2",
|
|
176
|
+
"eigrp_ipv4",
|
|
177
|
+
"ripv2",
|
|
178
|
+
"static_route",
|
|
179
|
+
"default_route",
|
|
180
|
+
"dhcp_relay",
|
|
181
|
+
"nat_static",
|
|
182
|
+
"nat_dynamic",
|
|
183
|
+
"pat",
|
|
184
|
+
"ssh_ios",
|
|
185
|
+
"ntp_ios",
|
|
186
|
+
"syslog_ios",
|
|
187
|
+
"asa_acl_nat",
|
|
188
|
+
"asa_service_policy",
|
|
189
|
+
"clientless_vpn",
|
|
190
|
+
"cbac",
|
|
191
|
+
"zfw",
|
|
192
|
+
"sniffer",
|
|
193
|
+
"wlc",
|
|
194
|
+
"wpa_enterprise",
|
|
195
|
+
"wep",
|
|
196
|
+
"guest_wifi",
|
|
197
|
+
"beamforming",
|
|
198
|
+
"meraki",
|
|
199
|
+
"cellular_5g",
|
|
200
|
+
"bluetooth",
|
|
201
|
+
"network_controller",
|
|
202
|
+
"python_programming",
|
|
203
|
+
"javascript_programming",
|
|
204
|
+
"blockly_programming",
|
|
205
|
+
"tcp_udp_app",
|
|
206
|
+
"vm_iox",
|
|
207
|
+
"voip",
|
|
208
|
+
"ip_phone",
|
|
209
|
+
"call_manager",
|
|
210
|
+
"linksys_voice",
|
|
211
|
+
"mqtt",
|
|
212
|
+
"real_http",
|
|
213
|
+
"real_websocket",
|
|
214
|
+
"visual_scripting",
|
|
215
|
+
"ptp",
|
|
216
|
+
"profinet",
|
|
217
|
+
"l2nat",
|
|
218
|
+
"cyberobserver",
|
|
219
|
+
"industrial_firewall",
|
|
220
|
+
"coaxial",
|
|
221
|
+
"cable_dsl",
|
|
222
|
+
"central_office",
|
|
223
|
+
"cell_tower",
|
|
224
|
+
"power_distribution",
|
|
225
|
+
"hot_swappable",
|
|
226
|
+
"ios_license",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
TYPE_NORMALIZATION = {
|
|
230
|
+
"pc": "PC",
|
|
231
|
+
"pc-pt": "PC",
|
|
232
|
+
"server": "Server",
|
|
233
|
+
"server-pt": "Server",
|
|
234
|
+
"router": "Router",
|
|
235
|
+
"switch": "Switch",
|
|
236
|
+
"multilayerswitch": "MultiLayerSwitch",
|
|
237
|
+
"layer3switch": "MultiLayerSwitch",
|
|
238
|
+
"wirelessrouter": "WirelessRouter",
|
|
239
|
+
"wirelessrouternewgeneration": "WirelessRouter",
|
|
240
|
+
"homegateway": "WirelessRouter",
|
|
241
|
+
# A controller is not an access point: it manages them, carries gigabit
|
|
242
|
+
# uplinks rather than a radio, and a prompt asking for one wants that
|
|
243
|
+
# device. Folding it here meant the planner's `WirelessLanController` kind
|
|
244
|
+
# matched nothing in any donor, and `1 wlc 2 access point qur` was refused
|
|
245
|
+
# by the very lab that carries a WLC.
|
|
246
|
+
"wirelesslancontroller": "WirelessLanController",
|
|
247
|
+
"lightweightaccesspoint": "LightWeightAccessPoint",
|
|
248
|
+
"accesspoint": "LightWeightAccessPoint",
|
|
249
|
+
"pda": "Tablet",
|
|
250
|
+
"tabletpc": "Tablet",
|
|
251
|
+
"tablet": "Tablet",
|
|
252
|
+
"laptop": "Laptop",
|
|
253
|
+
"printer": "Printer",
|
|
254
|
+
"smartphone": "Smartphone",
|
|
255
|
+
"mcucomponent": "IoT",
|
|
256
|
+
"networkcontroller": "NetworkController",
|
|
257
|
+
"centralofficeserver": "CentralOfficeServer",
|
|
258
|
+
"celltower": "CellTower",
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
LICENSE_FILENAMES = [
|
|
262
|
+
"LICENSE",
|
|
263
|
+
"LICENSE.txt",
|
|
264
|
+
"LICENSE.md",
|
|
265
|
+
"COPYING",
|
|
266
|
+
"COPYING.txt",
|
|
267
|
+
"NOTICE",
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class SampleDescriptor:
|
|
273
|
+
path: str
|
|
274
|
+
relative_path: str
|
|
275
|
+
version: str
|
|
276
|
+
device_count: int
|
|
277
|
+
link_count: int
|
|
278
|
+
devices: list[dict[str, Any]]
|
|
279
|
+
links: list[dict[str, Any]]
|
|
280
|
+
capability_tags: list[str]
|
|
281
|
+
topology_tags: list[str]
|
|
282
|
+
preferred_roles: list[str]
|
|
283
|
+
trust_level: str = "trusted"
|
|
284
|
+
origin: str = "cisco-local"
|
|
285
|
+
role: str = "primary"
|
|
286
|
+
prototype_eligible: bool = True
|
|
287
|
+
license_or_permission: str = "local-cisco"
|
|
288
|
+
promotion_status: str = "validated_primary"
|
|
289
|
+
validation_status: str = "validated"
|
|
290
|
+
packet_tracer_version: str = ""
|
|
291
|
+
wireless_mode_tags: list[str] = field(default_factory=list)
|
|
292
|
+
donor_eligible: bool = True
|
|
293
|
+
device_families: list[str] = field(default_factory=list)
|
|
294
|
+
model_families: list[str] = field(default_factory=list)
|
|
295
|
+
port_media_types: list[str] = field(default_factory=list)
|
|
296
|
+
service_support: list[str] = field(default_factory=list)
|
|
297
|
+
iot_roles: list[str] = field(default_factory=list)
|
|
298
|
+
runtime_features: list[str] = field(default_factory=list)
|
|
299
|
+
donor_graph_fingerprint: dict[str, Any] = field(default_factory=dict)
|
|
300
|
+
apply_safety_level: str = "reference-known"
|
|
301
|
+
promotion_evidence: list[str] = field(default_factory=list)
|
|
302
|
+
validated_edit_capabilities: list[str] = field(default_factory=list)
|
|
303
|
+
acceptance_notes: list[str] = field(default_factory=list)
|
|
304
|
+
archetype_tags: list[str] = field(default_factory=list)
|
|
305
|
+
acceptance_fixtures: list[str] = field(default_factory=list)
|
|
306
|
+
provenance: str = ""
|
|
307
|
+
workspace_validation_status: str = ""
|
|
308
|
+
evidence_source: str = "inferred"
|
|
309
|
+
|
|
310
|
+
def normalized_device_counts(self) -> dict[str, int]:
|
|
311
|
+
counts: dict[str, int] = {}
|
|
312
|
+
for device in self.devices:
|
|
313
|
+
normalized = normalize_device_type(device.get("type", ""))
|
|
314
|
+
if normalized:
|
|
315
|
+
counts[normalized] = counts.get(normalized, 0) + 1
|
|
316
|
+
return counts
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class SampleCandidate:
|
|
321
|
+
sample: SampleDescriptor
|
|
322
|
+
capability_score: int
|
|
323
|
+
topology_score: int
|
|
324
|
+
total_score: int
|
|
325
|
+
reasons: list[str]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass
|
|
329
|
+
class ReferencePattern:
|
|
330
|
+
relative_path: str
|
|
331
|
+
origin: str
|
|
332
|
+
capability_tags: list[str]
|
|
333
|
+
topology_tags: list[str]
|
|
334
|
+
device_summary: dict[str, int]
|
|
335
|
+
wireless_mode_tags: list[str] = field(default_factory=list)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@dataclass
|
|
339
|
+
class DonorValidationResult:
|
|
340
|
+
packet_tracer_version: str
|
|
341
|
+
validation_status: str
|
|
342
|
+
promotion_status: str
|
|
343
|
+
donor_eligible: bool
|
|
344
|
+
issues: list[str]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass
|
|
348
|
+
class CuratedDonorRecord:
|
|
349
|
+
sample: SampleDescriptor
|
|
350
|
+
validation: DonorValidationResult
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _normalize_registry_key(value: str) -> str:
|
|
354
|
+
return value.strip().replace("\\", "/").lower()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@lru_cache(maxsize=4)
|
|
358
|
+
def _load_curated_donor_registry_cached(path_str: str) -> dict[str, dict[str, Any]]:
|
|
359
|
+
path = Path(path_str)
|
|
360
|
+
if not path.exists():
|
|
361
|
+
return {}
|
|
362
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
363
|
+
donors = list(payload.get("donors", [])) if isinstance(payload, dict) else []
|
|
364
|
+
registry: dict[str, dict[str, Any]] = {}
|
|
365
|
+
for item in donors:
|
|
366
|
+
if not isinstance(item, dict):
|
|
367
|
+
continue
|
|
368
|
+
key = _normalize_registry_key(str(item.get("relative_path", "")))
|
|
369
|
+
if not key:
|
|
370
|
+
continue
|
|
371
|
+
registry[key] = dict(item)
|
|
372
|
+
return registry
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def load_curated_donor_registry(path: Path | None = None) -> dict[str, dict[str, Any]]:
|
|
376
|
+
return dict(_load_curated_donor_registry_cached(str(path or DEFAULT_CURATED_DONOR_REGISTRY)))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def normalize_device_type(raw_type: str) -> str:
|
|
380
|
+
key = raw_type.strip().lower().replace(" ", "")
|
|
381
|
+
return TYPE_NORMALIZATION.get(key, raw_type.strip())
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _normalized_counts_for_item(item: dict[str, Any]) -> dict[str, int]:
|
|
385
|
+
counts: dict[str, int] = {}
|
|
386
|
+
for device in item.get("devices", []):
|
|
387
|
+
normalized = normalize_device_type(device.get("type", ""))
|
|
388
|
+
if normalized:
|
|
389
|
+
counts[normalized] = counts.get(normalized, 0) + 1
|
|
390
|
+
return counts
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
#
|
|
394
|
+
# Evidence read from device configuration rather than from the file name.
|
|
395
|
+
#
|
|
396
|
+
# Every keyword capability below `CAPABILITY_KEYWORDS` is matched against the
|
|
397
|
+
# path, so a lab called `telnet.pkt` counts as having telnet and a lab whose
|
|
398
|
+
# switches actually carry `line vty` with `transport input telnet` does not.
|
|
399
|
+
# That is how a campus donor with nineteen `line vty` blocks and nineteen
|
|
400
|
+
# `interface Vlan` blocks was credited with no capabilities at all, which in
|
|
401
|
+
# turn refused every management/telnet prompt as "missing critical capability
|
|
402
|
+
# coverage".
|
|
403
|
+
#
|
|
404
|
+
# Only capabilities whose evidence is unambiguous in config text belong here.
|
|
405
|
+
#
|
|
406
|
+
# Measured across all 292 bundled samples, filename credit against real config:
|
|
407
|
+
#
|
|
408
|
+
# capability name-only correct missed
|
|
409
|
+
# rip 5 1 36
|
|
410
|
+
# static_route 0 0 22
|
|
411
|
+
# acl 5 1 18
|
|
412
|
+
# nat 6 4 7
|
|
413
|
+
# eigrp 2 8 4
|
|
414
|
+
# hsrp 3 0 0
|
|
415
|
+
#
|
|
416
|
+
# Both directions are wrong. Every `hsrp` credit was a filename coincidence, and
|
|
417
|
+
# 36 labs that configure RIP were invisible. Config evidence is *added* to the
|
|
418
|
+
# filename tags rather than replacing them: withdrawing credit is a larger
|
|
419
|
+
# change and needs its own measurement, but a capability the config proves is
|
|
420
|
+
# never wrong to record.
|
|
421
|
+
CONFIG_EVIDENCE_PATTERNS: dict[str, tuple[str, ...]] = {
|
|
422
|
+
"telnet": (r"^\s*line vty\b", r"transport input (?:all|telnet)"),
|
|
423
|
+
"management_vlan": (r"^\s*interface vlan\s*\d+", r"^\s*ip default-gateway\b"),
|
|
424
|
+
"ospf": (r"^\s*router ospf\b",),
|
|
425
|
+
"ospfv2": (r"^\s*router ospf\b",),
|
|
426
|
+
"eigrp": (r"^\s*router eigrp\b",),
|
|
427
|
+
"eigrp_ipv4": (r"^\s*router eigrp\b",),
|
|
428
|
+
"rip": (r"^\s*router rip\b",),
|
|
429
|
+
"ripv2": (r"^\s*router rip\b.*|^\s*version 2\b",),
|
|
430
|
+
"bgp": (r"^\s*router bgp\b",),
|
|
431
|
+
"nat": (r"^\s*ip nat\b",),
|
|
432
|
+
"pat": (r"^\s*ip nat inside source list .*overload",),
|
|
433
|
+
"static_route": (r"^\s*ip route\b",),
|
|
434
|
+
"default_route": (r"^\s*ip route 0\.0\.0\.0 0\.0\.0\.0",),
|
|
435
|
+
"acl": (r"^\s*(?:ip )?access-list\b",),
|
|
436
|
+
"port_security": (r"switchport port-security",),
|
|
437
|
+
"etherchannel": (r"^\s*channel-group\b",),
|
|
438
|
+
"hsrp": (r"^\s*standby \d+ ip\b",),
|
|
439
|
+
"vtp": (r"^\s*vtp (?:mode|domain)\b",),
|
|
440
|
+
"stp": (r"^\s*spanning-tree mode\b",),
|
|
441
|
+
"dhcp_relay": (r"^\s*ip helper-address\b",),
|
|
442
|
+
"dhcp_pool": (r"^\s*ip dhcp pool\b",),
|
|
443
|
+
"router_dhcp": (r"^\s*ip dhcp pool\b",),
|
|
444
|
+
"trunk": (r"switchport mode trunk",),
|
|
445
|
+
"access_port": (r"switchport mode access",),
|
|
446
|
+
"router_on_a_stick": (r"^\s*encapsulation dot1q\b",),
|
|
447
|
+
"ssh_ios": (r"^\s*ip ssh\b", r"transport input ssh"),
|
|
448
|
+
"ntp_ios": (r"^\s*ntp server\b",),
|
|
449
|
+
"syslog_ios": (r"^\s*logging (?:host|\d+\.)",),
|
|
450
|
+
"snmp": (r"^\s*snmp-server\b",),
|
|
451
|
+
"lldp": (r"^\s*lldp run\b",),
|
|
452
|
+
"dhcp_snooping": (r"^\s*ip dhcp snooping\b",),
|
|
453
|
+
"dai": (r"^\s*ip arp inspection\b",),
|
|
454
|
+
"dot1x": (r"^\s*dot1x\b",),
|
|
455
|
+
"vlan": (r"^\s*interface vlan\s*\d+", r"switchport access vlan"),
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def config_capability_tags(config_text: str) -> set[str]:
|
|
460
|
+
"""Capabilities a lab demonstrably configures, not ones its name suggests."""
|
|
461
|
+
if not config_text:
|
|
462
|
+
return set()
|
|
463
|
+
lowered = config_text.lower()
|
|
464
|
+
found: set[str] = set()
|
|
465
|
+
for capability, patterns in CONFIG_EVIDENCE_PATTERNS.items():
|
|
466
|
+
if any(re.search(pattern, lowered, flags=re.MULTILINE) for pattern in patterns):
|
|
467
|
+
found.add(capability)
|
|
468
|
+
return found
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def infer_capability_tags(item: dict[str, Any]) -> list[str]:
|
|
472
|
+
tags: set[str] = set(item.get("config_capability_tags") or ())
|
|
473
|
+
rel = item.get("relative_path", "").lower().replace("\\", "/")
|
|
474
|
+
rel_flat = rel.replace("/", " ")
|
|
475
|
+
devices = item.get("devices", [])
|
|
476
|
+
raw_types = {str(device.get("type", "")).strip() for device in devices if str(device.get("type", "")).strip()}
|
|
477
|
+
normalized_types = [normalize_device_type(device.get("type", "")) for device in devices]
|
|
478
|
+
router_count = normalized_types.count("Router")
|
|
479
|
+
switch_count = normalized_types.count("Switch")
|
|
480
|
+
host_count = sum(1 for dtype in normalized_types if dtype in {"PC", "Server"})
|
|
481
|
+
|
|
482
|
+
for tag, words in CAPABILITY_KEYWORDS.items():
|
|
483
|
+
if any(word in rel or word in rel_flat for word in words):
|
|
484
|
+
tags.add(tag)
|
|
485
|
+
if router_count >= 2:
|
|
486
|
+
tags.add("multi_router")
|
|
487
|
+
# Two routers with serial hardware is what a leased line needs: one has
|
|
488
|
+
# nothing to reach. Ranking had no way to tell these apart from any other
|
|
489
|
+
# multi-router lab, so a WAN prompt was answered with firewall samples that
|
|
490
|
+
# could only ever produce the link over copper.
|
|
491
|
+
if (
|
|
492
|
+
sum(
|
|
493
|
+
1
|
|
494
|
+
for device, dtype in zip(devices, normalized_types)
|
|
495
|
+
if dtype == "Router" and int(device.get("serial_ports") or 0) > 0
|
|
496
|
+
)
|
|
497
|
+
>= 2
|
|
498
|
+
):
|
|
499
|
+
tags.add("serial_wan")
|
|
500
|
+
tags.add("wan")
|
|
501
|
+
if switch_count >= 1:
|
|
502
|
+
tags.add("switching")
|
|
503
|
+
if host_count >= 1:
|
|
504
|
+
tags.add("host_server")
|
|
505
|
+
if any(dtype in {"WirelessRouter", "LightWeightAccessPoint"} for dtype in normalized_types):
|
|
506
|
+
tags.add("wireless")
|
|
507
|
+
tags.add("wireless_ap")
|
|
508
|
+
if "HomeGateway" in raw_types or any(dtype == "IoT" for dtype in normalized_types):
|
|
509
|
+
tags.add("iot")
|
|
510
|
+
if any(dtype in {"ASA", "Security Appliance"} for dtype in normalized_types) or raw_types & {"ASA", "Security Appliance"}:
|
|
511
|
+
tags.add("security_edge")
|
|
512
|
+
tags.add("acl")
|
|
513
|
+
if any(dtype in {"Cloud", "Cable Modem", "Dsl Modem"} for dtype in normalized_types):
|
|
514
|
+
tags.add("wan")
|
|
515
|
+
if any(dtype == "MultiLayerSwitch" for dtype in normalized_types):
|
|
516
|
+
tags.add("multilayer_switching")
|
|
517
|
+
if any(dtype in {"Tablet", "Laptop", "Smartphone"} for dtype in normalized_types):
|
|
518
|
+
tags.add("wireless_client")
|
|
519
|
+
if any(dtype == "Tablet" for dtype in normalized_types):
|
|
520
|
+
tags.add("tablet")
|
|
521
|
+
if any(dtype == "Printer" for dtype in normalized_types):
|
|
522
|
+
tags.add("printer")
|
|
523
|
+
if "telnet" in rel or "terminal server" in rel_flat:
|
|
524
|
+
tags.add("telnet")
|
|
525
|
+
if "management" in rel_flat:
|
|
526
|
+
tags.add("management_vlan")
|
|
527
|
+
return sorted(tags or {"switching"})
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def infer_topology_tags(item: dict[str, Any]) -> list[str]:
|
|
531
|
+
tags: set[str] = set()
|
|
532
|
+
rel = item.get("relative_path", "").lower().replace("\\", "/")
|
|
533
|
+
counts = _normalized_counts_for_item(item)
|
|
534
|
+
link_count = int(item.get("link_count", 0))
|
|
535
|
+
switch_count = counts.get("Switch", 0)
|
|
536
|
+
router_count = counts.get("Router", 0)
|
|
537
|
+
wireless_count = counts.get("LightWeightAccessPoint", 0) + counts.get("WirelessRouter", 0)
|
|
538
|
+
server_count = counts.get("Server", 0)
|
|
539
|
+
|
|
540
|
+
if "router_on_a_stick" in item.get("capability_tags", []):
|
|
541
|
+
tags.add("router_on_a_stick")
|
|
542
|
+
if "acl" in item.get("capability_tags", []):
|
|
543
|
+
tags.add("acl_policy")
|
|
544
|
+
if wireless_count:
|
|
545
|
+
tags.add("wireless_edge")
|
|
546
|
+
if server_count:
|
|
547
|
+
tags.add("server_services")
|
|
548
|
+
if switch_count >= 3 and router_count >= 1:
|
|
549
|
+
tags.add("department_lan")
|
|
550
|
+
if switch_count >= 2 and router_count >= 1:
|
|
551
|
+
tags.add("core_access")
|
|
552
|
+
if switch_count >= 2 and link_count >= switch_count:
|
|
553
|
+
tags.add("chain")
|
|
554
|
+
if router_count == 1 and switch_count <= 1 and wireless_count:
|
|
555
|
+
tags.add("small_office")
|
|
556
|
+
if "campus" in rel or "department" in rel or "vlan" in rel:
|
|
557
|
+
tags.add("department_lan")
|
|
558
|
+
return sorted(tags or {"general"})
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def infer_preferred_roles(item: dict[str, Any]) -> list[str]:
|
|
562
|
+
roles: list[str] = []
|
|
563
|
+
tags = set(item.get("capability_tags", []))
|
|
564
|
+
if "vlan" in tags:
|
|
565
|
+
roles.append("preferred_vlan")
|
|
566
|
+
if "dhcp_pool" in tags:
|
|
567
|
+
roles.append("preferred_dhcp")
|
|
568
|
+
if {"ospf", "eigrp", "rip", "bgp", "ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route"} & tags:
|
|
569
|
+
roles.append("preferred_routing")
|
|
570
|
+
if {"dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"} & tags:
|
|
571
|
+
roles.append("preferred_ipv4_management")
|
|
572
|
+
if {"stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"} & tags:
|
|
573
|
+
roles.append("preferred_l2_resiliency")
|
|
574
|
+
if {"nat", "acl", "vpn"} & tags:
|
|
575
|
+
roles.append("preferred_security")
|
|
576
|
+
if {"vpn", "ipsec", "gre", "ppp", "multilayer_switching"} & tags:
|
|
577
|
+
roles.append("preferred_wan")
|
|
578
|
+
if "wireless" in tags:
|
|
579
|
+
roles.append("preferred_wireless")
|
|
580
|
+
if "telnet" in tags or "management_vlan" in tags:
|
|
581
|
+
roles.append("preferred_management")
|
|
582
|
+
if "server_dns" in tags or "server_dhcp" in tags:
|
|
583
|
+
roles.append("preferred_server")
|
|
584
|
+
if "iot" in tags:
|
|
585
|
+
roles.append("preferred_iot")
|
|
586
|
+
if not roles:
|
|
587
|
+
roles.append("preferred_general")
|
|
588
|
+
return roles
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def infer_wireless_mode_tags(item: dict[str, Any]) -> list[str]:
|
|
592
|
+
normalized_types = [normalize_device_type(device.get("type", "")) for device in item.get("devices", [])]
|
|
593
|
+
tags: set[str] = set()
|
|
594
|
+
if "LightWeightAccessPoint" in normalized_types:
|
|
595
|
+
tags.add("ap_bridge")
|
|
596
|
+
if "WirelessRouter" in normalized_types:
|
|
597
|
+
tags.add("home_router_edge")
|
|
598
|
+
if len(tags) > 1:
|
|
599
|
+
tags.add("mixed")
|
|
600
|
+
return sorted(tags)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def infer_iot_roles(item: dict[str, Any]) -> list[str]:
|
|
604
|
+
roles = {str(role).strip() for role in item.get("iot_roles", []) if str(role).strip()}
|
|
605
|
+
raw_types = {str(device.get("type", "")).strip() for device in item.get("devices", []) if str(device.get("type", "")).strip()}
|
|
606
|
+
normalized_types = {normalize_device_type(device.get("type", "")) for device in item.get("devices", []) if device.get("type")}
|
|
607
|
+
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
608
|
+
has_thing = bool(raw_types & {"MCUComponent", "Board", "Sensor", "Actuator"}) or "iot devices" in families
|
|
609
|
+
has_gateway = "HomeGateway" in raw_types or ("home/wireless routers" in families and has_thing)
|
|
610
|
+
has_server = "Server" in normalized_types or "servers" in families
|
|
611
|
+
if has_thing:
|
|
612
|
+
roles.add("thing")
|
|
613
|
+
if has_gateway:
|
|
614
|
+
roles.add("gateway")
|
|
615
|
+
if has_server:
|
|
616
|
+
roles.add("server")
|
|
617
|
+
return sorted(roles)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def infer_device_families(item: dict[str, Any]) -> list[str]:
|
|
621
|
+
family_map = {
|
|
622
|
+
"Router": "routers",
|
|
623
|
+
"Switch": "switches",
|
|
624
|
+
"MultiLayerSwitch": "multilayer switches",
|
|
625
|
+
"Server": "servers",
|
|
626
|
+
"PC": "end devices",
|
|
627
|
+
"Laptop": "end devices",
|
|
628
|
+
"Tablet": "end devices",
|
|
629
|
+
"Smartphone": "end devices",
|
|
630
|
+
"Printer": "end devices",
|
|
631
|
+
"IpPhone": "end devices",
|
|
632
|
+
"HomeVoip": "end devices",
|
|
633
|
+
"AnalogPhone": "end devices",
|
|
634
|
+
"LightWeightAccessPoint": "access points",
|
|
635
|
+
"WirelessRouter": "home/wireless routers",
|
|
636
|
+
"WirelessRouterNewGeneration": "home/wireless routers",
|
|
637
|
+
"HomeGateway": "home/wireless routers",
|
|
638
|
+
"WirelessLanController": "access points",
|
|
639
|
+
"Power Distribution Device": "pt-specific edge/utility devices",
|
|
640
|
+
"Cloud": "wan/cloud/dsl/cable devices",
|
|
641
|
+
"Cable Modem": "wan/cloud/dsl/cable devices",
|
|
642
|
+
"Dsl Modem": "wan/cloud/dsl/cable devices",
|
|
643
|
+
"Security Appliance": "security devices",
|
|
644
|
+
"ASA": "security devices",
|
|
645
|
+
"NetworkController": "network controllers",
|
|
646
|
+
"CentralOfficeServer": "wan/cloud/dsl/cable devices",
|
|
647
|
+
"CellTower": "wan/cloud/dsl/cable devices",
|
|
648
|
+
"IoT": "iot devices",
|
|
649
|
+
"Sensor": "iot devices",
|
|
650
|
+
"Actuator": "iot devices",
|
|
651
|
+
"MCUComponent": "iot devices",
|
|
652
|
+
}
|
|
653
|
+
families = {
|
|
654
|
+
family_map.get(normalize_device_type(device.get("type", "")), "pt-specific edge/utility devices")
|
|
655
|
+
for device in item.get("devices", [])
|
|
656
|
+
if device.get("type")
|
|
657
|
+
}
|
|
658
|
+
return sorted(families)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def infer_model_families(item: dict[str, Any]) -> list[str]:
|
|
662
|
+
return sorted(
|
|
663
|
+
{
|
|
664
|
+
str(device.get("model", "")).strip()
|
|
665
|
+
for device in item.get("devices", [])
|
|
666
|
+
if str(device.get("model", "")).strip()
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def infer_port_media_types(item: dict[str, Any]) -> list[str]:
|
|
672
|
+
media = {
|
|
673
|
+
str(link.get("cable_type") or link.get("media") or link.get("type") or "").strip()
|
|
674
|
+
for link in item.get("links", [])
|
|
675
|
+
if str(link.get("cable_type") or link.get("media") or link.get("type") or "").strip()
|
|
676
|
+
}
|
|
677
|
+
return sorted(media)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def infer_service_support(item: dict[str, Any]) -> list[str]:
|
|
681
|
+
tags = set(item.get("capability_tags", []))
|
|
682
|
+
services: set[str] = set()
|
|
683
|
+
service_map = {
|
|
684
|
+
"server_dhcp": "dhcp",
|
|
685
|
+
"dhcp_pool": "dhcp",
|
|
686
|
+
"dns": "dns",
|
|
687
|
+
"server_dns": "dns",
|
|
688
|
+
"server_http": "http",
|
|
689
|
+
"server_https": "https",
|
|
690
|
+
"server_ftp": "ftp",
|
|
691
|
+
"server_tftp": "tftp",
|
|
692
|
+
"ntp": "ntp",
|
|
693
|
+
"server_email": "email",
|
|
694
|
+
"server_syslog": "syslog",
|
|
695
|
+
"server_aaa": "aaa",
|
|
696
|
+
"ftp_http_https": "http",
|
|
697
|
+
"telnet": "telnet",
|
|
698
|
+
"management_vlan": "management",
|
|
699
|
+
}
|
|
700
|
+
for tag in tags:
|
|
701
|
+
service = service_map.get(tag)
|
|
702
|
+
if service:
|
|
703
|
+
services.add(service)
|
|
704
|
+
return sorted(services)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def infer_runtime_features(item: dict[str, Any]) -> list[str]:
|
|
708
|
+
features: set[str] = set()
|
|
709
|
+
tags = set(item.get("capability_tags", []))
|
|
710
|
+
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
711
|
+
if item.get("wireless_mode_tags"):
|
|
712
|
+
features.add("wireless_runtime")
|
|
713
|
+
if item.get("service_support"):
|
|
714
|
+
features.add("service_runtime")
|
|
715
|
+
if infer_iot_roles(item):
|
|
716
|
+
features.add("iot_runtime")
|
|
717
|
+
if families & {"wan/cloud/dsl/cable devices"} or tags & {"wan", "ppp"}:
|
|
718
|
+
features.add("wan_runtime")
|
|
719
|
+
if families & {"security devices"} or tags & {"security_edge", "acl", "nat"}:
|
|
720
|
+
features.add("security_runtime")
|
|
721
|
+
if tags & {"vpn", "ipsec", "gre"}:
|
|
722
|
+
features.add("tunnel_runtime")
|
|
723
|
+
if tags & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
724
|
+
features.add("ipv6_runtime")
|
|
725
|
+
if tags & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
|
|
726
|
+
features.add("l2_resiliency_runtime")
|
|
727
|
+
if tags & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
|
|
728
|
+
features.add("ipv4_routing_management_runtime")
|
|
729
|
+
if families & {"multilayer switches"} or tags & {"multilayer_switching"}:
|
|
730
|
+
features.add("multilayer_runtime")
|
|
731
|
+
if item.get("workspace_validation"):
|
|
732
|
+
features.add("workspace_validated")
|
|
733
|
+
if any(str(link.get("from", "")) and str(link.get("to", "")) for link in item.get("links", [])):
|
|
734
|
+
features.add("named_link_graph")
|
|
735
|
+
return sorted(features)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def infer_archetype_tags(item: dict[str, Any]) -> list[str]:
|
|
739
|
+
tags: set[str] = set()
|
|
740
|
+
topology = set(item.get("topology_tags", []))
|
|
741
|
+
families = set(item.get("device_families", []))
|
|
742
|
+
services = set(item.get("service_support", []))
|
|
743
|
+
wireless_modes = set(item.get("wireless_mode_tags", []))
|
|
744
|
+
iot_roles = set(infer_iot_roles(item))
|
|
745
|
+
|
|
746
|
+
if topology & {"chain", "core_access", "department_lan", "router_on_a_stick"} or (
|
|
747
|
+
families & {"routers", "switches", "multilayer switches"}
|
|
748
|
+
and set(item.get("capability_tags", [])) & {"vlan", "router_on_a_stick", "management_vlan", "telnet"}
|
|
749
|
+
):
|
|
750
|
+
tags.add("campus/core")
|
|
751
|
+
if services or "servers" in families or "server_services" in topology:
|
|
752
|
+
tags.add("service-heavy")
|
|
753
|
+
if wireless_modes or families & {"access points", "home/wireless routers"}:
|
|
754
|
+
tags.add("wireless-heavy")
|
|
755
|
+
if iot_roles or "iot devices" in families or "HomeGateway" in set(item.get("model_families", [])):
|
|
756
|
+
tags.add("IoT/home gateway")
|
|
757
|
+
if families & {"wan/cloud/dsl/cable devices", "security devices"} or set(item.get("capability_tags", [])) & {
|
|
758
|
+
"vpn",
|
|
759
|
+
"ipsec",
|
|
760
|
+
"gre",
|
|
761
|
+
"ppp",
|
|
762
|
+
"wan",
|
|
763
|
+
"security_edge",
|
|
764
|
+
"multilayer_switching",
|
|
765
|
+
"nat",
|
|
766
|
+
"acl",
|
|
767
|
+
}:
|
|
768
|
+
tags.add("WAN/security edge")
|
|
769
|
+
capability_tags = set(item.get("capability_tags", []))
|
|
770
|
+
if capability_tags & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
771
|
+
tags.add("IPv6/routing")
|
|
772
|
+
if capability_tags & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
|
|
773
|
+
tags.add("L2 security/monitoring")
|
|
774
|
+
if capability_tags & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
|
|
775
|
+
tags.add("L2 resiliency/routing")
|
|
776
|
+
if capability_tags & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
|
|
777
|
+
tags.add("IPv4 routing/management")
|
|
778
|
+
if capability_tags & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
|
|
779
|
+
tags.add("advanced wireless")
|
|
780
|
+
if capability_tags & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"} or "network controllers" in families:
|
|
781
|
+
tags.add("automation/controller")
|
|
782
|
+
if capability_tags & {"voip", "ip_phone", "call_manager", "linksys_voice"}:
|
|
783
|
+
tags.add("voice/collaboration")
|
|
784
|
+
if capability_tags & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
|
|
785
|
+
tags.add("industrial IoT")
|
|
786
|
+
return sorted(tags)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def infer_donor_graph_fingerprint(item: dict[str, Any]) -> dict[str, Any]:
|
|
790
|
+
pairs: list[str] = []
|
|
791
|
+
media_types = infer_port_media_types(item)
|
|
792
|
+
for link in item.get("links", []):
|
|
793
|
+
left = str(link.get("from", "")).strip()
|
|
794
|
+
right = str(link.get("to", "")).strip()
|
|
795
|
+
if left and right:
|
|
796
|
+
pairs.append(" <-> ".join(sorted((left, right))))
|
|
797
|
+
return {
|
|
798
|
+
"link_count": int(item.get("link_count", 0)),
|
|
799
|
+
"named_pairs": sorted(dict.fromkeys(pairs)),
|
|
800
|
+
"media_types": media_types,
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def infer_apply_safety_level(item: dict[str, Any]) -> str:
|
|
805
|
+
if item.get("origin") == "cisco-local" and item.get("prototype_eligible", True) and str(item.get("version", "")).startswith("9."):
|
|
806
|
+
return "acceptance-verified"
|
|
807
|
+
if item.get("donor_eligible") and item.get("prototype_eligible", False):
|
|
808
|
+
return "safe-open-generate-supported"
|
|
809
|
+
if item.get("capability_tags"):
|
|
810
|
+
return "config-mutation-supported"
|
|
811
|
+
if item.get("devices"):
|
|
812
|
+
return "inventory-supported"
|
|
813
|
+
return "reference-known"
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def infer_validated_edit_capabilities(item: dict[str, Any]) -> list[str]:
|
|
817
|
+
capabilities: set[str] = {str(tag) for tag in item.get("capability_tags", []) if str(tag).strip()}
|
|
818
|
+
inferred_iot_roles = set(infer_iot_roles(item))
|
|
819
|
+
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
820
|
+
service_capability_map = {
|
|
821
|
+
"dhcp": "server_dhcp",
|
|
822
|
+
"dns": "server_dns",
|
|
823
|
+
"http": "server_http",
|
|
824
|
+
"https": "server_https",
|
|
825
|
+
"ftp": "server_ftp",
|
|
826
|
+
"tftp": "server_tftp",
|
|
827
|
+
"ntp": "server_ntp",
|
|
828
|
+
"email": "server_email",
|
|
829
|
+
"syslog": "server_syslog",
|
|
830
|
+
"aaa": "server_aaa",
|
|
831
|
+
}
|
|
832
|
+
for service in item.get("service_support", []):
|
|
833
|
+
mapped = service_capability_map.get(str(service))
|
|
834
|
+
if mapped:
|
|
835
|
+
capabilities.add(mapped)
|
|
836
|
+
if item.get("wireless_mode_tags"):
|
|
837
|
+
capabilities.add("wireless_mutation")
|
|
838
|
+
if any(device_family == "end devices" for device_family in item.get("device_families", [])):
|
|
839
|
+
capabilities.add("end_device_mutation")
|
|
840
|
+
if inferred_iot_roles:
|
|
841
|
+
capabilities.add("iot")
|
|
842
|
+
capabilities.add("iot_registration")
|
|
843
|
+
if {"server", "gateway"} & inferred_iot_roles:
|
|
844
|
+
capabilities.add("iot_control")
|
|
845
|
+
if capabilities & {"vpn", "ipsec", "gre"}:
|
|
846
|
+
capabilities.update({"vpn", "ipsec", "gre"})
|
|
847
|
+
if capabilities & {"ppp", "wan"} or families & {"wan/cloud/dsl/cable devices"}:
|
|
848
|
+
capabilities.add("wan")
|
|
849
|
+
if "ppp" in capabilities:
|
|
850
|
+
capabilities.add("ppp")
|
|
851
|
+
if capabilities & {"acl", "nat", "security_edge"} or families & {"security devices"}:
|
|
852
|
+
capabilities.add("security_edge")
|
|
853
|
+
if "multilayer switches" in families or "multilayer_switching" in capabilities:
|
|
854
|
+
capabilities.add("multilayer_switching")
|
|
855
|
+
if item.get("apply_safety_level") == "inventory-supported":
|
|
856
|
+
return []
|
|
857
|
+
capabilities -= REPORT_ONLY_CAPABILITIES
|
|
858
|
+
return sorted(capabilities)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def infer_promotion_evidence(item: dict[str, Any]) -> list[str]:
|
|
862
|
+
evidence: list[str] = []
|
|
863
|
+
version = str(item.get("packet_tracer_version") or item.get("version") or "").strip()
|
|
864
|
+
target_version = get_packet_tracer_target_version()
|
|
865
|
+
if version and version == target_version:
|
|
866
|
+
evidence.append(f"version:{version}")
|
|
867
|
+
validation_status = str(item.get("validation_status", "")).strip()
|
|
868
|
+
if validation_status:
|
|
869
|
+
evidence.append(f"validation:{validation_status}")
|
|
870
|
+
promotion_status = str(item.get("promotion_status", "")).strip()
|
|
871
|
+
if promotion_status:
|
|
872
|
+
evidence.append(f"promotion:{promotion_status}")
|
|
873
|
+
if item.get("donor_eligible"):
|
|
874
|
+
evidence.append("donor_eligible")
|
|
875
|
+
runtime_features = set(item.get("runtime_features", []))
|
|
876
|
+
if "workspace_validated" in runtime_features:
|
|
877
|
+
evidence.append("workspace_validated")
|
|
878
|
+
workspace_validation_status = str(item.get("workspace_validation_status", "")).strip()
|
|
879
|
+
if workspace_validation_status:
|
|
880
|
+
evidence.append(f"workspace_validation:{workspace_validation_status}")
|
|
881
|
+
apply_safety_level = str(item.get("apply_safety_level", "")).strip()
|
|
882
|
+
if apply_safety_level:
|
|
883
|
+
evidence.append(f"apply_safety:{apply_safety_level}")
|
|
884
|
+
validated_edit_capabilities = list(item.get("validated_edit_capabilities", []))
|
|
885
|
+
if validated_edit_capabilities:
|
|
886
|
+
evidence.append(f"edit_capabilities:{len(validated_edit_capabilities)}")
|
|
887
|
+
explicit_fixtures = [str(fixture) for fixture in item.get("acceptance_fixtures", []) if str(fixture).strip()]
|
|
888
|
+
for fixture in explicit_fixtures:
|
|
889
|
+
evidence.append(f"acceptance_fixture:{fixture}")
|
|
890
|
+
archetype_to_fixture = {
|
|
891
|
+
"campus/core": "campus",
|
|
892
|
+
"service-heavy": "service_heavy",
|
|
893
|
+
"IoT/home gateway": "home_iot",
|
|
894
|
+
"WAN/security edge": "wan_security_edge",
|
|
895
|
+
}
|
|
896
|
+
if not explicit_fixtures and promotion_status in {"validated_primary", "validated_compat", "acceptance_verified_curated"}:
|
|
897
|
+
for archetype in item.get("archetype_tags", []):
|
|
898
|
+
fixture = archetype_to_fixture.get(str(archetype))
|
|
899
|
+
if fixture:
|
|
900
|
+
evidence.append(f"acceptance_fixture:{fixture}")
|
|
901
|
+
provenance = str(item.get("provenance", "")).strip()
|
|
902
|
+
if provenance:
|
|
903
|
+
evidence.append(f"provenance:{provenance}")
|
|
904
|
+
evidence_source = str(item.get("evidence_source", "")).strip()
|
|
905
|
+
if evidence_source:
|
|
906
|
+
evidence.append(f"evidence_source:{evidence_source}")
|
|
907
|
+
return evidence
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def infer_acceptance_notes(item: dict[str, Any]) -> list[str]:
|
|
911
|
+
notes: list[str] = []
|
|
912
|
+
workspace_validation = item.get("workspace_validation", {})
|
|
913
|
+
if isinstance(workspace_validation, dict):
|
|
914
|
+
logical_status = str(workspace_validation.get("logical_status", "")).strip()
|
|
915
|
+
physical_status = str(workspace_validation.get("physical_status", "")).strip()
|
|
916
|
+
if logical_status or physical_status:
|
|
917
|
+
notes.append(f"workspace logical={logical_status or 'unknown'} physical={physical_status or 'unknown'}")
|
|
918
|
+
wireless_modes = [str(item) for item in item.get("wireless_mode_tags", []) if str(item).strip()]
|
|
919
|
+
if wireless_modes:
|
|
920
|
+
notes.append(f"wireless modes: {', '.join(wireless_modes)}")
|
|
921
|
+
archetypes = [str(item) for item in item.get("archetype_tags", []) if str(item).strip()]
|
|
922
|
+
if archetypes:
|
|
923
|
+
notes.append(f"archetypes: {', '.join(archetypes)}")
|
|
924
|
+
acceptance_fixtures = [str(item) for item in item.get("acceptance_fixtures", []) if str(item).strip()]
|
|
925
|
+
if acceptance_fixtures:
|
|
926
|
+
notes.append(f"acceptance fixtures: {', '.join(acceptance_fixtures)}")
|
|
927
|
+
provenance = str(item.get("provenance", "")).strip()
|
|
928
|
+
if provenance:
|
|
929
|
+
notes.append(f"provenance: {provenance}")
|
|
930
|
+
return notes
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _detect_license_or_permission(root: Path) -> str:
|
|
934
|
+
for filename in LICENSE_FILENAMES:
|
|
935
|
+
candidate = root / filename
|
|
936
|
+
if not candidate.exists():
|
|
937
|
+
continue
|
|
938
|
+
try:
|
|
939
|
+
text = candidate.read_text(encoding="utf-8", errors="ignore").lower()
|
|
940
|
+
except Exception:
|
|
941
|
+
return f"present:{candidate.name}"
|
|
942
|
+
if "mit license" in text:
|
|
943
|
+
return "MIT"
|
|
944
|
+
if "apache license" in text and "version 2.0" in text:
|
|
945
|
+
return "Apache-2.0"
|
|
946
|
+
if "bsd" in text:
|
|
947
|
+
return "BSD"
|
|
948
|
+
if "creative commons" in text and "noncommercial" in text:
|
|
949
|
+
return "CC-BY-NC"
|
|
950
|
+
if "creative commons" in text:
|
|
951
|
+
return "CC"
|
|
952
|
+
return f"present:{candidate.name}"
|
|
953
|
+
return "unknown"
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def validate_external_sample_summary(item: dict[str, Any], curated: bool) -> DonorValidationResult:
|
|
957
|
+
target_version = get_packet_tracer_target_version()
|
|
958
|
+
packet_tracer_version = str(item.get("version", ""))
|
|
959
|
+
issues: list[str] = []
|
|
960
|
+
# Use the shared compatibility ladder rather than string equality: the build
|
|
961
|
+
# field of a `<VERSION>` is not a schema identifier, and the target may be a
|
|
962
|
+
# three-field version detected from the install directory.
|
|
963
|
+
if not donor_tier_is_accepted(donor_compatibility(packet_tracer_version, target_version)):
|
|
964
|
+
issues.append(f"version_mismatch:{packet_tracer_version or 'unknown'}")
|
|
965
|
+
if int(item.get("device_count", 0)) < 2:
|
|
966
|
+
issues.append("insufficient_devices")
|
|
967
|
+
if int(item.get("link_count", 0)) < 1:
|
|
968
|
+
issues.append("insufficient_links")
|
|
969
|
+
counts = _normalized_counts_for_item(item)
|
|
970
|
+
if curated and not any(counts.get(kind, 0) for kind in ("Switch", "Router")):
|
|
971
|
+
issues.append("missing_network_core")
|
|
972
|
+
workspace_validation = item.get("workspace_validation", {})
|
|
973
|
+
if curated and isinstance(workspace_validation, dict):
|
|
974
|
+
workspace_mode = str(workspace_validation.get("workspace_mode", ""))
|
|
975
|
+
logical_status = str(workspace_validation.get("logical_status", ""))
|
|
976
|
+
physical_status = str(workspace_validation.get("physical_status", ""))
|
|
977
|
+
blocking_issues = [str(issue) for issue in workspace_validation.get("blocking_issues", []) if str(issue)]
|
|
978
|
+
legacy_memaddr_only = (
|
|
979
|
+
workspace_mode == "legacy_uuid_physical"
|
|
980
|
+
and physical_status == "ok"
|
|
981
|
+
and logical_status == "invalid"
|
|
982
|
+
and bool(blocking_issues)
|
|
983
|
+
and all(issue == "Link device MEM_ADDR references do not match device workspace records" for issue in blocking_issues)
|
|
984
|
+
)
|
|
985
|
+
if physical_status != "ok":
|
|
986
|
+
issues.append("workspace_physical_invalid")
|
|
987
|
+
elif logical_status != "ok" and not legacy_memaddr_only:
|
|
988
|
+
issues.append("workspace_logical_invalid")
|
|
989
|
+
donor_eligible = curated and not issues
|
|
990
|
+
validation_status = "validated" if not issues else "blocked"
|
|
991
|
+
promotion_status = "validated_curated" if donor_eligible else "reference_only"
|
|
992
|
+
return DonorValidationResult(
|
|
993
|
+
packet_tracer_version=packet_tracer_version,
|
|
994
|
+
validation_status=validation_status,
|
|
995
|
+
promotion_status=promotion_status,
|
|
996
|
+
donor_eligible=donor_eligible,
|
|
997
|
+
issues=issues,
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _merge_curated_registry_entry(
|
|
1002
|
+
item: dict[str, Any],
|
|
1003
|
+
registry_entry: dict[str, Any] | None,
|
|
1004
|
+
validation: DonorValidationResult,
|
|
1005
|
+
) -> dict[str, Any]:
|
|
1006
|
+
merged = dict(item)
|
|
1007
|
+
if not registry_entry:
|
|
1008
|
+
return merged
|
|
1009
|
+
for field in [
|
|
1010
|
+
"packet_tracer_version",
|
|
1011
|
+
"apply_safety_level",
|
|
1012
|
+
"archetype_tags",
|
|
1013
|
+
"validated_edit_capabilities",
|
|
1014
|
+
"acceptance_fixtures",
|
|
1015
|
+
"acceptance_notes",
|
|
1016
|
+
"provenance",
|
|
1017
|
+
]:
|
|
1018
|
+
if field in registry_entry:
|
|
1019
|
+
merged[field] = registry_entry[field]
|
|
1020
|
+
requested_promotion = str(registry_entry.get("promotion_status", "")).strip()
|
|
1021
|
+
if requested_promotion and validation.donor_eligible:
|
|
1022
|
+
merged["promotion_status"] = requested_promotion
|
|
1023
|
+
if not validation.donor_eligible:
|
|
1024
|
+
merged["promotion_status"] = "reference_only"
|
|
1025
|
+
merged["donor_eligible"] = False
|
|
1026
|
+
if registry_entry:
|
|
1027
|
+
merged["evidence_source"] = "registry+inferred"
|
|
1028
|
+
workspace_validation_status = str(registry_entry.get("workspace_validation", "")).strip()
|
|
1029
|
+
if workspace_validation_status:
|
|
1030
|
+
merged["workspace_validation_status"] = workspace_validation_status
|
|
1031
|
+
return merged
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _descriptor_from_item(item: dict[str, Any]) -> SampleDescriptor:
|
|
1035
|
+
return SampleDescriptor(
|
|
1036
|
+
path=item["path"],
|
|
1037
|
+
relative_path=item["relative_path"],
|
|
1038
|
+
version=item.get("version", ""),
|
|
1039
|
+
device_count=item.get("device_count", 0),
|
|
1040
|
+
link_count=item.get("link_count", 0),
|
|
1041
|
+
devices=item.get("devices", []),
|
|
1042
|
+
links=item.get("links", []),
|
|
1043
|
+
capability_tags=item.get("capability_tags", []),
|
|
1044
|
+
topology_tags=item.get("topology_tags", []),
|
|
1045
|
+
preferred_roles=item.get("preferred_roles", []),
|
|
1046
|
+
trust_level=item.get("trust_level", "trusted"),
|
|
1047
|
+
origin=item.get("origin", "cisco-local"),
|
|
1048
|
+
role=item.get("role", "primary"),
|
|
1049
|
+
prototype_eligible=bool(item.get("prototype_eligible", True)),
|
|
1050
|
+
license_or_permission=item.get("license_or_permission", "local-cisco"),
|
|
1051
|
+
promotion_status=item.get("promotion_status", "validated_primary"),
|
|
1052
|
+
validation_status=item.get("validation_status", "validated"),
|
|
1053
|
+
packet_tracer_version=item.get("packet_tracer_version", item.get("version", "")),
|
|
1054
|
+
wireless_mode_tags=item.get("wireless_mode_tags", []),
|
|
1055
|
+
donor_eligible=bool(item.get("donor_eligible", item.get("prototype_eligible", True))),
|
|
1056
|
+
device_families=item.get("device_families", []),
|
|
1057
|
+
model_families=item.get("model_families", []),
|
|
1058
|
+
port_media_types=item.get("port_media_types", []),
|
|
1059
|
+
service_support=item.get("service_support", []),
|
|
1060
|
+
iot_roles=item.get("iot_roles", []),
|
|
1061
|
+
runtime_features=item.get("runtime_features", []),
|
|
1062
|
+
donor_graph_fingerprint=item.get("donor_graph_fingerprint", {}),
|
|
1063
|
+
apply_safety_level=item.get("apply_safety_level", "reference-known"),
|
|
1064
|
+
promotion_evidence=item.get("promotion_evidence", []),
|
|
1065
|
+
validated_edit_capabilities=item.get("validated_edit_capabilities", []),
|
|
1066
|
+
acceptance_notes=item.get("acceptance_notes", []),
|
|
1067
|
+
archetype_tags=item.get("archetype_tags", []),
|
|
1068
|
+
acceptance_fixtures=item.get("acceptance_fixtures", []),
|
|
1069
|
+
provenance=item.get("provenance", ""),
|
|
1070
|
+
workspace_validation_status=item.get("workspace_validation_status", ""),
|
|
1071
|
+
evidence_source=item.get("evidence_source", "inferred"),
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def enrich_catalog_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1076
|
+
enriched: list[dict[str, Any]] = []
|
|
1077
|
+
for item in items:
|
|
1078
|
+
if "error" in item:
|
|
1079
|
+
enriched.append(item)
|
|
1080
|
+
continue
|
|
1081
|
+
new_item = dict(item)
|
|
1082
|
+
new_item["capability_tags"] = infer_capability_tags(new_item)
|
|
1083
|
+
new_item["topology_tags"] = infer_topology_tags(new_item)
|
|
1084
|
+
new_item["preferred_roles"] = infer_preferred_roles(new_item)
|
|
1085
|
+
new_item["wireless_mode_tags"] = infer_wireless_mode_tags(new_item)
|
|
1086
|
+
new_item.setdefault("trust_level", "trusted")
|
|
1087
|
+
new_item.setdefault("origin", "cisco-local")
|
|
1088
|
+
new_item.setdefault("role", "primary")
|
|
1089
|
+
new_item.setdefault("prototype_eligible", True)
|
|
1090
|
+
new_item.setdefault("license_or_permission", "local-cisco")
|
|
1091
|
+
new_item.setdefault("promotion_status", "validated_primary")
|
|
1092
|
+
new_item.setdefault("validation_status", "validated")
|
|
1093
|
+
new_item.setdefault("packet_tracer_version", new_item.get("version", ""))
|
|
1094
|
+
new_item.setdefault("donor_eligible", bool(new_item.get("prototype_eligible", True)))
|
|
1095
|
+
new_item.setdefault("device_families", infer_device_families(new_item))
|
|
1096
|
+
new_item.setdefault("model_families", infer_model_families(new_item))
|
|
1097
|
+
new_item.setdefault("port_media_types", infer_port_media_types(new_item))
|
|
1098
|
+
new_item.setdefault("service_support", infer_service_support(new_item))
|
|
1099
|
+
new_item.setdefault("iot_roles", infer_iot_roles(new_item))
|
|
1100
|
+
new_item["runtime_features"] = sorted(set(new_item.get("runtime_features", [])) | set(infer_runtime_features(new_item)))
|
|
1101
|
+
new_item.setdefault("donor_graph_fingerprint", infer_donor_graph_fingerprint(new_item))
|
|
1102
|
+
new_item.setdefault("apply_safety_level", infer_apply_safety_level(new_item))
|
|
1103
|
+
new_item.setdefault("archetype_tags", infer_archetype_tags(new_item))
|
|
1104
|
+
new_item.setdefault("validated_edit_capabilities", infer_validated_edit_capabilities(new_item))
|
|
1105
|
+
new_item.setdefault("acceptance_fixtures", [])
|
|
1106
|
+
new_item.setdefault("provenance", "")
|
|
1107
|
+
new_item.setdefault("promotion_evidence", infer_promotion_evidence(new_item))
|
|
1108
|
+
new_item.setdefault("acceptance_notes", infer_acceptance_notes(new_item))
|
|
1109
|
+
new_item.setdefault("workspace_validation_status", "")
|
|
1110
|
+
new_item.setdefault("evidence_source", "inferred")
|
|
1111
|
+
enriched.append(new_item)
|
|
1112
|
+
return enriched
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
@lru_cache(maxsize=8)
|
|
1116
|
+
def _load_catalog_cached(path_str: str) -> tuple[SampleDescriptor, ...]:
|
|
1117
|
+
raw_items = json.loads(Path(path_str).read_text(encoding="utf-8"))
|
|
1118
|
+
items = enrich_catalog_items(raw_items)
|
|
1119
|
+
saves_root = get_packet_tracer_saves_root()
|
|
1120
|
+
return tuple(
|
|
1121
|
+
_descriptor_from_item(
|
|
1122
|
+
{
|
|
1123
|
+
**item,
|
|
1124
|
+
"path": str((saves_root / item["relative_path"]) if saves_root is not None else item.get("path", item["relative_path"])),
|
|
1125
|
+
}
|
|
1126
|
+
)
|
|
1127
|
+
for item in items
|
|
1128
|
+
if "error" not in item
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def load_catalog(path: Path | None = None) -> list[SampleDescriptor]:
|
|
1133
|
+
return list(_load_catalog_cached(str(path or DEFAULT_CATALOG_JSON)))
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _summarize_pkt(path: Path, relative_path: str, origin: str, prototype_eligible: bool) -> dict[str, Any]:
|
|
1137
|
+
# Catalogued labs are parsed immediately below, so a bad file shows up as
|
|
1138
|
+
# unparseable XML without paying for the authentication pass.
|
|
1139
|
+
xml, _container = decode_pkt_auto(path.read_bytes(), verify=False)
|
|
1140
|
+
root = parse_pkt_xml(xml)
|
|
1141
|
+
config_text = "\n".join(
|
|
1142
|
+
line.text or "" for line in root.findall(".//ENGINE/RUNNINGCONFIG/LINE")
|
|
1143
|
+
)
|
|
1144
|
+
from pkt_editor import inventory_root
|
|
1145
|
+
|
|
1146
|
+
inventory = inventory_root(root)
|
|
1147
|
+
workspace_result = inspect_workspace_integrity(root)
|
|
1148
|
+
devices = []
|
|
1149
|
+
indexed_devices = root.findall(".//DEVICES/DEVICE")
|
|
1150
|
+
index_to_name = {str(index): device.findtext("./ENGINE/NAME", default="") for index, device in enumerate(indexed_devices)}
|
|
1151
|
+
save_ref_to_name = {
|
|
1152
|
+
device.findtext("./ENGINE/SAVE_REF_ID", default=""): device.findtext("./ENGINE/NAME", default="")
|
|
1153
|
+
for device in indexed_devices
|
|
1154
|
+
if device.findtext("./ENGINE/SAVE_REF_ID", default="")
|
|
1155
|
+
}
|
|
1156
|
+
# Imported here rather than at module scope: `pkt_transformer` imports this
|
|
1157
|
+
# module, so a top-level import would close the circle.
|
|
1158
|
+
from pkt_transformer import port_capacity
|
|
1159
|
+
|
|
1160
|
+
for device in root.findall(".//DEVICES/DEVICE"):
|
|
1161
|
+
type_node = device.find("./ENGINE/TYPE")
|
|
1162
|
+
devices.append(
|
|
1163
|
+
{
|
|
1164
|
+
"name": device.findtext("./ENGINE/NAME", default=""),
|
|
1165
|
+
"type": device.findtext("./ENGINE/TYPE", default=""),
|
|
1166
|
+
"model": type_node.get("model", "") if type_node is not None else "",
|
|
1167
|
+
# Whether a device can carry a serial WAN is not visible from
|
|
1168
|
+
# its type or model: the ports live on an installed module. A
|
|
1169
|
+
# prompt asking for a leased line was ranked against samples
|
|
1170
|
+
# that had no serial hardware at all, because nothing in the
|
|
1171
|
+
# catalogue recorded it.
|
|
1172
|
+
"serial_ports": port_capacity(device).get("Serial", 0),
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
links = []
|
|
1176
|
+
for link in root.findall(".//LINKS/LINK"):
|
|
1177
|
+
cable = link.find("./CABLE")
|
|
1178
|
+
ports = cable.findall("PORT") if cable is not None else []
|
|
1179
|
+
from_ref = cable.findtext("FROM", default="") if cable is not None else ""
|
|
1180
|
+
to_ref = cable.findtext("TO", default="") if cable is not None else ""
|
|
1181
|
+
links.append(
|
|
1182
|
+
{
|
|
1183
|
+
"from": save_ref_to_name.get(from_ref, index_to_name.get(from_ref, "")),
|
|
1184
|
+
"to": save_ref_to_name.get(to_ref, index_to_name.get(to_ref, "")),
|
|
1185
|
+
"type": link.findtext("./TYPE", default=""),
|
|
1186
|
+
"cable_type": cable.findtext("./TYPE", default="") if cable is not None else "",
|
|
1187
|
+
"media": cable.findtext("./TYPE", default="") if cable is not None else "",
|
|
1188
|
+
"ports": [port.text or "" for port in ports[:2]],
|
|
1189
|
+
}
|
|
1190
|
+
)
|
|
1191
|
+
service_aliases = {
|
|
1192
|
+
"dhcp_server": "dhcp",
|
|
1193
|
+
"dns_server": "dns",
|
|
1194
|
+
"http_server": "http",
|
|
1195
|
+
"https_server": "https",
|
|
1196
|
+
"ftp_server": "ftp",
|
|
1197
|
+
"tftp_server": "tftp",
|
|
1198
|
+
"ntp_server": "ntp",
|
|
1199
|
+
"email_server": "email",
|
|
1200
|
+
"syslog_server": "syslog",
|
|
1201
|
+
"acs_server": "aaa",
|
|
1202
|
+
}
|
|
1203
|
+
service_support = sorted(
|
|
1204
|
+
{
|
|
1205
|
+
service_aliases[service]
|
|
1206
|
+
for services in inventory.get("services", {}).values()
|
|
1207
|
+
for service in services
|
|
1208
|
+
if service in service_aliases
|
|
1209
|
+
}
|
|
1210
|
+
)
|
|
1211
|
+
iot_roles = sorted({str(entry.get("role", "")).strip() for entry in inventory.get("iot", {}).values() if str(entry.get("role", "")).strip()})
|
|
1212
|
+
return {
|
|
1213
|
+
"path": str(path),
|
|
1214
|
+
"relative_path": relative_path,
|
|
1215
|
+
"config_capability_tags": sorted(config_capability_tags(config_text)),
|
|
1216
|
+
"version": root.findtext("./VERSION", default=""),
|
|
1217
|
+
"device_count": len(devices),
|
|
1218
|
+
"link_count": len(links),
|
|
1219
|
+
"devices": devices,
|
|
1220
|
+
"links": links,
|
|
1221
|
+
"origin": origin,
|
|
1222
|
+
"trust_level": "reference-only" if origin != "cisco-local" else "trusted",
|
|
1223
|
+
"role": "reference" if origin != "cisco-local" else "primary",
|
|
1224
|
+
"prototype_eligible": prototype_eligible,
|
|
1225
|
+
"service_support": service_support,
|
|
1226
|
+
"iot_roles": iot_roles,
|
|
1227
|
+
"workspace_validation": {
|
|
1228
|
+
"workspace_mode": workspace_result.workspace_mode,
|
|
1229
|
+
"logical_status": workspace_result.logical_status,
|
|
1230
|
+
"physical_status": workspace_result.physical_status,
|
|
1231
|
+
"blocking_issue_count": len(workspace_result.blocking_issues),
|
|
1232
|
+
"blocking_issues": workspace_result.blocking_issues,
|
|
1233
|
+
},
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def summarize_pkt_descriptor(
|
|
1238
|
+
path: Path,
|
|
1239
|
+
relative_path: str | None = None,
|
|
1240
|
+
*,
|
|
1241
|
+
origin: str = "external-reference",
|
|
1242
|
+
prototype_eligible: bool = False,
|
|
1243
|
+
trust_level: str | None = None,
|
|
1244
|
+
role: str | None = None,
|
|
1245
|
+
license_or_permission: str | None = None,
|
|
1246
|
+
promotion_status: str | None = None,
|
|
1247
|
+
validation_status: str | None = None,
|
|
1248
|
+
donor_eligible: bool | None = None,
|
|
1249
|
+
) -> SampleDescriptor:
|
|
1250
|
+
item = _summarize_pkt(path, relative_path or path.name, origin, prototype_eligible)
|
|
1251
|
+
if trust_level is not None:
|
|
1252
|
+
item["trust_level"] = trust_level
|
|
1253
|
+
if role is not None:
|
|
1254
|
+
item["role"] = role
|
|
1255
|
+
if license_or_permission is not None:
|
|
1256
|
+
item["license_or_permission"] = license_or_permission
|
|
1257
|
+
if promotion_status is not None:
|
|
1258
|
+
item["promotion_status"] = promotion_status
|
|
1259
|
+
if validation_status is not None:
|
|
1260
|
+
item["validation_status"] = validation_status
|
|
1261
|
+
if donor_eligible is not None:
|
|
1262
|
+
item["donor_eligible"] = donor_eligible
|
|
1263
|
+
enriched = enrich_catalog_items([item])[0]
|
|
1264
|
+
return _descriptor_from_item(enriched)
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
@lru_cache(maxsize=4)
|
|
1268
|
+
def _load_reference_catalog_cached(roots_key: tuple[str, ...]) -> tuple[SampleDescriptor, ...]:
|
|
1269
|
+
items: list[dict[str, Any]] = []
|
|
1270
|
+
for root_str in roots_key:
|
|
1271
|
+
root = Path(root_str)
|
|
1272
|
+
if not root.exists():
|
|
1273
|
+
continue
|
|
1274
|
+
for pkt_path in sorted(root.rglob("*.pkt")):
|
|
1275
|
+
try:
|
|
1276
|
+
rel = str(pkt_path.relative_to(root))
|
|
1277
|
+
item = _summarize_pkt(pkt_path, rel, "external-reference", False)
|
|
1278
|
+
validation = validate_external_sample_summary(item, curated=False)
|
|
1279
|
+
item["license_or_permission"] = _detect_license_or_permission(root)
|
|
1280
|
+
item["validation_status"] = validation.validation_status
|
|
1281
|
+
item["promotion_status"] = validation.promotion_status
|
|
1282
|
+
item["donor_eligible"] = validation.donor_eligible
|
|
1283
|
+
items.append(item)
|
|
1284
|
+
except Exception:
|
|
1285
|
+
continue
|
|
1286
|
+
enriched = enrich_catalog_items(items)
|
|
1287
|
+
return tuple(_descriptor_from_item(item) for item in enriched)
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def load_reference_catalog(reference_roots: list[Path] | None = None) -> list[SampleDescriptor]:
|
|
1291
|
+
if not reference_roots:
|
|
1292
|
+
return []
|
|
1293
|
+
roots_key = tuple(sorted(str(path) for path in reference_roots))
|
|
1294
|
+
return list(_load_reference_catalog_cached(roots_key))
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
@lru_cache(maxsize=4)
|
|
1298
|
+
def _load_curated_donor_catalog_cached(roots_key: tuple[str, ...]) -> tuple[SampleDescriptor, ...]:
|
|
1299
|
+
items: list[dict[str, Any]] = []
|
|
1300
|
+
registry = load_curated_donor_registry()
|
|
1301
|
+
for root_str in roots_key:
|
|
1302
|
+
root = Path(root_str)
|
|
1303
|
+
if not root.exists():
|
|
1304
|
+
continue
|
|
1305
|
+
license_or_permission = _detect_license_or_permission(root)
|
|
1306
|
+
for pkt_path in sorted(root.rglob("*.pkt")):
|
|
1307
|
+
try:
|
|
1308
|
+
rel = str(pkt_path.relative_to(root))
|
|
1309
|
+
item = _summarize_pkt(pkt_path, rel, "external-curated", True)
|
|
1310
|
+
validation = validate_external_sample_summary(item, curated=True)
|
|
1311
|
+
item["trust_level"] = "curated" if validation.donor_eligible else "reference-only"
|
|
1312
|
+
item["role"] = "secondary" if validation.donor_eligible else "reference"
|
|
1313
|
+
item["prototype_eligible"] = validation.donor_eligible
|
|
1314
|
+
item["license_or_permission"] = license_or_permission
|
|
1315
|
+
item["validation_status"] = validation.validation_status
|
|
1316
|
+
item["promotion_status"] = validation.promotion_status
|
|
1317
|
+
item["packet_tracer_version"] = validation.packet_tracer_version
|
|
1318
|
+
item["donor_eligible"] = validation.donor_eligible
|
|
1319
|
+
registry_entry = registry.get(_normalize_registry_key(rel)) or registry.get(_normalize_registry_key(pkt_path.name))
|
|
1320
|
+
item = _merge_curated_registry_entry(item, registry_entry, validation)
|
|
1321
|
+
items.append(item)
|
|
1322
|
+
except Exception:
|
|
1323
|
+
continue
|
|
1324
|
+
enriched = enrich_catalog_items(items)
|
|
1325
|
+
return tuple(_descriptor_from_item(item) for item in enriched)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def load_curated_donor_catalog(donor_roots: list[Path] | None = None) -> list[SampleDescriptor]:
|
|
1329
|
+
if not donor_roots:
|
|
1330
|
+
return []
|
|
1331
|
+
roots_key = tuple(sorted(str(path) for path in donor_roots))
|
|
1332
|
+
return list(_load_curated_donor_catalog_cached(roots_key))
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def extract_reference_patterns(samples: list[SampleDescriptor]) -> list[ReferencePattern]:
|
|
1336
|
+
patterns: list[ReferencePattern] = []
|
|
1337
|
+
for sample in samples:
|
|
1338
|
+
patterns.append(
|
|
1339
|
+
ReferencePattern(
|
|
1340
|
+
relative_path=sample.relative_path,
|
|
1341
|
+
origin=sample.origin,
|
|
1342
|
+
capability_tags=sample.capability_tags,
|
|
1343
|
+
topology_tags=sample.topology_tags,
|
|
1344
|
+
device_summary=sample.normalized_device_counts(),
|
|
1345
|
+
wireless_mode_tags=sample.wireless_mode_tags,
|
|
1346
|
+
)
|
|
1347
|
+
)
|
|
1348
|
+
return patterns
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def write_catalog_outputs(items: list[dict[str, Any]], json_path: Path | None = None, md_path: Path | None = None) -> None:
|
|
1352
|
+
enriched = enrich_catalog_items(items)
|
|
1353
|
+
compact_items: list[dict[str, Any]] = []
|
|
1354
|
+
for item in enriched:
|
|
1355
|
+
saved = dict(item)
|
|
1356
|
+
saved.pop("path", None)
|
|
1357
|
+
compact_items.append(saved)
|
|
1358
|
+
(json_path or DEFAULT_CATALOG_JSON).write_text(json.dumps(compact_items, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
1359
|
+
root_label = "<PACKET_TRACER_SAVES_ROOT>"
|
|
1360
|
+
lines = ["# Packet Tracer Installed Sample Catalog", "", f"Source root: `{root_label}`", ""]
|
|
1361
|
+
for item in enriched:
|
|
1362
|
+
if "error" in item:
|
|
1363
|
+
lines.append(f"- `{item['relative_path']}`")
|
|
1364
|
+
lines.append(f" decode error: `{item['error']}`")
|
|
1365
|
+
continue
|
|
1366
|
+
labels = ", ".join(f"{device['name']} ({device['type']}/{device['model']})" for device in item["devices"])
|
|
1367
|
+
lines.append(f"- `{item['relative_path']}`")
|
|
1368
|
+
lines.append(f" version: `{item['version']}`, devices: `{item['device_count']}`, links: `{item['link_count']}`")
|
|
1369
|
+
lines.append(f" tags: {', '.join(item.get('capability_tags', []))}")
|
|
1370
|
+
lines.append(f" topology: {', '.join(item.get('topology_tags', []))}")
|
|
1371
|
+
lines.append(f" devices: {labels}")
|
|
1372
|
+
(md_path or DEFAULT_CATALOG_MD).write_text("\n".join(lines) + "\n", encoding="utf-8")
|