1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2025-06-19 00:17:51 +02:00

initial working parser for both linux and bsd

This commit is contained in:
Kelly Brazil
2021-09-19 13:18:23 -07:00
parent 758f27945d
commit a19c12096a

View File

@ -56,6 +56,7 @@ Examples:
... ...
""" """
import string import string
import ipaddress
import jc.utils import jc.utils
from jc.utils import stream_success, stream_error from jc.utils import stream_success, stream_error
@ -102,23 +103,12 @@ def _process(proc_data):
return proc_data return proc_data
def parse(data, raw=False, quiet=False): class state:
""" os_detected = None
Main text parsing generator function. Produces an iterable object. linux = None
bsd = None
Parameters: ipv4 = None
hostname = None
data: (string) line-based text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages and ignore parsing errors if True
Yields:
Dictionary. Raw or processed structured data.
"""
if not quiet:
jc.utils.compatibility(__name__, info.compatible)
destination_ip = None destination_ip = None
sent_bytes = None sent_bytes = None
pattern = None pattern = None
@ -128,88 +118,269 @@ def parse(data, raw=False, quiet=False):
packet_loss_percent = None packet_loss_percent = None
time_ms = None time_ms = None
duplicates = None duplicates = None
ping_error = None
for line in data:
def _ipv6_in(line):
line_list = line.replace('(', ' ').replace(')', ' ').replace(',', ' ').replace('%', ' ').split()
ipv6 = False
for item in line_list:
try: try:
_ = ipaddress.IPv6Address(item)
ipv6 = True
except Exception:
pass
return ipv6
def _error_type(line):
# from https://github.com/dgibson/iputils/blob/master/ping.c
# https://android.googlesource.com/platform/external/ping/+/8fc3c91cf9e7f87bc20b9e6d3ea2982d87b70d9a/ping.c
# https://opensource.apple.com/source/network_cmds/network_cmds-328/ping.tproj/ping.c
type_map = {
'Destination Net Unreachable': 'destination_net_unreachable',
'Destination Host Unreachable': 'destination_host_unreachable',
'Destination Protocol Unreachable': 'destination_protocol_unreachable',
'Destination Port Unreachable': 'destination_port_unreachable',
'Frag needed and DF set': 'frag_needed_and_df_set',
'Source Route Failed': 'source_route_failed',
'Destination Net Unknown': 'destination_net_unknown',
'Destination Host Unknown': 'destination_host_unknown',
'Source Host Isolated': 'source_host_isolated',
'Destination Net Prohibited': 'destination_net_prohibited',
'Destination Host Prohibited': 'destination_host_prohibited',
'Destination Net Unreachable for Type of Service': 'destination_net_unreachable_for_type_of_service',
'Destination Host Unreachable for Type of Service': 'destination_host_unreachable_for_type_of_service',
'Packet filtered': 'packet_filtered',
'Precedence Violation': 'precedence_violation',
'Precedence Cutoff': 'precedence_cutoff',
'Dest Unreachable, Bad Code': 'dest_unreachable_bad_code',
'Redirect Network': 'redirect_network',
'Redirect Host': 'redirect_host',
'Redirect Type of Service and Network': 'redirect_type_of_service_and_network',
'Redirect, Bad Code': 'redirect_bad_code',
'Time to live exceeded': 'time_to_live_exceeded',
'Frag reassembly time exceeded': 'frag_reassembly_time_exceeded',
'Time exceeded, Bad Code': 'time_exceeded_bad_code'
}
for err_type, code in type_map.items():
if err_type in line:
return code
def _bsd_parse(line, s):
output_line = {} output_line = {}
# check for PATTERN
if line.startswith('PATTERN: '):
pattern = line.strip().split(': ')[1]
continue
if line.startswith('PING '): if line.startswith('PING '):
ipv4 = True if 'bytes of data' in line else False s.destination_ip = line.split()[2].lstrip('(').rstrip(':').rstrip(')')
s.sent_bytes = line.split()[3]
return None
if line.startswith('PING6('):
line = line.replace('(', ' ').replace(')', ' ').replace('=', ' ')
s.source_ip = line.split()[4]
s.destination_ip = line.split()[6]
s.sent_bytes = line.split()[1]
return None
if line.startswith('---'):
s.footer = True
return None
if s.footer:
if 'packets transmitted' in line:
if ' duplicates,' in line:
s.packets_transmitted = line.split()[0]
s.packets_received = line.split()[3]
s.packet_loss_percent = line.split()[8].rstrip('%')
s.duplicates = line.split()[6].lstrip('+')
return None
if ipv4 and line[5] not in string.digits:
hostname = True
elif ipv4 and line[5] in string.digits:
hostname = False
elif not ipv4 and ' (' in line:
hostname = True
else: else:
hostname = False s.packets_transmitted = line.split()[0]
s.packets_received = line.split()[3]
s.packet_loss_percent = line.split()[6].rstrip('%')
s.duplicates = '0'
return None
if ipv4 and not hostname: else:
split_line = line.split(' = ')[1]
split_line = split_line.split('/')
output_line = {
'type': 'summary',
'destination_ip': s.destination_ip or None,
'sent_bytes': s.sent_bytes or None,
'pattern': s.pattern or None,
'packets_transmitted': s.packets_transmitted or None,
'packets_received': s.packets_received or None,
'packet_loss_percent': s.packet_loss_percent or None,
'duplicates': s.duplicates or None,
'round_trip_ms_min': split_line[0],
'round_trip_ms_avg': split_line[1],
'round_trip_ms_max': split_line[2],
'round_trip_ms_stddev': split_line[3].replace(' ms', '')
}
return output_line
# ping response lines
else:
# ipv4 lines
if not _ipv6_in(line):
# request timeout
if line.startswith('Request timeout for '):
output_line = {
'type': 'timeout',
'destination_ip': s.destination_ip or None,
'sent_bytes': s.sent_bytes or None,
'pattern': s.pattern or None,
'icmp_seq': line.split()[4]
}
return output_line
# catch error responses
err = _error_type(line)
if err:
output_line = {
'type': err
}
try:
output_line['bytes'] = line.split()[0]
output_line['response_ip'] = line.split()[4].strip(':').strip('(').strip(')')
except Exception:
pass
return output_line
# normal response
elif ' bytes from ' in line:
line = line.replace(':', ' ').replace('=', ' ')
output_line = {
'type': 'reply',
'destination_ip': s.destination_ip or None,
'sent_bytes': s.sent_bytes or None,
'pattern': s.pattern or None,
'response_bytes': line.split()[0],
'response_ip': line.split()[3],
'icmp_seq': line.split()[5],
'ttl': line.split()[7],
'time_ms': line.split()[9]
}
return output_line
# ipv6 lines
elif ' bytes from ' in line:
line = line.replace(',', ' ').replace('=', ' ')
output_line = {
'type': 'reply',
'destination_ip': s.destination_ip or None,
'sent_bytes': s.sent_bytes or None,
'pattern': s.pattern or None,
'bytes': line.split()[0],
'response_ip': line.split()[3],
'icmp_seq': line.split()[5],
'ttl': line.split()[7],
'time_ms': line.split()[9]
}
return output_line
def _linux_parse(line, s):
"""
Linux ping line parsing function.
Parameters:
line: (string) line of text data to parse
s: (state object) global state
Returns:
Dictionary. Raw structured data.
"""
output_line = {}
if line.startswith('PING '):
s.ipv4 = True if 'bytes of data' in line else False
if s.ipv4 and line[5] not in string.digits:
s.hostname = True
elif s.ipv4 and line[5] in string.digits:
s.hostname = False
elif not s.ipv4 and ' (' in line:
s.hostname = True
else:
s.hostname = False
if s.ipv4 and not s.hostname:
dst_ip, dta_byts = (2, 3) dst_ip, dta_byts = (2, 3)
elif ipv4 and hostname: elif s.ipv4 and s.hostname:
dst_ip, dta_byts = (2, 3) dst_ip, dta_byts = (2, 3)
elif not ipv4 and not hostname: elif not s.ipv4 and not s.hostname:
dst_ip, dta_byts = (2, 3) dst_ip, dta_byts = (2, 3)
else: else:
dst_ip, dta_byts = (3, 4) dst_ip, dta_byts = (3, 4)
line = line.replace('(', ' ').replace(')', ' ') line = line.replace('(', ' ').replace(')', ' ')
destination_ip = line.split()[dst_ip].lstrip('(').rstrip(')') s.destination_ip = line.split()[dst_ip].lstrip('(').rstrip(')')
sent_bytes = line.split()[dta_byts] s.sent_bytes = line.split()[dta_byts]
continue return None
if line.startswith('---'): if line.startswith('---'):
footer = True s.footer = True
continue return None
if footer: if s.footer:
if 'packets transmitted' in line: if 'packets transmitted' in line:
if ' duplicates,' in line: if ' duplicates,' in line:
packets_transmitted = line.split()[0] s.packets_transmitted = line.split()[0]
packets_received = line.split()[3] s.packets_received = line.split()[3]
packet_loss_percent = line.split()[7].rstrip('%') s.packet_loss_percent = line.split()[7].rstrip('%')
duplicates = line.split()[5].lstrip('+') s.duplicates = line.split()[5].lstrip('+')
time_ms = line.split()[11].replace('ms', '') s.time_ms = line.split()[11].replace('ms', '')
continue return None
else: else:
packets_transmitted = line.split()[0] s.packets_transmitted = line.split()[0]
packets_received = line.split()[3] s.packets_received = line.split()[3]
packet_loss_percent = line.split()[5].rstrip('%') s.packet_loss_percent = line.split()[5].rstrip('%')
duplicates = '0' s.duplicates = '0'
time_ms = line.split()[9].replace('ms', '') s.time_ms = line.split()[9].replace('ms', '')
continue return None
else: else:
split_line = line.split(' = ')[1] split_line = line.split(' = ')[1]
split_line = split_line.split('/') split_line = split_line.split('/')
output_line = { output_line = {
'type': 'summary', 'type': 'summary',
'destination_ip': destination_ip or None, 'destination_ip': s.destination_ip or None,
'sent_bytes': sent_bytes or None, 'sent_bytes': s.sent_bytes or None,
'pattern': pattern or None, 'pattern': s.pattern or None,
'packets_transmitted': packets_transmitted or None, 'packets_transmitted': s.packets_transmitted or None,
'packets_received': packets_received or None, 'packets_received': s.packets_received or None,
'packet_loss_percent': packet_loss_percent or None, 'packet_loss_percent': s.packet_loss_percent or None,
'duplicates': duplicates or None, 'duplicates': s.duplicates or None,
'time_ms': time_ms or None, 'time_ms': s.time_ms or None,
'round_trip_ms_min': split_line[0], 'round_trip_ms_min': split_line[0],
'round_trip_ms_avg': split_line[1], 'round_trip_ms_avg': split_line[1],
'round_trip_ms_max': split_line[2], 'round_trip_ms_max': split_line[2],
'round_trip_ms_stddev': split_line[3].split()[0] 'round_trip_ms_stddev': split_line[3].split()[0]
} }
yield stream_success(output_line, quiet) if raw else stream_success(_process(output_line), quiet) return output_line
continue
# ping response lines # ping response lines
else: else:
@ -225,15 +396,14 @@ def parse(data, raw=False, quiet=False):
output_line = { output_line = {
'type': 'timeout', 'type': 'timeout',
'destination_ip': destination_ip or None, 'destination_ip': s.destination_ip or None,
'sent_bytes': sent_bytes or None, 'sent_bytes': s.sent_bytes or None,
'pattern': pattern or None, 'pattern': s.pattern or None,
'timestamp': line.split()[0].lstrip('[').rstrip(']') if timestamp else None, 'timestamp': line.split()[0].lstrip('[').rstrip(']') if timestamp else None,
'icmp_seq': line.replace('=', ' ').split()[isequence] 'icmp_seq': line.replace('=', ' ').split()[isequence]
} }
yield stream_success(output_line, quiet) if raw else stream_success(_process(output_line), quiet) return output_line
continue
# normal responses # normal responses
elif ' bytes from ' in line: elif ' bytes from ' in line:
@ -241,13 +411,13 @@ def parse(data, raw=False, quiet=False):
line = line.replace('(', ' ').replace(')', ' ').replace('=', ' ') line = line.replace('(', ' ').replace(')', ' ').replace('=', ' ')
# positions of items depend on whether ipv4/ipv6 and/or ip/hostname is used # positions of items depend on whether ipv4/ipv6 and/or ip/hostname is used
if ipv4 and not hostname: if s.ipv4 and not s.hostname:
bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9) bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9)
elif ipv4 and hostname: elif s.ipv4 and s.hostname:
bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11) bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11)
elif not ipv4 and not hostname: elif not s.ipv4 and not s.hostname:
bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9) bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9)
elif not ipv4 and hostname: elif not s.ipv4 and s.hostname:
bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11) bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11)
# if timestamp option is specified, then shift everything right by one # if timestamp option is specified, then shift everything right by one
@ -258,9 +428,9 @@ def parse(data, raw=False, quiet=False):
output_line = { output_line = {
'type': 'reply', 'type': 'reply',
'destination_ip': destination_ip or None, 'destination_ip': s.destination_ip or None,
'sent_bytes': sent_bytes or None, 'sent_bytes': s.sent_bytes or None,
'pattern': pattern or None, 'pattern': s.pattern or None,
'timestamp': line.split()[0].lstrip('[').rstrip(']') if timestamp else None, 'timestamp': line.split()[0].lstrip('[').rstrip(']') if timestamp else None,
'response_bytes': line.split()[bts], 'response_bytes': line.split()[bts],
'response_ip': line.split()[rip].rstrip(':'), 'response_ip': line.split()[rip].rstrip(':'),
@ -270,7 +440,67 @@ def parse(data, raw=False, quiet=False):
'duplicate': True if 'DUP!' in line else False 'duplicate': True if 'DUP!' in line else False
} }
return output_line
def parse(data, raw=False, quiet=False):
"""
Main text parsing generator function. Produces an iterable object.
Parameters:
data: (string) line-based text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages and ignore parsing errors if True
Yields:
Dictionary. Raw or processed structured data.
"""
s = state()
if not quiet:
jc.utils.compatibility(__name__, info.compatible)
for line in data:
output_line = {}
try:
# check for PATTERN
if line.startswith('PATTERN: '):
s.pattern = line.strip().split(': ')[1]
continue
# detect Linux vs. BSD ping
if not s.os_detected and line.strip().endswith('bytes of data.'):
s.os_detected = True
s.linux = True
if not s.os_detected and '-->' in line:
s.os_detected = True
s.bsd = True
if not s.os_detected and _ipv6_in(line) and line.strip().endswith('data bytes'):
s.os_detected = True
s.linux = True
if not s.os_detected:
s.os_detected = True
s.bsd = True
# parse the data
if s.os_detected and s.linux:
output_line = _linux_parse(line, s)
if s.os_detected and s.bsd:
output_line = _bsd_parse(line, s)
# yield the output line if it has data
if output_line:
yield stream_success(output_line, quiet) if raw else stream_success(_process(output_line), quiet) yield stream_success(output_line, quiet) if raw else stream_success(_process(output_line), quiet)
else:
continue
except Exception as e: except Exception as e:
yield stream_error(e, quiet, line) yield stream_error(e, quiet, line)