mirror of
https://github.com/kellyjonbrazil/jc.git
synced 2025-06-17 00:07:37 +02:00
414 lines
11 KiB
Python
414 lines
11 KiB
Python
"""jc - JSON CLI output utility ss Parser
|
|
|
|
Usage:
|
|
|
|
specify --ss as the first argument if the piped input is coming from ss
|
|
|
|
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.0'
|
|
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: (dictionary) 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 = []
|
|
cleandata = data.splitlines()
|
|
|
|
# Clear any blank lines
|
|
cleandata = list(filter(None, cleandata))
|
|
|
|
if cleandata:
|
|
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)
|