diff options
Diffstat (limited to 'tools/sops-inventory/sops_inventory/__main__.py')
-rw-r--r-- | tools/sops-inventory/sops_inventory/__main__.py | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/tools/sops-inventory/sops_inventory/__main__.py b/tools/sops-inventory/sops_inventory/__main__.py new file mode 100644 index 00000000..68f72b60 --- /dev/null +++ b/tools/sops-inventory/sops_inventory/__main__.py | |||
@@ -0,0 +1,85 @@ | |||
1 | import os,sys | ||
2 | |||
3 | from pathlib import Path | ||
4 | from collections import deque, defaultdict | ||
5 | |||
6 | import argparse | ||
7 | |||
8 | from yaml import load, YAMLError | ||
9 | try: | ||
10 | from yaml import CLoader as Loader | ||
11 | except ImportError: | ||
12 | from yaml import Loader | ||
13 | |||
14 | |||
15 | SOPS_TYPES = frozenset({'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'age', 'pgp'}) | ||
16 | |||
17 | |||
18 | class BooleanAction(argparse.Action): | ||
19 | def __init__(self, option_strings, dest, nargs=None, **kwargs): | ||
20 | super(BooleanAction, self).__init__(option_strings, dest, nargs=0, **kwargs) | ||
21 | |||
22 | def __call__(self, parser, namespace, values, option_string=None): | ||
23 | setattr(namespace, self.dest, False if option_string.startswith('--no') else True) | ||
24 | |||
25 | |||
26 | 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) | ||
32 | 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') | ||
34 | args = parser.parse_args() | ||
35 | |||
36 | inventory = defaultdict(set) | ||
37 | |||
38 | queue = deque([args.path]) | ||
39 | while queue: | ||
40 | baseDir = queue.popleft() | ||
41 | for child in baseDir.iterdir(): | ||
42 | if child.is_dir(): | ||
43 | queue.append(child) | ||
44 | else: | ||
45 | try: | ||
46 | with child.open(mode='r') as fh: | ||
47 | yaml = load(fh, Loader=Loader) | ||
48 | if not yaml: | ||
49 | raise ValueError('Could not parse YAML') | ||
50 | if not isinstance(yaml, dict) or not 'sops' in yaml: | ||
51 | raise ValueError('Did not find "sops" key') | ||
52 | sops = yaml['sops'] | ||
53 | |||
54 | key_info = set() | ||
55 | for k in SOPS_TYPES: | ||
56 | if k in sops: | ||
57 | v = sops[k] | ||
58 | if not v: | ||
59 | continue | ||
60 | |||
61 | match k: | ||
62 | case 'pgp': | ||
63 | for r in v: | ||
64 | key_info.add(r['fp']) | ||
65 | case 'age': | ||
66 | for r in v: | ||
67 | key_info.add(r['recipient']) | ||
68 | case _: | ||
69 | raise NotImplementedError | ||
70 | inventory[frozenset(key_info)].add(child.relative_to(args.path)) | ||
71 | except (YAMLError, ValueError) as e: | ||
72 | pass | ||
73 | |||
74 | if not args.list_files: | ||
75 | for keys, files in inventory.items(): | ||
76 | print(','.join(keys) + ':') | ||
77 | for file in files: | ||
78 | print(' - ' + str(file)) | ||
79 | else: | ||
80 | for _, files in inventory.items(): | ||
81 | for file in files: | ||
82 | print(file) | ||
83 | |||
84 | if __name__ == '__main__': | ||
85 | os.exit(main()) | ||