├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── new-collaborator.md │ └── proposed-change.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── python-publish.yml │ └── python-test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DC01.1.txt ├── LICENSE ├── Makefile ├── README.md ├── plant ├── __init__.py ├── cli.py └── locker.py ├── setup.cfg ├── setup.py └── test ├── __init__.py ├── fixtures ├── faux_config.json ├── faux_creds.ini └── faux_git_config.json ├── test_cli.py └── test_plant_locker.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. 3 | * @alfinkel @drsm79 @cletomartin 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-collaborator.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Collaborator 3 | about: Request collaborator access 4 | --- 5 | 6 | I would like collaborator (write) access to this repository, so I can contribute 7 | to the Auditree Plant tool. 8 | 9 | - [ ] I have read the [contributing guidelines][CONTRIBUTING] 10 | 11 | 12 | [CONTRIBUTING]: https://github.com/ComplianceAsCode/auditree-plant/blob/master/CONTRIBUTING.md 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposed-change.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Proposed Change 3 | about: Use this template to file defects, propose enhancements or new features. 4 | 5 | --- 6 | 7 | ## Overview 8 | 9 | _Provide a high level summary of the issue to be resolved. This is mandatory 10 | for issue creation._ 11 | 12 | ## Requirements 13 | 14 | _Provide a bulleted list of requirements that when implemented will define the 15 | closure of the issue. This level of detail may not be available at the time of 16 | issue creation and can be completed at a later time._ 17 | 18 | ## Approach 19 | 20 | _Provide a detailed approach to satisfy all of the requirements listed in the 21 | previous section. This level of detail may not be available at the time of 22 | issue creation and can be completed at a later time._ 23 | 24 | ## Security and Privacy 25 | 26 | _Provide the impact on security and privacy as it relates to the completion of 27 | this issue. This level of detail may not be available at the time of 28 | issue creation and can be completed at a later time. N/A if not applicable._ 29 | 30 | ## Test Plan 31 | 32 | _Provide the test process that will be followed to adequately verify that the 33 | approach above satisfies the requirements provided. This level of detail may 34 | not be available at the time of issue creation and can be completed at a later 35 | time._ 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt) 2 | 3 | 4 | ## What 5 | 6 | _Provide an overview of the scope of the pull request._ 7 | 8 | ## Why 9 | 10 | _Provide the business justification for the work included in the pull request._ 11 | 12 | ## How 13 | 14 | _Provide a bulleted list of the changes included in the pull request._ 15 | 16 | ## Test 17 | 18 | _Provide a bulleted list of tests included in the pull request and/or tests 19 | performed to validate the work included in the pull request._ 20 | 21 | ## Context 22 | 23 | _Provide a bulleted list of GitHub issues, or any other references (mailing list discussion, etc...) that reviewers can reference 24 | for additional information regarding scope of the pull request._ 25 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: PyPI upload 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.8' 21 | - name: Install dependencies 22 | run: | 23 | make develop 24 | - name: Test & lint 25 | run: | 26 | make code-lint 27 | make test 28 | 29 | - name: Build and publish 30 | env: 31 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 32 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 33 | run: | 34 | python setup.py sdist bdist_wheel 35 | twine upload dist/* 36 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: format | lint | test 2 | on: [push, pull_request] 3 | jobs: 4 | lint_unit_tests_coverage: 5 | name: Run code validation steps 6 | 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: '3.8' 15 | - name: Install dependencies 16 | run: | 17 | make develop 18 | - name: Run formatter 19 | run: | 20 | make code-format 21 | - name: Run linter 22 | run: | 23 | make code-lint 24 | - name: Run unit tests with coverage 25 | run: | 26 | make test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -*- mode: gitignore; -*- 2 | 3 | ### Emacs 4 | 5 | *~ 6 | \#*\# 7 | /.emacs.desktop 8 | /.emacs.desktop.lock 9 | *.elc 10 | auto-save-list 11 | tramp 12 | .\#* 13 | 14 | # Org-mode 15 | .org-id-locations 16 | *_archive 17 | 18 | # flymake-mode 19 | *_flymake.* 20 | 21 | # eshell files 22 | /eshell/history 23 | /eshell/lastdir 24 | 25 | # elpa packages 26 | /elpa/ 27 | 28 | # reftex files 29 | *.rel 30 | 31 | # AUCTeX auto folder 32 | /auto/ 33 | 34 | # cask packages 35 | .cask/ 36 | dist/ 37 | 38 | # Flycheck 39 | flycheck_*.el 40 | 41 | # server auth directory 42 | /server/ 43 | 44 | # projectiles files 45 | .projectile 46 | 47 | # directory configuration 48 | .dir-locals.el 49 | 50 | 51 | ### Vim 52 | 53 | # Swap 54 | [._]*.s[a-v][a-z] 55 | [._]*.sw[a-p] 56 | [._]s[a-v][a-z] 57 | [._]sw[a-p] 58 | 59 | # Session 60 | Session.vim 61 | 62 | # Temporary 63 | .netrwhist 64 | # Auto-generated tag files 65 | tags 66 | # Byte-compiled / optimized / DLL files 67 | __pycache__/ 68 | *.py[cod] 69 | *$py.class 70 | 71 | # C extensions 72 | *.so 73 | 74 | # Distribution / packaging 75 | .Python 76 | build/ 77 | develop-eggs/ 78 | dist/ 79 | downloads/ 80 | eggs/ 81 | .eggs/ 82 | lib/ 83 | lib64/ 84 | parts/ 85 | sdist/ 86 | var/ 87 | wheels/ 88 | *.egg-info/ 89 | .installed.cfg 90 | *.egg 91 | MANIFEST 92 | 93 | # PyInstaller 94 | # Usually these files are written by a python script from a template 95 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 96 | *.manifest 97 | *.spec 98 | 99 | # Installer logs 100 | pip-log.txt 101 | pip-delete-this-directory.txt 102 | 103 | # Unit test / coverage reports 104 | htmlcov/ 105 | .tox/ 106 | .coverage 107 | .coverage.* 108 | .cache 109 | nosetests.xml 110 | coverage.xml 111 | *.cover 112 | .hypothesis/ 113 | 114 | # Translations 115 | *.mo 116 | *.pot 117 | 118 | # Django stuff: 119 | *.log 120 | .static_storage/ 121 | .media/ 122 | local_settings.py 123 | 124 | # Flask stuff: 125 | instance/ 126 | .webassets-cache 127 | 128 | # Scrapy stuff: 129 | .scrapy 130 | 131 | # Sphinx documentation 132 | doc/_build/ 133 | doc/modules.rst 134 | 135 | # PyBuilder 136 | target/ 137 | 138 | # Jupyter Notebook 139 | .ipynb_checkpoints 140 | 141 | # pyenv 142 | .python-version 143 | 144 | # celery beat schedule file 145 | celerybeat-schedule 146 | 147 | # SageMath parsed files 148 | *.sage.py 149 | 150 | # Environments 151 | .env 152 | .venv 153 | env/ 154 | venv/ 155 | ENV/ 156 | env.bak/ 157 | venv.bak/ 158 | 159 | # Spyder project settings 160 | .spyderproject 161 | .spyproject 162 | 163 | # Rope project settings 164 | .ropeproject 165 | 166 | # mkdocs documentation 167 | /site 168 | 169 | # mypy 170 | .mypy_cache/ 171 | 172 | # creds 173 | .pip.conf 174 | .pypirc 175 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: check-yaml 7 | - id: fix-encoding-pragma 8 | args: ["--remove"] # Not needed on python3 9 | - repo: https://github.com/ambv/black 10 | rev: 22.12.0 11 | hooks: 12 | - id: black 13 | - repo: https://github.com/PyCQA/flake8 14 | rev: 6.0.0 15 | hooks: 16 | - id: flake8 17 | - repo: https://github.com/PyCQA/bandit 18 | rev: 1.7.4 19 | hooks: 20 | - id: bandit 21 | args: [--recursive] 22 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # [1.0.1](https://github.com/ComplianceAsCode/auditree-plant/releases/tag/v1.0.1) 2 | 3 | - [CHANGED] Removed yapf in favour of black as code formatter. 4 | - [FIXED] Unit tests fixed based on the latest auditree framework version. 5 | 6 | # [1.0.0](https://github.com/ComplianceAsCode/auditree-plant/releases/tag/v1.0.0) 7 | 8 | - [ADDED] Made the Auditree Plant tool public. 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team ([Al Finkelstein](https://github.com/alfinkel) 59 | or [Simon Metson](https://github.com/drsm79)). All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you want to add to plant, please familiarize yourself with the code & our [Coding Standards][]. Make a fork of the repository & file a Pull Request from your fork with the changes. You will need to click the checkbox in the template to show you agree to the [Developer Certificate of Origin](https://github.com/ComplianceAsCode/auditree-plant/blob/main/DCO1.1.txt). 4 | 5 | If you make **regular & substantial contributions** to Auditree, you may want to become a collaborator. This means you can approve pull requests (though not your own) & create releases of the tool. Please [file an issue][new collab] to request collaborator access. A collaborator supports the project, ensuring coding standards are met & best practices are followed in contributed code, cutting & documenting releases, promoting the project etc. 6 | 7 | ## Code formatting and style 8 | 9 | Please ensure all code contributions are formatted by `black` and pass all `flake8` linter requirements. 10 | CI/CD will run `black` and `flake8` on all new commits and reject changes if there are failures. If you 11 | run `make develop` to setup and maintain your virtual environment then `black` and `flake8` will be executed 12 | automatically as part of all git commits. If you'd like to run things manually you can do so locally by using: 13 | 14 | ```shell 15 | make code-format 16 | make code-lint 17 | ``` 18 | 19 | ## Testing 20 | 21 | Please ensure all code contributions are covered by appropriate unit tests and that all tests run cleanly. 22 | CI/CD will run tests on all new commits and reject changes if there are failures. You should run the test 23 | suite locally by using: 24 | 25 | ```shell 26 | make test 27 | ``` 28 | 29 | ## Releases and change logs 30 | 31 | We follow [semantic versioning][semver] and [changelog standards][changelog] with 32 | the following addendum: 33 | 34 | - We set the main package `__init__.py` `version` for version tracking. 35 | - Our change log is CHANGES.md. 36 | - In addition to the _types of changes_ outlined in the 37 | [changelog standards][changelog] we also include a BREAKING _type of change_ to 38 | call out any change that may cause downstream execution disruption. 39 | - Change types are always capitalized and enclosed in square brackets. For 40 | example `[ADDED]`, `[CHANGED]`, etc. 41 | - Changes are in the form of complete sentences with appropriate punctuation. 42 | 43 | [semver]: https://semver.org/ 44 | [changelog]: https://keepachangelog.com/en/1.0.0/#how 45 | [Coding Standards]: https://complianceascode.github.io/auditree-framework/coding-standards.html 46 | [new collab]: https://github.com/ComplianceAsCode/auditree-plant/issues/new?template=new-collaborator.md 47 | -------------------------------------------------------------------------------- /DC01.1.txt: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # -*- mode:makefile; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2020 IBM Corp. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | develop: 18 | pip install -q -e .[dev] --upgrade --upgrade-strategy eager 19 | pre-commit install 20 | 21 | update-pre-commit: 22 | pre-commit autoupdate 23 | 24 | install: 25 | pip install -q --upgrade pip setuptools 26 | pip install -q . --upgrade --upgrade-strategy eager 27 | 28 | uninstall: 29 | pip uninstall auditree-plant 30 | 31 | code-format: 32 | pre-commit run black --all-files 33 | 34 | code-lint: 35 | pre-commit run flake8 --all-files 36 | 37 | test:: 38 | pytest --cov plant test -v 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![OS Compatibility][platform-badge]](#prerequisites) 2 | [![Python Compatibility][python-badge]][python-dl] 3 | [![pre-commit][pre-commit-badge]][pre-commit] 4 | [![Code validation](https://github.com/ComplianceAsCode/auditree-plant/workflows/format%20%7C%20lint%20%7C%20test/badge.svg)][lint-test] 5 | [![Upload Python Package](https://github.com/ComplianceAsCode/auditree-plant/workflows/PyPI%20upload/badge.svg)][pypi-upload] 6 | 7 | # auditree-plant 8 | 9 | The Auditree tool for adding external evidence. 10 | 11 | ## Introduction 12 | 13 | Auditree `plant` is a command line tool that assists in adding evidence to an 14 | evidence locker. It provides a thoughtful way to add evidence to an 15 | evidence locker by managing the evidence metadata so that checks and dependent fetchers 16 | executed as part of the [Auditree compliance framework][auditree-framework] can apply 17 | appropriate time to live validations. 18 | 19 | ## Prerequisites 20 | 21 | - Supported for execution on OSX and LINUX. 22 | - Supported for execution with Python 3.6 and above. 23 | 24 | Python 3 must be installed, it can be downloaded from the [Python][python-dl] 25 | site or installed using your package manager. 26 | 27 | Python version can be checked with: 28 | 29 | ```sh 30 | python --version 31 | ``` 32 | 33 | or 34 | 35 | ```sh 36 | python3 --version 37 | ``` 38 | 39 | The `plant` tool is available for download from [PyPI](https://pypi.org/project/auditree-plant/). 40 | 41 | ## Installation 42 | 43 | It is best practice, but not mandatory, to run `plant` from a dedicated Python 44 | virtual environment. Assuming that you have the Python [virtualenv][virtual-env] 45 | package already installed, you can create a virtual environment named `venv` by 46 | executing `virtualenv venv` which will create a `venv` folder at the location of 47 | where you executed the command. Alternatively you can use the python `venv` module 48 | to do the same. 49 | 50 | ```sh 51 | python3 -m venv venv 52 | ``` 53 | 54 | Assuming that you have a virtual environment and that virtual environment is in 55 | the current directory then to install a new instance of `plant`, activate 56 | your virtual environment and use `pip` to install `plant` like so: 57 | 58 | ```sh 59 | . ./venv/bin/activate 60 | pip install auditree-plant 61 | ``` 62 | 63 | As we add new features to `plant` you will want to upgrade your `plant` 64 | package. To upgrade `plant` to the most recent version do: 65 | 66 | ```sh 67 | . ./venv/bin/activate 68 | pip install auditree-plant --upgrade 69 | ``` 70 | 71 | See [pip documentation][pip-docs] for additional options when using `pip`. 72 | 73 | ## Configuration 74 | 75 | Since Auditree `plant` interacts with Git repositories, it requires Git remote 76 | hosting service credentials in order to do its thing. Auditree `plant` will by 77 | default look for a `username` and `token` in a `~/.credentials` file. You can 78 | override the credentials file location by using the `--creds` option on a `plant` 79 | CLI execution. Valid section headings include `github`, `github_enterprise`, `bitbucket`, 80 | and `gitlab`. Below is an example of the expected credentials entry. 81 | 82 | ```ini 83 | [github] 84 | username=your-gh-username 85 | token=your-gh-token 86 | ``` 87 | 88 | ## Execution 89 | 90 | Auditree `plant` is a simple CLI that performs the function of adding evidence 91 | to an evidence locker. As such, it has two execution modes; a `push-remote` mode 92 | and a `dry-run` mode. Both modes will clone a git repository and place it into the 93 | `$TMPDIR/plant` folder. Both modes will also provide handy progress output as 94 | `plant` processes the new evidence. However, `push-remote` will push the changes 95 | to the remote repository before removing the locally cloned copy whereas `dry-run` 96 | will not. When provided an absolute path to a local git repository using the 97 | `--repo-path` option, `plant` will perform its plant-like duties as described 98 | on the specified local git repository. This can come in handy when looking to 99 | chain your `plant` execution after a successful run of the compliance automation 100 | fetchers and checks. 101 | 102 | As most CLIs, Auditree `plant` comes with a help facility. 103 | 104 | ```sh 105 | plant -h 106 | ``` 107 | 108 | ```sh 109 | plant push-remote -h 110 | ``` 111 | 112 | ```sh 113 | plant dry-run -h 114 | ``` 115 | 116 | ### push-remote mode 117 | 118 | Use the `push-remote` mode when you want your changes to be applied to the remote 119 | evidence locker. You can provide as many _evidence path_/_evidence detail_ 120 | key/value pairs as you need as part of the `--config` or as contents of your 121 | `--config-file`. 122 | 123 | ```sh 124 | plant push-remote https://github.com/org-foo/repo-bar --config '{"/absolute/path/to/my/evidence.ext":{"category":"foo"}}' 125 | ``` 126 | 127 | ```sh 128 | plant push-remote https://github.com/org-foo/repo-bar --config-file ./path/to/my/config_file.json 129 | ``` 130 | 131 | ```sh 132 | plant push-remote https://github.com/org-foo/repo-bar --repo-path $TMPDIR"compliance" --config-file ./path/to/my/config_file.json 133 | ``` 134 | 135 | ### dry-run mode 136 | 137 | Use the `dry-run` mode when you don't want your changes to be applied to the remote 138 | evidence locker and are just interested in seeing what effect the execution will have 139 | on your evidence locker before you commit to pushing your changes to the remote repository. 140 | You can provide as many _evidence path_/_evidence detail_ key/value pairs as you 141 | need as part of the `--config` or as contents of your `--config-file`. 142 | 143 | ```sh 144 | plant dry-run https://github.com/org-foo/repo-bar --config '{"/absolute/path/to/my/evidence.ext":{"category":"foo"}}' 145 | ``` 146 | 147 | ```sh 148 | plant dry-run https://github.com/org-foo/repo-bar --config-file ./path/to/my/config_file.json 149 | ``` 150 | 151 | ```sh 152 | plant dry-run https://github.com/org-foo/repo-bar --repo-path $TMPDIR"compliance" --config-file ./path/to/my/config_file.json 153 | ``` 154 | 155 | 156 | [platform-badge]: https://img.shields.io/badge/platform-osx%20|%20linux-orange.svg 157 | [python-badge]: https://img.shields.io/badge/python-v3.6+-blue.svg 158 | [pre-commit-badge]: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white 159 | [python-dl]: https://www.python.org/downloads/ 160 | [pre-commit]: https://github.com/pre-commit/pre-commit 161 | [pip-docs]: https://pip.pypa.io/en/stable/reference/pip/ 162 | [virtual-env]: https://pypi.org/project/virtualenv/ 163 | [auditree-framework]: https://github.com/ComplianceAsCode/auditree-framework 164 | [lint-test]: https://github.com/ComplianceAsCode/auditree-plant/actions?query=workflow%3A%22format+%7C+lint+%7C+test%22 165 | [pypi-upload]: https://github.com/ComplianceAsCode/auditree-plant/actions?query=workflow%3A%22PyPI+upload%22 166 | -------------------------------------------------------------------------------- /plant/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """The Auditree tool for adding evidence to an evidence locker.""" 15 | 16 | __version__ = "1.0.0" 17 | -------------------------------------------------------------------------------- /plant/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Plant command line interface.""" 15 | 16 | import json 17 | import os 18 | import shutil 19 | import tempfile 20 | from urllib.parse import urlparse 21 | 22 | from compliance.evidence import ExternalEvidence, YEAR 23 | from compliance.utils.credentials import Config 24 | from compliance.config import get_config 25 | 26 | from ilcli import Command 27 | 28 | from plant import __version__ as version 29 | from plant.locker import PlantLocker 30 | 31 | 32 | class _CorePlantCommand(Command): 33 | def _init_arguments(self): 34 | self.add_argument( 35 | "locker", 36 | help=( 37 | "the URL to the evidence locker repository, " 38 | "as an example https://github.com/my-org/my-repo" 39 | ), 40 | ) 41 | self.add_argument( 42 | "--branch", help="Branch name for locker repository", default=False 43 | ) 44 | self.add_argument( 45 | "--creds", 46 | metavar="~/path/creds", 47 | help="the path to credentials file - defaults to %(default)s", 48 | default="~/.credentials", 49 | ) 50 | self.add_argument( 51 | "--config", 52 | help=( 53 | "JSON evidence-path/detail pairs needed to plant evidence. " 54 | "Evidence path must be the absolute path to the file. The " 55 | "detail is a dictionary of category, ttl, and description. " 56 | "Only the category is required." 57 | ), 58 | type=json.loads, 59 | metavar=( 60 | '\'{"/absolute/path/to/my/evidence.ext":{"category":"foo",' 61 | '"ttl": 86400,"description":"this is my evidence"},...}\'' 62 | ), 63 | default={}, 64 | ) 65 | self.add_argument( 66 | "--config-file", 67 | help="path to a file containing the files (with config) to plant", 68 | metavar="~/path/to/config_file.json", 69 | default=False, 70 | ) 71 | self.add_argument( 72 | "--git-config", 73 | help="JSON git configuration for signing commits", 74 | type=json.loads, 75 | metavar=( 76 | '\'{"commit":{"gpgsign": true},' 77 | '"user":{"signingKey":"...","email":"...","name":"..."}}\'' 78 | ), 79 | default=False, 80 | ) 81 | self.add_argument( 82 | "--git-config-file", 83 | help=( 84 | "path to a file containing the " "git configuration for signing commits" 85 | ), 86 | metavar="~/path/to/git_config_file.json", 87 | default=False, 88 | ) 89 | self.add_argument( 90 | "--repo-path", 91 | help=( 92 | "the operating system location of a local git repository - " 93 | "if not provided, repo will be cloned to $TMPDIR/plant" 94 | ), 95 | metavar="~/path/evidence-locker", 96 | default=None, 97 | ) 98 | 99 | def _validate_arguments(self, args): 100 | parsed = urlparse(args.locker) 101 | if not (parsed.scheme and parsed.hostname and parsed.path): 102 | return "ERROR: locker url must be of the form " "https://hostname/org/repo" 103 | if bool(args.config) == bool(args.config_file): 104 | return "ERROR: Provide either a --config or a --config-file." 105 | if args.git_config and args.git_config_file: 106 | return "ERROR: Provide either a --git-config or a --git-config-file." 107 | 108 | def _run(self, args): 109 | self.out(self.intro_msg) 110 | gitconfig = None 111 | if args.git_config or args.git_config_file: 112 | gitconfig = args.git_config or json.loads(open(args.git_config_file).read()) 113 | if args.branch: 114 | c = get_config() 115 | c.load() 116 | c.raw_config["locker"]["default_branch"] = args.branch 117 | # self.name drives the Locker push mode. 118 | # - dry-run translates to locker no-push mode 119 | # - push-remote translates to locker full-remote mode 120 | locker_args = [args.locker, args.creds, self.name, gitconfig, args.repo_path] 121 | files = args.config 122 | if not files: 123 | files = json.loads(open(args.config_file).read()) 124 | with self._get_locker(*locker_args) as locker: 125 | self.out(f"Local locker location is {locker.local_path}") 126 | for file_path, details in files.items(): 127 | evidence = ExternalEvidence( 128 | file_path.rsplit("/", 1).pop(), 129 | details["category"], 130 | details.get("ttl", YEAR), 131 | details.get("description", ""), 132 | ) 133 | evidence.set_content(open(file_path).read()) 134 | locker.add_evidence(evidence) 135 | self.out( 136 | f"\nEvidence {file_path} added to " 137 | f'external/{details["category"]}, metadata applied...' 138 | ) 139 | self.out(self.outro_msg) 140 | 141 | def _get_locker(self, repo, creds, mode, gitconfig=None, repo_path=None): 142 | locker_name = "plant" 143 | if repo_path: 144 | locker_name = repo_path.rsplit("/", 1).pop() 145 | else: 146 | local_locker_path = f"{tempfile.gettempdir()}/{locker_name}" 147 | if os.path.isdir(local_locker_path): 148 | self.out("Local locker found...") 149 | self._remove_locker(local_locker_path) 150 | self.out( 151 | f"Cloning local locker for {repo}. Depending on the " 152 | "size of your locker, this may take a while..." 153 | ) 154 | return PlantLocker( 155 | name=locker_name, 156 | repo_url=repo, 157 | creds=Config(creds), 158 | do_push=True if mode == "push-remote" else False, 159 | gitconfig=gitconfig, 160 | repo_path=repo_path, 161 | ) 162 | 163 | def _remove_locker(self, locker_path): 164 | self.out("Removing local locker...") 165 | shutil.rmtree(locker_path) 166 | self.out("Local locker has been removed...") 167 | 168 | 169 | class DryRun(_CorePlantCommand): 170 | """Perform requested changes locally and show results of changes.""" 171 | 172 | name = "dry-run" 173 | intro_msg = "This is a dry run. Remote locker will not be updated..." 174 | outro_msg = "Remote locker was not updated..." 175 | 176 | 177 | class PushToRemote(_CorePlantCommand): 178 | """Perform requested changes and push to the remote repository.""" 179 | 180 | name = "push-remote" 181 | intro_msg = "This is an official run. Remote locker will be updated..." 182 | outro_msg = "Remote locker was updated..." 183 | 184 | 185 | class Plant(Command): 186 | """The plant CLI base command.""" 187 | 188 | subcommands = [DryRun, PushToRemote] 189 | 190 | def _init_arguments(self): 191 | self.add_argument( 192 | "--version", 193 | help="the plant version", 194 | action="version", 195 | version=f"v{version}", 196 | ) 197 | 198 | 199 | def run(): 200 | """Execute the plant CLI.""" 201 | plant = Plant() 202 | exit(plant.run()) 203 | 204 | 205 | if __name__ == "__main__": 206 | run() 207 | -------------------------------------------------------------------------------- /plant/locker.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Plant Locker.""" 15 | 16 | import json 17 | import os 18 | import time 19 | 20 | from compliance.locker import Locker 21 | from compliance.utils.data_parse import format_json 22 | 23 | 24 | class PlantLocker(Locker): 25 | """Provide plant specific locker functionality.""" 26 | 27 | def __init__( 28 | self, 29 | name=None, 30 | repo_url=None, 31 | creds=None, 32 | do_push=False, 33 | gitconfig=None, 34 | repo_path=None, 35 | ): 36 | """Plant locker constructor to add external evidence.""" 37 | super().__init__( 38 | name=name, 39 | repo_url=repo_url, 40 | creds=creds, 41 | do_push=do_push, 42 | gitconfig=gitconfig, 43 | ) 44 | if repo_path is not None: 45 | self.local_path = os.path.normpath(repo_path) 46 | self.planted = [] 47 | 48 | def __exit__(self, exc_type, exc_val, exc_tb): 49 | """Override check in routine with a custom plant commit message.""" 50 | if exc_type: 51 | self.logger.error(" ".join([str(exc_type), str(exc_val)])) 52 | planted_files = "\n".join(self.planted) 53 | self.checkin( 54 | ( 55 | "Planted external evidence at local time " 56 | f"{time.ctime(time.time())}\n\n{planted_files}" 57 | ) 58 | ) 59 | if self.repo_url_with_creds: 60 | self.push() 61 | return 62 | 63 | def index(self, evidence, checks=None, evidence_used=None): 64 | """ 65 | Add external evidence to the git index. 66 | 67 | Overrides the base Locker index method called by add_evidence. 68 | """ 69 | with self.lock: 70 | index_file = self.get_index_file(evidence) 71 | if not os.path.exists(index_file): 72 | metadata = {} 73 | else: 74 | metadata = json.loads(open(index_file).read()) 75 | planter = self.repo.config_reader().get_value("user", "email") 76 | metadata[evidence.name] = { 77 | "last_update": self.commit_date, 78 | "ttl": evidence.ttl, 79 | "planted_by": planter, 80 | "description": evidence.description, 81 | } 82 | with open(index_file, "w") as f: 83 | f.write(format_json(metadata)) 84 | self.repo.index.add([index_file, self.get_file(evidence.path)]) 85 | self.planted.append(evidence.path) 86 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = auditree-plant 3 | version = attr: plant.__version__ 4 | description = The Auditree tool for adding external evidence 5 | author = Auditree Security and Compliance 6 | author_email = al.finkelstein@ibm.com 7 | url = https://auditree.github.io/ 8 | license = Apache License 2.0 9 | classifiers = 10 | Programming Language :: Python :: 3.6 11 | Programming Language :: Python :: 3.7 12 | Programming Language :: Python :: 3.8 13 | Programming Language :: Python :: 3.9 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: MacOS :: MacOS X 16 | Operating System :: POSIX :: Linux 17 | long_description_content_type = text/markdown 18 | long_description = file: README.md 19 | 20 | [options] 21 | packages = find: 22 | install_requires = 23 | auditree-framework>=1.0.0 24 | 25 | [options.packages.find] 26 | exclude = 27 | test.* 28 | test 29 | 30 | [bdist_wheel] 31 | universal = 1 32 | 33 | [options.entry_points] 34 | console_scripts = 35 | plant=plant.cli:run 36 | 37 | [options.extras_require] 38 | dev = 39 | pre-commit>=2.4.0 40 | pytest>=4.4.1 41 | pytest-cov>=2.6.1 42 | recommonmark 43 | Sphinx>=1.7.2 44 | setuptools 45 | wheel 46 | twine 47 | 48 | [flake8] 49 | max-line-length = 88 50 | extend-ignore = E203 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from setuptools import setup 16 | 17 | setup() 18 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Plant unit tests.""" 15 | -------------------------------------------------------------------------------- /test/fixtures/faux_config.json: -------------------------------------------------------------------------------- 1 | {"/home/foo/bar.json": {"category": "foo", "description": "meh"}} 2 | -------------------------------------------------------------------------------- /test/fixtures/faux_creds.ini: -------------------------------------------------------------------------------- 1 | [github] 2 | username=that-guy 3 | token=1a2b3c4d5e6f7g8h9i0 4 | -------------------------------------------------------------------------------- /test/fixtures/faux_git_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "commit": {"gpgsign": true}, 3 | "user": {"signingKey": "...", "email": "...", "name": "..."} 4 | } 5 | -------------------------------------------------------------------------------- /test/test_cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Plant CLI tests.""" 15 | 16 | import json 17 | import logging 18 | import tempfile 19 | import unittest 20 | from unittest.mock import MagicMock, mock_open, patch 21 | 22 | from plant.cli import Plant 23 | 24 | 25 | class TestPlantCLI(unittest.TestCase): 26 | """Test Plant CLI execution.""" 27 | 28 | def setUp(self): 29 | """Initialize supporting test objects before each test.""" 30 | logging.disable(logging.CRITICAL) 31 | self.plant = Plant() 32 | self.grc_patcher = patch("git.Repo.clone_from") 33 | self.git_repo_clone_from_mock = self.grc_patcher.start() 34 | push_info_mock = MagicMock() 35 | push_info_mock.flags = 0 36 | self.git_remote_push_mock = MagicMock(return_value=[push_info_mock]) 37 | git_remote_mock = MagicMock() 38 | git_remote_mock.push = self.git_remote_push_mock 39 | git_config_parser_mock = MagicMock() 40 | git_config_parser_mock.get_value = MagicMock(return_value="finkel") 41 | repo_mock = MagicMock() 42 | repo_mock.config_reader = MagicMock(return_value=git_config_parser_mock) 43 | repo_mock.remote.return_value = git_remote_mock 44 | self.git_repo_clone_from_mock.return_value = repo_mock 45 | self.lic_patcher = patch("compliance.locker.Locker.init_config") 46 | self.locker_init_config_mock = self.lic_patcher.start() 47 | self.lci_patcher = patch("compliance.locker.Locker.checkin") 48 | self.locker_checkin_mock = self.lci_patcher.start() 49 | self.lae_patcher = patch("compliance.locker.Locker.add_evidence") 50 | self.locker_add_evidence_mock = self.lae_patcher.start() 51 | self.srm_patcher = patch("plant.cli.shutil.rmtree") 52 | self.shutil_rmtree_mock = self.srm_patcher.start() 53 | self.dry_run = [ 54 | "dry-run", 55 | "https://github.com/foo/bar", 56 | "--creds", 57 | "./test/fixtures/faux_creds.ini", 58 | ] 59 | self.push_remote = ["push-remote"] + self.dry_run[1:] 60 | 61 | def tearDown(self): 62 | """Cleanup supporting test objects after each test.""" 63 | logging.disable(logging.NOTSET) 64 | self.grc_patcher.stop() 65 | self.lic_patcher.stop() 66 | self.lci_patcher.stop() 67 | self.lae_patcher.stop() 68 | self.srm_patcher.stop() 69 | 70 | def test_no_config_validation(self): 71 | """Ensures processing stops when no evidence config is provided.""" 72 | self.plant.run(self.push_remote) 73 | self.git_repo_clone_from_mock.assert_not_called() 74 | self.locker_init_config_mock.assert_not_called() 75 | self.locker_add_evidence_mock.assert_not_called() 76 | self.locker_checkin_mock.assert_not_called() 77 | self.git_remote_push_mock.assert_not_called() 78 | self.shutil_rmtree_mock.assert_not_called() 79 | 80 | def test_multiple_config_validation(self): 81 | """Ensures processing stops when both config and path provided.""" 82 | self.plant.run( 83 | self.push_remote 84 | + [ 85 | "--config", 86 | json.dumps({"foo": "bar"}), 87 | "--config-file", 88 | "foo/bar/baz_cfg.json", 89 | ] 90 | ) 91 | self.git_repo_clone_from_mock.assert_not_called() 92 | self.locker_init_config_mock.assert_not_called() 93 | self.locker_add_evidence_mock.assert_not_called() 94 | self.locker_checkin_mock.assert_not_called() 95 | self.git_remote_push_mock.assert_not_called() 96 | self.shutil_rmtree_mock.assert_not_called() 97 | 98 | def test_multiple_git_config_validation(self): 99 | """Ensures processing stops when both git-config and path provided.""" 100 | self.plant.run( 101 | self.push_remote 102 | + [ 103 | "--git-config", 104 | json.dumps({"foo": "bar"}), 105 | "--git-config-file", 106 | "foo/bar/baz_cfg.json", 107 | ] 108 | ) 109 | self.git_repo_clone_from_mock.assert_not_called() 110 | self.locker_init_config_mock.assert_not_called() 111 | self.locker_add_evidence_mock.assert_not_called() 112 | self.locker_checkin_mock.assert_not_called() 113 | self.git_remote_push_mock.assert_not_called() 114 | self.shutil_rmtree_mock.assert_not_called() 115 | 116 | def test_dry_run_config(self): 117 | """Ensures dry-run mode works when config JSON is provided.""" 118 | config = {"/home/foo/bar.json": {"category": "foo", "description": "meh"}} 119 | with patch("plant.cli.open", mock_open(read_data="{}")): 120 | self.plant.run(self.dry_run + ["--config", json.dumps(config)]) 121 | self.git_repo_clone_from_mock.assert_called_once_with( 122 | "https://1a2b3c4d5e6f7g8h9i0@github.com/foo/bar", 123 | f"{tempfile.gettempdir()}/plant", 124 | single_branch=True, 125 | branch="master", 126 | ) 127 | self.locker_init_config_mock.assert_called_once() 128 | self.locker_add_evidence_mock.assert_called_once() 129 | self.git_remote_push_mock.assert_not_called() 130 | self.shutil_rmtree_mock.assert_not_called() 131 | 132 | def test_dry_run_config_file(self): 133 | """Ensures dry-run mode works when a config file is provided.""" 134 | config_file = "./test/fixtures/faux_config.json" 135 | config_content = open(config_file).read() 136 | with patch("plant.cli.open", mock_open(read_data=config_content)): 137 | self.plant.run(self.dry_run + ["--config-file", config_file]) 138 | self.git_repo_clone_from_mock.assert_called_once_with( 139 | "https://1a2b3c4d5e6f7g8h9i0@github.com/foo/bar", 140 | f"{tempfile.gettempdir()}/plant", 141 | single_branch=True, 142 | branch="master", 143 | ) 144 | self.locker_init_config_mock.assert_called_once() 145 | self.locker_add_evidence_mock.assert_called_once() 146 | self.git_remote_push_mock.assert_not_called() 147 | self.shutil_rmtree_mock.assert_not_called() 148 | 149 | def test_dry_run_git_config(self): 150 | """Ensures dry-run mode works when a git config is provided.""" 151 | config = {"/home/foo/bar.json": {"category": "foo", "description": "meh"}} 152 | git_config = { 153 | "commit": {"gpgsign": True}, 154 | "user": {"signingKey": "...", "email": "...", "name": "..."}, 155 | } 156 | with patch("plant.cli.open", mock_open(read_data="{}")): 157 | self.plant.run( 158 | self.dry_run 159 | + [ 160 | "--config", 161 | json.dumps(config), 162 | "--git-config", 163 | json.dumps(git_config), 164 | ] 165 | ) 166 | self.git_repo_clone_from_mock.assert_called_once_with( 167 | "https://1a2b3c4d5e6f7g8h9i0@github.com/foo/bar", 168 | f"{tempfile.gettempdir()}/plant", 169 | single_branch=True, 170 | branch="master", 171 | ) 172 | self.locker_init_config_mock.assert_called_once() 173 | self.locker_add_evidence_mock.assert_called_once() 174 | self.git_remote_push_mock.assert_not_called() 175 | self.shutil_rmtree_mock.assert_not_called() 176 | 177 | def test_dry_run_git_config_file(self): 178 | """Ensures dry-run mode works when a git config file is provided.""" 179 | config = {"/home/foo/bar.json": {"category": "foo", "description": "meh"}} 180 | git_config_file = "./test/fixtures/faux_git_config.json" 181 | git_config_content = open(git_config_file).read() 182 | with patch("plant.cli.open", mock_open(read_data=git_config_content)): 183 | self.plant.run( 184 | self.dry_run 185 | + [ 186 | "--config", 187 | json.dumps(config), 188 | "--git-config-file", 189 | "./test/fixtures/faux_git_config.json", 190 | ] 191 | ) 192 | self.git_repo_clone_from_mock.assert_called_once_with( 193 | "https://1a2b3c4d5e6f7g8h9i0@github.com/foo/bar", 194 | f"{tempfile.gettempdir()}/plant", 195 | single_branch=True, 196 | branch="master", 197 | ) 198 | self.locker_init_config_mock.assert_called_once() 199 | self.locker_add_evidence_mock.assert_called_once() 200 | self.git_remote_push_mock.assert_not_called() 201 | self.shutil_rmtree_mock.assert_not_called() 202 | 203 | @patch("git.Repo") 204 | @patch("os.path.isdir") 205 | def test_repo_path(self, isdir_mock, repo_mock): 206 | """Ensures providing a repo path does not clone a remote repo.""" 207 | isdir_mock.return_value = True 208 | repo_mock.return_value = "REPO" 209 | config = {"/home/foo/bar.json": {"category": "foo", "description": "meh"}} 210 | with patch("plant.cli.open", mock_open(read_data="{}")): 211 | self.plant.run( 212 | self.dry_run 213 | + [ 214 | "--config", 215 | json.dumps(config), 216 | "--repo-path", 217 | "/tmp/meh", # nosec B108: open is mocked so no real file. 218 | ] 219 | ) 220 | self.git_repo_clone_from_mock.assert_not_called() 221 | self.locker_init_config_mock.assert_called_once() 222 | self.locker_add_evidence_mock.assert_called_once() 223 | self.git_remote_push_mock.assert_not_called() 224 | self.shutil_rmtree_mock.assert_not_called() 225 | 226 | def test_push_remote(self): 227 | """ 228 | Ensures push-remote mode works as expected. 229 | 230 | No other tests needed for push-remote since push-remote and dry-run 231 | use the same core plant command logic. 232 | """ 233 | config = {"/home/foo/bar.json": {"category": "foo", "description": "meh"}} 234 | with patch("plant.cli.open", mock_open(read_data="{}")): 235 | self.plant.run(self.push_remote + ["--config", json.dumps(config)]) 236 | self.git_repo_clone_from_mock.assert_called_once_with( 237 | "https://1a2b3c4d5e6f7g8h9i0@github.com/foo/bar", 238 | f"{tempfile.gettempdir()}/plant", 239 | single_branch=True, 240 | branch="master", 241 | ) 242 | self.locker_init_config_mock.assert_called_once() 243 | self.locker_add_evidence_mock.assert_called_once() 244 | self.git_remote_push_mock.assert_called_once() 245 | self.shutil_rmtree_mock.assert_not_called() 246 | -------------------------------------------------------------------------------- /test/test_plant_locker.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 IBM Corp. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Plant locker tests.""" 15 | 16 | import logging 17 | import tempfile 18 | import unittest 19 | from unittest.mock import MagicMock, create_autospec, mock_open, patch 20 | 21 | from compliance.evidence import ExternalEvidence 22 | 23 | from plant.locker import PlantLocker 24 | 25 | 26 | class TestPlantLocker(unittest.TestCase): 27 | """Test PlantLocker.""" 28 | 29 | def setUp(self): 30 | """Initialize supporting test objects before each test.""" 31 | logging.disable(logging.CRITICAL) 32 | self.mock_logger_error = MagicMock() 33 | self.mock_logger = create_autospec(logging.Logger) 34 | self.mock_logger.error = self.mock_logger_error 35 | self.ctime_patcher = patch("plant.locker.time.ctime") 36 | self.ctime_mock = self.ctime_patcher.start() 37 | self.ctime_mock.return_value = "NOW" 38 | self.push_patcher = patch("compliance.locker.Locker.push") 39 | self.push_mock = self.push_patcher.start() 40 | self.checkin_patcher = patch("compliance.locker.Locker.checkin") 41 | self.checkin_mock = self.checkin_patcher.start() 42 | self.init_patcher = patch("compliance.locker.Locker.init") 43 | self.init_mock = self.init_patcher.start() 44 | 45 | def tearDown(self): 46 | """Cleanup supporting test objects after each test.""" 47 | logging.disable(logging.NOTSET) 48 | self.ctime_patcher.stop() 49 | self.push_patcher.stop() 50 | self.checkin_patcher.stop() 51 | self.init_patcher.stop() 52 | 53 | def test_constructor(self): 54 | """Ensures a planted list attribute and local path is set.""" 55 | locker = PlantLocker(name="repo-foo", repo_path="/foo/bar//baz") 56 | self.assertEqual(locker.local_path, "/foo/bar/baz") 57 | self.assertEqual(locker.planted, []) 58 | 59 | def test_custom_exit_no_push(self): 60 | """Ensures that the context manager exit routine does not run push.""" 61 | with PlantLocker("repo-foo") as locker: 62 | locker.logger = self.mock_logger 63 | locker.planted = ["foo", "bar", "baz"] 64 | self.mock_logger_error.assert_not_called() 65 | self.checkin_mock.assert_called_once_with( 66 | "Planted external evidence at local time NOW\n\nfoo\nbar\nbaz" 67 | ) 68 | self.push_mock.assert_not_called() 69 | 70 | def test_custom_exit_push(self): 71 | """Ensures that the context manager exit routine runs push.""" 72 | with PlantLocker("repo-foo") as locker: 73 | locker.logger = self.mock_logger 74 | locker.planted = ["foo", "bar", "baz"] 75 | locker.repo_url_with_creds = "my repo" 76 | self.mock_logger_error.assert_not_called() 77 | self.checkin_mock.assert_called_once_with( 78 | "Planted external evidence at local time NOW\n\nfoo\nbar\nbaz" 79 | ) 80 | self.push_mock.assert_called_once() 81 | 82 | def test_custom_exit_error_logging(self): 83 | """Ensures that the context manager exit routine logs an exception.""" 84 | with self.assertRaises(ValueError): 85 | with PlantLocker("repo-foo") as locker: 86 | locker.logger = self.mock_logger 87 | locker.planted = ["foo", "bar", "baz"] 88 | locker.repo_url_with_creds = "repo-foo-url" 89 | raise ValueError("meh") 90 | self.mock_logger_error.assert_called_once_with(" meh") 91 | self.checkin_mock.assert_called_once_with( 92 | "Planted external evidence at local time NOW\n\nfoo\nbar\nbaz" 93 | ) 94 | self.push_mock.assert_called_once() 95 | 96 | @patch("plant.locker.format_json") 97 | def test_locker_index_override(self, format_json_mock): 98 | """Ensures plant locker indexing works.""" 99 | format_json_mock.return_value = "formatted metadata" 100 | mo = mock_open() 101 | get_value_mock = MagicMock(return_value="this_guy") 102 | config_reader_mock = MagicMock() 103 | config_reader_mock.get_value = get_value_mock 104 | index_add_mock = MagicMock() 105 | index_mock = MagicMock() 106 | index_mock.add = index_add_mock 107 | repo_mock = MagicMock() 108 | repo_mock.config_reader = MagicMock(return_value=config_reader_mock) 109 | repo_mock.index = index_mock 110 | evidence = ExternalEvidence("foo.json", "bar", description="meh") 111 | with patch("builtins.open", mo): 112 | with PlantLocker("repo-foo") as locker: 113 | locker = PlantLocker("repo-foo") 114 | locker.repo = repo_mock 115 | locker.commit_date = "NOW" 116 | locker.index(evidence) 117 | self.assertEqual(locker.planted, ["external/bar/foo.json"]) 118 | mo.assert_called_once_with( 119 | f"{tempfile.gettempdir()}/repo-foo/external/bar/index.json", "w" 120 | ) 121 | expected_meta = { 122 | "foo.json": { 123 | "last_update": "NOW", 124 | "ttl": 31536000, 125 | "planted_by": "this_guy", 126 | "description": "meh", 127 | } 128 | } 129 | format_json_mock.assert_called_once_with(expected_meta) 130 | mo_handle = mo() 131 | mo_handle.write.assert_called_once_with("formatted metadata") 132 | index_add_mock.assert_called_once_with( 133 | [ 134 | f"{tempfile.gettempdir()}/repo-foo/external/bar/index.json", 135 | f"{tempfile.gettempdir()}/repo-foo/external/bar/foo.json", 136 | ] 137 | ) 138 | 139 | @patch("json.loads") 140 | @patch("os.path.exists") 141 | @patch("plant.locker.format_json") 142 | def test_locker_index_override_metadata_found( 143 | self, format_json_mock, exists_mock, json_loads_mock 144 | ): 145 | """Ensures plant locker indexing works when metadata is found.""" 146 | format_json_mock.return_value = "formatted metadata" 147 | exists_mock.return_value = True 148 | json_loads_mock.return_value = {"other_meta": "SOME OTHER METADATA"} 149 | mo = mock_open() 150 | get_value_mock = MagicMock(return_value="this_guy") 151 | config_reader_mock = MagicMock() 152 | config_reader_mock.get_value = get_value_mock 153 | index_add_mock = MagicMock() 154 | index_mock = MagicMock() 155 | index_mock.add = index_add_mock 156 | repo_mock = MagicMock() 157 | repo_mock.config_reader = MagicMock(return_value=config_reader_mock) 158 | repo_mock.index = index_mock 159 | evidence = ExternalEvidence("foo.json", "bar", description="meh") 160 | with patch("builtins.open", mo): 161 | with PlantLocker("repo-foo") as locker: 162 | locker = PlantLocker("repo-foo") 163 | locker.repo = repo_mock 164 | locker.commit_date = "NOW" 165 | locker.index(evidence) 166 | self.assertEqual(locker.planted, ["external/bar/foo.json"]) 167 | mo.assert_called_with( 168 | f"{tempfile.gettempdir()}/repo-foo/external/bar/index.json", "w" 169 | ) 170 | expected_meta = { 171 | "other_meta": "SOME OTHER METADATA", 172 | "foo.json": { 173 | "last_update": "NOW", 174 | "ttl": 31536000, 175 | "planted_by": "this_guy", 176 | "description": "meh", 177 | }, 178 | } 179 | format_json_mock.assert_called_once_with(expected_meta) 180 | mo_handle = mo() 181 | mo_handle.write.assert_called_once_with("formatted metadata") 182 | index_add_mock.assert_called_once_with( 183 | [ 184 | f"{tempfile.gettempdir()}/repo-foo/external/bar/index.json", 185 | f"{tempfile.gettempdir()}/repo-foo/external/bar/foo.json", 186 | ] 187 | ) 188 | --------------------------------------------------------------------------------