From c26a46e9a440be2094a315a8f7e68e81943055a4 Mon Sep 17 00:00:00 2001 From: Micke Nordin Date: Mon, 30 Sep 2024 16:34:34 +0200 Subject: [PATCH] Early support for auditlog --- knotctl/__init__.py | 151 ++++++++++++++++++++++++++++++++------------ pyproject.toml | 2 +- 2 files changed, 110 insertions(+), 43 deletions(-) diff --git a/knotctl/__init__.py b/knotctl/__init__.py index 2ff7347..56ccc18 100755 --- a/knotctl/__init__.py +++ b/knotctl/__init__.py @@ -7,7 +7,7 @@ import os import sys import urllib.parse 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 urllib.parse import urlparse @@ -16,6 +16,7 @@ import requests import yaml from requests.models import HTTPBasicAuth from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError + try: from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError except ImportError: @@ -45,8 +46,7 @@ def nested_out(input, tabs="") -> str: string += "{}\n".format(input) elif isinstance(input, dict): for key, value in input.items(): - string += "{}{}: {}".format(tabs, key, - nested_out(value, tabs + " ")) + string += "{}{}: {}".format(tabs, key, nested_out(value, tabs + " ")) elif isinstance(input, list): for entry in input: string += "{}\n{}".format(tabs, nested_out(entry, tabs + " ")) @@ -70,15 +70,57 @@ def run_add(url: str, jsonout: bool, headers: dict): out = response.json() if isinstance(out, list): for record in out: - if (record["data"] == parsed["data"] - and record["name"] == parsed["name"] - and record["rtype"] == parsed["rtype"]): + if ( + record["data"] == parsed["data"] + and record["name"] == parsed["name"] + and record["rtype"] == parsed["rtype"] + ): output(record, jsonout) break else: 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]): if not shell or shell in ["bash", "zsh"]: os.system("register-python-argcomplete knotctl") @@ -98,7 +140,7 @@ def run_config( config = {"baseurl": baseurl, "username": username, "password": password} needed = [] if context: - symlink = f'{config_filename}-{context}' + symlink = f"{config_filename}-{context}" found = os.path.isfile(symlink) if os.path.islink(config_filename): os.remove(config_filename) @@ -118,7 +160,8 @@ def run_config( error( "Can not configure without {}".format(need), "No {}".format(need), - )) + ) + ) sys.exit(1) config[need] = input("Enter {}: ".format(need)) @@ -142,10 +185,7 @@ def run_delete(url: str, jsonout: bool, headers: dict): output(reply, jsonout) -def run_list(url: str, - jsonout: bool, - headers: dict, - ret=False) -> Union[None, str]: +def run_list(url: str, jsonout: bool, headers: dict, ret=False) -> Union[None, str]: response = requests.get(url, headers=headers) string = response.json() if ret: @@ -176,7 +216,7 @@ def setup_url( url += "/{}".format(zone) if name and zone: if name.endswith(zone.rstrip(".")): - name += '.' + name += "." url += "/records/{}".format(name) if zone and name and rtype: url += "/{}".format(rtype) @@ -197,14 +237,16 @@ def setup_url( error( "ttl only makes sense with rtype, name and zone", "Missing parameter", - )) + ) + ) sys.exit(1) if rtype and (not name or not zone): output( error( "rtype only makes sense with name and zone", "Missing parameter", - )) + ) + ) sys.exit(1) if name and not zone: 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 """ # 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) subparsers = parser.add_subparsers(dest="command") @@ -305,6 +351,9 @@ def main() -> int: addcmd.add_argument("-t", "--ttl") 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." completecmd = subparsers.add_parser("completion", description=complete_description) completecmd.add_argument("-s", "--shell") @@ -330,24 +379,34 @@ def main() -> int: listcmd.add_argument("-r", "--rtype") 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 += "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 = ( + "Update a record in the zone. The record must exist in the zone.\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_epilog = """Available arguments are: data: New record data. name: New record domain name. rtype: New record type. 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( "-a", "--argument", nargs="*", action="append", metavar="KEY=VALUE", - help= - "Specify key - value pairs to be updated: name=dns1.example.com. or data=127.0.0.1 for example. --argument can be repeated", + help="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, ) updatecmd.add_argument("-d", "--data", required=True) @@ -370,8 +429,9 @@ def main() -> int: mkdir(config_basepath) if args.command == "config": - run_config(config_filename, args.context, args.baseurl, args.username, - args.password) + run_config( + config_filename, args.context, args.baseurl, args.username, args.password + ) return 0 if not isfile(config_filename): @@ -392,8 +452,7 @@ def main() -> int: output(response.json()) return 1 except requests.exceptions.JSONDecodeError: - output( - error("Could not decode api response as JSON", "Could not decode")) + output(error("Could not decode api response as JSON", "Could not decode")) return 1 headers = {"Authorization": "Bearer {}".format(token)} @@ -411,20 +470,22 @@ def main() -> int: soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone) soa_json = run_list(soa_url, True, headers, ret=True) ttl = soa_json[0]["ttl"] - - try: - url = setup_url( - baseurl, - args.argument, - args.data, - args.name, - args.rtype, - ttl, - args.zone, - ) - except AttributeError: - parser.print_help(sys.stderr) - return 1 + if args.command == "audit": + url = baseurl + "/user/auditlog" + else: + try: + url = setup_url( + baseurl, + args.argument, + args.data, + args.name, + args.rtype, + ttl, + args.zone, + ) + except AttributeError: + parser.print_help(sys.stderr) + return 1 try: if args.command == "add": @@ -435,9 +496,15 @@ def main() -> int: run_list(url, args.json, headers) elif args.command == "update": 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): - output( - error("Could not decode api response as JSON", "Could not decode")) + output(error("Could not decode api response as JSON", "Could not decode")) return 0 diff --git a/pyproject.toml b/pyproject.toml index 6f53e4d..7f72fa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers=[ "Operating System :: OS Independent", ] requires-python= ">=3.9" -version = "0.0.7" +version = "0.0.8" dependencies = [ "argcomplete==2.0.0",