├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE ├── release-drafter.yml └── workflows │ ├── codeql.yml │ ├── linter.yaml │ ├── publish.yml │ ├── release-drafter.yml │ └── test.yaml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dev_requirements.txt ├── requirements.txt ├── scpkit ├── __init__.py ├── main.py └── src │ ├── __init__.py │ ├── merge.py │ ├── model.py │ ├── util.py │ ├── validate.py │ └── visualize.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test.py └── testfiles │ ├── test-scp-1.json │ └── test-scp-2.json └── visualize-org.png /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create bug report. 4 | title: 'Example: Application breaks when I run validate.' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | # Bug Report Description 10 | 11 | 12 | ## What is the current behavior? 13 | 14 | 15 | ## How can this issue be reproduced? 16 | 17 | 18 | ## What is the expected behavior? 19 | 20 | 21 | ## What is the motivation / use case for changing the behavior? 22 | 23 | 24 | ## Please tell us about your environment: 25 | 26 | 27 | ## Other information: 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature. 4 | title: 'Example: I would like this project to use an additional authentication method.' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | # Feature Request Description 10 | 11 | 12 | ## What is the current behavior? 13 | 14 | 15 | ## What is the motivation / use case for changing the behavior? 16 | 17 | 18 | ## Please describe any alternatives you have explored. 19 | 20 | 21 | ## Other information: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | 3 | [//]: # (Required: Describe the effects of your pull request in detail. If 4 | multiple changes are involved, a bulleted list is often useful.) 5 | 6 | ## Completion checklist 7 | 8 | - [ ] Additions and changes have unit tests 9 | - [ ] Unit tests, Pylint, security testing, and Integration tests are passing. GitHub actions does this automatically 10 | - [ ] The pull request has been appropriately labeled using the provided PR labels (major, minor, fix, maintenance, documentation) -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | template: | 4 | # What's Changed 5 | $CHANGES 6 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 7 | 8 | version-resolver: 9 | major: 10 | labels: 11 | - 'major' 12 | minor: 13 | labels: 14 | - 'minor' 15 | patch: 16 | labels: 17 | - 'fix' 18 | - 'maintenance' 19 | - 'docs' 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | schedule: 19 | - cron: '22 0 * * 3' 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Use only 'java' to analyze code written in Java, Kotlin or both 36 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/linter.yaml: -------------------------------------------------------------------------------- 1 | ################################# 2 | ################################# 3 | ## Super Linter GitHub Actions ## 4 | ################################# 5 | ################################# 6 | name: Lint Code Base 7 | 8 | # 9 | # Documentation: 10 | # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 11 | # 12 | 13 | ############################# 14 | # Start the job on all push # 15 | ############################# 16 | on: 17 | pull_request: 18 | branches: [master, main] 19 | 20 | ############### 21 | # Set the Job # 22 | ############### 23 | jobs: 24 | build: 25 | # Name the Job 26 | name: Lint Code Base 27 | # Set the agent to run on 28 | runs-on: ubuntu-latest 29 | 30 | ################## 31 | # Load all steps # 32 | ################## 33 | steps: 34 | ########################## 35 | # Checkout the code base # 36 | ########################## 37 | - name: Checkout Code 38 | uses: actions/checkout@v2 39 | with: 40 | # Full git history is needed to get a proper list of changed files within `super-linter` 41 | fetch-depth: 0 42 | 43 | ################################ 44 | # Run Linter against code base # 45 | ################################ 46 | - name: Lint Code Base 47 | uses: github/super-linter/slim@v4 48 | env: 49 | VALIDATE_ALL_CODEBASE: false 50 | DEFAULT_BRANCH: 'main' 51 | VALIDATE_PYTHON_PYLINT: true 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package to PYPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | publish-release: 12 | name: Build and publish to PyPI 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Python 3.10 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.10' 22 | 23 | - name: Update setup.cfg version from tag 24 | run: | 25 | VERSION=${{ github.event.release.tag_name }} 26 | VERSION=${VERSION:1} 27 | sed -i "s/version =.*/version = ${VERSION}/g" setup.cfg 28 | 29 | - name: Install build 30 | run: | 31 | pip install -q build 32 | 33 | - name: Build package 34 | run: | 35 | python -m build 36 | 37 | - name: Publish distribution 📦 to PyPI 38 | uses: pypa/gh-action-pypi-publish@release/v1 39 | with: 40 | password: ${{ secrets.PYPI_API_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | update_release_draft: 14 | permissions: 15 | # write permission is required to create a github release 16 | contents: write 17 | # write permission is required for autolabeler 18 | # otherwise, read permission is required at least 19 | pull-requests: read 20 | runs-on: ubuntu-latest 21 | steps: 22 | # Drafts your next Release notes as Pull Requests are merged into "master" 23 | - uses: release-drafter/release-drafter@v5 24 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 25 | with: 26 | disable-autolabeler: true 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Pytest 2 | 3 | on: 4 | pull_request: 5 | branches: [master, main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.7", "3.8", "3.9", "3.10"] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r dev_requirements.txt 24 | - name: Test with pytest 25 | run: | 26 | pytest ./tests/*.py -vvv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aquia-inc/scpkit-admins -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | info@aquia.us. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTING.md 2 | =============== 3 | 4 | Contributing to SCPkit 5 | ------------------------------ 6 | 7 | Thank you for your interest in contributing to SCPkit! We're excited to have you as part of our growing community. This document outlines the guidelines and best practices for contributing to this project. 8 | 9 | ### Table of Contents 10 | 11 | 1. [CONTRIBUTING.md](#contributingmd) 12 | 1. [Contributing to SCPkit](#contributing-to-project-name) 13 | 1. [Table of Contents](#table-of-contents) 14 | 2. [Code of Conduct](#code-of-conduct) 15 | 3. [Issues](#issues) 16 | 4. [Pull Requests](#pull-requests) 17 | 5. [Coding Standards](#coding-standards) 18 | 6. [Testing](#testing) 19 | 7. [Documentation](#documentation) 20 | 8. [Community](#community) 21 | 22 | ### Code of Conduct 23 | 24 | All contributors to this project are expected to follow our [Code of Conduct][code-of-conduct]. By participating in this project, you agree to abide by its terms. 25 | 26 | ### Issues 27 | 28 | Before submitting an issue, please: 29 | 30 | 1. Check if the issue has already been reported by searching the existing issues. 31 | 2. Follow the issue template provided and fill in all the required information. 32 | 33 | When creating an issue, please use one of the templates for Bug reports or feature requests. For any fields that are not applicable, please use N/A or None. 34 | 35 | Remember that the more detailed and specific your feature request is, the easier it will be for us to evaluate and potentially implement it. 36 | 37 | ### Pull Requests 38 | 39 | We follow a trunk-based development model for our codebase. This means that all changes should be made in short-lived feature branches and merged directly into the `main` branch through pull requests. Please follow these guidelines to ensure a smooth and efficient contribution process: 40 | 41 | 1. Create a new branch from the latest `main` branch for each new feature or bug fix. Use a descriptive branch name that reflects the changes being made. 42 | 2. Keep your changes focused and limited to a single feature or bug fix. If you find another issue while working on your branch, create a new issue and a separate branch for it. 43 | 3. Commit your changes using the [Conventional Commits](https://www.conventionalcommits.org/) standard. This helps maintain a clean and readable commit history. Your commit messages should follow this format: `(): `. For example: 44 | ``` 45 | fix(auth): resolve login failure due to incorrect token validation 46 | ``` 47 | 4. Sign your commits using GPG. This adds an extra layer of security and ensures the authenticity of your contributions. Follow the [GitHub documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) to set up and use GPG. 48 | 5. Before submitting a pull request, ensure that your changes are up-to-date with the latest `main` branch. Rebase your branch if necessary. 49 | 6. Open a pull request, and provide a clear and concise title and description that follows the Conventional Commits standard. Include any relevant issue numbers in the description by using keywords like "Closes #123" or "Fixes #123". 50 | 7. Request a review from one or more project maintainers or collaborators. Be prepared to address any feedback, suggestions, or requested changes. 51 | 8. Once your pull request is approved and all tests have passed, a project maintainer will squash and merge your changes into the `main` branch. 52 | 53 | Please note that pull requests that do not follow the guidelines or are incomplete may experience a delay in order to gather additional information. We appreciate your understanding and cooperation in maintaining a high-quality codebase. 54 | 55 | ### Coding Standards 56 | 57 | Adhering to a consistent set of coding standards is crucial for maintaining a clean, readable, and maintainable codebase. By following these guidelines, you help ensure that SCPkit remains a high-quality project that is easy to understand, modify, and extend. 58 | 59 | Here are some general guidelines for writing code that conforms to the project's coding standards: 60 | 61 | 1. Write clean and readable code: Keep your code simple, clear, and concise. Use descriptive variable and function names, add comments to explain complex or non-obvious parts of the code, and break down large functions or classes into smaller, more manageable pieces. 62 | 63 | 2. Optimize for performance and security: Write code that is efficient, secure, and avoids common pitfalls and vulnerabilities. Be mindful of performance bottlenecks, memory leaks, and security risks when designing and implementing your solutions. 64 | 65 | 3. Document your code: Include comments and docstrings to explain the purpose, functionality, and usage of your code. This helps other contributors understand your code and makes it easier for them to maintain and extend it. 66 | 67 | 4. Stay consistent with existing code: When making changes or additions to the codebase, try to match the style, structure, and conventions of the existing code. This ensures that the codebase remains coherent and easy to navigate. 68 | 69 | 5. Adhere to the DRY (Don't Repeat Yourself) principle: Avoid duplicating code and logic across the codebase. Instead, refactor and reuse code whenever possible to minimize maintenance overhead and potential inconsistencies. 70 | 71 | To contribute code that follows the project's coding standards: 72 | 73 | 1. Create a new branch from the latest `main` branch. 74 | 2. Make your changes or additions to the code, following the guidelines above. 75 | 3. Commit your changes using the [Conventional Commits](https://www.conventionalcommits.org/) standard, as described in the [Pull Requests](#pull-requests section. 76 | 4. Sign your commits using GPG, as described in the [Pull Requests](#pull-requests) section. 77 | 5. Open a pull request with a clear and concise title and description, following the Conventional Commits standard. 78 | 6. Request a review from one or more project maintainers or collaborators. 79 | 80 | By adhering to the project's coding standards, you help create a strong foundation for the continued growth and success of SCPkit. Thank you for your commitment to maintaining a high-quality codebase! 81 | 82 | ### Testing 83 | 84 | Thorough and consistent testing is essential for maintaining the stability, reliability, and security of SCPkit. By contributing tests, you help ensure that the project remains robust and resistant to bugs, regressions, and vulnerabilities. Here are some guidelines for writing and contributing tests: 85 | 86 | 1. Follow the testing framework and conventions: Familiarize yourself with the testing framework, tools, and conventions used in the project. Write tests that are consistent with the existing test suite and follow best practices. 87 | 2. Write tests for new features and bug fixes: When adding a new feature or fixing a bug, make sure to include tests that cover the changes. This helps prevent regressions and ensures that the changes work as intended across different environments and configurations. 88 | 3. Improve existing tests: If you find existing tests that are incomplete, unclear, or lacking in coverage, feel free to improve them. This may involve refactoring, adding new test cases, or improving test descriptions. 89 | 4. Ensure tests are reliable and maintainable: Write tests that are easy to understand, maintain, and update. Avoid using hard-coded values, magic numbers, or brittle logic that may cause tests to fail unexpectedly or become difficult to maintain. 90 | 5. Run tests locally: Before submitting a pull request, run the entire test suite locally to ensure that your changes do not introduce new test failures or break existing functionality. 91 | 92 | To contribute tests, follow the same process as for code contributions: 93 | 94 | 1. Create a new branch from the latest `main` branch. 95 | 2. Add or modify tests as needed. 96 | 3. Commit your changes using the [Conventional Commits](https://www.conventionalcommits.org/) standard, with a `test` type in the commit message. For example: 97 | ``` 98 | test(auth): add test cases for login failure scenarios 99 | ``` 100 | 4. Sign your commits using GPG, as described in the [Pull Requests](#pull-requests) section. 101 | 5. Open a pull request with a clear and concise title and description, following the Conventional Commits standard. 102 | 6. Request a review from one or more project maintainers or collaborators. 103 | 104 | Your contributions to the project's test suite help ensure the long-term quality and stability of SCPkit. Thank you for your efforts in keeping the project robust and reliable! 105 | 106 | ### Documentation 107 | 108 | Well-written and up-to-date documentation is crucial for the success and usability of any project. Contributions to improve the documentation are highly appreciated and play a vital role in helping other users and developers understand, use, and extend SCPkit. 109 | 110 | Here are some guidelines for contributing to the project's documentation: 111 | 112 | 1. Stay consistent: Follow the existing documentation style, structure, and format. This ensures that the documentation remains coherent and easy to navigate. 113 | 2. Use clear and concise language: Write in simple, easy-to-understand language. Avoid jargon, complex sentences, or unnecessary information. Keep in mind that the documentation should be accessible to users with various levels of expertise. 114 | 3. Provide examples: Whenever possible, include examples, code snippets, or screenshots to illustrate your points. This helps users better understand the concepts and apply them in practice. 115 | 4. Keep documentation up-to-date: Make sure to update the documentation whenever you make changes to the code, fix bugs, or introduce new features. Outdated documentation can cause confusion and hinder the adoption of the project. 116 | 5. Proofread and review: Before submitting your changes, proofread your work to ensure it is free of grammatical errors, typos, or inconsistencies. Request reviews from other contributors or maintainers to ensure the quality and accuracy of the documentation. 117 | 6. Organize and structure: Ensure that the documentation is well-organized and structured, with a logical flow of information. Use headings, subheadings, and lists to break up large blocks of text and make the content more readable. 118 | 119 | To contribute to the documentation, follow the same process as for code contributions: 120 | 121 | 1. Create a new branch from the latest `main` branch. 122 | 2. Make your changes or additions to the documentation. 123 | 3. Commit your changes using the [Conventional Commits](https://www.conventionalcommits.org/) standard, with a `docs` type in the commit message. For example: 124 | ``` 125 | docs(api): add examples and improve clarity for authentication section 126 | ``` 127 | 4. Sign your commits using GPG, as described in the [Pull Requests](#pull-requests) section. 128 | 5. Open a pull request with a clear and concise title and description, following the Conventional Commits standard. 129 | 6. Request a review from one or more project maintainers or collaborators. 130 | 131 | By contributing to the documentation, you're helping to make SCPkit more accessible and user-friendly for everyone. Thank you for your valuable contributions! 132 | 133 | ### Community 134 | 135 | We are committed to fostering an open, inclusive, and welcoming community around SCPkit. By contributing to this project, you are joining a diverse group of developers, users, and enthusiasts who share a common passion for improving and advancing cybersecurity. 136 | 137 | Here are some ways to get involved and stay connected with the community: 138 | 139 | 140 | 1. Join the discussion: Participate in the project's discussions on GitHub, forums, mailing lists, or chat rooms. Ask questions, share your ideas, or help others with their issues. 141 | 2. Attend community events: Look for meetups, conferences, webinars, or other events related to SCPkit and cybersecurity. These gatherings provide opportunities for learning, networking, and collaboration. 142 | 3. Spread the word: Share your experiences and knowledge about SCPkit with your network, colleagues, or friends. Write blog posts, create tutorials or present talks about the project and its benefits. 143 | 4. Provide feedback: Your feedback is invaluable to the continued development and improvement of the project. Share your thoughts on new features, report bugs, or suggest enhancements through GitHub issues or other communication channels. 144 | 5. Support other contributors: Encourage and support fellow contributors by reviewing their pull requests, answering their questions, or providing mentorship. 145 | 6. Stay up-to-date: Follow the project's news, announcements, and releases on social media, newsletters, or the project's website. This will help you stay informed about the latest developments and opportunities for collaboration. 146 | 147 | Remember that everyone in the community is expected to follow the [Code of Conduct][code-of-conduct] and contribute respectfully and constructively. Let's work together to make SCPkit a thriving and successful project that benefits everyone involved. 148 | 149 | 150 | 151 | [code-of-conduct]: https://github.com/aquia-inc/scpkit/blob/main/CODE_OF_CONDUCT.md 152 | 153 | 154 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SCPkit 2 | [![GitHub Super-Linter](https://github.com/aquia-inc/scpkit/workflows/Lint%20Code%20Base/badge.svg)](https://github.com/aquia-inc/scpkit/actions/workflows/linter.yaml) 3 | 4 | ## Overview 5 | 6 | This project provides a Python module to aid in Service Control Policy (SCP) management in AWS accounts. 7 | 8 | SCPs have a current limit of 5 total per entity, and a size limit on each of 5120 characters. This tool will merge selected SCPs into the fewest amount of policies, and optionally remove whitespace characters as they count toward the character limit. 9 | 10 | 11 | ```mermaid 12 | stateDiagram-v2 13 | [SCPTool] --> Validate 14 | [SCPTool] --> Merge 15 | [SCPTool] --> Visualize 16 | Merge --> Validate 17 | Validate --> [*] 18 | Merge --> [*] 19 | Visualize --> [*] 20 | ``` 21 | ## Using SCPkit 22 | SCPkit can be installed from PyPI 23 | ``` 24 | pip install scpkit 25 | ``` 26 | 27 | ### Validating a directory of SCPs 28 | Validating a directory requires active AWS credentials through a profile or environment. SCPkit will recursively search the directory for json files and validate them with [Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-validation.html)'s [ValidatePolicy API](https://docs.aws.amazon.com/access-analyzer/latest/APIReference/API_ValidatePolicy.html). 29 | ``` 30 | scpkit validate --sourcefiles /path/to/scps --profile yourawsprofile --outdir /path/to/findings 31 | ``` 32 | 33 | ### Merging a directory of SCPs 34 | Merging a directory of SCPs does not require active AWS credentials, but can optionally validate after merging. 35 | ``` 36 | scpkit merge --sourcefiles /path/to/scps --outdir /path/to/directory 37 | ``` 38 | Optional validation with output locally: 39 | ``` 40 | scpkit merge --sourcefiles /path/to/scps --outdir /path/to/directory --validate-after-merge --profile yourawsprofile 41 | ``` 42 | 43 | ### Creating a visualization of an AWS Organization, OUs, Accounts, and SCPs 44 | Creating this visualization requires you be authenticated with either the Org management account, or a delegated administrator. See the [AWS Documentation](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_delegate_policies_example_view_accts_orgs.html) page for more info on delegating Organizations. 45 | 46 | This will output a graph pdf and graphviz data file in the specified directory (or local directory, if outdir is not specified.) 47 | 48 | ``` 49 | scpkit visualize --profile yourawsprofile --outdir ./org-graph 50 | ``` 51 | Accounts are presented as ellipses, organizational units are rectangles, and SCPs are trapezoids. 52 | 53 | ![Visualization of an Organization](./visualize-org.png) 54 | 55 | The full CLI is documented through docopt 56 | ``` 57 | """SCPkit 58 | Usage: 59 | main.py (validate | merge | visualize) [--sourcefiles sourcefiles] [--profile profile] [ --outdir outdir] [--validate-after-merge] [--readable] [--console] 60 | 61 | Options: 62 | -h --help Show this screen. 63 | --version Show version. 64 | --sourcefiles sourcefiles Directory path to SCP files in json format or a single SCP file 65 | --outdir outdir Directory to write new SCP files [Default: ./] 66 | --profile profile AWS profile name 67 | --validate-after-merge Validate the policies after merging them 68 | --readable Leave indentation and some whitespace to make the SCPs readable 69 | --console Adds Log to console 70 | """ 71 | ``` 72 | 73 | ## Local development 74 | From the root of the folder: 75 | ``` 76 | python3 -m venv .venv 77 | source .venv/bin/activate 78 | pip install -r requirements.txt 79 | python -m scpkit.main validate --sourcefiles ./scps --profile yourawsprofile 80 | ``` 81 | Install as a package 82 | ``` 83 | python3 -m venv .venv 84 | source .venv/bin/activate 85 | pip install -U git+https://github.com/aquia-inc/scpkit.git 86 | ``` 87 | 88 | ## References 89 | This project would not be possible without the contributions of the following: 90 | * https://summitroute.com/blog/2020/03/25/aws_scp_best_practices/ 91 | * https://github.com/ScaleSec/terraform_aws_scp 92 | * https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_examples.html 93 | * https://asecure.cloud/l/scp/ 94 | * https://github.com/aws-samples/service-control-policy-examples 95 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest 3 | build 4 | twine -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>=1.28.66 2 | docopt>=0.6.2 3 | graphviz>=0.20.1 -------------------------------------------------------------------------------- /scpkit/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = 0.1 -------------------------------------------------------------------------------- /scpkit/main.py: -------------------------------------------------------------------------------- 1 | """SCPkit 2 | Usage: 3 | main.py (validate | merge | visualize) [--sourcefiles sourcefiles] [--profile profile] [ --outdir outdir] [--validate-after-merge] [--readable] [--console] 4 | 5 | Options: 6 | -h --help Show this screen. 7 | --version Show version. 8 | --sourcefiles sourcefiles Directory path to SCP files in json format or a single SCP file 9 | --outdir outdir Directory to write new SCP files [Default: ./] 10 | --profile profile AWS profile name 11 | --validate-after-merge Validate the policies after merging them 12 | --readable Leave indentation and some whitespace to make the SCPs readable 13 | --console Adds Log to console 14 | """ 15 | from docopt import docopt 16 | from .src.validate import validate_policies 17 | from .src.merge import scp_merge 18 | from .src.util import get_files_in_dir 19 | from .src.visualize import visualize_policies 20 | 21 | def main(): 22 | arguments = { 23 | k.lstrip('-'): v for k, v in docopt(__doc__).items() 24 | } 25 | 26 | if arguments.get("visualize"): 27 | visualize_policies(arguments['profile'], arguments['outdir']) 28 | else: 29 | arguments['scps'] = get_files_in_dir(arguments["sourcefiles"]) 30 | 31 | if arguments.get("merge"): 32 | scp_merge(**arguments) 33 | 34 | if arguments.get("validate"): 35 | validate_policies(arguments['scps'], arguments['profile'], arguments['outdir'], arguments['console']) 36 | 37 | 38 | if __name__ == '__main__': 39 | main() -------------------------------------------------------------------------------- /scpkit/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquia-inc/scpkit/36ca196d39cb7f932652120a34c8ab1d3ad81dc8/scpkit/src/__init__.py -------------------------------------------------------------------------------- /scpkit/src/merge.py: -------------------------------------------------------------------------------- 1 | from .util import write_json, find_key_in_json, load_json, make_actions_and_resources_lists, dump_json, get_files_in_dir 2 | from copy import deepcopy 3 | from pathlib import Path 4 | from .model import SCP 5 | from .validate import validate_policies 6 | from itertools import groupby 7 | 8 | def sort_list_of_dicts(content): 9 | """Sorts a list of dictionaries 10 | Args: 11 | content ([list]): List containing dictionaries 12 | Returns: 13 | [list]: Sorted list of dictionaries 14 | """ 15 | content.sort(key=lambda x: sum(len(str(v)) + len(str(k)) 16 | for k, v in x.items())) 17 | return content 18 | 19 | 20 | def merge_json(json_blobs): 21 | """Combines all of the JSON in an array into one large array. Finds the Statement key in each JSON file and returns that. 22 | Args: 23 | json_blobs (list): list of json dicts 24 | Returns: 25 | [list]: List of all SIDs across all JSON files. 26 | """ 27 | content = [item for blob in json_blobs for item in find_key_in_json( 28 | blob, 'Statement')] 29 | return content 30 | 31 | 32 | def make_policies(content, readable, max_size: int = 5120): 33 | """Combines the policies in order, counts the characters, and starts a new file when it goes over the limit. 34 | Theres probably a better way to do this with permutations, but that could also be resource intensive. 35 | 36 | Args: 37 | content (list): List of Sid dictionaries (in order of smallest to largest preferred) 38 | max_size (int, optional): Max character count. Defaults to 5120. 39 | Returns: 40 | list: List of condensed SCP documents. 41 | """ 42 | file_list = [] 43 | stage = {"Version": "2012-10-17", "Statement": []} 44 | total_chars = 0 45 | 46 | for sid in content: 47 | 48 | # Get the number of characters for the Sid 49 | chars = len((dump_json(sid, readable=readable)).encode('utf-8')) 50 | 51 | # If the total number of characters plus the sid exceeds the max, make a new policy document 52 | if (total_chars + chars) > max_size: 53 | file_list.append(deepcopy(stage)) 54 | stage = {"Version": "2012-10-17", "Statement": []} 55 | total_chars = 0 56 | 57 | # Keep a running tally of the total characters, append the Sid to the policy doc and remove it from the content. 58 | total_chars = total_chars+chars 59 | stage['Statement'].append(sid) 60 | 61 | if total_chars > 0: 62 | file_list.append(stage) 63 | return file_list 64 | 65 | 66 | def scp_merge(**kwargs): 67 | """This is the main function that grabs the files, transforms, and writes new files. 68 | """ 69 | all_scps = [ scp.content for scp in kwargs['scps'] ] 70 | 71 | merged_scps = merge_json(all_scps) 72 | 73 | cleaned_scps = make_actions_and_resources_lists(merged_scps) 74 | 75 | # combine statements with same condition+resource+effect 76 | merged_scps = combine_similar_sids(cleaned_scps) 77 | 78 | sort_list_of_dicts(merged_scps) 79 | 80 | new_policies = make_policies(merged_scps, readable=kwargs.get("readable")) 81 | 82 | write_json(new_policies, kwargs['outdir'], readable=kwargs.get("readable")) 83 | 84 | if kwargs.get("validate-after-merge"): 85 | scps = [ SCP(name=i, content=scp) for i, scp in enumerate(new_policies, 1) ] 86 | validate_policies(scps, kwargs['profile'], kwargs['outdir']) 87 | 88 | 89 | def combine_similar_sids(content): 90 | """Combines SIDs that have the same Resource, Effect, and Condition (if exists) 91 | 92 | Args: 93 | content (list): List of SCP dictionaries 94 | 95 | Returns: 96 | list: List of SCP dictionaries minimized where possible. 97 | """ 98 | # groupby works best when dicts are sorted, this sorts by condition, resource, and effect 99 | content.sort(key= lambda x: (x.get("Condition") is not None, x['Resource'], x["Effect"]), reverse=True) 100 | 101 | # groups the sids that have the same condition, resource, and effect 102 | grouped_data = groupby(content, key=lambda x: (x["Resource"], x["Effect"], x.get("Condition"))) 103 | 104 | merged_content = [] 105 | 106 | # walk through the groups 107 | for (resource, effect, condition), group in grouped_data: 108 | new_dict = {"Effect":effect, "Resource":resource} 109 | 110 | 111 | if condition is not None: 112 | new_dict["Condition"] = condition 113 | 114 | # handling combining actions 115 | new_dict["Action"] = [] 116 | new_dict["NotAction"] = [] 117 | for g in list(group): 118 | if g.get("NotAction"): 119 | new_dict.pop("Action", None) 120 | for action in g.get("NotAction"): 121 | new_dict["NotAction"].append(action) 122 | else: 123 | new_dict.pop("NotAction", None) 124 | for action in g.get("Action"): 125 | new_dict["Action"].append(action) 126 | merged_content.append(new_dict) 127 | 128 | return merged_content 129 | -------------------------------------------------------------------------------- /scpkit/src/model.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | class SCP: 5 | """Main class for a single Service Control Policy 6 | """ 7 | def __init__(self, name, content): 8 | """ 9 | Args: 10 | name (str): name or short filename for scp 11 | content (dict): The SCP content 12 | """ 13 | self.name = name 14 | self.content = content 15 | 16 | def validate(self, aa_client): 17 | """Runs the SCP through Access Analyzer validate_policy command and adds findings to self.findings 18 | 19 | Args: 20 | aa_client ([client]): Authenticated access analyzer boto client to analyze SCPs 21 | """ 22 | self.findings = aa_client.validate_policy(policyDocument=self.json, policyType="SERVICE_CONTROL_POLICY").get("findings") 23 | 24 | @property 25 | def json(self): 26 | """JSON.dumps with no spaces in separators 27 | Returns: 28 | str: string of condensed json 29 | """ 30 | return json.dumps(self.content, separators=(',', ':')) 31 | 32 | @property 33 | def pretty_json(self): 34 | """JSON.dumps readable indented SCP 35 | Returns: 36 | str: string of readable json 37 | """ 38 | return json.dumps(self.content, indent=2) 39 | 40 | @property 41 | def findings_json(self): 42 | """JSON.dumps readable indented findings 43 | Returns: 44 | str: string of readable json 45 | """ 46 | return json.dumps(self.findings, indent=2) 47 | 48 | def write_findings_for_scp(self, directory): 49 | p = Path(directory) 50 | if not p.is_dir(): 51 | p.mkdir() 52 | with open(f'{p}/{self.name}-findings.json', 'w') as f: 53 | json.dump(self.findings, f, indent=2) -------------------------------------------------------------------------------- /scpkit/src/util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from pathlib import Path 4 | from .model import SCP 5 | 6 | 7 | def load_json(filepath): 8 | """Loads json content from files 9 | Args: 10 | filepath (str): Path to a file containing json 11 | Returns: 12 | [dict]: JSON loaded from the file 13 | """ 14 | with open(filepath) as f: 15 | data = json.load(f) 16 | return data 17 | 18 | 19 | def write_json(content, directory, readable=False): 20 | """Writes json to a file 21 | Args: 22 | content (dict): JSON to write 23 | directory ([type]): File output location 24 | """ 25 | i = 0 26 | for scp in content: 27 | i = i + 1 28 | p = Path(directory) 29 | if not p.is_dir(): 30 | p.mkdir() 31 | with open(f'{p}/scp-{i}.json', 'w') as f: 32 | if readable: 33 | json.dump(scp, f, indent=2) 34 | else: 35 | json.dump(scp, f, separators=(',', ':')) 36 | 37 | 38 | def dump_json(content, readable=False): 39 | """Dumps json to a string with either indent 2 or smashed together and no whitespace 40 | 41 | Args: 42 | content (dict): SCP 43 | readable (bool, optional): If true, adds indent=2 to json, otherwise smashes it all together. Defaults to False. 44 | 45 | Returns: 46 | str: SCP 47 | """ 48 | if readable: 49 | return json.dumps(content, indent=2) 50 | else: 51 | return json.dumps(content) 52 | 53 | 54 | def get_files_in_dir(filepath): 55 | """Loads all JSON files from a directory 56 | Args: 57 | filepath (str): Folder that contains JSON files or an individual json file 58 | Returns: 59 | [list]: list of JSON content from all files 60 | """ 61 | 62 | p = Path(filepath) 63 | 64 | if not p.exists(): 65 | raise FileNotFoundError(f"The file {p} does not exist.") 66 | 67 | if p.is_dir(): 68 | p = list(p.glob('**/*.json')) 69 | elif p.is_file(): 70 | p = [p] 71 | else: 72 | raise Exception 73 | 74 | all_content = [ SCP(name=file.name, content=load_json(file)) for file in p ] 75 | return all_content 76 | 77 | 78 | def find_key_in_json(content, key_to_find): 79 | """Recursive function to find a key 80 | Args: 81 | content ([dict]): IAM Policy document 82 | key_to_find ([str]): str of key to find, example: 'Statement' 83 | Returns: 84 | [list]: Contents of "key_to_find" 85 | """ 86 | for key, value in content.items(): 87 | if key.lower() == key_to_find.lower(): 88 | 89 | # Normalize Statement content into a list. It is valid to not be a list. 90 | if type(content[key]) is not list: 91 | content[key] = [content[key]] 92 | return content[key] 93 | 94 | # If we havent found the content and the value is not a str, iterate through. 95 | elif type(value) is not str: 96 | find_key_in_json(content[key], key_to_find) 97 | 98 | 99 | def make_actions_and_resources_lists(content): 100 | """Makes Actions and Resources values lists if they are not lists. 101 | 102 | Args: 103 | content (list): List of SIDs 104 | 105 | Returns: 106 | list: List of SIDs that have actions and resources in list format rather than string 107 | """ 108 | for sid in content: 109 | if sid.get("Action") and type(sid.get("Action")) is not list: 110 | sid["Action"] = [sid.get("Action")] 111 | if sid.get("NotAction") and type(sid.get("NotAction")) is not list: 112 | sid["NotAction"] = [sid.get("NotAction")] 113 | # no such thing as NotResource in SCPs - https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_syntax.html#scp-elements-table 114 | if type(sid.get("Resource")) is not list: 115 | sid["Resource"] = [sid.get("Resource")] 116 | return content 117 | 118 | 119 | def create_session(profile=None): 120 | """Creates a boto session 121 | 122 | Args: 123 | profile (string): AWS profile name 124 | 125 | Returns: 126 | [object]: Authenticated Boto3 session 127 | """ 128 | if profile: 129 | return boto3.Session(profile_name=profile) 130 | else: 131 | return boto3.Session() 132 | 133 | 134 | def create_client(session, service): 135 | """Creates a service client from a boto session 136 | 137 | Args: 138 | session (object): Authenicated boto3 session 139 | service (string): service name to create the client for 140 | 141 | Returns: 142 | [object]: client session for specific aws service (eg. accessanalyzer) 143 | """ 144 | return session.client(service) 145 | 146 | 147 | def paginate(service, method, **method_args): 148 | """Paginates through the results of a method. 149 | 150 | Args: 151 | service (boto3.client): The AWS service client. 152 | method (str): The name of the method to paginate. 153 | method_args (dict): The arguments to pass to the method. 154 | 155 | Returns: 156 | list: A list of paginated results. 157 | """ 158 | paginator = service.get_paginator(method) 159 | results = paginator.paginate(**method_args) 160 | return results -------------------------------------------------------------------------------- /scpkit/src/validate.py: -------------------------------------------------------------------------------- 1 | from .util import create_session, create_client 2 | 3 | 4 | def validate_policies(scps, profile, outdir=None, console=False): 5 | """Validates SCPs 6 | 7 | Args: 8 | scps (list of objects): SCP objects 9 | profile (object): AWS profile name 10 | """ 11 | access_analyzer = create_client(create_session(profile), "accessanalyzer") 12 | 13 | for scp in scps: 14 | if(console): 15 | print(f"🧪 Validate SCP: {scp.name}") 16 | scp.validate(access_analyzer) 17 | if scp.findings: 18 | if(console): 19 | print(f" 🚨 Error(s) in {scp.name}:") 20 | for finding in scp.findings: 21 | print(f" {finding['issueCode']} - {finding['findingDetails']}") 22 | if outdir: 23 | scp.write_findings_for_scp(outdir) 24 | if(console): 25 | print(f" ℹ️ More details check log file {outdir}/{scp.name}-findings.json") 26 | else: 27 | print(scp.findings_json) 28 | if(console): 29 | print(f" ℹ️ More details check log file ./{scp.name}-findings.json") -------------------------------------------------------------------------------- /scpkit/src/visualize.py: -------------------------------------------------------------------------------- 1 | from graphviz import Digraph 2 | from .util import create_session, paginate 3 | 4 | 5 | def add_child_nodes(ou_id, org_client, graph): 6 | """Adds child nodes to the graph. 7 | 8 | Args: 9 | ou_id (str): The ID of the organizational unit. 10 | org_client (boto3.client): The AWS Organizations client. 11 | graph (Digraph): The Graphviz Digraph object. 12 | 13 | """ 14 | accounts = list_children(org_client, ou_id, 'ACCOUNT') 15 | ous = list_children(org_client, ou_id, 'ORGANIZATIONAL_UNIT') 16 | 17 | children = accounts + ous 18 | 19 | if children: 20 | for child in children: 21 | child_id = child['Id'] 22 | child_type = child['Type'] 23 | 24 | if child_type == 'ACCOUNT': 25 | account = org_client.describe_account(AccountId=child_id).get('Account') 26 | account_name = account.get('Name') 27 | account_id = account.get('Id') 28 | 29 | graph.node(child_id, label=account_name, shape='ellipse') 30 | graph.edge(ou_id, child_id) 31 | 32 | policies = get_policies_for_entity(account_id, org_client) 33 | 34 | add_policies_to_graph(graph, child_id, policies=policies) 35 | 36 | # Get the name of the child (OU or Account) 37 | if child_type == 'ORGANIZATIONAL_UNIT': 38 | current_ou = org_client.describe_organizational_unit(OrganizationalUnitId=child_id).get('OrganizationalUnit') 39 | ou_name = current_ou.get('Name') 40 | current_ou_id = current_ou.get('Id') 41 | 42 | graph.node(child_id, label=ou_name, shape='box') 43 | graph.edge(ou_id, child_id) 44 | 45 | policies = get_policies_for_entity(current_ou_id, org_client) 46 | add_policies_to_graph(graph, child_id, policies=policies) 47 | 48 | add_child_nodes(child_id, org_client, graph) 49 | 50 | 51 | def list_children(org_client, parent_id, child_type): 52 | """Lists the children of a parent entity. 53 | 54 | Args: 55 | org_client (boto3.client): The AWS Organizations client. 56 | parent_id (str): The ID of the parent entity. 57 | child_type (str): The type of the child entities to list. 58 | 59 | Returns: 60 | list: A list of child entities. 61 | """ 62 | all_children = paginate(org_client, 'list_children', ParentId=parent_id, ChildType=child_type) 63 | children = [ child for page in all_children for child in page.get('Children')] 64 | return children 65 | 66 | 67 | def get_policies_for_entity(entity_id, org_client, filter='SERVICE_CONTROL_POLICY'): 68 | """Gets the policies associated with an entity. 69 | 70 | Args: 71 | entity_id (str): The ID of the entity. 72 | org_client (boto3.client): The AWS Organizations client. 73 | filter (str): The filter to apply when retrieving policies. 74 | 75 | Returns: 76 | list: A list of policies associated with the entity. 77 | """ 78 | policies = org_client.list_policies_for_target( 79 | TargetId=entity_id, 80 | Filter=filter 81 | ) 82 | return policies.get('Policies') 83 | 84 | 85 | def add_policies_to_graph(graph, entity_id, policies=None): 86 | """Adds policies to the graph. 87 | 88 | Args: 89 | graph (Digraph): The Graphviz Digraph object. 90 | entity_id (str): The ID of the entity. 91 | policies (list): A list of policies to add to the graph. 92 | """ 93 | if policies: 94 | for policy in policies: 95 | policy_name = policy.get('Name') 96 | graph.node(policy_name, label=policy_name, shape='trapezium') 97 | graph.edge(entity_id, policy_name) 98 | 99 | 100 | def visualize_policies(profile, outdir): 101 | session = create_session(profile) 102 | org_client = session.client('organizations') 103 | 104 | # Initialize a Graphviz Digraph object 105 | graph = Digraph('AWS_Organizations', graph_attr={'rankdir':'LR'}) 106 | 107 | # Get the root information 108 | root_id = org_client.list_roots()['Roots'][0]['Id'] 109 | graph.node(root_id, label="Root", shape='box') 110 | get_policies_for_entity(root_id, org_client) 111 | add_policies_to_graph(graph, root_id) 112 | 113 | # Start building the tree 114 | add_child_nodes(root_id, org_client, graph) 115 | 116 | # Output the graphical tree hierarchy to a file 117 | graph.render(directory=outdir, filename='aws_org_tree', view=True) 118 | 119 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = scpkit 3 | author = Aquia 4 | author_email = info@aquia.us 5 | url = https://www.aquia.us 6 | project_urls = 7 | Bug Tracker = https://github.com/aquia-inc/scpkit/issues 8 | Source = https://github.com/aquia-inc/scpkit 9 | description = This package helps consolidate service control policies in AWS 10 | license = Apache License 2.0 11 | long_description = file: README.md 12 | long_description_content_type = text/markdown 13 | version = 0.1.0 14 | 15 | [options] 16 | zip_safe = False 17 | include_package_data = True 18 | packages = find: 19 | install_requires = file: requirements.txt 20 | 21 | [options.entry_points] 22 | console_scripts = 23 | scpkit = scpkit.main:main 24 | 25 | [options.packages.find] 26 | exclude = 27 | tests* 28 | my_package.tests* 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquia-inc/scpkit/36ca196d39cb7f932652120a34c8ab1d3ad81dc8/tests/__init__.py -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests 3 | """ 4 | 5 | import pytest #pylint: disable=import-errors 6 | 7 | from scpkit.src.util import find_key_in_json, get_files_in_dir, load_json 8 | from scpkit.src.merge import sort_list_of_dicts, merge_json, combine_similar_sids 9 | 10 | 11 | test_scp_1 = { 12 | "Version": "2012-10-17", 13 | "Statement": [ 14 | { 15 | "Sid": "test", 16 | "Action": [ 17 | "s3:PutObject" 18 | ], 19 | "Resource": "*", 20 | "Effect": "Deny", 21 | "Condition": { 22 | "Null": { 23 | "s3:x-amz-server-side-encryption": "true" 24 | }, 25 | "StringNotEquals": { 26 | "s3:x-amz-server-side-encryption": [ 27 | "aws:kms" 28 | ] 29 | } 30 | } 31 | }, 32 | { 33 | "Effect": "Deny", 34 | "Action": [ 35 | "organizations:LeaveOrganization" 36 | ], 37 | "Resource": "*" 38 | } 39 | ] 40 | } 41 | 42 | test_scp_2 = { 43 | "Version": "2012-10-17", 44 | "Statement": [ 45 | { 46 | "Action": "access-analyzer:DeleteAnalyzer", 47 | "Resource": "*", 48 | "Effect": "Deny" 49 | } 50 | ] 51 | } 52 | 53 | 54 | def test_find_key_in_json(): 55 | found_key = find_key_in_json(test_scp_1, "Statement") 56 | statement = test_scp_1.get("Statement") 57 | assert statement == found_key 58 | 59 | 60 | def test_merge_json(): 61 | merged = merge_json([test_scp_1, test_scp_2]) 62 | expected = [] 63 | expected.extend(test_scp_1.get("Statement")) 64 | expected.extend(test_scp_2.get("Statement")) 65 | assert merged == expected 66 | 67 | 68 | # manipulates dictionary - affects subsequent tests 69 | def test_sort_list_of_dicts(): 70 | sorted = sort_list_of_dicts(test_scp_1.get("Statement")) 71 | assert sorted == [ 72 | { 73 | "Effect": "Deny", 74 | "Action": [ 75 | "organizations:LeaveOrganization" 76 | ], 77 | "Resource": "*" 78 | }, 79 | { 80 | "Sid": "test", 81 | "Action": [ 82 | "s3:PutObject" 83 | ], 84 | "Resource": "*", 85 | "Effect": "Deny", 86 | "Condition": { 87 | "Null": { 88 | "s3:x-amz-server-side-encryption": "true" 89 | }, 90 | "StringNotEquals": { 91 | "s3:x-amz-server-side-encryption": [ 92 | "aws:kms" 93 | ] 94 | } 95 | }, 96 | } 97 | ] 98 | 99 | def test_combine_similar_sids(): 100 | test_data = [{ 101 | "Action": ["access-analyzer:DeleteAnalyzer"], 102 | "Resource": ["*"], 103 | "Effect": "Deny" 104 | }, 105 | { 106 | "Effect": "Deny", 107 | "Action": [ 108 | "organizations:LeaveOrganization" 109 | ], 110 | "Resource": ["*"] 111 | }, 112 | { 113 | "Sid": "test", 114 | "Action": [ 115 | "s3:PutObject" 116 | ], 117 | "Resource": ["*"], 118 | "Effect": "Deny", 119 | "Condition": { 120 | "Null": { 121 | "s3:x-amz-server-side-encryption": "true" 122 | }, 123 | "StringNotEquals": { 124 | "s3:x-amz-server-side-encryption": [ 125 | "aws:kms" 126 | ] 127 | } 128 | } 129 | } 130 | ] 131 | result = [{ 132 | "Action": [ 133 | "s3:PutObject" 134 | ], 135 | "Resource": ["*"], 136 | "Effect": "Deny", 137 | "Condition": { 138 | "Null": { 139 | "s3:x-amz-server-side-encryption": "true" 140 | }, 141 | "StringNotEquals": { 142 | "s3:x-amz-server-side-encryption": [ 143 | "aws:kms" 144 | ] 145 | } 146 | } 147 | }, 148 | { 149 | "Action": ["access-analyzer:DeleteAnalyzer", 150 | "organizations:LeaveOrganization"], 151 | "Resource": ["*"], 152 | "Effect": "Deny" 153 | } 154 | ] 155 | scps = combine_similar_sids(test_data) 156 | assert result == scps 157 | 158 | 159 | def test_get_files_in_dir(): 160 | f = get_files_in_dir("./tests/testfiles") 161 | assert len(f) == 2 162 | 163 | def test_get_file_in_dir(): 164 | f = get_files_in_dir("./tests/testfiles/test-scp-1.json") 165 | assert len(f) == 1 -------------------------------------------------------------------------------- /tests/testfiles/test-scp-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Deny", 6 | "Action": [ 7 | "account:DisableRegion", 8 | "account:EnableRegion" 9 | ], 10 | "Resource": "*" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/testfiles/test-scp-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Deny", 6 | "Action": "ec2:RunInstances", 7 | "Resource": "arn:aws:ec2:*:*:instance/*", 8 | "Condition": { 9 | "StringNotEquals": { 10 | "ec2:MetadataHttpTokens": "required" 11 | } 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /visualize-org.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquia-inc/scpkit/36ca196d39cb7f932652120a34c8ab1d3ad81dc8/visualize-org.png --------------------------------------------------------------------------------