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.
- standstill-0.2.0/.dockerignore +22 -0
- standstill-0.2.0/.github/workflows/release.yml +256 -0
- standstill-0.2.0/.gitignore +51 -0
- standstill-0.2.0/CHANGELOG.md +43 -0
- standstill-0.2.0/CONTRIBUTING.md +54 -0
- standstill-0.2.0/Dockerfile +47 -0
- standstill-0.2.0/LICENSE +373 -0
- standstill-0.2.0/PKG-INFO +885 -0
- standstill-0.2.0/README.md +475 -0
- standstill-0.2.0/ROADMAP.md +85 -0
- standstill-0.2.0/SECURITY.md +53 -0
- standstill-0.2.0/detective_controls.yaml +22 -0
- standstill-0.2.0/docs/coverage.svg +1 -0
- standstill-0.2.0/examples/controls.yaml +23 -0
- standstill-0.2.0/examples/detective_controls.yaml +46 -0
- standstill-0.2.0/examples/preventive_controls.yaml +37 -0
- standstill-0.2.0/examples/proactive_controls.yaml +37 -0
- standstill-0.2.0/examples/security_services.yaml +138 -0
- standstill-0.2.0/pyproject.toml +83 -0
- standstill-0.2.0/standstill/__init__.py +0 -0
- standstill-0.2.0/standstill/audit.py +53 -0
- standstill-0.2.0/standstill/aws/__init__.py +0 -0
- standstill-0.2.0/standstill/aws/account_factory.py +428 -0
- standstill-0.2.0/standstill/aws/blueprint.py +350 -0
- standstill-0.2.0/standstill/aws/config_recorder.py +339 -0
- standstill-0.2.0/standstill/aws/controltower.py +524 -0
- standstill-0.2.0/standstill/aws/lake.py +622 -0
- standstill-0.2.0/standstill/aws/landing_zone.py +261 -0
- standstill-0.2.0/standstill/aws/notifications.py +316 -0
- standstill-0.2.0/standstill/aws/organizations.py +139 -0
- standstill-0.2.0/standstill/aws/scp.py +191 -0
- standstill-0.2.0/standstill/aws/security_services.py +1281 -0
- standstill-0.2.0/standstill/aws/session.py +111 -0
- standstill-0.2.0/standstill/aws/sso.py +390 -0
- standstill-0.2.0/standstill/commands/__init__.py +0 -0
- standstill-0.2.0/standstill/commands/_engine.py +462 -0
- standstill-0.2.0/standstill/commands/accounts.py +743 -0
- standstill-0.2.0/standstill/commands/apply.py +174 -0
- standstill-0.2.0/standstill/commands/blueprint.py +400 -0
- standstill-0.2.0/standstill/commands/catalog.py +253 -0
- standstill-0.2.0/standstill/commands/check.py +42 -0
- standstill-0.2.0/standstill/commands/config.py +80 -0
- standstill-0.2.0/standstill/commands/disable.py +221 -0
- standstill-0.2.0/standstill/commands/lake.py +335 -0
- standstill-0.2.0/standstill/commands/lz.py +515 -0
- standstill-0.2.0/standstill/commands/notifications.py +223 -0
- standstill-0.2.0/standstill/commands/operations.py +125 -0
- standstill-0.2.0/standstill/commands/ou.py +231 -0
- standstill-0.2.0/standstill/commands/recorder.py +257 -0
- standstill-0.2.0/standstill/commands/scp.py +259 -0
- standstill-0.2.0/standstill/commands/security.py +767 -0
- standstill-0.2.0/standstill/commands/sso.py +350 -0
- standstill-0.2.0/standstill/commands/view.py +58 -0
- standstill-0.2.0/standstill/config.py +107 -0
- standstill-0.2.0/standstill/data/controls_catalog.yaml +193 -0
- standstill-0.2.0/standstill/data/security_services_default.yaml +195 -0
- standstill-0.2.0/standstill/data/securityhub_resource_types.yaml +132 -0
- standstill-0.2.0/standstill/display/__init__.py +0 -0
- standstill-0.2.0/standstill/display/_notifications.py +102 -0
- standstill-0.2.0/standstill/display/_scp.py +115 -0
- standstill-0.2.0/standstill/display/_security.py +350 -0
- standstill-0.2.0/standstill/display/_sso.py +133 -0
- standstill-0.2.0/standstill/display/renderer.py +467 -0
- standstill-0.2.0/standstill/main.py +144 -0
- standstill-0.2.0/standstill/models/__init__.py +0 -0
- standstill-0.2.0/standstill/models/blueprint_config.py +121 -0
- standstill-0.2.0/standstill/models/schemas.py +35 -0
- standstill-0.2.0/standstill/models/security_config.py +278 -0
- standstill-0.2.0/standstill/state.py +82 -0
- standstill-0.2.0/tests/__init__.py +0 -0
- standstill-0.2.0/tests/conftest.py +36 -0
- standstill-0.2.0/tests/test_account_management.py +1030 -0
- standstill-0.2.0/tests/test_apply_extended.py +681 -0
- standstill-0.2.0/tests/test_blueprint.py +953 -0
- standstill-0.2.0/tests/test_commands.py +495 -0
- standstill-0.2.0/tests/test_commands_extra.py +395 -0
- standstill-0.2.0/tests/test_config_recorder.py +301 -0
- standstill-0.2.0/tests/test_disable.py +162 -0
- standstill-0.2.0/tests/test_lake.py +485 -0
- standstill-0.2.0/tests/test_landing_zone.py +493 -0
- standstill-0.2.0/tests/test_notifications.py +282 -0
- standstill-0.2.0/tests/test_organizations.py +69 -0
- standstill-0.2.0/tests/test_prod_readiness.py +408 -0
- standstill-0.2.0/tests/test_renderer.py +456 -0
- standstill-0.2.0/tests/test_scp.py +370 -0
- standstill-0.2.0/tests/test_security_config.py +464 -0
- standstill-0.2.0/tests/test_security_services.py +699 -0
- standstill-0.2.0/tests/test_session.py +19 -0
- standstill-0.2.0/tests/test_sso.py +308 -0
- 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"]
|