├── .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 | [](https://slsa.dev)
4 | [](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 |
--------------------------------------------------------------------------------