hyperscale-trust 0.1.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.
- hyperscale_trust-0.1.0/LICENSE +21 -0
- hyperscale_trust-0.1.0/PKG-INFO +305 -0
- hyperscale_trust-0.1.0/README.md +277 -0
- hyperscale_trust-0.1.0/pyproject.toml +94 -0
- hyperscale_trust-0.1.0/pyproject.toml.orig +83 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/__init__.py +3 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/apps.py +12 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/catalogue.py +138 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/claims.py +203 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/conf.py +66 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/documents.py +208 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/evidence.py +190 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/permissions.py +6 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/platform.py +66 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/py.typed +0 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/templates/hyperscale_trust/evidence.html +58 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/templates/hyperscale_trust/policy.md +8 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/templates/hyperscale_trust/trust.html +126 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/urls.py +15 -0
- hyperscale_trust-0.1.0/src/hyperscale/trust/views.py +99 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hyperscale Consulting
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperscale-trust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trust center for Hyperscale Django apps: public policy claims, a questionnaire CSV and a private evidence pack
|
|
5
|
+
Keywords: django,trust-center,security,compliance,evidence
|
|
6
|
+
Author: Andy Caine
|
|
7
|
+
Author-email: Andy Caine <andy@hyperscale.consulting>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 5.2
|
|
13
|
+
Classifier: Framework :: Django :: 6.1
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: django>=5.2
|
|
20
|
+
Requires-Dist: hyperscale-crypto>=0.1.0 ; extra == 'crypto'
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Project-URL: Homepage, https://github.com/hyperscale-consulting/hyperscale-trust
|
|
23
|
+
Project-URL: Repository, https://github.com/hyperscale-consulting/hyperscale-trust
|
|
24
|
+
Project-URL: Changelog, https://github.com/hyperscale-consulting/hyperscale-trust/blob/main/CHANGELOG.md
|
|
25
|
+
Project-URL: Issues, https://github.com/hyperscale-consulting/hyperscale-trust/issues
|
|
26
|
+
Provides-Extra: crypto
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# hyperscale-trust
|
|
30
|
+
|
|
31
|
+
Trust center for Hyperscale Django apps: a public `/trust` page of
|
|
32
|
+
policy-level security claims generated from the app's data classification and
|
|
33
|
+
the platform's published controls, a questionnaire CSV export, and a private
|
|
34
|
+
evidence pack for auditors and buyers under NDA. Imported as
|
|
35
|
+
`hyperscale.trust`.
|
|
36
|
+
|
|
37
|
+
Every claim on the page comes from the host app's data classification, this
|
|
38
|
+
package's control catalogue, or the platform's published document — not from
|
|
39
|
+
prose someone typed into a form — so the page cannot go stale because
|
|
40
|
+
someone forgot to edit it. The only hand-written fields carried through
|
|
41
|
+
`trust.json` are the operator name and the security contact.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add hyperscale-trust
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
INSTALLED_APPS = [
|
|
51
|
+
# ...
|
|
52
|
+
"hyperscale.trust",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
urlpatterns = [
|
|
56
|
+
# ...
|
|
57
|
+
path("trust/", include("hyperscale.trust.urls")),
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`hyperscale/trust/urls.py` sets `app_name = "hyperscale_trust"`, so including
|
|
62
|
+
the module resolves URLs as `hyperscale_trust:page` etc. without an explicit
|
|
63
|
+
`namespace=` (passing one is harmless).
|
|
64
|
+
|
|
65
|
+
The evidence pack's key inventory (`keyset_status.json`,
|
|
66
|
+
`key_rotation_events.csv`) comes from `hyperscale.crypto`. Install it with the
|
|
67
|
+
`crypto` extra:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv add "hyperscale-trust[crypto]"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Without it — or before its tables are migrated — the pack still builds; those
|
|
74
|
+
two files just report that the key inventory is unavailable rather than being
|
|
75
|
+
omitted (see [The evidence pack](#the-evidence-pack)).
|
|
76
|
+
|
|
77
|
+
## Settings
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
|
|
82
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
83
|
+
|
|
84
|
+
HYPERSCALE_TRUST = {
|
|
85
|
+
"trust_json_path": BASE_DIR / "trust.json", # required
|
|
86
|
+
"snapshot_path": BASE_DIR / "platform-controls.json", # required
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Key | Default | Meaning |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `trust_json_path` | none (required) | path to the app's `trust.json` |
|
|
93
|
+
| `snapshot_path` | none (required) | path to the build-time platform-document snapshot |
|
|
94
|
+
| `document_url` | `""` | URL to fetch the live platform document from; empty means never fetch — always serve the snapshot |
|
|
95
|
+
| `cache_seconds` | `86400` | how long a successful platform-document fetch is cached |
|
|
96
|
+
| `evidence_permission` | `"hyperscale.trust.permissions.superuser_only"` | dotted path to a `Callable[[HttpRequest], bool]` deciding who may open the evidence pack |
|
|
97
|
+
|
|
98
|
+
An unknown key, a missing `trust_json_path`/`snapshot_path`, a non-positive
|
|
99
|
+
`cache_seconds`, or an `evidence_permission` path that doesn't import to a
|
|
100
|
+
callable all raise `ImproperlyConfigured`. This is checked in `AppConfig.ready()`,
|
|
101
|
+
so a bad setting fails at Django startup, not on the first request.
|
|
102
|
+
|
|
103
|
+
The default `evidence_permission`,
|
|
104
|
+
`hyperscale.trust.permissions.superuser_only`, is `True` only for an
|
|
105
|
+
authenticated Django superuser. Supply your own dotted path to gate the
|
|
106
|
+
evidence pack on an app-specific owner or admin role instead.
|
|
107
|
+
|
|
108
|
+
## URLs
|
|
109
|
+
|
|
110
|
+
| Name | Path | Access |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `hyperscale_trust:page` | `/trust/` | public |
|
|
113
|
+
| `hyperscale_trust:questionnaire` | `/trust/questionnaire.csv` | public |
|
|
114
|
+
| `hyperscale_trust:evidence` | `/trust/evidence/` | login + `evidence_permission` |
|
|
115
|
+
| `hyperscale_trust:evidence_zip` | `/trust/evidence.zip` | login + `evidence_permission` |
|
|
116
|
+
|
|
117
|
+
(Paths shown are the conventional mount point above; the host app chooses
|
|
118
|
+
where to `include()` the URLconf.)
|
|
119
|
+
|
|
120
|
+
When `trust.json`'s `app.enabled` is `false` (or `trust.json` itself is
|
|
121
|
+
missing or malformed), the public routes 404, and the evidence routes 404 once
|
|
122
|
+
the caller is logged in (anonymous callers are redirected to `LOGIN_URL`
|
|
123
|
+
first) — a disabled trust center gives no sign of what it would otherwise
|
|
124
|
+
contain. The evidence routes are checked in this order: an unauthenticated
|
|
125
|
+
request is redirected to `LOGIN_URL` first; an authenticated request against
|
|
126
|
+
a disabled trust center 404s; only then is `evidence_permission` consulted,
|
|
127
|
+
raising `PermissionDenied` (403) if it returns `False`. This ordering means a
|
|
128
|
+
disabled center never reveals whether the caller would have been authorised.
|
|
129
|
+
|
|
130
|
+
## What the page shows, and what it deliberately does not
|
|
131
|
+
|
|
132
|
+
`GET /trust/` renders, in order: an overview (app name and description,
|
|
133
|
+
operator and security contact — each labelled "Declared by the operator",
|
|
134
|
+
since they're the only founder-supplied inputs — region, and the date the
|
|
135
|
+
page's source was generated); the data categories processed and how each is
|
|
136
|
+
handled, plus a negative claim for every category the app doesn't process;
|
|
137
|
+
the controls that protect that data (app-specific and platform, each with an
|
|
138
|
+
ISO 27001:2022 Annex A reference, labelled "Enforced by the platform"); the
|
|
139
|
+
cryptography policy in prose (algorithms, key custody, rotation, transport);
|
|
140
|
+
the subprocessor list; and the operator's own responsibilities (such as a
|
|
141
|
+
DPIA) that the data categories imply.
|
|
142
|
+
|
|
143
|
+
It deliberately carries no category or control ids, no row or attribute
|
|
144
|
+
counts, no key ids or rotation dates, no deployment identifiers, and no
|
|
145
|
+
canary results — only two ISO dates ever appear: `trust.json`'s
|
|
146
|
+
`generated_at` (as "last updated on …") and, whenever the page is rendered
|
|
147
|
+
from the bundled snapshot rather than a live fetch (the default when
|
|
148
|
+
`document_url` is unset, and also the fallback after a failed fetch), that
|
|
149
|
+
snapshot's `published_at`, shown as a "Platform information as of …" badge so
|
|
150
|
+
a stale or never-fetched document is visible rather than served as current
|
|
151
|
+
without comment. `GET /trust/questionnaire.csv`
|
|
152
|
+
carries the same restriction — it is the same catalogue, exported as
|
|
153
|
+
`question,answer,annex_a,source` rows, one per control and per data-category
|
|
154
|
+
question plus rows for region, operator, contact and subprocessors.
|
|
155
|
+
|
|
156
|
+
## Templates
|
|
157
|
+
|
|
158
|
+
`trust.html` and `evidence.html` both `{% extends "base_anonymous.html" %}`
|
|
159
|
+
and use only the `title` and `content` blocks — the host app must supply
|
|
160
|
+
`base_anonymous.html`. Markup uses DaisyUI class names (`card`, `badge`,
|
|
161
|
+
`table`, `btn`) but renders sensibly without DaisyUI or Tailwind loaded.
|
|
162
|
+
|
|
163
|
+
## `trust.json`
|
|
164
|
+
|
|
165
|
+
Produced by the host app (App Studio's `build_bundle`, for a generated app)
|
|
166
|
+
and shipped next to the app. Full shape and the rules behind it are in the
|
|
167
|
+
design spec:
|
|
168
|
+
[`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md).
|
|
169
|
+
A worked example is in
|
|
170
|
+
[`tests/fixtures/trust.full.json`](tests/fixtures/trust.full.json). Excerpt:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"generated_at": "2026-09-24T12:00:00Z",
|
|
175
|
+
"app": {"name": "Clinic Notes", "operator_name": "Acme Health Ltd", "enabled": true},
|
|
176
|
+
"data": {
|
|
177
|
+
"categories": [
|
|
178
|
+
{"id": "health", "label": "Health", "definition": "…", "level": "restricted", "handling": "…", "attribute_count": 2, "models": ["Patient"]}
|
|
179
|
+
],
|
|
180
|
+
"not_processed": [{"id": "payment_card", "label": "Payment card"}],
|
|
181
|
+
"free_text_attribute_count": 3
|
|
182
|
+
},
|
|
183
|
+
"controls": [{"id": "app.encryption.restricted", "annex_a": "A.8.24"}],
|
|
184
|
+
"levels": {"restricted": "Encrypted by the application before storage, …"}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Two things worth calling out because they're easy to get wrong:
|
|
189
|
+
|
|
190
|
+
- Each category may carry its own `definition` and `handling` prose, and the
|
|
191
|
+
top-level `levels` map gives each sensitivity level a plain description —
|
|
192
|
+
this is the host app's classification vocabulary, copied in so the package
|
|
193
|
+
owns no category wording. `not_processed` is a list of `{id, label}`
|
|
194
|
+
objects, not a list of ids.
|
|
195
|
+
- The package's catalogue (`hyperscale.trust.catalogue.APP_CONTROLS`) is
|
|
196
|
+
authoritative for a control's Annex A reference, title, statement,
|
|
197
|
+
question and answer, looked up by `id`. The `annex_a` value on a control
|
|
198
|
+
entry in `trust.json` itself is informational only and is not rendered.
|
|
199
|
+
|
|
200
|
+
## The platform document and the snapshot
|
|
201
|
+
|
|
202
|
+
`hyperscale.trust.documents.PlatformDocument` — `version`, `published_at`,
|
|
203
|
+
`region`, `hosting_provider`, `email_provider` and a list of controls (`id`,
|
|
204
|
+
`title`, `statement`, `annex_a`) — either fetched live from
|
|
205
|
+
`HYPERSCALE_TRUST["document_url"]` or loaded from the on-disk
|
|
206
|
+
`snapshot_path`. See
|
|
207
|
+
[`tests/fixtures/platform-controls.json`](tests/fixtures/platform-controls.json)
|
|
208
|
+
for a complete example.
|
|
209
|
+
|
|
210
|
+
- **`document_url` unset (the default):** the snapshot at `snapshot_path` is
|
|
211
|
+
loaded fresh on every request; nothing is fetched over the network.
|
|
212
|
+
- **`document_url` set:** a successful fetch (5-second timeout) is cached
|
|
213
|
+
under a fixed key for `cache_seconds` (default 24 hours) before it is
|
|
214
|
+
fetched again.
|
|
215
|
+
- **A failed fetch** — network error, non-200 status, invalid JSON, or a
|
|
216
|
+
document missing a required field — falls back to the on-disk snapshot.
|
|
217
|
+
That fallback is itself cached, but only for `min(300, cache_seconds)`
|
|
218
|
+
seconds: long enough that a dead `document_url` doesn't pay the fetch
|
|
219
|
+
timeout on every request, short enough to retry soon after the upstream
|
|
220
|
+
recovers.
|
|
221
|
+
- A document served from the snapshot (whether because `document_url` is
|
|
222
|
+
unset, or as a fallback) is marked stale. The public page then shows a
|
|
223
|
+
"Platform information as of `<published_at>`" badge, and the evidence
|
|
224
|
+
pack's `platform-controls.json` carries `"stale_snapshot": true` and an
|
|
225
|
+
explanatory `note`.
|
|
226
|
+
|
|
227
|
+
## The evidence pack
|
|
228
|
+
|
|
229
|
+
`GET /trust/evidence/` (an HTML summary) and `GET /trust/evidence.zip` (the
|
|
230
|
+
download) are gated on login plus `evidence_permission` — the app's owner,
|
|
231
|
+
admin, or whoever the host app's callable decides; by default, a Django
|
|
232
|
+
superuser. The zip contains:
|
|
233
|
+
|
|
234
|
+
| File | Contents |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `README.md` | index of the other files and which Annex A control each evidences |
|
|
237
|
+
| `cryptography-policy.md` | the cryptography policy rendered as a document, with the operator name and date |
|
|
238
|
+
| `keyset_status.json` | `hyperscale.crypto`'s live key inventory (statuses, last rotation, canary result), or `{"unavailable": "<reason>"}` if `hyperscale.crypto` isn't installed or its tables aren't migrated |
|
|
239
|
+
| `key_rotation_events.csv` | every recorded `KeyRotationEvent`, or a single row explaining why none could be read |
|
|
240
|
+
| `platform-controls.json` | the whole platform document as fetched or loaded, plus `stale_snapshot` and, when stale, a `note` |
|
|
241
|
+
| `classification.json` | Data categories processed, with their sensitivity levels and the level definitions |
|
|
242
|
+
| `deployment.json` | `GIT_SHA`, `DEPLOYED_AT`, and installed versions of `hyperscale-trust`, `hyperscale-crypto` and `django` |
|
|
243
|
+
|
|
244
|
+
The spec also lists a per-attribute classification export; that is not yet
|
|
245
|
+
part of the `trust.json` contract, so `classification.json` carries category
|
|
246
|
+
aggregates until App Studio exports it.
|
|
247
|
+
|
|
248
|
+
`keyset_status.json` and `key_rotation_events.csv` never disappear from the
|
|
249
|
+
pack just because `hyperscale.crypto` is absent or not yet migrated — the
|
|
250
|
+
failure is recorded in the file instead, so an auditor sees why evidence is
|
|
251
|
+
missing rather than a shorter zip.
|
|
252
|
+
|
|
253
|
+
## Environment
|
|
254
|
+
|
|
255
|
+
`GIT_SHA` and `DEPLOYED_AT` are read from the process environment
|
|
256
|
+
(`os.environ`) when the evidence pack is built and written into
|
|
257
|
+
`deployment.json`; they default to an empty string when unset. Neither ever
|
|
258
|
+
appears on the public page — only inside the evidence pack.
|
|
259
|
+
|
|
260
|
+
## Wording policy
|
|
261
|
+
|
|
262
|
+
Control titles, statements, questions, answers (in
|
|
263
|
+
`hyperscale.trust.catalogue`) and the cryptography-policy paragraphs are
|
|
264
|
+
vendor-neutral: a reviewer reads "FIPS 140-3 validated key management
|
|
265
|
+
service", never a product name. `tests/test_catalogue.py` asserts none of
|
|
266
|
+
that prose contains a name from `catalogue.VENDOR_DENY_LIST` (currently AWS,
|
|
267
|
+
Amazon, KMS, Secrets Manager, RDS, CloudFront, S3, Stripe, Anthropic). The
|
|
268
|
+
one place a vendor is named is the subprocessor list, resolved from the
|
|
269
|
+
platform document's `hosting_provider`/`email_provider` or from names the
|
|
270
|
+
host app declares in `trust.json` — subprocessor lists must name who they
|
|
271
|
+
are.
|
|
272
|
+
|
|
273
|
+
## Development
|
|
274
|
+
|
|
275
|
+
Requires [uv](https://docs.astral.sh/uv/) and Python 3.14+.
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
uv sync # create the venv and install dependencies
|
|
279
|
+
uv run pytest # run the tests
|
|
280
|
+
uv run pre-commit run --all-files # lint, format and lockfile checks
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full set of checks.
|
|
284
|
+
|
|
285
|
+
## Releasing
|
|
286
|
+
|
|
287
|
+
Bump `version` in `pyproject.toml` and `__version__` in
|
|
288
|
+
`src/hyperscale/trust/__init__.py`, update `CHANGELOG.md`, then publish a
|
|
289
|
+
GitHub release tagged `v<version>`. The `publish` workflow checks the tag
|
|
290
|
+
matches the package version, builds, runs `twine check` and publishes to PyPI
|
|
291
|
+
with trusted publishing.
|
|
292
|
+
|
|
293
|
+
Before the first release, register the project on PyPI with this repository
|
|
294
|
+
and `publish.yml` as a trusted publisher, and create a `pypi` environment in
|
|
295
|
+
the repository settings.
|
|
296
|
+
|
|
297
|
+
## Design
|
|
298
|
+
|
|
299
|
+
Design spec: [`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md)
|
|
300
|
+
|
|
301
|
+
Implementation plan: [`docs/superpowers/plans/2026-09-25-hyperscale-trust.md`](docs/superpowers/plans/2026-09-25-hyperscale-trust.md)
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# hyperscale-trust
|
|
2
|
+
|
|
3
|
+
Trust center for Hyperscale Django apps: a public `/trust` page of
|
|
4
|
+
policy-level security claims generated from the app's data classification and
|
|
5
|
+
the platform's published controls, a questionnaire CSV export, and a private
|
|
6
|
+
evidence pack for auditors and buyers under NDA. Imported as
|
|
7
|
+
`hyperscale.trust`.
|
|
8
|
+
|
|
9
|
+
Every claim on the page comes from the host app's data classification, this
|
|
10
|
+
package's control catalogue, or the platform's published document — not from
|
|
11
|
+
prose someone typed into a form — so the page cannot go stale because
|
|
12
|
+
someone forgot to edit it. The only hand-written fields carried through
|
|
13
|
+
`trust.json` are the operator name and the security contact.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add hyperscale-trust
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
INSTALLED_APPS = [
|
|
23
|
+
# ...
|
|
24
|
+
"hyperscale.trust",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
urlpatterns = [
|
|
28
|
+
# ...
|
|
29
|
+
path("trust/", include("hyperscale.trust.urls")),
|
|
30
|
+
]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`hyperscale/trust/urls.py` sets `app_name = "hyperscale_trust"`, so including
|
|
34
|
+
the module resolves URLs as `hyperscale_trust:page` etc. without an explicit
|
|
35
|
+
`namespace=` (passing one is harmless).
|
|
36
|
+
|
|
37
|
+
The evidence pack's key inventory (`keyset_status.json`,
|
|
38
|
+
`key_rotation_events.csv`) comes from `hyperscale.crypto`. Install it with the
|
|
39
|
+
`crypto` extra:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv add "hyperscale-trust[crypto]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Without it — or before its tables are migrated — the pack still builds; those
|
|
46
|
+
two files just report that the key inventory is unavailable rather than being
|
|
47
|
+
omitted (see [The evidence pack](#the-evidence-pack)).
|
|
48
|
+
|
|
49
|
+
## Settings
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
55
|
+
|
|
56
|
+
HYPERSCALE_TRUST = {
|
|
57
|
+
"trust_json_path": BASE_DIR / "trust.json", # required
|
|
58
|
+
"snapshot_path": BASE_DIR / "platform-controls.json", # required
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Key | Default | Meaning |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `trust_json_path` | none (required) | path to the app's `trust.json` |
|
|
65
|
+
| `snapshot_path` | none (required) | path to the build-time platform-document snapshot |
|
|
66
|
+
| `document_url` | `""` | URL to fetch the live platform document from; empty means never fetch — always serve the snapshot |
|
|
67
|
+
| `cache_seconds` | `86400` | how long a successful platform-document fetch is cached |
|
|
68
|
+
| `evidence_permission` | `"hyperscale.trust.permissions.superuser_only"` | dotted path to a `Callable[[HttpRequest], bool]` deciding who may open the evidence pack |
|
|
69
|
+
|
|
70
|
+
An unknown key, a missing `trust_json_path`/`snapshot_path`, a non-positive
|
|
71
|
+
`cache_seconds`, or an `evidence_permission` path that doesn't import to a
|
|
72
|
+
callable all raise `ImproperlyConfigured`. This is checked in `AppConfig.ready()`,
|
|
73
|
+
so a bad setting fails at Django startup, not on the first request.
|
|
74
|
+
|
|
75
|
+
The default `evidence_permission`,
|
|
76
|
+
`hyperscale.trust.permissions.superuser_only`, is `True` only for an
|
|
77
|
+
authenticated Django superuser. Supply your own dotted path to gate the
|
|
78
|
+
evidence pack on an app-specific owner or admin role instead.
|
|
79
|
+
|
|
80
|
+
## URLs
|
|
81
|
+
|
|
82
|
+
| Name | Path | Access |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `hyperscale_trust:page` | `/trust/` | public |
|
|
85
|
+
| `hyperscale_trust:questionnaire` | `/trust/questionnaire.csv` | public |
|
|
86
|
+
| `hyperscale_trust:evidence` | `/trust/evidence/` | login + `evidence_permission` |
|
|
87
|
+
| `hyperscale_trust:evidence_zip` | `/trust/evidence.zip` | login + `evidence_permission` |
|
|
88
|
+
|
|
89
|
+
(Paths shown are the conventional mount point above; the host app chooses
|
|
90
|
+
where to `include()` the URLconf.)
|
|
91
|
+
|
|
92
|
+
When `trust.json`'s `app.enabled` is `false` (or `trust.json` itself is
|
|
93
|
+
missing or malformed), the public routes 404, and the evidence routes 404 once
|
|
94
|
+
the caller is logged in (anonymous callers are redirected to `LOGIN_URL`
|
|
95
|
+
first) — a disabled trust center gives no sign of what it would otherwise
|
|
96
|
+
contain. The evidence routes are checked in this order: an unauthenticated
|
|
97
|
+
request is redirected to `LOGIN_URL` first; an authenticated request against
|
|
98
|
+
a disabled trust center 404s; only then is `evidence_permission` consulted,
|
|
99
|
+
raising `PermissionDenied` (403) if it returns `False`. This ordering means a
|
|
100
|
+
disabled center never reveals whether the caller would have been authorised.
|
|
101
|
+
|
|
102
|
+
## What the page shows, and what it deliberately does not
|
|
103
|
+
|
|
104
|
+
`GET /trust/` renders, in order: an overview (app name and description,
|
|
105
|
+
operator and security contact — each labelled "Declared by the operator",
|
|
106
|
+
since they're the only founder-supplied inputs — region, and the date the
|
|
107
|
+
page's source was generated); the data categories processed and how each is
|
|
108
|
+
handled, plus a negative claim for every category the app doesn't process;
|
|
109
|
+
the controls that protect that data (app-specific and platform, each with an
|
|
110
|
+
ISO 27001:2022 Annex A reference, labelled "Enforced by the platform"); the
|
|
111
|
+
cryptography policy in prose (algorithms, key custody, rotation, transport);
|
|
112
|
+
the subprocessor list; and the operator's own responsibilities (such as a
|
|
113
|
+
DPIA) that the data categories imply.
|
|
114
|
+
|
|
115
|
+
It deliberately carries no category or control ids, no row or attribute
|
|
116
|
+
counts, no key ids or rotation dates, no deployment identifiers, and no
|
|
117
|
+
canary results — only two ISO dates ever appear: `trust.json`'s
|
|
118
|
+
`generated_at` (as "last updated on …") and, whenever the page is rendered
|
|
119
|
+
from the bundled snapshot rather than a live fetch (the default when
|
|
120
|
+
`document_url` is unset, and also the fallback after a failed fetch), that
|
|
121
|
+
snapshot's `published_at`, shown as a "Platform information as of …" badge so
|
|
122
|
+
a stale or never-fetched document is visible rather than served as current
|
|
123
|
+
without comment. `GET /trust/questionnaire.csv`
|
|
124
|
+
carries the same restriction — it is the same catalogue, exported as
|
|
125
|
+
`question,answer,annex_a,source` rows, one per control and per data-category
|
|
126
|
+
question plus rows for region, operator, contact and subprocessors.
|
|
127
|
+
|
|
128
|
+
## Templates
|
|
129
|
+
|
|
130
|
+
`trust.html` and `evidence.html` both `{% extends "base_anonymous.html" %}`
|
|
131
|
+
and use only the `title` and `content` blocks — the host app must supply
|
|
132
|
+
`base_anonymous.html`. Markup uses DaisyUI class names (`card`, `badge`,
|
|
133
|
+
`table`, `btn`) but renders sensibly without DaisyUI or Tailwind loaded.
|
|
134
|
+
|
|
135
|
+
## `trust.json`
|
|
136
|
+
|
|
137
|
+
Produced by the host app (App Studio's `build_bundle`, for a generated app)
|
|
138
|
+
and shipped next to the app. Full shape and the rules behind it are in the
|
|
139
|
+
design spec:
|
|
140
|
+
[`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md).
|
|
141
|
+
A worked example is in
|
|
142
|
+
[`tests/fixtures/trust.full.json`](tests/fixtures/trust.full.json). Excerpt:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"generated_at": "2026-09-24T12:00:00Z",
|
|
147
|
+
"app": {"name": "Clinic Notes", "operator_name": "Acme Health Ltd", "enabled": true},
|
|
148
|
+
"data": {
|
|
149
|
+
"categories": [
|
|
150
|
+
{"id": "health", "label": "Health", "definition": "…", "level": "restricted", "handling": "…", "attribute_count": 2, "models": ["Patient"]}
|
|
151
|
+
],
|
|
152
|
+
"not_processed": [{"id": "payment_card", "label": "Payment card"}],
|
|
153
|
+
"free_text_attribute_count": 3
|
|
154
|
+
},
|
|
155
|
+
"controls": [{"id": "app.encryption.restricted", "annex_a": "A.8.24"}],
|
|
156
|
+
"levels": {"restricted": "Encrypted by the application before storage, …"}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Two things worth calling out because they're easy to get wrong:
|
|
161
|
+
|
|
162
|
+
- Each category may carry its own `definition` and `handling` prose, and the
|
|
163
|
+
top-level `levels` map gives each sensitivity level a plain description —
|
|
164
|
+
this is the host app's classification vocabulary, copied in so the package
|
|
165
|
+
owns no category wording. `not_processed` is a list of `{id, label}`
|
|
166
|
+
objects, not a list of ids.
|
|
167
|
+
- The package's catalogue (`hyperscale.trust.catalogue.APP_CONTROLS`) is
|
|
168
|
+
authoritative for a control's Annex A reference, title, statement,
|
|
169
|
+
question and answer, looked up by `id`. The `annex_a` value on a control
|
|
170
|
+
entry in `trust.json` itself is informational only and is not rendered.
|
|
171
|
+
|
|
172
|
+
## The platform document and the snapshot
|
|
173
|
+
|
|
174
|
+
`hyperscale.trust.documents.PlatformDocument` — `version`, `published_at`,
|
|
175
|
+
`region`, `hosting_provider`, `email_provider` and a list of controls (`id`,
|
|
176
|
+
`title`, `statement`, `annex_a`) — either fetched live from
|
|
177
|
+
`HYPERSCALE_TRUST["document_url"]` or loaded from the on-disk
|
|
178
|
+
`snapshot_path`. See
|
|
179
|
+
[`tests/fixtures/platform-controls.json`](tests/fixtures/platform-controls.json)
|
|
180
|
+
for a complete example.
|
|
181
|
+
|
|
182
|
+
- **`document_url` unset (the default):** the snapshot at `snapshot_path` is
|
|
183
|
+
loaded fresh on every request; nothing is fetched over the network.
|
|
184
|
+
- **`document_url` set:** a successful fetch (5-second timeout) is cached
|
|
185
|
+
under a fixed key for `cache_seconds` (default 24 hours) before it is
|
|
186
|
+
fetched again.
|
|
187
|
+
- **A failed fetch** — network error, non-200 status, invalid JSON, or a
|
|
188
|
+
document missing a required field — falls back to the on-disk snapshot.
|
|
189
|
+
That fallback is itself cached, but only for `min(300, cache_seconds)`
|
|
190
|
+
seconds: long enough that a dead `document_url` doesn't pay the fetch
|
|
191
|
+
timeout on every request, short enough to retry soon after the upstream
|
|
192
|
+
recovers.
|
|
193
|
+
- A document served from the snapshot (whether because `document_url` is
|
|
194
|
+
unset, or as a fallback) is marked stale. The public page then shows a
|
|
195
|
+
"Platform information as of `<published_at>`" badge, and the evidence
|
|
196
|
+
pack's `platform-controls.json` carries `"stale_snapshot": true` and an
|
|
197
|
+
explanatory `note`.
|
|
198
|
+
|
|
199
|
+
## The evidence pack
|
|
200
|
+
|
|
201
|
+
`GET /trust/evidence/` (an HTML summary) and `GET /trust/evidence.zip` (the
|
|
202
|
+
download) are gated on login plus `evidence_permission` — the app's owner,
|
|
203
|
+
admin, or whoever the host app's callable decides; by default, a Django
|
|
204
|
+
superuser. The zip contains:
|
|
205
|
+
|
|
206
|
+
| File | Contents |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `README.md` | index of the other files and which Annex A control each evidences |
|
|
209
|
+
| `cryptography-policy.md` | the cryptography policy rendered as a document, with the operator name and date |
|
|
210
|
+
| `keyset_status.json` | `hyperscale.crypto`'s live key inventory (statuses, last rotation, canary result), or `{"unavailable": "<reason>"}` if `hyperscale.crypto` isn't installed or its tables aren't migrated |
|
|
211
|
+
| `key_rotation_events.csv` | every recorded `KeyRotationEvent`, or a single row explaining why none could be read |
|
|
212
|
+
| `platform-controls.json` | the whole platform document as fetched or loaded, plus `stale_snapshot` and, when stale, a `note` |
|
|
213
|
+
| `classification.json` | Data categories processed, with their sensitivity levels and the level definitions |
|
|
214
|
+
| `deployment.json` | `GIT_SHA`, `DEPLOYED_AT`, and installed versions of `hyperscale-trust`, `hyperscale-crypto` and `django` |
|
|
215
|
+
|
|
216
|
+
The spec also lists a per-attribute classification export; that is not yet
|
|
217
|
+
part of the `trust.json` contract, so `classification.json` carries category
|
|
218
|
+
aggregates until App Studio exports it.
|
|
219
|
+
|
|
220
|
+
`keyset_status.json` and `key_rotation_events.csv` never disappear from the
|
|
221
|
+
pack just because `hyperscale.crypto` is absent or not yet migrated — the
|
|
222
|
+
failure is recorded in the file instead, so an auditor sees why evidence is
|
|
223
|
+
missing rather than a shorter zip.
|
|
224
|
+
|
|
225
|
+
## Environment
|
|
226
|
+
|
|
227
|
+
`GIT_SHA` and `DEPLOYED_AT` are read from the process environment
|
|
228
|
+
(`os.environ`) when the evidence pack is built and written into
|
|
229
|
+
`deployment.json`; they default to an empty string when unset. Neither ever
|
|
230
|
+
appears on the public page — only inside the evidence pack.
|
|
231
|
+
|
|
232
|
+
## Wording policy
|
|
233
|
+
|
|
234
|
+
Control titles, statements, questions, answers (in
|
|
235
|
+
`hyperscale.trust.catalogue`) and the cryptography-policy paragraphs are
|
|
236
|
+
vendor-neutral: a reviewer reads "FIPS 140-3 validated key management
|
|
237
|
+
service", never a product name. `tests/test_catalogue.py` asserts none of
|
|
238
|
+
that prose contains a name from `catalogue.VENDOR_DENY_LIST` (currently AWS,
|
|
239
|
+
Amazon, KMS, Secrets Manager, RDS, CloudFront, S3, Stripe, Anthropic). The
|
|
240
|
+
one place a vendor is named is the subprocessor list, resolved from the
|
|
241
|
+
platform document's `hosting_provider`/`email_provider` or from names the
|
|
242
|
+
host app declares in `trust.json` — subprocessor lists must name who they
|
|
243
|
+
are.
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
Requires [uv](https://docs.astral.sh/uv/) and Python 3.14+.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
uv sync # create the venv and install dependencies
|
|
251
|
+
uv run pytest # run the tests
|
|
252
|
+
uv run pre-commit run --all-files # lint, format and lockfile checks
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full set of checks.
|
|
256
|
+
|
|
257
|
+
## Releasing
|
|
258
|
+
|
|
259
|
+
Bump `version` in `pyproject.toml` and `__version__` in
|
|
260
|
+
`src/hyperscale/trust/__init__.py`, update `CHANGELOG.md`, then publish a
|
|
261
|
+
GitHub release tagged `v<version>`. The `publish` workflow checks the tag
|
|
262
|
+
matches the package version, builds, runs `twine check` and publishes to PyPI
|
|
263
|
+
with trusted publishing.
|
|
264
|
+
|
|
265
|
+
Before the first release, register the project on PyPI with this repository
|
|
266
|
+
and `publish.yml` as a trusted publisher, and create a `pypi` environment in
|
|
267
|
+
the repository settings.
|
|
268
|
+
|
|
269
|
+
## Design
|
|
270
|
+
|
|
271
|
+
Design spec: [`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md)
|
|
272
|
+
|
|
273
|
+
Implementation plan: [`docs/superpowers/plans/2026-09-25-hyperscale-trust.md`](docs/superpowers/plans/2026-09-25-hyperscale-trust.md)
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT. See [LICENSE](LICENSE).
|