1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2025-06-17 00:07:37 +02:00
Files
jc/jc/parsers/ss.py
Kelly Brazil 1d0ad2f045 doc fixes
2021-01-04 18:01:16 -08:00

423 lines
11 KiB
Python

"""jc - JSON CLI output utility `ss` command output parser
Usage (cli):
$ ss | jc --ss
or
$ jc ss
Usage (module):
import jc.parsers.ss
result = jc.parsers.ss.parse(ss_command_output)
Limitations:
Extended information options like -e and -p are not supported and may cause parsing irregularities
Compatibility:
'linux'
Examples:
$ sudo ss -a | jc --ss -p
[
{
"netid": "nl",
"state": "UNCONN",
"recv_q": 0,
"send_q": 0,
"peer_address": "*",
"channel": "rtnl:kernel"
},
{
"netid": "nl",
"state": "UNCONN",
"recv_q": 0,
"send_q": 0,
"peer_address": "*",
"pid": 893,
"channel": "rtnl:systemd-resolve"
},
...
{
"netid": "p_raw",
"state": "UNCONN",
"recv_q": 0,
"send_q": 0,
"peer_address": "*",
"link_layer": "LLDP",
"interface": "ens33"
},
{
"netid": "u_dgr",
"state": "UNCONN",
"recv_q": 0,
"send_q": 0,
"local_port": "93066",
"peer_address": "*",
"peer_port": "0",
"path": "/run/user/1000/systemd/notify"
},
{
"netid": "u_seq",
"state": "LISTEN",
"recv_q": 0,
"send_q": 128,
"local_port": "20699",
"peer_address": "*",
"peer_port": "0",
"path": "/run/udev/control"
},
...
{
"netid": "icmp6",
"state": "UNCONN",
"recv_q": 0,
"send_q": 0,
"local_address": "*",
"local_port": "ipv6-icmp",
"peer_address": "*",
"peer_port": "*",
"interface": "ens33"
},
{
"netid": "udp",
"state": "UNCONN",
"recv_q": 0,
"send_q": 0,
"local_address": "127.0.0.53",
"local_port": "domain",
"peer_address": "0.0.0.0",
"peer_port": "*",
"interface": "lo"
},
{
"netid": "tcp",
"state": "LISTEN",
"recv_q": 0,
"send_q": 128,
"local_address": "127.0.0.53",
"local_port": "domain",
"peer_address": "0.0.0.0",
"peer_port": "*",
"interface": "lo"
},
{
"netid": "tcp",
"state": "LISTEN",
"recv_q": 0,
"send_q": 128,
"local_address": "0.0.0.0",
"local_port": "ssh",
"peer_address": "0.0.0.0",
"peer_port": "*"
},
{
"netid": "tcp",
"state": "LISTEN",
"recv_q": 0,
"send_q": 128,
"local_address": "[::]",
"local_port": "ssh",
"peer_address": "[::]",
"peer_port": "*"
},
{
"netid": "v_str",
"state": "ESTAB",
"recv_q": 0,
"send_q": 0,
"local_address": "999900439",
"local_port": "1023",
"peer_address": "0",
"peer_port": "976",
"local_port_num": 1023,
"peer_port_num": 976
}
]
$ sudo ss -a | jc --ss -p -r
[
{
"netid": "nl",
"state": "UNCONN",
"recv_q": "0",
"send_q": "0",
"peer_address": "*",
"channel": "rtnl:kernel"
},
{
"netid": "nl",
"state": "UNCONN",
"recv_q": "0",
"send_q": "0",
"peer_address": "*",
"pid": "893",
"channel": "rtnl:systemd-resolve"
},
...
{
"netid": "p_raw",
"state": "UNCONN",
"recv_q": "0",
"send_q": "0",
"peer_address": "*",
"link_layer": "LLDP",
"interface": "ens33"
},
{
"netid": "u_dgr",
"state": "UNCONN",
"recv_q": "0",
"send_q": "0",
"local_port": "93066",
"peer_address": "*",
"peer_port": "0",
"path": "/run/user/1000/systemd/notify"
},
{
"netid": "u_seq",
"state": "LISTEN",
"recv_q": "0",
"send_q": "128",
"local_port": "20699",
"peer_address": "*",
"peer_port": "0",
"path": "/run/udev/control"
},
...
{
"netid": "icmp6",
"state": "UNCONN",
"recv_q": "0",
"send_q": "0",
"local_address": "*",
"local_port": "ipv6-icmp",
"peer_address": "*",
"peer_port": "*",
"interface": "ens33"
},
{
"netid": "udp",
"state": "UNCONN",
"recv_q": "0",
"send_q": "0",
"local_address": "127.0.0.53",
"local_port": "domain",
"peer_address": "0.0.0.0",
"peer_port": "*",
"interface": "lo"
},
{
"netid": "tcp",
"state": "LISTEN",
"recv_q": "0",
"send_q": "128",
"local_address": "127.0.0.53",
"local_port": "domain",
"peer_address": "0.0.0.0",
"peer_port": "*",
"interface": "lo"
},
{
"netid": "tcp",
"state": "LISTEN",
"recv_q": "0",
"send_q": "128",
"local_address": "0.0.0.0",
"local_port": "ssh",
"peer_address": "0.0.0.0",
"peer_port": "*"
},
{
"netid": "tcp",
"state": "LISTEN",
"recv_q": "0",
"send_q": "128",
"local_address": "[::]",
"local_port": "ssh",
"peer_address": "[::]",
"peer_port": "*"
},
{
"netid": "v_str",
"state": "ESTAB",
"recv_q": "0",
"send_q": "0",
"local_address": "999900439",
"local_port": "1023",
"peer_address": "0",
"peer_port": "976"
}
]
"""
import string
import jc.utils
class info():
version = '1.2'
description = 'ss command parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
# compatible options: linux, darwin, cygwin, win32, aix, freebsd
compatible = ['linux']
magic_commands = ['ss']
__version__ = info.version
def process(proc_data):
"""
Final processing to conform to the schema.
Parameters:
proc_data: (List of Dictionaries) raw structured data to process
Returns:
List of Dictionaries. Structured data with the following schema:
[
{
"netid": string,
"state": string,
"recv_q": integer,
"send_q": integer,
"local_address": string,
"local_port": string,
"local_port_num": integer,
"peer_address": string,
"peer_port": string,
"peer_port_num": integer,
"interface": string,
"link_layer" string,
"channel": string,
"path": string,
"pid": integer
}
]
Information from https://www.cyberciti.biz/files/ss.html used to define field names
"""
for entry in proc_data:
int_list = ['recv_q', 'send_q', 'pid']
for key in int_list:
if key in entry:
try:
key_int = int(entry[key])
entry[key] = key_int
except (ValueError):
entry[key] = None
if 'local_port' in entry:
try:
entry['local_port_num'] = int(entry['local_port'])
except (ValueError):
pass
if 'peer_port' in entry:
try:
entry['peer_port_num'] = int(entry['peer_port'])
except (ValueError):
pass
return proc_data
def parse(data, raw=False, quiet=False):
"""
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages if True
Returns:
List of Dictionaries. Raw or processed structured data.
"""
if not quiet:
jc.utils.compatibility(__name__, info.compatible)
contains_colon = ['nl', 'p_raw', 'raw', 'udp', 'tcp', 'v_str', 'icmp6']
raw_output = []
# Clear any blank lines
cleandata = list(filter(None, data.splitlines()))
if jc.utils.has_data(data):
header_text = cleandata[0].lower()
header_text = header_text.replace('netidstate', 'netid state')
header_text = header_text.replace('local address:port', 'local_address local_port')
header_text = header_text.replace('peer address:port', 'peer_address peer_port')
header_text = header_text.replace('-', '_')
header_list = header_text.split()
for entry in cleandata[1:]:
output_line = {}
if entry[0] not in string.whitespace:
# fix weird ss bug where first two columns have no space between them sometimes
entry = entry[:5] + ' ' + entry[5:]
entry_list = entry.split()
if entry_list[0] in contains_colon and ':' in entry_list[4]:
l_field = entry_list[4].rsplit(':', maxsplit=1)
l_address = l_field[0]
l_port = l_field[1]
entry_list[4] = l_address
entry_list.insert(5, l_port)
if entry_list[0] in contains_colon and ':' in entry_list[6]:
p_field = entry_list[6].rsplit(':', maxsplit=1)
p_address = p_field[0]
p_port = p_field[1]
entry_list[6] = p_address
entry_list.insert(7, p_port)
output_line = dict(zip(header_list, entry_list))
# some post processing to pull out fields: interface, link_layer, path, pid, channel
# Information from https://www.cyberciti.biz/files/ss.html used to define field names
if '%' in output_line['local_address']:
i_field = output_line['local_address'].rsplit('%', maxsplit=1)
output_line['local_address'] = i_field[0]
output_line['interface'] = i_field[1]
if output_line['netid'] == 'nl':
channel = output_line.pop('local_address')
channel = channel + ':' + output_line.pop('local_port')
if '/' in channel:
pid = channel.rsplit('/', maxsplit=1)[1]
channel = channel.rsplit('/', maxsplit=1)[0]
output_line['pid'] = pid
output_line['channel'] = channel
if output_line['netid'] == 'p_raw':
output_line['link_layer'] = output_line.pop('local_address')
output_line['interface'] = output_line.pop('local_port')
if output_line['netid'] not in contains_colon:
output_line['path'] = output_line.pop('local_address')
raw_output.append(output_line)
if raw:
return raw_output
else:
return process(raw_output)