diff options
Diffstat (limited to 'tools/sops-inventory')
-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)) |