├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── publish.yml │ └── scorecards.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── newsfragments ├── .gitkeep └── title_template.j2 ├── pyproject.toml ├── requirements ├── publish.in ├── publish.txt ├── test.in └── test.txt ├── src └── secure_package_template │ ├── __init__.py │ ├── _version.py │ └── py.typed └── tests └── test_secure_package_template.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/ @sethmlarson 2 | pyproject.toml @sethmlarson 3 | src/secure_package_template/_version.py @sethmlarson 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # Security sensitive updates (ie: upgrades which fix vulnerabilities) 4 | # aren't affected by any filtering we define below so this is 5 | # purely to do with keeping dependencies compatible and supported. 6 | updates: 7 | 8 | # These entries are for our development dependencies 9 | # which we want to keep up to date but only really 10 | # care about major versions for supportability. 11 | - package-ecosystem: "github-actions" 12 | directory: "/.github/workflows" 13 | schedule: 14 | interval: "daily" 15 | ignore: 16 | - dependency-name: "*" 17 | update-types: 18 | - "version-update:semver-minor" 19 | - "version-update:semver-patch" 20 | 21 | - package-ecosystem: "pip" 22 | directory: "/requirements" 23 | schedule: 24 | interval: "daily" 25 | allow: 26 | - dependency-type: "direct" 27 | - dependency-type: "indirect" 28 | ignore: 29 | - dependency-name: "*" 30 | update-types: 31 | - "version-update:semver-minor" 32 | - "version-update:semver-patch" 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | permissions: "read-all" 4 | 5 | on: 6 | push: 7 | branches: 8 | - "main" 9 | pull_request: 10 | 11 | defaults: 12 | run: 13 | shell: "bash" 14 | 15 | env: 16 | FORCE_COLOR: "1" 17 | 18 | jobs: 19 | test: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev"] 24 | 25 | name: "Test (${{ matrix.python-version }})" 26 | runs-on: "ubuntu-latest" 27 | continue-on-error: false 28 | 29 | steps: 30 | - name: "Checkout repository" 31 | uses: "actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b" 32 | 33 | - name: "Setup Python" 34 | uses: "actions/setup-python@2c3dd9e7e29afd70cc0950079bde6c979d1f69f9" 35 | with: 36 | python-version: "${{ matrix.python-version }}" 37 | 38 | - name: "Run tests" 39 | run: | 40 | python -m pip install -r requirements/test.txt 41 | python -m pip install . 42 | pytest tests/ 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | schedule: 9 | - cron: "0 0 * * 5" 10 | 11 | permissions: "read-all" 12 | 13 | jobs: 14 | analyze: 15 | name: "Analyze" 16 | runs-on: "ubuntu-latest" 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: ["python"] 26 | 27 | steps: 28 | - name: "Checkout repository" 29 | uses: "actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b" 30 | 31 | - name: "Run CodeQL init" 32 | uses: "github/codeql-action/init@d8c9c723a57c026c525f404cf42aa0575f3f0bd8" 33 | with: 34 | languages: "${{ matrix.language }}" 35 | 36 | - name: "Run CodeQL autobuild" 37 | uses: "github/codeql-action/autobuild@d8c9c723a57c026c525f404cf42aa0575f3f0bd8" 38 | 39 | - name: "Run CodeQL analyze" 40 | uses: "github/codeql-action/analyze@d8c9c723a57c026c525f404cf42aa0575f3f0bd8" 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | 13 | Build: 14 | name: "Build" 15 | runs-on: "ubuntu-latest" 16 | outputs: 17 | hashes: ${{ steps.hash.outputs.hashes }} 18 | 19 | steps: 20 | - name: "Checkout repository" 21 | uses: "actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b" 22 | 23 | - name: "Setup Python" 24 | uses: "actions/setup-python@2c3dd9e7e29afd70cc0950079bde6c979d1f69f9" 25 | with: 26 | python-version: "3.x" 27 | 28 | - name: "Install dependencies" 29 | run: | 30 | python -m pip install -r requirements/publish.txt 31 | 32 | - name: "Build dists" 33 | # Uses 'SOURCE_DATE_EPOCH' for build reproducibility. 34 | run: | 35 | SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \ 36 | python -m build 37 | 38 | # Create hashes of all the built distributables. 39 | # This is the input for "subject" of the SLSA builder. 40 | - name: "Generate hashes" 41 | id: hash 42 | run: | 43 | cd dist && echo "hashes=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT 44 | 45 | - name: "Upload dists" 46 | uses: "actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb" 47 | with: 48 | name: "dist" 49 | path: "dist/" 50 | if-no-files-found: error 51 | retention-days: 5 52 | 53 | Provenance: 54 | needs: ["Build"] 55 | uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0" 56 | permissions: 57 | actions: read 58 | id-token: write 59 | # contents: write is only needed to upload the 60 | # attestation to the GitHub release. 61 | contents: write 62 | with: 63 | base64-subjects: "${{ needs.Build.outputs.hashes }}" 64 | upload-assets: true 65 | 66 | Publish: 67 | name: "Publish" 68 | if: startsWith(github.ref, 'refs/tags/') 69 | needs: ["Build", "Provenance"] 70 | runs-on: "ubuntu-latest" 71 | 72 | permissions: 73 | # contents: write is only needed to upload the 74 | # dists to the GitHub release. 75 | contents: write 76 | 77 | # This permission allows for the gh-action-pypi-publish 78 | # step to access GitHub OpenID Connect tokens. 79 | id-token: write 80 | 81 | # This job requires the 'publish' GitHub Environment to run. 82 | # This value is also set in the Trusted Publisher. 83 | environment: 84 | name: "publish" 85 | 86 | # Now that we've built and attested to the distributables 87 | # provenance we can upload them to PyPI and add to the GitHub release. 88 | steps: 89 | - name: "Download dists" 90 | uses: "actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7" 91 | with: 92 | name: "dist" 93 | path: "dist/" 94 | 95 | - name: "Upload dists to GitHub Release" 96 | env: 97 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 98 | run: | 99 | gh release upload ${{ github.ref_name }} dist/* --repo ${{ github.repository }} 100 | 101 | - name: "Publish dists to PyPI" 102 | uses: "pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d" 103 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: "Scorecards" 2 | 3 | on: 4 | branch_protection_rule: 5 | schedule: 6 | - cron: "0 0 * * 5" 7 | push: 8 | branches: ["main"] 9 | 10 | permissions: "read-all" 11 | 12 | jobs: 13 | analyze: 14 | name: Scorecards analysis 15 | runs-on: "ubuntu-latest" 16 | permissions: 17 | security-events: write 18 | id-token: write 19 | contents: read 20 | actions: read 21 | 22 | steps: 23 | - name: "Checkout repository" 24 | uses: "actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b" 25 | with: 26 | persist-credentials: false 27 | 28 | - name: "Run analysis" 29 | uses: "ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d" 30 | with: 31 | results_file: results.sarif 32 | results_format: sarif 33 | repo_token: ${{ secrets.GITHUB_TOKEN }} 34 | publish_results: true 35 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## 0.7.1 (2023-04-22) 6 | 7 | ### Changed 8 | 9 | - Changed the `build.outputs.hashes` state to use `$GITHUB_OUTPUT` instead of deprecated `::set-output` method. 10 | 11 | 12 | ## 0.7.0 (2023-04-22) 13 | 14 | ### Added 15 | 16 | - Added instructions on how to configure a Trusted Publisher. 17 | 18 | 19 | ## 0.6.0 (2023-01-10) 20 | 21 | ### Added 22 | 23 | - Added the `repo_token` parameter to the `ossf/scorecard-action` GitHub Action. 24 | 25 | - Added documentation on how to upgrade dependencies in lock files manually with `pip-compile` and the `--upgrade-package` option. 26 | 27 | ### Changed 28 | 29 | - Changed Dependabot configuration to reduce the total number of opened pull requests without sacrificing timely security fixes or upgrades signalling a new major version. 30 | 31 | - Changed the `publish` job to only use the `publish` GitHub Environment, rather than both `publish` and `build` jobs. 32 | This means that there will only be one approval required to publish to PyPI since all other steps before can either be 33 | rolled back without harming users (ie deleting GitHub releases, git tags) or are idempotent (provenance attestation). 34 | 35 | 36 | ## 0.5.0 (2022-12-10) 37 | 38 | ### Added 39 | 40 | - Added instructions for configuring signed commits and tags automatically from git. 41 | - Added security policy and instructions for configuring private vulnerability reporting. 42 | 43 | 44 | ## 0.4.0 (2022-12-09) 45 | 46 | ### Added 47 | 48 | - Added scriv for tracking changelog fragments 49 | 50 | ### Changed 51 | 52 | - Changed from flit to hatch for building the package 53 | 54 | ## 0.3.0 55 | 56 | ### Added 57 | 58 | - Added deployment pipeline to PyPI 59 | - Added provenance signing with SLSA GitHub Action 60 | - Added instructions on how to configure branch protections 61 | - Added instructions for opting-in to required 2FA on PyPI 62 | - Added the OpenSSF Scorecard GitHub Action 63 | 64 | ### Changed 65 | 66 | - Changed default permissions to `read-all` for GitHub Actions 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 4 | 5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure Python package template 2 | 3 | [![SLSA level 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev) 4 | [![OpenSSF Scorecards](https://api.securityscorecards.dev/projects/github.com/sethmlarson/secure-python-package-template/badge)](https://deps.dev/pypi/secure-package-template) 5 | 6 | Template for a Python package with a secure 7 | project host and package repository configuration. 8 | 9 | The goals of this project are to: 10 | 11 | - Show how to configure a Python package hosted on GitHub with: 12 | - Operational security best-practices 13 | - Automated publishing to PyPI 14 | - Code quality and vulnerability scanning 15 | - Build reproducibility 16 | - Releases with provenance attestation 17 | - Obtain a perfect rating from [OpenSSF Scorecard](https://github.com/ossf/scorecard) 18 | - [SLSA Level 3](https://slsa.dev) using GitHub OIDC 19 | 20 | ## Configuring git for commit and tag signing 21 | 22 | > **Info** 23 | > Commit and tag signing is a practice that's recommended to avoid commit author spoofing 24 | > but isn't strictly required for a secure project configuration. 25 | > If you'd like to skip this step, you can jump ahead to [creating a GitHub repository](https://github.com/sethmlarson/secure-python-package-template/#creating-the-github-repository). 26 | 27 | Git needs to be configured to be able to sign commits and tags. Git uses GPG for signing, so you need to 28 | [create a GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) 29 | if you don't have one already. Make sure you use a [email address associated with your GitHub account](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address) 30 | as the email address for the key. If you wish to keep your email address private you should use GitHub's provided `noreply` email address. 31 | 32 | ```sh 33 | gpg --full-generate-key 34 | ``` 35 | 36 | After you've generated a GPG key you need to [add the GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account). 37 | Then locally you can [configure git to use your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key): 38 | 39 | ```sh 40 | git config --global --unset gpg.format 41 | ``` 42 | 43 | List GPG secret keys, in this example the key ID is '3AA5C34371567BD2' 44 | 45 | ```sh 46 | $ gpg --list-secret-keys --keyid-format=long 47 | /Users/hubot/.gnupg/secring.gpg 48 | ------------------------------------ 49 | sec 4096R/3AA5C34371567BD2 2016-03-10 [expires: 2017-03-10] 50 | uid Hubot 51 | ssb 4096R/4BB6D45482678BE3 2016-03-10 52 | ``` 53 | 54 | Tell git about your signing key: 55 | 56 | ```sh 57 | git config --global user.signingkey 3AA5C34371567BD2 58 | ```` 59 | 60 | Then tell git to auto-sign commits and tags: 61 | 62 | ```sh 63 | git config --global commit.gpgsign true 64 | git config --global tag.gpgSign true 65 | ``` 66 | 67 | Now all commits and tags you create from this git instances will be signed and show up as "verified" on GitHub. 68 | 69 | ## Creating the GitHub repository 70 | 71 | Clone this repository locally: 72 | 73 | ```sh 74 | git clone ssh://git@github.com/sethmlarson/secure-python-package-template 75 | ``` 76 | 77 | Rename the folder to the name of the package and remove existing git repository: 78 | 79 | ```sh 80 | mv secure-python-package-template package-name 81 | cd package-name 82 | rm -rf .git 83 | ``` 84 | 85 | Create a new git repository and ensure the branch name is `main`: 86 | 87 | ```sh 88 | $ git init 89 | Initialized empty Git repository in .../package-name/.git/ 90 | 91 | $ git status 92 | On branch main 93 | 94 | No commits yet 95 | ... 96 | ``` 97 | 98 | If the branch isn't named `main` you can rename the branch: 99 | 100 | ```sh 101 | git branch -m master main 102 | ``` 103 | 104 | Create an **empty** repository on GitHub. To ensure the repository is empty you shouldn't add a README file, .gitignore file, or a license yet. For the examples below the GitHub repository will be named `sethmlarson/package-name` but you should substitute that with the GitHub repository name you chose. 105 | 106 | We need to tell our git repository about our new GitHub repository: 107 | 108 | ```sh 109 | git remote add origin ssh://git@github.com/sethmlarson/package-name 110 | ``` 111 | 112 | Change all the names and URLs be for your own package. Places to update include: 113 | 114 | - `README.md` 115 | - `pyproject.toml` (`project.name` and `project.urls.Home`) 116 | - `src/{{secure_package_template}}` 117 | - `tests/test_{{secure_package_template}}.py` 118 | 119 | You should also change the license to the one you want to use for the package. Update the value in here: 120 | 121 | - `LICENSE` 122 | - `README.md` 123 | 124 | Now we can create our initial commit: 125 | 126 | ```sh 127 | git add . 128 | 129 | git commit -m "Initial commit" 130 | ``` 131 | 132 | Verify that this commit is signed. If not you should configure git to auto-sign commits: 133 | 134 | ```sh 135 | $ git verify-commit HEAD 136 | gpg: Signature made Fri 15 Jul 2022 10:55:10 AM CDT 137 | gpg: using RSA key 9B2E1343B0B201B8883C79E3A99A0A21AD478212 138 | gpg: Good signature from "Seth Michael Larson " [ultimate] 139 | ``` 140 | 141 | Now we push our commit and branch: 142 | 143 | ```sh 144 | $ git push origin main 145 | 146 | Enumerating objects: 25, done. 147 | Counting objects: 100% (25/25), done. 148 | Delta compression using up to 12 threads 149 | Compressing objects: 100% (21/21), done. 150 | Writing objects: 100% (25/25), 17.92 KiB | 1.28 MiB/s, done. 151 | Total 25 (delta 0), reused 0 (delta 0), pack-reused 0 152 | To ssh://github.com/sethmlarson/package-name 153 | * [new branch] main -> main 154 | ``` 155 | 156 | Success! You should now see the commit and all files on your GitHub repository. 157 | 158 | ## Configuring the GitHub repository 159 | 160 | ### Dependabot 161 | 162 | [Dependabot](https://docs.github.com/en/code-security/dependabot) is a service provided by GitHub that keeps your dependencies up-to-date automatically by creating 163 | pull requests updating individually dependencies on your behalf. Unfortunately, when using Dependabot with any non-trivial number 164 | of dependencies the number of pull requests quickly becomes too much to handle, especially 165 | when you think about a single maintainer needing to manage multiple 166 | projects worth of dependency updates. 167 | 168 | The approach taken with Dependabot in this repository is to keep the number of pull requests from 169 | Dependabot to a minimum while still maintaining a secure and maintained set of 170 | dependencies for developing and publishing packages. The policy is described below: 171 | 172 | - Always create pull requests upgrading dependencies if the pinned version has a public vulnerability. 173 | **This is the default behavior of Dependabot and can't be disabled.** 174 | - Create pull requests when new major versions of development dependencies are made available. 175 | This is important because usually major versions contain backwards-incompatible changes so 176 | may actually require changes on our part. 177 | - Create pull requests when there's a new version of a dependency that carries security sensitive data like 178 | `certifi`. It's always important to have this package be up-to-date to avoid monster-in-the-middle (MITM) attacks. 179 | - All other upgrades to dependencies need to be done manually. These are cases like bug fixes that 180 | are impacting the project or new features. The developer experience here is the same 181 | as if Dependabot wasn't automatically upgrading dependencies. 182 | 183 | You can [read the `dependabot.yml` configuration file](https://github.com/sethmlarson/secure-python-package-template/blob/main/.github/dependabot.yml) to learn how to 184 | encode the above policy or [read the Dependabot documentation](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file) on the configuration format. 185 | 186 | #### Enabling Dependabot 187 | 188 | - Settings > Code security and analysis 189 | - Dependency graph should be enabled. This is the default for public repos. 190 | - Enable Dependabot security updates 191 | 192 | #### Upgrading dependencies manually 193 | 194 | Any upgrades to development dependencies to fix bugs or use new features 195 | will require a manual upgrade instead of relying on Dependabot to keep things up to date 196 | automatically. This can be done by running the following to upgrade only one package: 197 | 198 | ```shell 199 | # We want to only upgrade the 'keyring' package 200 | # so we use the --upgrade-package option. 201 | pip-compile \ 202 | requirements/publish.in \ 203 | -o requirements/publish.txt \ 204 | --no-header \ 205 | --no-annotate \ 206 | --generate-hashes \ 207 | --upgrade-package=keyring 208 | ``` 209 | 210 | ### CodeQL and vulnerable code scanning 211 | 212 | - CodeQL is already configured in `.github/workflows/codeql-analysis.yml` 213 | - Configure as desired after reading the [documentation for CodeQL](https://codeql.github.com/docs). 214 | 215 | ### Protected branches 216 | 217 | - Settings > Branches 218 | - Select the "Add rule" button 219 | - Branch name pattern should be your default branch, usually `main` 220 | - Enable "Require a pull request before merging" 221 | - Enable "Require approvals". To get a perfect score from OpenSSF scorecard metric "[Branch Protection](https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection)" you must set the number of required reviewers to 2 or more. 222 | - Enable "Dismiss stale pull request approvals when new commits are pushed" 223 | - Enable "Require review from Code Owners" 224 | - Enable "Require status checks to pass before merging" 225 | - Add all status checks that should be required. For this template they will be: 226 | - `Analyze (python)` 227 | - `Test (3.8)` 228 | - `Test (3.9)` 229 | - `Test (3.10)` 230 | - Ensure the "source" of all status checks makes sense and isn't set to "Any source". 231 | By default this should be configured properly to "GitHub Actions" for all the above status checks. 232 | - Enable "Require branches to be up to date before merging". **Warning: This will increase the difficulty to receive contributions from new contributors.** 233 | - Enable "Require signed commits". **Warning: This will increase the difficulty to receive contributions from new contributors.** 234 | - Enable "Require linear history" 235 | - Enable "Include administrators". This setting is more a reminder and doesn't prevent administrators from temporarily disabling this setting in order to merge a stuck PR in a pinch. 236 | - Ensure that "Allow force pushes" is disabled. 237 | - Ensure that "Allow deletions" is disabled. 238 | - Select the "Create" button. 239 | 240 | ### Protected tags 241 | 242 | - Settings > Tags > New rule 243 | - Use a pattern of `*` to protect all tags 244 | - Select "Add rule" 245 | 246 | ### Publish GitHub Environment 247 | 248 | - Settings > Environments > New Environment 249 | - Name the environment: `publish` 250 | - Add required reviewers, should be maintainers 251 | - Select "Save protection rules" button 252 | - Select "Protected Branches" in the deployment branches dropdown 253 | - Select "Add secret" in the environment secrets section 254 | - Add the PyPI API token value under `PYPI_TOKEN` 255 | 256 | ### Private vulnerability reporting 257 | 258 | - Settings > Code security and analysis 259 | - Select "Enable" for "Private vulnerability reporting". This will allow 260 | users to privately submit vulnerability reports directly to the repository. 261 | - Update the URL in the `SECURITY.md` file to the URL of your own repository. 262 | 263 | ### Secret scanning 264 | 265 | - Settings > Code security and analysis 266 | - Select "Enable" for "Secret scanning". This will scan and report 267 | published tokens to their respective services (like AWS, GCP, GitHub Tokens, etc) 268 | so they can be revoked before they're used by a malicious party. 269 | - Also enable "Push Protection" which scans incoming commits for secrets before they 270 | are made publicly available. This should provide even more protection from accidentally 271 | publishing secrets to a git repository. 272 | 273 | ## Configuring PyPI 274 | 275 | PyPI is increasing the minimum requirements for account security and credential management to make consuming packages on PyPI more secure. This includes [eventually requiring 2FA for all users and requiring API tokens to publish packages](https://pyfound.blogspot.com/2020/01/start-using-2fa-and-api-tokens-on-pypi.html). Instead of waiting for these best practices to become required we can opt-in to them now. 276 | 277 | ### Opt-in to required 2FA 278 | 279 | If you don't have 2FA enabled on PyPI already there's a section in the [PyPI Help page](https://pypi.org/help) about how to enable 2FA for your account. To make 2FA required for the new project: 280 | 281 | - Open "Your projects" on PyPI 282 | - Select "Manage" for the project 283 | - Settings > Enable 2FA requirement for project 284 | 285 | ### Configuring a Trusted Publisher 286 | 287 | If your project is hosted on GitHub you can take advantage of a new PyPI feature called "[Trusted Publishers](https://docs.pypi.org/trusted-publishers/)". 288 | It's recommended to use a Trusted Publisher over an API key or password because it provides an additional layer of security 289 | by requiring the package to originate from a pre-configured GitHub repository, workflow, and environment. 290 | 291 | There's a [short guide on how to add a Trusted Publisher to the project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/). 292 | Below is an example of how to map the publishing GitHub Workflow definition to the PyPI Trusted Publisher. 293 | 294 | > **Warning** 295 | > Care should be taken that the publishing workflow can only be triggered 296 | > by the GitHub accounts that you intend. Remember that git tags (without Protected Tags enabled) 297 | > only require write access to the repository. This is why GitHub Environments with 298 | > a set of required reviewers is highly recommended to have an explicit list of 299 | > people who are allowed to completely execute the publish job. 300 | 301 | Configuring the Trusted Publisher requires 4 values: 302 | 303 | - GitHub repository owner 304 | - GitHub repository name 305 | - GitHub workflow filename 306 | - GitHub environment name (optional, but highly recommended!) 307 | 308 | Using this repository ([https://github.com/sethmlarson/secure-python-package-template](https://github.com/sethmlarson/secure-python-package-template)) as an example, the values to set up a Trusted Publisher would be: 309 | 310 | - GitHub repository owner: `sethmlarson` 311 | - GitHub repository name: `secure-python-package-template` 312 | - GitHub workflow filename: `publish.yml` 313 | - GitHub environment name: `publish` 314 | 315 | Below is the minimum configurations required from the GitHub Workflow: 316 | 317 | ```yaml 318 | # Filename: '.github/workflows/publish.yml' 319 | # Note that the 'publish.yml' filename doesn't need the '.github/workflows' prefix. 320 | jobs: 321 | publish: 322 | # ... 323 | permissions: 324 | # This permission allows for the gh-action-pypi-publish 325 | # step to access GitHub OpenID Connect tokens. 326 | id-token: write 327 | 328 | # This job requires the 'publish' GitHub Environment to run. 329 | # This value is also set in the Trusted Publisher. 330 | environment: 331 | name: "publish" 332 | 333 | steps: 334 | # - ... 335 | # The 'pypa/gh-action-pypi-publish' action reads OpenID Connect 336 | # Note that there's zero config below, it's all magically handled! 337 | - uses: "pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d" 338 | ``` 339 | 340 | ## Verifying configurations 341 | 342 | ### Verifying reproducible builds 343 | 344 | Find the latest release that was done via the publish GitHub Environment, I used [v0.1.0](https://github.com/sethmlarson/python-package-template/runs/7163956796?check_suite_focus=true) 345 | for this example. 346 | 347 | Open the [corresponding release page on PyPI](https://pypi.org/project/secure-package-template/0.1.0). 348 | Select the "[Download files](https://pypi.org/project/secure-package-template/0.1.0/#files)" tab. 349 | For each `.whl` file select "view hashes" and copy the SHA256 and save the value somewhere (`de58d65d34fe9548b14b82976b033b50e55840324053b5501073cb98155fc8af`) 350 | 351 | Clone the GitHub repository locally. Don't use an existing clone of the repository to avoid tainting the workspace: 352 | 353 | ```sh 354 | git clone ssh://git@github.com/sethmlarson/secure-python-package-template 355 | ``` 356 | 357 | Check out the corresponding git tag. 358 | 359 | ```sh 360 | git checkout v0.1.0 361 | ``` 362 | 363 | Run below command and export the stored value into `SOURCE_DATE_EPOCH`: 364 | 365 | ```sh 366 | $ git log -1 --pretty=%ct 367 | 1656789393 368 | 369 | $ export SOURCE_DATE_EPOCH=1656789393 370 | ``` 371 | 372 | Install the dependencies for publishing and build the package: 373 | 374 | ```sh 375 | python -m pip install -r requirements/publish.txt 376 | python -m build 377 | ``` 378 | 379 | Compare SHA256 hashes with the values on PyPI, they should match the SHA256 values that we saw on PyPI earlier. 380 | 381 | ```sh 382 | $ sha256sum dist/*.whl 383 | de58d65d34fe9548b14b82976b033b50e55840324053b5501073cb98155fc8af 384 | ``` 385 | 386 | ## License 387 | 388 | CC0-1.0 389 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This is where you'd put your project's security policy. Be sure to 4 | enable "Private vulnerability reporting" on GitHub within the "Code security and analysis" 5 | section of repository settings and update the below URL to your repository's (owner/name) slug. 6 | 7 | ## Supported Versions 8 | 9 | Use this section to inform users about which versions of your project are 10 | currently being supported with security updates. 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Vulnerabilities can be disclosed privately by [creating a new security advisory](https://github.com/sethmlarson/secure-python-package-template/security/advisories). 15 | Maintainers will follow up with a fix and coordinate a release within the security advisory. 16 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sethmlarson/secure-python-package-template/1e05186381a52c686246743684df699477da88a3/newsfragments/.gitkeep -------------------------------------------------------------------------------- /newsfragments/title_template.j2: -------------------------------------------------------------------------------- 1 | {{ version }} ({{ date.strftime('%Y-%m-%d') }}) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.6.0,<2"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "secure_package_template" 7 | description = "Template for a Python package with a secure project host and package repository configuration." 8 | authors = [ 9 | {name = "Seth Michael Larson", email = "sethmichaellarson@gmail.com"}, 10 | ] 11 | readme = "README.md" 12 | license = {file = "LICENSE"} 13 | requires-python = ">=3.7,<4" 14 | dynamic = ["version"] 15 | 16 | [project.urls] 17 | Source = "https://github.com/sethmlarson/secure-python-package-template" 18 | 19 | [tool.hatch.version] 20 | path = "src/secure_package_template/_version.py" 21 | 22 | [tool.hatch.build.targets.sdist] 23 | include = [ 24 | "/src", 25 | "/LICENSE", 26 | "/CHANGELOG.md" 27 | ] 28 | 29 | [tool.scriv] 30 | version = "literal: src/secure_package_template/_version.py: __version__" 31 | fragment_directory = "newsfragments" 32 | format = "md" 33 | md_header_level = "2" 34 | entry_title_template = "file: title_template.j2" 35 | -------------------------------------------------------------------------------- /requirements/publish.in: -------------------------------------------------------------------------------- 1 | build 2 | hatch -------------------------------------------------------------------------------- /requirements/publish.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 \ 2 | --hash=sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421 \ 3 | --hash=sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3 4 | build==0.9.0 \ 5 | --hash=sha256:1a07724e891cbd898923145eb7752ee7653674c511378eb9c7691aab1612bc3c \ 6 | --hash=sha256:38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69 7 | certifi==2023.7.22 \ 8 | --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ 9 | --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 10 | cffi==1.15.1 \ 11 | --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ 12 | --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ 13 | --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ 14 | --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ 15 | --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ 16 | --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ 17 | --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ 18 | --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ 19 | --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ 20 | --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ 21 | --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ 22 | --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ 23 | --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ 24 | --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ 25 | --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ 26 | --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ 27 | --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ 28 | --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ 29 | --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ 30 | --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ 31 | --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ 32 | --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ 33 | --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ 34 | --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ 35 | --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ 36 | --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ 37 | --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ 38 | --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ 39 | --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ 40 | --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ 41 | --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ 42 | --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ 43 | --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ 44 | --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ 45 | --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ 46 | --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ 47 | --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ 48 | --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ 49 | --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ 50 | --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ 51 | --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ 52 | --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ 53 | --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ 54 | --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ 55 | --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ 56 | --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ 57 | --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ 58 | --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ 59 | --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ 60 | --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ 61 | --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ 62 | --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ 63 | --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ 64 | --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ 65 | --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ 66 | --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ 67 | --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ 68 | --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ 69 | --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ 70 | --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ 71 | --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ 72 | --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ 73 | --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ 74 | --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 75 | click==8.1.3 \ 76 | --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ 77 | --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 78 | cryptography==41.0.3 \ 79 | --hash=sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306 \ 80 | --hash=sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84 \ 81 | --hash=sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47 \ 82 | --hash=sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d \ 83 | --hash=sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116 \ 84 | --hash=sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207 \ 85 | --hash=sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81 \ 86 | --hash=sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087 \ 87 | --hash=sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd \ 88 | --hash=sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507 \ 89 | --hash=sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858 \ 90 | --hash=sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae \ 91 | --hash=sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34 \ 92 | --hash=sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906 \ 93 | --hash=sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd \ 94 | --hash=sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922 \ 95 | --hash=sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7 \ 96 | --hash=sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4 \ 97 | --hash=sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574 \ 98 | --hash=sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1 \ 99 | --hash=sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c \ 100 | --hash=sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e \ 101 | --hash=sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de 102 | distlib==0.3.6 \ 103 | --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ 104 | --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e 105 | editables==0.3 \ 106 | --hash=sha256:167524e377358ed1f1374e61c268f0d7a4bf7dbd046c656f7b410cde16161b1a \ 107 | --hash=sha256:ee686a8db9f5d91da39849f175ffeef094dd0e9c36d6a59a2e8c7f92a3b80020 108 | filelock==3.8.2 \ 109 | --hash=sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2 \ 110 | --hash=sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c 111 | h11==0.14.0 \ 112 | --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ 113 | --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 114 | hatch==1.6.3 \ 115 | --hash=sha256:650e671ba300318e498ef93bbe3b99b32ce14920764fb8753f89993f63eed79a \ 116 | --hash=sha256:e9b3ceb2d4022c23fffd38aaed97774a6368bfdf0a2a5b67bc88b1577df58e9d 117 | hatchling==1.11.1 \ 118 | --hash=sha256:7ed931b61e845f3c5ed4b1cad981bf5cd07ddb9219eaa986240a63ef6cfbb4a5 \ 119 | --hash=sha256:9f84361f70cf3a7ab9543b0c3ecc64211ed2ba8a606a71eb6a473c1c9b08e1d0 120 | httpcore==0.16.2 \ 121 | --hash=sha256:52c79095197178856724541e845f2db86d5f1527640d9254b5b8f6f6cebfdee6 \ 122 | --hash=sha256:c35c5176dc82db732acfd90b581a3062c999a72305df30c0fc8fafd8e4aca068 123 | httpx==0.23.1 \ 124 | --hash=sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8 \ 125 | --hash=sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19 126 | hyperlink==21.0.0 \ 127 | --hash=sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b \ 128 | --hash=sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4 129 | idna==3.4 \ 130 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ 131 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 132 | importlib-metadata==6.0.0 \ 133 | --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \ 134 | --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d 135 | jaraco-classes==3.2.3 \ 136 | --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ 137 | --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a 138 | jeepney==0.8.0 \ 139 | --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ 140 | --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755 141 | keyring==24.2.0 \ 142 | --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \ 143 | --hash=sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509 144 | markdown-it-py==2.2.0 \ 145 | --hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \ 146 | --hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1 147 | mdurl==0.1.2 \ 148 | --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ 149 | --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba 150 | more-itertools==9.0.0 \ 151 | --hash=sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41 \ 152 | --hash=sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab 153 | packaging==23.0 \ 154 | --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \ 155 | --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97 156 | pathspec==0.10.3 \ 157 | --hash=sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6 \ 158 | --hash=sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6 159 | pep517==0.13.0 \ 160 | --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \ 161 | --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59 162 | pexpect==4.8.0 \ 163 | --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \ 164 | --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c 165 | platformdirs==2.6.0 \ 166 | --hash=sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca \ 167 | --hash=sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e 168 | pluggy==1.0.0 \ 169 | --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ 170 | --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 171 | ptyprocess==0.7.0 \ 172 | --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ 173 | --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 174 | pycparser==2.21 \ 175 | --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ 176 | --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 177 | pygments==2.15.0 \ 178 | --hash=sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094 \ 179 | --hash=sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500 180 | pyperclip==1.8.2 \ 181 | --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 182 | rfc3986[idna2008]==1.5.0 \ 183 | --hash=sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835 \ 184 | --hash=sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97 185 | rich==13.2.0 \ 186 | --hash=sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003 \ 187 | --hash=sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5 188 | secretstorage==3.3.3 \ 189 | --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ 190 | --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 191 | shellingham==1.5.0 \ 192 | --hash=sha256:72fb7f5c63103ca2cb91b23dee0c71fe8ad6fbfd46418ef17dbe40db51592dad \ 193 | --hash=sha256:a8f02ba61b69baaa13facdba62908ca8690a94b8119b69f5ec5873ea85f7391b 194 | sniffio==1.3.0 \ 195 | --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ 196 | --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 197 | tomli-w==1.0.0 \ 198 | --hash=sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463 \ 199 | --hash=sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9 200 | tomlkit==0.11.6 \ 201 | --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \ 202 | --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73 203 | userpath==1.8.0 \ 204 | --hash=sha256:04233d2fcfe5cff911c1e4fb7189755640e1524ff87a4b82ab9d6b875fee5787 \ 205 | --hash=sha256:f133b534a8c0b73511fc6fa40be68f070d9474de1b5aada9cded58cdf23fb557 206 | virtualenv==20.17.1 \ 207 | --hash=sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4 \ 208 | --hash=sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058 209 | zipp==3.11.0 \ 210 | --hash=sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa \ 211 | --hash=sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766 212 | -------------------------------------------------------------------------------- /requirements/test.in: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | attrs==23.1.0 \ 2 | --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ 3 | --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 4 | iniconfig==2.0.0 \ 5 | --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ 6 | --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 7 | packaging==23.0 \ 8 | --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \ 9 | --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97 10 | pluggy==1.0.0 \ 11 | --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ 12 | --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 13 | py==1.11.0 \ 14 | --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ 15 | --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 16 | pytest==7.1.2 \ 17 | --hash=sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c \ 18 | --hash=sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45 19 | tomli==2.0.1 \ 20 | --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ 21 | --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f 22 | -------------------------------------------------------------------------------- /src/secure_package_template/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ as __version__ # noqa: F401 2 | -------------------------------------------------------------------------------- /src/secure_package_template/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.7.1" 2 | -------------------------------------------------------------------------------- /src/secure_package_template/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sethmlarson/secure-python-package-template/1e05186381a52c686246743684df699477da88a3/src/secure_package_template/py.typed -------------------------------------------------------------------------------- /tests/test_secure_package_template.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import secure_package_template 4 | 5 | 6 | def test_version() -> None: 7 | assert isinstance(secure_package_template.__version__, str) 8 | assert re.match(r"^[0-9][0-9\.]*[0-9]$", secure_package_template.__version__) 9 | --------------------------------------------------------------------------------