diff options
Diffstat (limited to 'tools/sops-inventory')
-rw-r--r-- | tools/sops-inventory/default.nix | 19 | ||||
-rw-r--r-- | tools/sops-inventory/setup.py | 11 | ||||
-rw-r--r-- | tools/sops-inventory/sops_inventory/__init__.py | 0 | ||||
-rw-r--r-- | tools/sops-inventory/sops_inventory/__main__.py | 85 |
4 files changed, 115 insertions, 0 deletions
diff --git a/tools/sops-inventory/default.nix b/tools/sops-inventory/default.nix new file mode 100644 index 00000000..94c455e5 --- /dev/null +++ b/tools/sops-inventory/default.nix | |||
@@ -0,0 +1,19 @@ | |||
1 | { system, self, mach-nix, ... }: | ||
2 | let | ||
3 | pkgs = self.legacyPackages.${system}; | ||
4 | in mach-nix.lib.${system}.buildPythonPackage { | ||
5 | pname = "sops-inventory"; | ||
6 | version = "0.0.0"; | ||
7 | |||
8 | src = pkgs.lib.sourceByRegex ./. ["^setup\.py$" "^sops_inventory(/[^/]+.*)?$"]; | ||
9 | |||
10 | ignoreDataOutdated = true; | ||
11 | requirements = '' | ||
12 | pyyaml | ||
13 | ''; | ||
14 | |||
15 | postInstall = '' | ||
16 | wrapProgram $out/bin/sops-inventory \ | ||
17 | --set-default SOPS_INVENTORY_BASE ${self} | ||
18 | ''; | ||
19 | } | ||
diff --git a/tools/sops-inventory/setup.py b/tools/sops-inventory/setup.py new file mode 100644 index 00000000..3ea2a5d1 --- /dev/null +++ b/tools/sops-inventory/setup.py | |||
@@ -0,0 +1,11 @@ | |||
1 | from setuptools import setup | ||
2 | |||
3 | setup( | ||
4 | name='sops-inventory', | ||
5 | packages=['sops_inventory'], | ||
6 | entry_points={ | ||
7 | 'console_scripts': [ | ||
8 | 'sops-inventory=sops_inventory.__main__:main' | ||
9 | ], | ||
10 | }, | ||
11 | ) | ||
diff --git a/tools/sops-inventory/sops_inventory/__init__.py b/tools/sops-inventory/sops_inventory/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tools/sops-inventory/sops_inventory/__init__.py | |||
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()) | ||