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

Add ISC 'host' support (#450)

* Add ISC 'host' support

Add ISC 'host' command support

* Update host.py

remove leading tab from string

* Add integer conversion

Per request, fix integer conversion

* Cleanup

Cleanup strip()'s

* Add tests

Add two tests for the 'host' parser

* Update test_host.py

nit

---------

Co-authored-by: Kelly Brazil <kellyjonbrazil@gmail.com>
This commit is contained in:
pettai
2023-10-01 00:26:03 +02:00
committed by GitHub
parent a9958841e4
commit 36fa08d711
7 changed files with 328 additions and 0 deletions

View File

@ -58,6 +58,7 @@ parsers: List[str] = [
'hashsum',
'hciconfig',
'history',
'host',
'hosts',
'id',
'ifconfig',

248
jc/parsers/host.py Normal file
View File

@ -0,0 +1,248 @@
"""jc - JSON Convert `host` command output parser
Supports parsing of the most commonly used RR types (A, AAAA, MX, TXT)
Usage (cli):
$ host google.com | jc --host
or
$ jc host google.com
Usage (module):
import jc
result = jc.parse('host', host_command_output)
Schema:
[
{
"hostname": string,
"address": [
string
],
"v6-address": [
string
],
"mail": [
string
]
}
]
[
{
"nameserver": string,
"zone": string,
"mname": string,
"rname": string,
"serial": integer,
"refresh": integer,
"retry": integer,
"expire": integer,
"minimum": integer
}
]
Examples:
$ host google.com | jc --host
[
{
"hostname": "google.com",
"address": [
"142.251.39.110"
],
"v6-address": [
"2a00:1450:400e:811::200e"
],
"mail": [
"smtp.google.com."
]
}
]
$ jc host -C sunet.se
[
{
"nameserver": "2001:6b0:7::2",
"zone": "sunet.se",
"mname": "sunic.sunet.se.",
"rname": "hostmaster.sunet.se.",
"serial": "2023090401",
"refresh": "28800",
"retry": "7200",
"expire": "604800",
"minimum": "300"
},
{
...
}
]
"""
from typing import Dict, List
import jc.utils
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = '`host` command parser'
author = 'Pettai'
author_email = 'pettai@sunet.se'
# details = 'enter any other details here'
# compatible options: linux, darwin, cygwin, win32, aix, freebsd
compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd']
# tags options: generic, standard, file, string, binary, command
tags = ['command']
magic_commands = ['host']
__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 to conform to the schema.
"""
int_list = {'serial', 'refresh', 'retry', 'expire', 'minimum'}
for entry in proc_data:
for key in entry:
if key in int_list:
entry[key] = jc.utils.convert_to_int(entry[key])
return proc_data
def parse(data: str, raw: bool = False, quiet: bool = False):
"""
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) unprocessed output if True
quiet: (boolean) suppress warning messages if True
Returns:
List of Dictionaries. Raw or processed structured data.
"""
jc.utils.compatibility(__name__, info.compatible, quiet)
jc.utils.input_type_check(data)
raw_output: List[Dict] = []
warned = False
if jc.utils.has_data(data):
addresses = []
v6addresses = []
mail = []
text = []
rrdata = {}
soaparse = False
for line in filter(None, data.splitlines()):
line = line.strip()
# default
if ' has address ' in line:
linedata = line.split(' ', maxsplit=3)
hostname = linedata[0]
address = linedata[3]
addresses.append(address)
rrdata.update({'hostname': hostname})
rrdata.update({'address': addresses})
continue
if ' has IPv6 address ' in line:
linedata = line.split(' ', maxsplit=4)
hostname = linedata[0]
v6address = linedata[4]
v6addresses.append(v6address)
rrdata.update({'hostname': hostname})
rrdata.update({'v6-address': v6addresses})
continue
if ' mail is handled by ' in line:
linedata = line.split(' ', maxsplit=6)
hostname = linedata[0]
mx = linedata[6]
mail.append(mx)
rrdata.update({'hostname': hostname})
rrdata.update({'mail': mail})
continue
# TXT parsing
if ' descriptive text ' in line:
linedata = line.split('descriptive text "', maxsplit=1)
hostname = linedata[0]
txt = linedata[1].strip('"')
text.append(txt)
rrdata.update({'hostname': hostname})
rrdata.update({'text': text})
continue
# -C / SOA parsing
if line.startswith('Nameserver '):
soaparse = True
rrdata = {}
linedata = line.split(' ', maxsplit=1)
nameserverip = linedata[1].rstrip(':')
rrdata.update({'nameserver': nameserverip})
continue
if ' has SOA record ' in line:
linedata = line.split(' ', maxsplit=10)
zone = linedata[0]
mname = linedata[4]
rname = linedata[5]
serial = linedata[6]
refresh = linedata[7]
retry = linedata[8]
expire = linedata[9]
minimum = linedata[10]
try:
rrdata.update(
{
'zone': zone,
'mname': mname,
'rname': rname,
'serial': serial,
'refresh': refresh,
'retry': retry,
'expire': expire,
'minimum': minimum
},
)
raw_output.append(rrdata)
except IndexError:
if not warned:
jc.utils.warning_message(['Unknown format detected.'])
warned = True
if not soaparse:
raw_output.append(rrdata)
return raw_output if raw else _process(raw_output)

View File

@ -0,0 +1 @@
[{"hostname":"google.com","address":["142.250.179.206"],"v6-address":["2a00:1450:400e:811::200e"],"mail":["smtp.google.com."]}]

View File

@ -0,0 +1,3 @@
google.com has address 142.250.179.206
google.com has IPv6 address 2a00:1450:400e:811::200e
google.com mail is handled by 10 smtp.google.com.

View File

@ -0,0 +1 @@
[{"nameserver":"192.36.125.2","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"193.10.252.19","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"2001:6b0:1::250","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"2001:948:4:2::19","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"2001:6b0:5a:4020::384","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"130.237.72.250","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"2001:6b0:7::2","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300},{"nameserver":"89.47.185.240","zone":"sunet.se","mname":"hidden-master.sunet.se.","rname":"hostmaster.sunet.se.","serial":2023091102,"refresh":28800,"retry":7200,"expire":604800,"minimum":300}]

16
tests/fixtures/generic/host-sunet.out vendored Normal file
View File

@ -0,0 +1,16 @@
Nameserver 192.36.125.2:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 193.10.252.19:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 2001:6b0:1::250:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 2001:948:4:2::19:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 2001:6b0:5a:4020::384:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 130.237.72.250:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 2001:6b0:7::2:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300
Nameserver 89.47.185.240:
sunet.se has SOA record hidden-master.sunet.se. hostmaster.sunet.se. 2023091102 28800 7200 604800 300

58
tests/test_host.py Normal file
View File

@ -0,0 +1,58 @@
import os
import unittest
import json
from typing import Dict
from jc.parsers.host import parse
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
class MyTests(unittest.TestCase):
f_in: Dict = {}
f_json: Dict = {}
@classmethod
def setUpClass(cls):
fixtures = {
'google': (
'fixtures/generic/host-google.out',
'fixtures/generic/host-google.json'),
'sunet': (
'fixtures/generic/host-sunet.out',
'fixtures/generic/host-sunet.json')
}
for file, filepaths in fixtures.items():
with open(os.path.join(THIS_DIR, filepaths[0]), 'r', encoding='utf-8') as a, \
open(os.path.join(THIS_DIR, filepaths[1]), 'r', encoding='utf-8') as b:
cls.f_in[file] = a.read()
cls.f_json[file] = json.loads(b.read())
# host cannot run without input (will only display help)
# def test_host_nodata(self):
# """
# Test 'host' with no data
# """
# self.assertEqual(parse('', quiet=True), {})
def test_host_google(self):
"""
Test 'host google'
"""
self.assertEqual(
parse(self.f_in['google'], quiet=True),
self.f_json['google']
)
def test_host_sunet(self):
"""
Test 'host sunet'
"""
self.assertEqual(
parse(self.f_in['sunet'], quiet=True),
self.f_json['sunet']
)
if __name__ == '__main__':
unittest.main()