diff options
| -rw-r--r-- | tools/sops-inventory/default.nix | 5 | ||||
| -rw-r--r-- | tools/sops-inventory/sops_inventory/__main__.py | 107 |
2 files changed, 63 insertions, 49 deletions
diff --git a/tools/sops-inventory/default.nix b/tools/sops-inventory/default.nix index 94c455e5..2fc485a7 100644 --- a/tools/sops-inventory/default.nix +++ b/tools/sops-inventory/default.nix | |||
| @@ -11,9 +11,4 @@ in mach-nix.lib.${system}.buildPythonPackage { | |||
| 11 | requirements = '' | 11 | requirements = '' |
| 12 | pyyaml | 12 | pyyaml |
| 13 | ''; | 13 | ''; |
| 14 | |||
| 15 | postInstall = '' | ||
| 16 | wrapProgram $out/bin/sops-inventory \ | ||
| 17 | --set-default SOPS_INVENTORY_BASE ${self} | ||
| 18 | ''; | ||
| 19 | } | 14 | } |
diff --git a/tools/sops-inventory/sops_inventory/__main__.py b/tools/sops-inventory/sops_inventory/__main__.py index 68f72b60..47100c17 100644 --- a/tools/sops-inventory/sops_inventory/__main__.py +++ b/tools/sops-inventory/sops_inventory/__main__.py | |||
| @@ -5,16 +5,39 @@ from collections import deque, defaultdict | |||
| 5 | 5 | ||
| 6 | import argparse | 6 | import argparse |
| 7 | 7 | ||
| 8 | import subprocess | ||
| 9 | |||
| 10 | from operator import attrgetter, itemgetter | ||
| 11 | |||
| 8 | from yaml import load, YAMLError | 12 | from yaml import load, YAMLError |
| 9 | try: | 13 | try: |
| 10 | from yaml import CLoader as Loader | 14 | from yaml import CLoader as Loader |
| 11 | except ImportError: | 15 | except ImportError: |
| 12 | from yaml import Loader | 16 | from yaml import Loader |
| 13 | 17 | ||
| 14 | |||
| 15 | SOPS_TYPES = frozenset({'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'age', 'pgp'}) | 18 | SOPS_TYPES = frozenset({'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'age', 'pgp'}) |
| 16 | 19 | ||
| 17 | 20 | ||
| 21 | def readnull(fh): | ||
| 22 | buffer = b'' | ||
| 23 | |||
| 24 | while True: | ||
| 25 | chunk = fh.read(4096) | ||
| 26 | buffer += chunk | ||
| 27 | if not buffer: | ||
| 28 | break | ||
| 29 | |||
| 30 | while True: | ||
| 31 | lines = buffer.split(b'\0', maxsplit=1) | ||
| 32 | match lines: | ||
| 33 | case [l, r]: | ||
| 34 | buffer = r | ||
| 35 | yield l | ||
| 36 | case _: | ||
| 37 | if not chunk: | ||
| 38 | yield buffer | ||
| 39 | break | ||
| 40 | |||
| 18 | class BooleanAction(argparse.Action): | 41 | class BooleanAction(argparse.Action): |
| 19 | def __init__(self, option_strings, dest, nargs=None, **kwargs): | 42 | def __init__(self, option_strings, dest, nargs=None, **kwargs): |
| 20 | super(BooleanAction, self).__init__(option_strings, dest, nargs=0, **kwargs) | 43 | super(BooleanAction, self).__init__(option_strings, dest, nargs=0, **kwargs) |
| @@ -24,55 +47,51 @@ class BooleanAction(argparse.Action): | |||
| 24 | 47 | ||
| 25 | 48 | ||
| 26 | def main(): | 49 | def main(): |
| 27 | default_base = os.getenv('SOPS_INVENTORY_BASE', default=[]) | ||
| 28 | if default_base: | ||
| 29 | default_base = Path(default_base) | ||
| 30 | |||
| 31 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) | 50 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 32 | parser.add_argument('--list-files', '--no-list-files', action=BooleanAction, default=False, help='Only list sops files') | 51 | parser.add_argument('--list-files', '--no-list-files', action=BooleanAction, default=False, help='Only list sops files') |
| 33 | parser.add_argument('path', metavar='PATH', nargs='?' if default_base else None, type=Path, default=default_base, help='Base directory to take inventory of') | 52 | parser.add_argument('path', metavar='PATH', nargs='?', type=Path, default=Path('.'), help='Base directory to take inventory of') |
| 34 | args = parser.parse_args() | 53 | args = parser.parse_args() |
| 35 | 54 | ||
| 36 | inventory = defaultdict(set) | 55 | inventory = defaultdict(list) |
| 37 | 56 | ||
| 38 | queue = deque([args.path]) | 57 | with subprocess.Popen(['git', '-C', args.path, 'ls-files', '-z'], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE) as proc: |
| 39 | while queue: | 58 | files = sorted(map(lambda child: args.path / child.decode('utf-8').strip(), readnull(proc.stdout)), key=attrgetter('parts')) |
| 40 | baseDir = queue.popleft() | 59 | for child in files: |
| 41 | for child in baseDir.iterdir(): | 60 | try: |
| 42 | if child.is_dir(): | 61 | with child.open(mode='r') as fh: |
| 43 | queue.append(child) | 62 | yaml = load(fh, Loader=Loader) |
| 44 | else: | 63 | if not yaml: |
| 45 | try: | 64 | raise ValueError('Could not parse YAML') |
| 46 | with child.open(mode='r') as fh: | 65 | if not isinstance(yaml, dict) or not 'sops' in yaml: |
| 47 | yaml = load(fh, Loader=Loader) | 66 | raise ValueError('Did not find "sops" key') |
| 48 | if not yaml: | 67 | sops = yaml['sops'] |
| 49 | raise ValueError('Could not parse YAML') | 68 | |
| 50 | if not isinstance(yaml, dict) or not 'sops' in yaml: | 69 | key_info = set() |
| 51 | raise ValueError('Did not find "sops" key') | 70 | for k in SOPS_TYPES: |
| 52 | sops = yaml['sops'] | 71 | if k in sops: |
| 53 | 72 | v = sops[k] | |
| 54 | key_info = set() | 73 | if not v: |
| 55 | for k in SOPS_TYPES: | 74 | continue |
| 56 | if k in sops: | 75 | |
| 57 | v = sops[k] | 76 | match k: |
| 58 | if not v: | 77 | case 'pgp': |
| 59 | continue | 78 | for r in v: |
| 60 | 79 | key_info.add(r['fp']) | |
| 61 | match k: | 80 | case 'age': |
| 62 | case 'pgp': | 81 | for r in v: |
| 63 | for r in v: | 82 | key_info.add(r['recipient']) |
| 64 | key_info.add(r['fp']) | 83 | case _: |
| 65 | case 'age': | 84 | raise NotImplementedError |
| 66 | for r in v: | 85 | inventory[frozenset(key_info)].append(child.relative_to(args.path)) |
| 67 | key_info.add(r['recipient']) | 86 | except (YAMLError, ValueError) as e: |
| 68 | case _: | 87 | pass |
| 69 | raise NotImplementedError | 88 | |
| 70 | inventory[frozenset(key_info)].add(child.relative_to(args.path)) | 89 | proc.wait(timeout=1) |
| 71 | except (YAMLError, ValueError) as e: | 90 | if proc.returncode != 0: |
| 72 | pass | 91 | raise RuntimeError(f'git ls-files returned with {proc.returncode}') |
| 73 | 92 | ||
| 74 | if not args.list_files: | 93 | if not args.list_files: |
| 75 | for keys, files in inventory.items(): | 94 | for keys, files in sorted(inventory.items(), key=itemgetter(0)): |
| 76 | print(','.join(keys) + ':') | 95 | print(','.join(keys) + ':') |
| 77 | for file in files: | 96 | for file in files: |
| 78 | print(' - ' + str(file)) | 97 | print(' - ' + str(file)) |
