Early support for auditlog

main
Micke Nordin 2 months ago
parent 3787ad12df
commit c26a46e9a4

@ -7,7 +7,7 @@ import os
import sys import sys
import urllib.parse import urllib.parse
from os import environ, mkdir from os import environ, mkdir
from os.path import isdir, isfile, join from os.path import isdir, isfile, join, split
from typing import Union from typing import Union
from urllib.parse import urlparse from urllib.parse import urlparse
@ -16,6 +16,7 @@ import requests
import yaml import yaml
from requests.models import HTTPBasicAuth from requests.models import HTTPBasicAuth
from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError
try: try:
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
except ImportError: except ImportError:
@ -45,8 +46,7 @@ def nested_out(input, tabs="") -> str:
string += "{}\n".format(input) string += "{}\n".format(input)
elif isinstance(input, dict): elif isinstance(input, dict):
for key, value in input.items(): for key, value in input.items():
string += "{}{}: {}".format(tabs, key, string += "{}{}: {}".format(tabs, key, nested_out(value, tabs + " "))
nested_out(value, tabs + " "))
elif isinstance(input, list): elif isinstance(input, list):
for entry in input: for entry in input:
string += "{}\n{}".format(tabs, nested_out(entry, tabs + " ")) string += "{}\n{}".format(tabs, nested_out(entry, tabs + " "))
@ -70,15 +70,57 @@ def run_add(url: str, jsonout: bool, headers: dict):
out = response.json() out = response.json()
if isinstance(out, list): if isinstance(out, list):
for record in out: for record in out:
if (record["data"] == parsed["data"] if (
record["data"] == parsed["data"]
and record["name"] == parsed["name"] and record["name"] == parsed["name"]
and record["rtype"] == parsed["rtype"]): and record["rtype"] == parsed["rtype"]
):
output(record, jsonout) output(record, jsonout)
break break
else: else:
output(out, jsonout) output(out, jsonout)
def run_audit(url: str, jsonout: bool, headers: dict):
response = requests.get(url, headers=headers)
string = response.content.decode("utf-8")
if jsonout:
out = []
lines = string.splitlines()
index = 0
text = ""
timestamp = ""
while index < len(lines):
line = lines[index]
index += 1
cur_has_timestamp = line.startswith("[")
next_has_timestamp = index < len(lines) and lines[index].startswith(
"["
)
# Simple case, just one line with timestamp
if cur_has_timestamp and next_has_timestamp:
text = ":". join(line.split(':')[1:])
timestamp = line.split(']')[0].split('[')[1]
out.append({'timestamp': timestamp, 'text': text})
text = ""
timestamp = ""
elif cur_has_timestamp:
timestamp = line.split(']')[0].split('[')[1]
text = ":". join(line.split(':')[1:])
elif next_has_timestamp:
text += f'\n{line}'
out.append({'timestamp': timestamp, 'text': text})
text = ""
timestamp = ""
else:
text += f'\n{line}'
else:
out = string
output(out, jsonout)
def run_complete(shell: Union[None, str]): def run_complete(shell: Union[None, str]):
if not shell or shell in ["bash", "zsh"]: if not shell or shell in ["bash", "zsh"]:
os.system("register-python-argcomplete knotctl") os.system("register-python-argcomplete knotctl")
@ -98,7 +140,7 @@ def run_config(
config = {"baseurl": baseurl, "username": username, "password": password} config = {"baseurl": baseurl, "username": username, "password": password}
needed = [] needed = []
if context: if context:
symlink = f'{config_filename}-{context}' symlink = f"{config_filename}-{context}"
found = os.path.isfile(symlink) found = os.path.isfile(symlink)
if os.path.islink(config_filename): if os.path.islink(config_filename):
os.remove(config_filename) os.remove(config_filename)
@ -118,7 +160,8 @@ def run_config(
error( error(
"Can not configure without {}".format(need), "Can not configure without {}".format(need),
"No {}".format(need), "No {}".format(need),
)) )
)
sys.exit(1) sys.exit(1)
config[need] = input("Enter {}: ".format(need)) config[need] = input("Enter {}: ".format(need))
@ -142,10 +185,7 @@ def run_delete(url: str, jsonout: bool, headers: dict):
output(reply, jsonout) output(reply, jsonout)
def run_list(url: str, def run_list(url: str, jsonout: bool, headers: dict, ret=False) -> Union[None, str]:
jsonout: bool,
headers: dict,
ret=False) -> Union[None, str]:
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
string = response.json() string = response.json()
if ret: if ret:
@ -176,7 +216,7 @@ def setup_url(
url += "/{}".format(zone) url += "/{}".format(zone)
if name and zone: if name and zone:
if name.endswith(zone.rstrip(".")): if name.endswith(zone.rstrip(".")):
name += '.' name += "."
url += "/records/{}".format(name) url += "/records/{}".format(name)
if zone and name and rtype: if zone and name and rtype:
url += "/{}".format(rtype) url += "/{}".format(rtype)
@ -197,14 +237,16 @@ def setup_url(
error( error(
"ttl only makes sense with rtype, name and zone", "ttl only makes sense with rtype, name and zone",
"Missing parameter", "Missing parameter",
)) )
)
sys.exit(1) sys.exit(1)
if rtype and (not name or not zone): if rtype and (not name or not zone):
output( output(
error( error(
"rtype only makes sense with name and zone", "rtype only makes sense with name and zone",
"Missing parameter", "Missing parameter",
)) )
)
sys.exit(1) sys.exit(1)
if name and not zone: if name and not zone:
output(error("name only makes sense with a zone", "Missing parameter")) output(error("name only makes sense with a zone", "Missing parameter"))
@ -293,7 +335,11 @@ def main() -> int:
* https://en.wikipedia.org/wiki/Zone_file * https://en.wikipedia.org/wiki/Zone_file
""" """
# Grab user input # Grab user input
parser = argparse.ArgumentParser(description=description, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter) parser = argparse.ArgumentParser(
description=description,
epilog=epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--json", action=argparse.BooleanOptionalAction) parser.add_argument("--json", action=argparse.BooleanOptionalAction)
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
@ -305,6 +351,9 @@ def main() -> int:
addcmd.add_argument("-t", "--ttl") addcmd.add_argument("-t", "--ttl")
addcmd.add_argument("-z", "--zone", required=True) addcmd.add_argument("-z", "--zone", required=True)
audit_description = "Audit the log file for errors."
subparsers.add_parser("audit", description=audit_description)
complete_description = "Generate shell completion script." complete_description = "Generate shell completion script."
completecmd = subparsers.add_parser("completion", description=complete_description) completecmd = subparsers.add_parser("completion", description=complete_description)
completecmd.add_argument("-s", "--shell") completecmd.add_argument("-s", "--shell")
@ -330,24 +379,34 @@ def main() -> int:
listcmd.add_argument("-r", "--rtype") listcmd.add_argument("-r", "--rtype")
listcmd.add_argument("-z", "--zone", required=True) listcmd.add_argument("-z", "--zone", required=True)
update_description = "Update a record in the zone. The record must exist in the zone.\n" update_description = (
update_description += "In this case --data, --name, --rtype and --ttl switches are used\n" "Update a record in the zone. The record must exist in the zone.\n"
update_description += "for searching for the appropriate record, while the --argument\n" )
update_description += (
"In this case --data, --name, --rtype and --ttl switches are used\n"
)
update_description += (
"for searching for the appropriate record, while the --argument\n"
)
update_description += "switches are used for updating the record." update_description += "switches are used for updating the record."
update_epilog = """Available arguments are: update_epilog = """Available arguments are:
data: New record data. data: New record data.
name: New record domain name. name: New record domain name.
rtype: New record type. rtype: New record type.
ttl: New record time to live (TTL).""" ttl: New record time to live (TTL)."""
updatecmd = subparsers.add_parser("update", description=update_description, epilog=update_epilog, formatter_class=argparse.RawDescriptionHelpFormatter ) updatecmd = subparsers.add_parser(
"update",
description=update_description,
epilog=update_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
updatecmd.add_argument( updatecmd.add_argument(
"-a", "-a",
"--argument", "--argument",
nargs="*", nargs="*",
action="append", action="append",
metavar="KEY=VALUE", metavar="KEY=VALUE",
help= help="Specify key - value pairs to be updated: name=dns1.example.com. or data=127.0.0.1 for example. --argument can be repeated",
"Specify key - value pairs to be updated: name=dns1.example.com. or data=127.0.0.1 for example. --argument can be repeated",
required=True, required=True,
) )
updatecmd.add_argument("-d", "--data", required=True) updatecmd.add_argument("-d", "--data", required=True)
@ -370,8 +429,9 @@ def main() -> int:
mkdir(config_basepath) mkdir(config_basepath)
if args.command == "config": if args.command == "config":
run_config(config_filename, args.context, args.baseurl, args.username, run_config(
args.password) config_filename, args.context, args.baseurl, args.username, args.password
)
return 0 return 0
if not isfile(config_filename): if not isfile(config_filename):
@ -392,8 +452,7 @@ def main() -> int:
output(response.json()) output(response.json())
return 1 return 1
except requests.exceptions.JSONDecodeError: except requests.exceptions.JSONDecodeError:
output( output(error("Could not decode api response as JSON", "Could not decode"))
error("Could not decode api response as JSON", "Could not decode"))
return 1 return 1
headers = {"Authorization": "Bearer {}".format(token)} headers = {"Authorization": "Bearer {}".format(token)}
@ -411,7 +470,9 @@ def main() -> int:
soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone) soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone)
soa_json = run_list(soa_url, True, headers, ret=True) soa_json = run_list(soa_url, True, headers, ret=True)
ttl = soa_json[0]["ttl"] ttl = soa_json[0]["ttl"]
if args.command == "audit":
url = baseurl + "/user/auditlog"
else:
try: try:
url = setup_url( url = setup_url(
baseurl, baseurl,
@ -435,9 +496,15 @@ def main() -> int:
run_list(url, args.json, headers) run_list(url, args.json, headers)
elif args.command == "update": elif args.command == "update":
run_update(url, args.json, headers) run_update(url, args.json, headers)
elif args.command == "audit":
run_audit(url, args.json, headers)
else:
parser.print_help(sys.stderr)
return 1
except requests.exceptions.RequestException as e:
output(error(e, "Could not connect to server"))
except (RequestsJSONDecodeError, SimplejsonJSONDecodeError): except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
output( output(error("Could not decode api response as JSON", "Could not decode"))
error("Could not decode api response as JSON", "Could not decode"))
return 0 return 0

@ -16,7 +16,7 @@ classifiers=[
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
requires-python= ">=3.9" requires-python= ">=3.9"
version = "0.0.7" version = "0.0.8"
dependencies = [ dependencies = [
"argcomplete==2.0.0", "argcomplete==2.0.0",

Loading…
Cancel
Save