From 26422700abd0571dbbf302c97573e9d93456272a Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 15 Aug 2022 00:15:00 +0200 Subject: [PATCH] feat: Adds module attachment w/ example --- doc/examples/example-playbook.yml | 20 +++- doc/examples/example.kdbx | Bin 3157 -> 3605 bytes plugins/modules/attachment.py | 188 ++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/attachment.py diff --git a/doc/examples/example-playbook.yml b/doc/examples/example-playbook.yml index 1af582f..9bbe776 100644 --- a/doc/examples/example-playbook.yml +++ b/doc/examples/example-playbook.yml @@ -10,7 +10,8 @@ slash_login: "{{ lookup('viczem.keepass.keepass', 'slash\\/group/slash\\/title', 'username') }}" slash_url: "{{ lookup('viczem.keepass.keepass', 'slash\\/group/slash\\/title', 'url') }}" pork_custom_property: "{{ lookup('viczem.keepass.keepass', 'example/pork', 'custom_properties', 'pork_custom_property')}}" - + keepass_attachment_1_name: "attachment_1.txt" + keepass_attachment_2_name: "attachment_2.zip" tasks: - debug: @@ -30,3 +31,20 @@ msg: "fetch entry: '/slash\\/group/slash\\/title'; username: '{{ slash_login }}'; url: '{{ slash_url }}'" - debug: "{{ lookup('viczem.keepass.keepass', 'close') }}" + + - name: "Export file: {{ keepass_attachment_1_name }}" + viczem.keepass.attachment: + database: "{{ keepass_dbx }}" + password: "{{ keepass_psw }}" + entrypath: example/attachments + attachment: "{{ keepass_attachment_1_name }}" + dest: "{{ keepass_attachment_1_name }}" + + - name: "Export file: {{ keepass_attachment_2_name }}" + viczem.keepass.attachment: + database: "{{ keepass_dbx }}" + password: "{{ keepass_psw }}" + entrypath: example/attachments + attachment: "{{ keepass_attachment_2_name }}" + dest: "{{ keepass_attachment_2_name }}" + mode: 0600 \ No newline at end of file diff --git a/doc/examples/example.kdbx b/doc/examples/example.kdbx index 2d7001fca394cc358ed565a0615fc166b24fd192..127e279a7db0e59747e4f56d90961cfdf772f448 100644 GIT binary patch delta 3498 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 diff --git a/plugins/modules/attachment.py b/plugins/modules/attachment.py new file mode 100644 index 0000000..e409145 --- /dev/null +++ b/plugins/modules/attachment.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Jimisola Laursen +# Copyright: (c) 2022, LFV + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: attachment +author: + - Jimisola Laursen (@lfvjimisola) + - Jimisola Laursen (@jimisola) + +short_description: Exports KeePass attachments +description: + - This module will export an attachment in a KeePass entry to a file. + +version_added: "0.1.0" + +extends_documentation_fragment: + - files + - action_common_attributes + +requirements: + - pykeepass + +options: + database: + description: Path to KeePass database file. + required: true + type: str + password: + description: Password for KeePass database file. + required: true + type: str + entrypath: + description: Path to KeePass entry containing the attachment that should be exported. + required: true + type: str + attachment: + description: Name of attachment that should be exported. + required: true + type: str + dest: + description: Absolute path where the file should be exported to. + required: true + type: str + +attributes: + check_mode: + support: none + diff_mode: + support: none + platform: + platforms: posix +''' + +EXAMPLES = r''' +# Export a file +- name: Export a file from KeePass + keepass: + database: database.kdbx + password: somepassword + path: "group/subgroup/entry" + attachment: somefile.txt + dest: somefile_exported.txt +''' + +RETURN = r''' # ''' + +from importlib.metadata import EntryPoint +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils._text import to_bytes, to_native + +import os +import tempfile + +LIB_IMP_ERR = None +try: + from pykeepass import PyKeePass + HAS_LIB = True +except Exception as e: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +def check_file_attrs(module, result, diff): + + changed, msg = result['changed'], result['msg'] + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False, diff=diff): + + if changed: + msg += " and " + changed = True + msg += "ownership, perms or SE linux context changed" + + result['changed'] = changed + result['msg'] = msg + + return result + + +def export_attachment(module, result): + try: + # load database + kp = PyKeePass(module.params['database'], password=module.params['password']) + + entrypath = module.params['entrypath'] + dest = module.params['dest'] + attachment = module.params['attachment'] + + # find entry + kp_entry = kp.find_entries(path=entrypath.split('/'), first=True) + + if (kp_entry is None): + module.fail_json(msg="Entry '{0}' not found".format(entrypath)) + + kp_attachment = None + for item in kp_entry.attachments: + if item.filename == attachment: + kp_attachment = item + + if (kp_attachment is None): + module.fail_json(msg="Entry '{0}' does not contain attachment '{1}'".format(entrypath, attachment)) + + b_data = kp_attachment.binary + + tmpfd, tmpfile = tempfile.mkstemp() + f = os.fdopen(tmpfd, 'wb') + f.write(b_data) + f.close() + + module.atomic_move(tmpfile, + to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'), + unsafe_writes=module.params['unsafe_writes']) + + result['changed'] = True + result['msg'] = "attachment '{0}' exported to file '{1}'".format(module.params['attachment'], dest) + + except Exception as e: + result['msg'] = "Module viczem.keepass.attachment failed: {0}".format(e) + module.fail_json(**result) + + attr_diff = None + + result = check_file_attrs(module, result, attr_diff) + + module.exit_json(**result, diff=attr_diff) + + +def main(): + module_args = dict( + database=dict(type='str', required=True), + password=dict(type='str', no_log=True, required=True), + entrypath=dict(type='str', required=True), + attachment=dict(type='str', required=True), + dest=dict(type='path', required=True), + ) + + module = AnsibleModule( + argument_spec=module_args, + add_file_common_args=True, + ) + + if not HAS_LIB: + module.fail_json(msg=missing_required_lib("pykeepass"), exception=LIB_IMP_ERR) + + result = dict( + changed=False, + ) + + dest = module.params['dest'] + b_dest = to_bytes(dest, errors='surrogate_or_strict') + + if os.path.isdir(b_dest): + module.fail_json(rc=256, msg='Destination {0} is a directory!'.format(dest)) + + export_attachment(module, result) + + +if __name__ == '__main__': + main()