standstill 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. standstill-0.2.0/.dockerignore +22 -0
  2. standstill-0.2.0/.github/workflows/release.yml +256 -0
  3. standstill-0.2.0/.gitignore +51 -0
  4. standstill-0.2.0/CHANGELOG.md +43 -0
  5. standstill-0.2.0/CONTRIBUTING.md +54 -0
  6. standstill-0.2.0/Dockerfile +47 -0
  7. standstill-0.2.0/LICENSE +373 -0
  8. standstill-0.2.0/PKG-INFO +885 -0
  9. standstill-0.2.0/README.md +475 -0
  10. standstill-0.2.0/ROADMAP.md +85 -0
  11. standstill-0.2.0/SECURITY.md +53 -0
  12. standstill-0.2.0/detective_controls.yaml +22 -0
  13. standstill-0.2.0/docs/coverage.svg +1 -0
  14. standstill-0.2.0/examples/controls.yaml +23 -0
  15. standstill-0.2.0/examples/detective_controls.yaml +46 -0
  16. standstill-0.2.0/examples/preventive_controls.yaml +37 -0
  17. standstill-0.2.0/examples/proactive_controls.yaml +37 -0
  18. standstill-0.2.0/examples/security_services.yaml +138 -0
  19. standstill-0.2.0/pyproject.toml +83 -0
  20. standstill-0.2.0/standstill/__init__.py +0 -0
  21. standstill-0.2.0/standstill/audit.py +53 -0
  22. standstill-0.2.0/standstill/aws/__init__.py +0 -0
  23. standstill-0.2.0/standstill/aws/account_factory.py +428 -0
  24. standstill-0.2.0/standstill/aws/blueprint.py +350 -0
  25. standstill-0.2.0/standstill/aws/config_recorder.py +339 -0
  26. standstill-0.2.0/standstill/aws/controltower.py +524 -0
  27. standstill-0.2.0/standstill/aws/lake.py +622 -0
  28. standstill-0.2.0/standstill/aws/landing_zone.py +261 -0
  29. standstill-0.2.0/standstill/aws/notifications.py +316 -0
  30. standstill-0.2.0/standstill/aws/organizations.py +139 -0
  31. standstill-0.2.0/standstill/aws/scp.py +191 -0
  32. standstill-0.2.0/standstill/aws/security_services.py +1281 -0
  33. standstill-0.2.0/standstill/aws/session.py +111 -0
  34. standstill-0.2.0/standstill/aws/sso.py +390 -0
  35. standstill-0.2.0/standstill/commands/__init__.py +0 -0
  36. standstill-0.2.0/standstill/commands/_engine.py +462 -0
  37. standstill-0.2.0/standstill/commands/accounts.py +743 -0
  38. standstill-0.2.0/standstill/commands/apply.py +174 -0
  39. standstill-0.2.0/standstill/commands/blueprint.py +400 -0
  40. standstill-0.2.0/standstill/commands/catalog.py +253 -0
  41. standstill-0.2.0/standstill/commands/check.py +42 -0
  42. standstill-0.2.0/standstill/commands/config.py +80 -0
  43. standstill-0.2.0/standstill/commands/disable.py +221 -0
  44. standstill-0.2.0/standstill/commands/lake.py +335 -0
  45. standstill-0.2.0/standstill/commands/lz.py +515 -0
  46. standstill-0.2.0/standstill/commands/notifications.py +223 -0
  47. standstill-0.2.0/standstill/commands/operations.py +125 -0
  48. standstill-0.2.0/standstill/commands/ou.py +231 -0
  49. standstill-0.2.0/standstill/commands/recorder.py +257 -0
  50. standstill-0.2.0/standstill/commands/scp.py +259 -0
  51. standstill-0.2.0/standstill/commands/security.py +767 -0
  52. standstill-0.2.0/standstill/commands/sso.py +350 -0
  53. standstill-0.2.0/standstill/commands/view.py +58 -0
  54. standstill-0.2.0/standstill/config.py +107 -0
  55. standstill-0.2.0/standstill/data/controls_catalog.yaml +193 -0
  56. standstill-0.2.0/standstill/data/security_services_default.yaml +195 -0
  57. standstill-0.2.0/standstill/data/securityhub_resource_types.yaml +132 -0
  58. standstill-0.2.0/standstill/display/__init__.py +0 -0
  59. standstill-0.2.0/standstill/display/_notifications.py +102 -0
  60. standstill-0.2.0/standstill/display/_scp.py +115 -0
  61. standstill-0.2.0/standstill/display/_security.py +350 -0
  62. standstill-0.2.0/standstill/display/_sso.py +133 -0
  63. standstill-0.2.0/standstill/display/renderer.py +467 -0
  64. standstill-0.2.0/standstill/main.py +144 -0
  65. standstill-0.2.0/standstill/models/__init__.py +0 -0
  66. standstill-0.2.0/standstill/models/blueprint_config.py +121 -0
  67. standstill-0.2.0/standstill/models/schemas.py +35 -0
  68. standstill-0.2.0/standstill/models/security_config.py +278 -0
  69. standstill-0.2.0/standstill/state.py +82 -0
  70. standstill-0.2.0/tests/__init__.py +0 -0
  71. standstill-0.2.0/tests/conftest.py +36 -0
  72. standstill-0.2.0/tests/test_account_management.py +1030 -0
  73. standstill-0.2.0/tests/test_apply_extended.py +681 -0
  74. standstill-0.2.0/tests/test_blueprint.py +953 -0
  75. standstill-0.2.0/tests/test_commands.py +495 -0
  76. standstill-0.2.0/tests/test_commands_extra.py +395 -0
  77. standstill-0.2.0/tests/test_config_recorder.py +301 -0
  78. standstill-0.2.0/tests/test_disable.py +162 -0
  79. standstill-0.2.0/tests/test_lake.py +485 -0
  80. standstill-0.2.0/tests/test_landing_zone.py +493 -0
  81. standstill-0.2.0/tests/test_notifications.py +282 -0
  82. standstill-0.2.0/tests/test_organizations.py +69 -0
  83. standstill-0.2.0/tests/test_prod_readiness.py +408 -0
  84. standstill-0.2.0/tests/test_renderer.py +456 -0
  85. standstill-0.2.0/tests/test_scp.py +370 -0
  86. standstill-0.2.0/tests/test_security_config.py +464 -0
  87. standstill-0.2.0/tests/test_security_services.py +699 -0
  88. standstill-0.2.0/tests/test_session.py +19 -0
  89. standstill-0.2.0/tests/test_sso.py +308 -0
  90. standstill-0.2.0/tests/test_standstill_config.py +119 -0
@@ -0,0 +1,22 @@
1
+ .git/
2
+ .github/
3
+ .venv/
4
+ venv/
5
+ env/
6
+ __pycache__/
7
+ *.pyc
8
+ *.pyo
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ .coverage
13
+ htmlcov/
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .mypy_cache/
17
+ tests/
18
+ *.yaml
19
+ *.yml
20
+ !pyproject.toml
21
+ .DS_Store
22
+ Thumbs.db
@@ -0,0 +1,256 @@
1
+ name: CI / Release
2
+
3
+ # Single pipeline, split by domain so a red check tells you exactly what broke:
4
+ #
5
+ # test ─┐
6
+ # security ─┼─► release ─► docker (only on push to main, version is new)
7
+ # package ─┘
8
+ #
9
+ # test / security / package run on every PR and push. release waits for all
10
+ # three, then publishes ONLY on a push to main whose pyproject version has no
11
+ # matching tag yet. docker publishes the image for the same new version.
12
+ #
13
+ # NOTE: this file is intentionally named release.yml. The PyPI Trusted Publisher
14
+ # is bound to the workflow *filename* release.yml -- renaming it would break OIDC
15
+ # publishing until the publisher is reconfigured to match.
16
+
17
+ on:
18
+ pull_request:
19
+ push:
20
+ branches: [main]
21
+
22
+ concurrency:
23
+ # Supersede in-progress PR runs, but never cancel a main run mid-release.
24
+ group: ${{ github.workflow }}-${{ github.ref }}
25
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ # ---- domain: tests ----
32
+ test:
33
+ name: Test (Python ${{ matrix.python-version }})
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ python-version: ["3.11", "3.12", "3.13"]
39
+ steps:
40
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
41
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ cache: pip
45
+ - name: Install (with dev extra)
46
+ run: pip install -e ".[dev]"
47
+ - name: Lint
48
+ run: ruff check .
49
+ - name: Type check (mypy)
50
+ if: matrix.python-version == '3.12'
51
+ run: mypy standstill
52
+ - name: Run tests with coverage
53
+ run: pytest --cov=standstill --cov-report=term-missing --cov-report=xml --cov-fail-under=80 -q
54
+ - name: Upload coverage report
55
+ if: matrix.python-version == '3.12'
56
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
57
+ with:
58
+ name: coverage-xml
59
+ path: coverage.xml
60
+ retention-days: 1
61
+
62
+ # ---- domain: static security ----
63
+ security:
64
+ name: Security (bandit)
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
68
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
69
+ with:
70
+ python-version: "3.12"
71
+ cache: pip
72
+ - name: Install bandit
73
+ run: pip install "bandit[toml]>=1.7,<2"
74
+ # Scan the library only (not tests). Documented skips live in pyproject
75
+ # [tool.bandit]; any new finding outside those classes fails the job.
76
+ - name: Run bandit
77
+ run: bandit -c pyproject.toml -r standstill -q
78
+
79
+ # ---- domain: packaging ----
80
+ package:
81
+ name: Package (build + validate)
82
+ runs-on: ubuntu-latest
83
+ steps:
84
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
85
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
86
+ with:
87
+ python-version: "3.12"
88
+ cache: pip
89
+ - name: Build sdist + wheel
90
+ run: |
91
+ python -m pip install --upgrade build twine
92
+ python -m build
93
+ - name: Validate distribution metadata (twine check)
94
+ run: python -m twine check dist/*
95
+ - name: Clean-install smoke test (wheel imports + console script runs)
96
+ run: |
97
+ python -m venv /tmp/smoke
98
+ /tmp/smoke/bin/pip install --upgrade pip
99
+ /tmp/smoke/bin/pip install dist/*.whl
100
+ /tmp/smoke/bin/python -c "import standstill; from standstill.main import main"
101
+ /tmp/smoke/bin/standstill --help >/dev/null
102
+ # Hand the validated artifacts to the release job so it publishes exactly
103
+ # what was built and tested here (build once, publish the same bytes).
104
+ - name: Upload dist artifact
105
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
106
+ with:
107
+ name: dist
108
+ path: dist/
109
+ retention-days: 7
110
+
111
+ # ---- coverage badge (main only) ----
112
+ badge:
113
+ name: Coverage badge
114
+ needs: test
115
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
116
+ runs-on: ubuntu-latest
117
+ permissions:
118
+ contents: write
119
+ steps:
120
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
121
+ with:
122
+ token: ${{ secrets.GITHUB_TOKEN }}
123
+ - name: Download coverage report
124
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
125
+ with:
126
+ name: coverage-xml
127
+ - name: Generate and commit coverage badge
128
+ run: |
129
+ pip install "genbadge[coverage]"
130
+ genbadge coverage -i coverage.xml -o docs/coverage.svg
131
+ git config user.name "github-actions[bot]"
132
+ git config user.email "github-actions[bot]@users.noreply.github.com"
133
+ git add docs/coverage.svg
134
+ git diff --cached --quiet || git commit -m "chore: update coverage badge [skip ci]"
135
+ git push
136
+
137
+ # ---- domain: release (publish) ----
138
+ release:
139
+ name: Release (PyPI + SBOM + GitHub Release)
140
+ needs: [test, security, package]
141
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
142
+ runs-on: ubuntu-latest
143
+ # Ties the PyPI publish to a GitHub Environment named "pypi" (match this in
144
+ # the PyPI pending/trusted publisher). Add required reviewers to gate here.
145
+ environment: pypi
146
+ permissions:
147
+ contents: write # create the tag + release
148
+ id-token: write # PyPI Trusted Publishing (OIDC) -- no stored token
149
+ outputs:
150
+ released: ${{ steps.check.outputs.release }}
151
+ version: ${{ steps.ver.outputs.version }}
152
+ concurrency:
153
+ # Never let two releases overlap, and never cancel one mid-publish.
154
+ group: release
155
+ cancel-in-progress: false
156
+ steps:
157
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
158
+ with:
159
+ fetch-depth: 0 # need tags to know whether this version was released
160
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
161
+ with:
162
+ python-version: "3.12"
163
+ cache: pip
164
+
165
+ - name: Resolve version from pyproject.toml
166
+ id: ver
167
+ run: |
168
+ v=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
169
+ echo "version=$v" >> "$GITHUB_OUTPUT"
170
+ echo "tag=v$v" >> "$GITHUB_OUTPUT"
171
+
172
+ - name: Has this version already been released?
173
+ id: check
174
+ env:
175
+ GH_TOKEN: ${{ github.token }}
176
+ run: |
177
+ if git rev-parse "refs/tags/${{ steps.ver.outputs.tag }}" >/dev/null 2>&1 \
178
+ || gh release view "${{ steps.ver.outputs.tag }}" >/dev/null 2>&1; then
179
+ echo "release=false" >> "$GITHUB_OUTPUT"
180
+ echo "::notice::${{ steps.ver.outputs.tag }} already exists -- nothing to release."
181
+ else
182
+ echo "release=true" >> "$GITHUB_OUTPUT"
183
+ fi
184
+
185
+ # ---- everything below only runs when the version is new ----
186
+
187
+ - name: Download the validated dist
188
+ if: steps.check.outputs.release == 'true'
189
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
190
+ with:
191
+ name: dist
192
+ path: dist/
193
+
194
+ # Publish to PyPI BEFORE the GitHub Release: the Release step creates the
195
+ # tag the idempotency guard keys on, so keeping it last makes a transient
196
+ # failure retryable. skip-existing keeps re-runs from failing on an
197
+ # already-uploaded (immutable) version.
198
+ - name: Publish to PyPI (Trusted Publishing)
199
+ if: steps.check.outputs.release == 'true'
200
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
201
+ with:
202
+ skip-existing: true
203
+
204
+ - name: Generate CycloneDX SBOM (runtime dependency tree)
205
+ if: steps.check.outputs.release == 'true'
206
+ run: |
207
+ python -m pip install cyclonedx-bom
208
+ # Resolve standstill + its runtime deps into a clean venv so the SBOM
209
+ # reflects what an installed user gets, not the CI/test toolchain.
210
+ python -m venv /tmp/sbomenv
211
+ /tmp/sbomenv/bin/pip install --upgrade pip
212
+ /tmp/sbomenv/bin/pip install .
213
+ python -m cyclonedx_py environment \
214
+ --of JSON \
215
+ --pyproject pyproject.toml \
216
+ -o "standstill-${{ steps.ver.outputs.version }}-sbom.cdx.json" \
217
+ /tmp/sbomenv/bin/python
218
+
219
+ - name: Create GitHub Release
220
+ if: steps.check.outputs.release == 'true'
221
+ env:
222
+ GH_TOKEN: ${{ github.token }}
223
+ run: |
224
+ gh release create "${{ steps.ver.outputs.tag }}" \
225
+ --target "${{ github.sha }}" \
226
+ --title "standstill ${{ steps.ver.outputs.version }}" \
227
+ --generate-notes \
228
+ dist/* "standstill-${{ steps.ver.outputs.version }}-sbom.cdx.json"
229
+
230
+ # ---- domain: container image (publish, same new version) ----
231
+ docker:
232
+ name: Docker image (GHCR)
233
+ needs: [release]
234
+ if: needs.release.outputs.released == 'true'
235
+ runs-on: ubuntu-latest
236
+ permissions:
237
+ contents: read
238
+ packages: write
239
+ steps:
240
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
241
+ # docker/* actions are pinned to their current major tags; pin to SHA when
242
+ # convenient (tracked in ROADMAP).
243
+ - name: Log in to GHCR
244
+ uses: docker/login-action@v3
245
+ with:
246
+ registry: ghcr.io
247
+ username: ${{ github.actor }}
248
+ password: ${{ secrets.GITHUB_TOKEN }}
249
+ - name: Build and push
250
+ uses: docker/build-push-action@v6
251
+ with:
252
+ context: .
253
+ push: true
254
+ tags: |
255
+ ghcr.io/${{ github.repository }}:${{ needs.release.outputs.version }}
256
+ ghcr.io/${{ github.repository }}:latest
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ *.whl
13
+
14
+ # Virtual environments
15
+ .venv/
16
+ venv/
17
+ env/
18
+ .env
19
+
20
+ # Distribution / packaging
21
+ MANIFEST
22
+ pip-wheel-metadata/
23
+
24
+ # Testing
25
+ .coverage
26
+ .coverage.*
27
+ htmlcov/
28
+ .pytest_cache/
29
+ .tox/
30
+
31
+ # Type checkers
32
+ .mypy_cache/
33
+ .pytype/
34
+ .pyre/
35
+
36
+ # Ruff
37
+ .ruff_cache/
38
+
39
+ # Editors
40
+ .idea/
41
+ .vscode/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+
46
+ # OS
47
+ .DS_Store
48
+ Thumbs.db
49
+
50
+ # Standstill runtime state
51
+ .standstill_pending.json
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.2.0] - 2026-08-31
6
+
7
+ ### Added
8
+
9
+ - Global CLI error boundary: expected AWS/runtime failures now surface as a single clean `Error:` line with a non-zero exit instead of a raw traceback.
10
+ - Audit log: every invocation is appended as JSON to `~/.standstill/audit.log` (override with `STANDSTILL_AUDIT_LOG`) with command, args, profile, region, and exit code.
11
+ - `security apply --regions` configures regional services (GuardDuty, Security Hub, Macie, Inspector) across multiple regions in one run.
12
+ - CI: unified pipeline (test / bandit / package → version-detected release) with a `mypy` type-check gate, `bandit` static analysis, SHA-pinned actions, packaging smoke test, and a CycloneDX SBOM on release.
13
+
14
+ ### Fixed
15
+
16
+ - **Account Factory**: `accounts create` / `enroll` / `deregister` called non-existent Control Tower boto3 methods and failed at runtime; reimplemented against the AWS Service Catalog Account Factory product with `describe_record` polling.
17
+ - **Notifications**: `notify setup` now applies the SNS topic policy granting EventBridge `sns:Publish` — previously the rule was created but findings never delivered.
18
+ - **GuardDuty**: org auto-enrollment used the deprecated boolean `AutoEnable` param instead of `AutoEnableOrganizationMembers`.
19
+ - Security status probes now surface access-denied as an error instead of rendering it as "disabled".
20
+ - Exit codes: `sso assign/unassign` and `blueprint apply` return a distinct "not confirmed" code (2) on poll timeout instead of 0 (success) or 1 (failure); SSO polling tolerates transient throttles.
21
+ - `operations list` labels its status column as last-known (it is not refreshed in place).
22
+ - `blueprint apply` now requires `--account` or `--ou` instead of failing deep in the call.
23
+
24
+ ### Removed
25
+
26
+ - The cost/FinOps command surface (`cost report/services/forecast/budgets/anomalies/trail/scan/optimize`) — out of scope for an org-security tool and well served by dedicated OSS.
27
+
28
+ ## [0.1.0] - 2026-04-08
29
+
30
+ Initial public release.
31
+
32
+ ### Added
33
+
34
+ - Declarative control management via YAML with diff-before-apply workflow
35
+ - Bulk enablement of control tiers (preventive, detective, proactive, all) per OU
36
+ - Parallel operation submission and polling bounded by the slowest operation
37
+ - Pending operations journal for resuming after credential expiry
38
+ - Interactive security services wizard (`standstill security init`) covering GuardDuty, Security Hub, Macie, Inspector, and Access Analyzer
39
+ - Config recorder audit and configuration across all org accounts
40
+ - Organization visibility: OU hierarchy, account listing, control status per OU
41
+ - Cross-account role verification (`standstill accounts check-roles`)
42
+ - Bundled control catalog with 1,200+ Control Tower controls
43
+ - Docker image published to GitHub Container Registry
@@ -0,0 +1,54 @@
1
+ # Contributing to standstill
2
+
3
+ ## Getting started
4
+
5
+ ```bash
6
+ git clone https://github.com/dbnz-io/standstill
7
+ cd standstill
8
+ pip install -e ".[dev]"
9
+ ```
10
+
11
+ ## Running tests
12
+
13
+ Tests mock all AWS calls — no real AWS account or credentials required.
14
+
15
+ ```bash
16
+ pytest
17
+ pytest --cov=standstill --cov-report=term-missing
18
+ ```
19
+
20
+ ## Linting
21
+
22
+ ```bash
23
+ ruff check .
24
+ ruff format .
25
+ ```
26
+
27
+ ## Pull requests
28
+
29
+ - Open an issue first for any non-trivial change so we can align on approach before you invest time writing code.
30
+ - Keep PRs focused. One logical change per PR.
31
+ - All tests must pass. Coverage should not decrease.
32
+ - Follow the existing code style — Ruff enforces it.
33
+
34
+ ## Adding or updating controls
35
+
36
+ The control catalog lives in `standstill/data/controls_catalog.yaml`. If Control Tower ships new controls, run:
37
+
38
+ ```bash
39
+ standstill catalog build
40
+ ```
41
+
42
+ This refreshes the catalog from the live Control Tower API. Commit the updated file.
43
+
44
+ ## Reporting bugs
45
+
46
+ Use [GitHub Issues](https://github.com/dbnz-io/standstill/issues). Include:
47
+
48
+ - The command you ran (redact account IDs and ARNs if needed)
49
+ - The full error output
50
+ - Your AWS region and Control Tower version if known
51
+
52
+ ## Security issues
53
+
54
+ Do not open a GitHub issue for security vulnerabilities. See [SECURITY.md](SECURITY.md).
@@ -0,0 +1,47 @@
1
+ # Pin to a specific patch release. Update deliberately when upgrading Python.
2
+ FROM python:3.12-slim AS builder
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ PIP_ROOT_USER_ACTION=ignore
7
+
8
+ WORKDIR /build
9
+
10
+ COPY pyproject.toml LICENSE README.md ./
11
+ COPY standstill/ standstill/
12
+
13
+ RUN pip install build && \
14
+ python -m build --wheel --outdir /dist
15
+
16
+
17
+ # Pin to the same release as the builder.
18
+ FROM python:3.12-slim
19
+
20
+ ENV PYTHONDONTWRITEBYTECODE=1 \
21
+ PYTHONUNBUFFERED=1 \
22
+ PIP_NO_CACHE_DIR=1 \
23
+ PIP_ROOT_USER_ACTION=ignore \
24
+ HOME=/home/standstill
25
+
26
+ LABEL org.opencontainers.image.title="standstill" \
27
+ org.opencontainers.image.description="AWS Control Tower management CLI" \
28
+ org.opencontainers.image.source="https://github.com/dbnz-io/standstill" \
29
+ org.opencontainers.image.licenses="MPL-2.0"
30
+
31
+ # Create a non-root user with a real home directory before installing anything.
32
+ RUN useradd -r -u 1000 -s /sbin/nologin -d /home/standstill standstill && \
33
+ mkdir -p /home/standstill /workspace && \
34
+ chown standstill:standstill /home/standstill /workspace
35
+
36
+ # Install the wheel, then strip pip and setuptools out of the runtime image.
37
+ # The CLI does not need a package manager at runtime.
38
+ COPY --from=builder /dist/*.whl /tmp/wheel/
39
+ RUN pip install /tmp/wheel/*.whl && \
40
+ pip uninstall -y pip setuptools && \
41
+ rm -rf /tmp/wheel
42
+
43
+ WORKDIR /workspace
44
+
45
+ USER standstill
46
+
47
+ ENTRYPOINT ["standstill"]