1
- r"""jc - JSON Convert `ipconfig` command output parser
2
-
1
+ r"""jc - JSON Convert `ipconfig` Windows command output parser
3
2
4
3
Usage (cli):
5
4
6
5
$ ipconfig /all | jc --ipconfig
7
6
$ ipconfig | jc --ipconfig
7
+ $ jc ipconfig /all
8
8
9
9
Usage (module):
10
10
20
20
"ip_routing_enabled": boolean,
21
21
"wins_proxy_enabled": boolean,
22
22
"dns_suffix_search_list": [
23
- string
23
+ string
24
24
],
25
25
"adapters": [
26
26
{
51
51
{
52
52
"address": string,
53
53
"status": string,
54
- "prefix_length": int ,
54
+ "prefix_length": integer ,
55
55
}
56
56
],
57
57
"ipv4_addresses": [
72
72
string
73
73
],
74
74
"primary_wins_server": string,
75
- "lease_expires": string, # [0]
76
- "lease_obtained": string, # [0]
75
+ "lease_expires": string,
76
+ "lease_expires_epoch": integer, # [0]
77
+ "lease_expires_iso": string,
78
+ "lease_obtained": string,
79
+ "lease_obtained_epoch": integer, # [0]
80
+ "lease_obtained_iso": string,
77
81
"netbios_over_tcpip": boolean,
78
82
"media_state": string,
79
83
"extras": [
80
- string: string
84
+ <string>: string
81
85
]
82
86
}
83
87
],
84
88
"extras": []
85
89
}
86
90
87
91
Notes:
88
- [0] - 'lease_expires' and 'lease_obtained' are parsed to ISO8601 format date strings. if the value was unable
89
- to be parsed by datetime, the fields will be in their raw form
90
- [1] - 'autoconfigured' under 'ipv4_address' is only providing indication if the ipv4 address was labeled as
91
- "Autoconfiguration IPv4 Address" vs "IPv4 Address". It does not infer any information from other fields
92
- [2] - Windows XP uses 'IP Address' instead of 'IPv4 Address'. Both values are parsed to the 'ipv4_address'
93
- object for consistency
92
+ [0] - The epoch calculated timestamp field is naive. (i.e. based on
93
+ the local time of the system the parser is run on)
94
+ [1] - 'autoconfigured' under 'ipv4_address' is only providing
95
+ indication if the ipv4 address was labeled as "Autoconfiguration
96
+ IPv4 Address" vs "IPv4 Address". It does not infer any
97
+ information from other fields
98
+ [2] - Windows XP uses 'IP Address' instead of 'IPv4 Address'. Both
99
+ values are parsed to the 'ipv4_address' object for consistency
94
100
95
101
Examples:
96
102
421
427
],
422
428
"extras": []
423
429
}
424
-
425
430
"""
426
431
from datetime import datetime
427
432
import re
431
436
class info ():
432
437
"""Provides parser metadata (version, author, etc.)"""
433
438
version = '1.0'
434
- description = '`ipconfig` command parser'
439
+ description = '`ipconfig` Windows command parser'
435
440
author = 'joehacksalot'
436
441
author_email = '[email protected] '
437
442
compatible = ['windows' ]
@@ -466,6 +471,7 @@ def parse(data, raw=False, quiet=False):
466
471
467
472
return raw_output if raw else _process (raw_output )
468
473
474
+
469
475
def _process_ipv6_address (ip_address ):
470
476
address_split = ip_address ["address" ].split ('%' )
471
477
try :
@@ -484,6 +490,7 @@ def _process_ipv6_address(ip_address):
484
490
"status" : ip_address ["status" ]
485
491
}
486
492
493
+
487
494
def _process_ipv4_address (ip_address ):
488
495
autoconfigured = True if ip_address .get ("autoconfigured" ,"" ) is not None and 'autoconfigured' in ip_address .get ("autoconfigured" ,"" ) else False
489
496
subnet_mask = ip_address ["subnet_mask" ]
@@ -494,6 +501,7 @@ def _process_ipv4_address(ip_address):
494
501
"autoconfigured" : autoconfigured
495
502
}
496
503
504
+
497
505
def _process (proc_data ):
498
506
"""
499
507
Final processing to conform to the schema.
@@ -507,8 +515,7 @@ def _process(proc_data):
507
515
Processed Dictionary. Structured data to conform to the schema.
508
516
"""
509
517
processed = proc_data
510
-
511
-
518
+
512
519
if "ip_routing_enabled" in processed and processed ["ip_routing_enabled" ] is not None :
513
520
processed ["ip_routing_enabled" ] = (processed ["ip_routing_enabled" ].lower () == "yes" )
514
521
@@ -518,38 +525,47 @@ def _process(proc_data):
518
525
for adapter in processed ["adapters" ]:
519
526
if "dhcp_enabled" in adapter and adapter ["dhcp_enabled" ] is not None :
520
527
adapter ["dhcp_enabled" ] = (adapter ["dhcp_enabled" ].lower () == "yes" )
528
+
521
529
if "autoconfiguration_enabled" in adapter and adapter ["autoconfiguration_enabled" ] is not None :
522
530
adapter ["autoconfiguration_enabled" ] = (adapter ["autoconfiguration_enabled" ].lower () == "yes" )
531
+
523
532
if "netbios_over_tcpip" in adapter and adapter ["netbios_over_tcpip" ] is not None :
524
533
adapter ["netbios_over_tcpip" ] = (adapter ["netbios_over_tcpip" ].lower () == "enabled" )
525
- if "lease_expires" in adapter and adapter ["lease_expires" ] is not None and adapter ["lease_expires" ] != "" :
526
- try :
527
- adapter ["lease_expires" ] = datetime .strptime (adapter ["lease_expires" ], "%A, %B %d, %Y %I:%M:%S %p" ).isoformat ()
528
- except :
529
- pass # Leave date in raw format if not parseable
530
- if "lease_obtained" in adapter and adapter ["lease_obtained" ] is not None and adapter ["lease_obtained" ] != "" :
531
- try :
532
- adapter ["lease_obtained" ] = datetime .strptime (adapter ["lease_obtained" ], "%A, %B %d, %Y %I:%M:%S %p" ).isoformat ()
533
- except :
534
- pass # Leave date in raw format if not parseable
534
+
535
+ if "lease_expires" in adapter and adapter ["lease_expires" ]:
536
+ ts = jc .utils .timestamp (adapter ['lease_expires' ], format_hint = (1720 ,))
537
+ adapter ["lease_expires_epoch" ] = ts .naive
538
+ adapter ["lease_expires_iso" ] = ts .iso
539
+
540
+ if "lease_obtained" in adapter and adapter ["lease_obtained" ]:
541
+ ts = jc .utils .timestamp (adapter ['lease_obtained' ], format_hint = (1720 ,))
542
+ adapter ["lease_obtained_epoch" ] = ts .naive
543
+ adapter ["lease_obtained_iso" ] = ts .iso
544
+
535
545
adapter ["link_local_ipv6_addresses" ] = [_process_ipv6_address (address ) for address in adapter .get ("link_local_ipv6_addresses" , [])]
536
546
adapter ["ipv4_addresses" ] = [_process_ipv4_address (address ) for address in adapter .get ("ipv4_addresses" , [])]
547
+
537
548
return processed
538
549
550
+
539
551
class _PushbackIterator :
540
552
def __init__ (self , iterator ):
541
553
self .iterator = iterator
542
554
self .pushback_stack = []
555
+
543
556
def __iter__ (self ):
544
557
return self
558
+
545
559
def __next__ (self ):
546
560
if self .pushback_stack :
547
561
return self .pushback_stack .pop ()
548
562
else :
549
563
return next (self .iterator )
564
+
550
565
def pushback (self , value ):
551
566
self .pushback_stack .append (value )
552
567
568
+
553
569
def _parse (data ):
554
570
# Initialize the parsed output dictionary with all fields set to None or empty lists
555
571
parse_output = {
@@ -609,10 +625,12 @@ def _parse(data):
609
625
610
626
return parse_output
611
627
628
+
612
629
def _is_adapter_start_line (line ):
613
630
# Detect adapter start lines, e.g., "Ethernet adapter Ethernet:"
614
631
return re .match (r"^[^\s].*adapter.*:" , line , re .IGNORECASE )
615
632
633
+
616
634
def _initialize_adapter (adapter_name ):
617
635
adapter_name_split = adapter_name .split (" adapter " , 1 )
618
636
if len (adapter_name_split ) > 1 :
@@ -650,6 +668,7 @@ def _initialize_adapter(adapter_name):
650
668
"extras" : [] # To store unrecognized fields
651
669
}
652
670
671
+
653
672
def _parse_line (line ):
654
673
# Split the line into key and value using ':' or multiple spaces
655
674
key_value = re .split (r":" , line .strip (), 1 )
@@ -662,6 +681,7 @@ def _parse_line(line):
662
681
else :
663
682
return None , None
664
683
684
+
665
685
def _parse_header_line (result , key , value , line_iter ):
666
686
if key in ["host_name" , "primary_dns_suffix" , "node_type" , "ip_routing_enabled" , "wins_proxy_enabled" ]:
667
687
result [key ] = value
@@ -674,11 +694,13 @@ def _parse_header_line(result, key, value, line_iter):
674
694
# Store unrecognized fields in extras
675
695
result ["extras" ].append ({key : value })
676
696
697
+
677
698
def _parse_adapter_line (adapter , key , value , line_iter ):
678
699
if key in ["connection_specific_dns_suffix" ,"media_state" , "description" , "physical_address" , "dhcp_enabled" ,
679
700
"autoconfiguration_enabled" , "dhcpv6_iaid" , "dhcpv6_client_duid" , "netbios_over_tcpip" , "dhcp_server" ,
680
701
"lease_obtained" , "lease_expires" , "primary_wins_server" ]:
681
702
adapter [key ] = value
703
+
682
704
elif key in ["ipv6_address" , "temporary_ipv6_address" , "link_local_ipv6_address" ]:
683
705
address_dict = _parse_ipv6_address (value )
684
706
if key == "ipv6_address" :
@@ -687,32 +709,39 @@ def _parse_adapter_line(adapter, key, value, line_iter):
687
709
adapter ["temporary_ipv6_addresses" ].append (address_dict )
688
710
elif key == "link_local_ipv6_address" :
689
711
adapter ["link_local_ipv6_addresses" ].append (address_dict )
712
+
690
713
elif key in ["ipv4_address" , "autoconfiguration_ipv4_address" , "ip_address" , "autoconfiguration_ip_address" ]:
691
714
ipv4_address_dict = _parse_ipv4_address (value , key , line_iter )
692
715
adapter ["ipv4_addresses" ].append (ipv4_address_dict )
716
+
693
717
elif key == "connection_specific_dns_suffix_search_list" :
694
718
if value :
695
719
adapter ["connection_specific_dns_suffix_search_list" ].append (value )
696
720
# Process additional connection specific dns suffix search list entries
697
721
_parse_additional_entries (adapter ["connection_specific_dns_suffix_search_list" ], line_iter )
722
+
698
723
elif key == "default_gateway" :
699
724
if value :
700
725
adapter ["default_gateways" ].append (value )
701
726
# Process additional gateways
702
727
_parse_additional_entries (adapter ["default_gateways" ], line_iter )
728
+
703
729
elif key == "dns_servers" :
704
730
if value :
705
731
adapter ["dns_servers" ].append (value )
706
732
# Process additional DNS servers
707
733
_parse_additional_entries (adapter ["dns_servers" ], line_iter )
734
+
708
735
elif key == "subnet_mask" :
709
736
# Subnet Mask should be associated with the last IPv4 address
710
737
if adapter ["ipv4_addresses" ]:
711
738
adapter ["ipv4_addresses" ][- 1 ]["subnet_mask" ] = value
739
+
712
740
else :
713
741
# Store unrecognized fields in extras
714
742
adapter ["extras" ].append ({key : value })
715
743
744
+
716
745
def _parse_ipv6_address (value ):
717
746
# Handle multiple status indicators
718
747
match = re .match (r"([^\(]+)\((.*)\)" , value ) if value else None
@@ -727,6 +756,7 @@ def _parse_ipv6_address(value):
727
756
"status" : status
728
757
}
729
758
759
+
730
760
def _parse_ipv4_address (value , key , line_iter ):
731
761
# Handle autoconfigured status
732
762
match = re .match (r"([^\(]+)\((.*)\)" , value ) if value else None
@@ -758,6 +788,7 @@ def _parse_ipv4_address(value, key, line_iter):
758
788
"status" : status
759
789
}
760
790
791
+
761
792
def _parse_additional_entries (entry_list , line_iter ):
762
793
# Process additional lines that belong to the current entry (e.g., additional DNS servers, DNS Suffix Search List)
763
794
while True :
@@ -775,4 +806,4 @@ def _parse_additional_entries(entry_list, line_iter):
775
806
line_iter .pushback (next_line )
776
807
break
777
808
except StopIteration :
778
- break
809
+ break
0 commit comments