Initial commit
This commit is contained in:
8
.pre-commit-hooks.yaml
Normal file
8
.pre-commit-hooks.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
- id: sops-encryption
|
||||
language: python
|
||||
entry: python3 -m pre_commit_hook_ensure_sops
|
||||
name: Ensure secrets are encrypted with sops
|
||||
# Be aggressive - ensure anything with the word secret in the filename
|
||||
# or file path is encryped. Users of individual repos can exclude things
|
||||
# with `exclude` if necessary.
|
||||
files: .*secret.*
|
||||
25
README.md
25
README.md
@@ -1,2 +1,25 @@
|
||||
# pre-commit-hook-ensure-sops
|
||||
pre-commit hook to ensure that files that should be encrypted with sops are
|
||||
|
||||
A [pre-commit](https://pre-commit.com/) hook to ensure that users don't
|
||||
accidentally check-in unencrypted files into a repository that uses
|
||||
[sops](https://github.com/mozilla/sops) to safely store encrypted secrets.
|
||||
|
||||
By default, any file with the word `secret` in its path is required to
|
||||
be encrypted with `sops`. This means any files under a directory
|
||||
named `secret` are also required to be encrypted. If you want to exempt
|
||||
specific files or directories from this requirement in your repository,
|
||||
use the `exclude` option in your `.pre-commit-config.yaml`. When pushing
|
||||
secrets to a repo, better safe than sorry :)
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `.pre-commit-config.yaml`:
|
||||
|
||||
```yaml
|
||||
- repo: https://github.com/yuvipanda/pre-commit-hook-ensure-sops
|
||||
rev: v1.0
|
||||
hooks:
|
||||
- id: sops-encryption
|
||||
# Uncomment to exclude all markdown files from encryption
|
||||
# exclude: *.\.md
|
||||
```
|
||||
|
||||
0
pre_commit_hook_ensure_sops/__init__.py
Normal file
0
pre_commit_hook_ensure_sops/__init__.py
Normal file
90
pre_commit_hook_ensure_sops/__main__.py
Normal file
90
pre_commit_hook_ensure_sops/__main__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate if given list of files are encrypted with sops.
|
||||
"""
|
||||
from argparse import ArgumentParser
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.parser import ParserError
|
||||
import sys
|
||||
|
||||
yaml = YAML(typ='safe')
|
||||
|
||||
|
||||
def validate_enc(item):
|
||||
"""
|
||||
Validate given item is encrypted.
|
||||
|
||||
All leaf values in a sops encrypted file must be strings that
|
||||
start with ENC[. We iterate through lists and dicts, checking
|
||||
only for leaf strings. Presence of any other data type (like
|
||||
bool, number, etc) also makes the file invalid.
|
||||
"""
|
||||
|
||||
if isinstance(item, str):
|
||||
return item.startswith('ENC[')
|
||||
elif isinstance(item, list):
|
||||
return all(validate_enc(i) for i in item)
|
||||
elif isinstance(item, dict):
|
||||
return all(validate_enc(i) for i in item.values())
|
||||
else:
|
||||
return False
|
||||
|
||||
def check_file(filename):
|
||||
"""
|
||||
Check if a file has been encrypted properly with sops.
|
||||
|
||||
Returns a boolean indicating wether given file is valid or not, as well as
|
||||
a string with a human readable success / failure message.
|
||||
"""
|
||||
# sops doesn't have a --verify (https://github.com/mozilla/sops/issues/437)
|
||||
# so we implement some heuristics, primarily to guard against unencrypted
|
||||
# files being checked in.
|
||||
with open(filename) as f:
|
||||
try:
|
||||
# Use the YAML parser to load files. All JSON is valid YAML, so this
|
||||
# properly deals with JSON files too
|
||||
doc = yaml.load(f)
|
||||
except ParserError:
|
||||
# All sops encrypted files are valid JSON or YAML
|
||||
return False, f"{filename}: Not valid JSON or YAML, is not properly encrypted"
|
||||
|
||||
if 'sops' not in doc:
|
||||
# sops puts a `sops` key in the encrypted output. If it is not
|
||||
# present, very likely the file is not encrypted.
|
||||
return False, f"{filename}: sops metadata key not found in file, is not properly encrypted"
|
||||
|
||||
invalid_keys = []
|
||||
for k in doc:
|
||||
if k != 'sops':
|
||||
# Values under the `sops` key are not encrypted.
|
||||
if not validate_enc(doc[k]):
|
||||
# Collect all invalid keys so we can provide useful error message
|
||||
invalid_keys.append(k)
|
||||
|
||||
if invalid_keys:
|
||||
return False, f"{filename}: Unencrypted values found nested under keys: {','.join(invalid_keys)}"
|
||||
|
||||
return True, f"{filename}: Valid encryption"
|
||||
|
||||
def main():
|
||||
argparser = ArgumentParser()
|
||||
argparser.add_argument('filenames', nargs='+')
|
||||
|
||||
args = argparser.parse_args()
|
||||
|
||||
failed_messages = []
|
||||
|
||||
for f in args.filenames:
|
||||
is_valid, message = check_file(f)
|
||||
|
||||
if not is_valid:
|
||||
failed_messages.append(message)
|
||||
|
||||
if failed_messages:
|
||||
print('\n'.join(failed_messages))
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
19
setup.py
Normal file
19
setup.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import setuptools
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="pre-commit-hook-ensure-sops",
|
||||
version="0.1",
|
||||
author="Yuvi Panda",
|
||||
author_email="yuvipanda@gmail.com",
|
||||
description="pre-commit hook to ensure that files that should be encrypted with sops are in fact encrypted",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/yuvipanda/pre-commit-hook-ensure-sops",
|
||||
packages=setuptools.find_packages(),
|
||||
install_requires=[
|
||||
"ruamel.yaml",
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user