@openauditmodel/cli 0.1.0

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 (134) hide show
  1. package/LICENSE +215 -0
  2. package/README.md +647 -0
  3. package/dist/conformance/src/cli.d.ts +20 -0
  4. package/dist/conformance/src/cli.js +560 -0
  5. package/dist/conformance/src/cli.js.map +1 -0
  6. package/dist/conformance/src/format-errors.d.ts +20 -0
  7. package/dist/conformance/src/format-errors.js +116 -0
  8. package/dist/conformance/src/format-errors.js.map +1 -0
  9. package/dist/conformance/src/integrity/canonicalize.d.ts +25 -0
  10. package/dist/conformance/src/integrity/canonicalize.js +100 -0
  11. package/dist/conformance/src/integrity/canonicalize.js.map +1 -0
  12. package/dist/conformance/src/integrity/digest.d.ts +54 -0
  13. package/dist/conformance/src/integrity/digest.js +126 -0
  14. package/dist/conformance/src/integrity/digest.js.map +1 -0
  15. package/dist/conformance/src/integrity/types.d.ts +86 -0
  16. package/dist/conformance/src/integrity/types.js +36 -0
  17. package/dist/conformance/src/integrity/types.js.map +1 -0
  18. package/dist/conformance/src/integrity/verify-chain.d.ts +23 -0
  19. package/dist/conformance/src/integrity/verify-chain.js +235 -0
  20. package/dist/conformance/src/integrity/verify-chain.js.map +1 -0
  21. package/dist/conformance/src/integrity/verify-event.d.ts +29 -0
  22. package/dist/conformance/src/integrity/verify-event.js +140 -0
  23. package/dist/conformance/src/integrity/verify-event.js.map +1 -0
  24. package/dist/conformance/src/privacy/entropy.d.ts +14 -0
  25. package/dist/conformance/src/privacy/entropy.js +86 -0
  26. package/dist/conformance/src/privacy/entropy.js.map +1 -0
  27. package/dist/conformance/src/privacy/field-names.d.ts +36 -0
  28. package/dist/conformance/src/privacy/field-names.js +121 -0
  29. package/dist/conformance/src/privacy/field-names.js.map +1 -0
  30. package/dist/conformance/src/privacy/lint-event.d.ts +18 -0
  31. package/dist/conformance/src/privacy/lint-event.js +272 -0
  32. package/dist/conformance/src/privacy/lint-event.js.map +1 -0
  33. package/dist/conformance/src/privacy/rules.d.ts +44 -0
  34. package/dist/conformance/src/privacy/rules.js +161 -0
  35. package/dist/conformance/src/privacy/rules.js.map +1 -0
  36. package/dist/conformance/src/privacy/safe-formats.d.ts +32 -0
  37. package/dist/conformance/src/privacy/safe-formats.js +118 -0
  38. package/dist/conformance/src/privacy/safe-formats.js.map +1 -0
  39. package/dist/conformance/src/privacy/size-analysis.d.ts +36 -0
  40. package/dist/conformance/src/privacy/size-analysis.js +87 -0
  41. package/dist/conformance/src/privacy/size-analysis.js.map +1 -0
  42. package/dist/conformance/src/privacy/token-patterns.d.ts +24 -0
  43. package/dist/conformance/src/privacy/token-patterns.js +126 -0
  44. package/dist/conformance/src/privacy/token-patterns.js.map +1 -0
  45. package/dist/conformance/src/privacy/traverse.d.ts +33 -0
  46. package/dist/conformance/src/privacy/traverse.js +61 -0
  47. package/dist/conformance/src/privacy/traverse.js.map +1 -0
  48. package/dist/conformance/src/privacy/types.d.ts +58 -0
  49. package/dist/conformance/src/privacy/types.js +32 -0
  50. package/dist/conformance/src/privacy/types.js.map +1 -0
  51. package/dist/conformance/src/privacy/url-analysis.d.ts +40 -0
  52. package/dist/conformance/src/privacy/url-analysis.js +148 -0
  53. package/dist/conformance/src/privacy/url-analysis.js.map +1 -0
  54. package/dist/conformance/src/profiles/check-profile.d.ts +22 -0
  55. package/dist/conformance/src/profiles/check-profile.js +72 -0
  56. package/dist/conformance/src/profiles/check-profile.js.map +1 -0
  57. package/dist/conformance/src/profiles/evaluate-rule.d.ts +27 -0
  58. package/dist/conformance/src/profiles/evaluate-rule.js +87 -0
  59. package/dist/conformance/src/profiles/evaluate-rule.js.map +1 -0
  60. package/dist/conformance/src/profiles/load-profile.d.ts +30 -0
  61. package/dist/conformance/src/profiles/load-profile.js +95 -0
  62. package/dist/conformance/src/profiles/load-profile.js.map +1 -0
  63. package/dist/conformance/src/profiles/resolve-pointer.d.ts +44 -0
  64. package/dist/conformance/src/profiles/resolve-pointer.js +123 -0
  65. package/dist/conformance/src/profiles/resolve-pointer.js.map +1 -0
  66. package/dist/conformance/src/profiles/select-rules.d.ts +13 -0
  67. package/dist/conformance/src/profiles/select-rules.js +42 -0
  68. package/dist/conformance/src/profiles/select-rules.js.map +1 -0
  69. package/dist/conformance/src/profiles/types.d.ts +101 -0
  70. package/dist/conformance/src/profiles/types.js +31 -0
  71. package/dist/conformance/src/profiles/types.js.map +1 -0
  72. package/dist/conformance/src/profiles/validate-profile-definition.d.ts +12 -0
  73. package/dist/conformance/src/profiles/validate-profile-definition.js +47 -0
  74. package/dist/conformance/src/profiles/validate-profile-definition.js.map +1 -0
  75. package/dist/conformance/src/sources.d.ts +53 -0
  76. package/dist/conformance/src/sources.js +153 -0
  77. package/dist/conformance/src/sources.js.map +1 -0
  78. package/dist/conformance/src/validate-core.d.ts +30 -0
  79. package/dist/conformance/src/validate-core.js +62 -0
  80. package/dist/conformance/src/validate-core.js.map +1 -0
  81. package/dist/conformance/src/validate.d.ts +34 -0
  82. package/dist/conformance/src/validate.js +62 -0
  83. package/dist/conformance/src/validate.js.map +1 -0
  84. package/dist/conformance/src/validator-interface.d.ts +35 -0
  85. package/dist/conformance/src/validator-interface.js +19 -0
  86. package/dist/conformance/src/validator-interface.js.map +1 -0
  87. package/package.json +85 -0
  88. package/profiles/README.md +201 -0
  89. package/profiles/api-and-integration-management/README.md +271 -0
  90. package/profiles/api-and-integration-management/profile.json +258 -0
  91. package/profiles/backup-and-recovery/README.md +318 -0
  92. package/profiles/backup-and-recovery/profile.json +178 -0
  93. package/profiles/customer-and-account-management/README.md +340 -0
  94. package/profiles/customer-and-account-management/profile.json +237 -0
  95. package/profiles/deployment-and-change-management/README.md +359 -0
  96. package/profiles/deployment-and-change-management/profile.json +298 -0
  97. package/profiles/document-management/README.md +120 -0
  98. package/profiles/document-management/profile.json +170 -0
  99. package/profiles/financial-transaction-management/README.md +339 -0
  100. package/profiles/financial-transaction-management/profile.json +247 -0
  101. package/profiles/identity-and-access-management/README.md +112 -0
  102. package/profiles/identity-and-access-management/profile.json +120 -0
  103. package/profiles/incident-management/README.md +338 -0
  104. package/profiles/incident-management/profile.json +256 -0
  105. package/profiles/message-broker-management/README.md +344 -0
  106. package/profiles/message-broker-management/profile.json +399 -0
  107. package/profiles/profile-definition.schema.json +213 -0
  108. package/profiles/secrets-and-key-management/README.md +331 -0
  109. package/profiles/secrets-and-key-management/profile.json +219 -0
  110. package/schemas/v0.1/audit-event.schema.json +911 -0
  111. package/semantic-conventions/README.md +52 -0
  112. package/semantic-conventions/authentication.md +111 -0
  113. package/semantic-conventions/configuration-and-change.md +122 -0
  114. package/semantic-conventions/correlation-and-tracing.md +387 -0
  115. package/semantic-conventions/data-access.md +115 -0
  116. package/semantic-conventions/event-naming.md +114 -0
  117. package/semantic-conventions/identity-and-access.md +112 -0
  118. package/semantic-conventions/privileged-operations.md +110 -0
  119. package/semantic-conventions/workflow-and-approval.md +109 -0
  120. package/specification/actor-model.md +135 -0
  121. package/specification/approval-and-delegation.md +150 -0
  122. package/specification/authentication.md +116 -0
  123. package/specification/authorization.md +117 -0
  124. package/specification/change-model.md +134 -0
  125. package/specification/delivery.md +136 -0
  126. package/specification/design-principles.md +137 -0
  127. package/specification/event-model.md +387 -0
  128. package/specification/evidence-model.md +109 -0
  129. package/specification/extension-model.md +159 -0
  130. package/specification/integrity.md +314 -0
  131. package/specification/overview.md +172 -0
  132. package/specification/privacy.md +386 -0
  133. package/specification/resource-model.md +116 -0
  134. package/specification/terminology.md +135 -0
@@ -0,0 +1,331 @@
1
+ # Secrets and Key Management Profile
2
+
3
+ **Profile version: 0.1 · Core versions: 0.1 · Status: Experimental · Implemented, 14 rules (12
4
+ enforceable).**
5
+
6
+ Additional conformance requirements for the **custody** of secrets, cryptographic keys and
7
+ certificates: the operations a secret store, a key management service, a hardware custody module or a
8
+ certificate authority performs on the material it holds.
9
+
10
+ The enforceable rules are in [profile.json](profile.json).
11
+
12
+ ```bash
13
+ auditmodel check-profile examples/profiles/secrets-and-key-management/valid \
14
+ --profile secrets-and-key-management
15
+ ```
16
+
17
+ ## Purpose
18
+
19
+ Custody operations are the ones an attacker performs after gaining a foothold and before doing
20
+ anything visible, and they are the ones an organization is least able to reconstruct afterwards —
21
+ because the evidence is material that was replaced, copied or destroyed. This profile requires the
22
+ handful of fields that make such an event reviewable months later: who decided it was allowed, how
23
+ sensitive the material was, what kind of material it was, who owns it, what changed, and why.
24
+
25
+ **The one thing this domain must never do is record the material itself.** An event records _that_ a
26
+ secret was rotated, revealed or exported. No rule in this profile requires a value, a hash of a
27
+ value, a key fingerprint or anything else from which material could be reconstructed or verified
28
+ against a guess, and no fixture contains one.
29
+
30
+ ## Scope
31
+
32
+ Applications that hold material on behalf of other systems — secret stores and vaults, key management
33
+ services, hardware custody modules, certificate authorities and the credential-issuing parts of
34
+ platform services.
35
+
36
+ The profile is vendor-neutral. It describes operations that any custody system performs, not the
37
+ feature list of any product, and it assumes no particular storage backend, cryptographic provider,
38
+ approval workflow or deployment model.
39
+
40
+ ## Event families
41
+
42
+ | Family | Events | Governed |
43
+ | ------------------------ | ------------------------------------------------------------------------------ | -------- |
44
+ | Secret lifecycle | `secret.create`, `.update`, `.rotate`, `.revoke`, `.delete` | yes |
45
+ | Secret value access | `secret.reveal`, `secret.export` | yes |
46
+ | Key lifecycle | `key.generate`, `.import`, `.rotate`, `.enable`, `.disable`, `.destroy` | yes |
47
+ | Key material export | `key.export` | yes |
48
+ | Certificate lifecycle | `certificate.issue`, `.renew`, `.revoke`, `.delete` | yes |
49
+ | Custody policy | `secret.policy.*`, `key.policy.*` | yes |
50
+ | Routine secret retrieval | `secret.retrieve`, `secret.cache-refresh` | **no** |
51
+ | Cryptographic data plane | `key.encrypt`, `key.decrypt`, `key.sign`, `key.verify`, `certificate.validate` | **no** |
52
+
53
+ Names follow [event-naming.md](../../semantic-conventions/event-naming.md): the two-segment form is
54
+ used where the domain _is_ the object acted on, and the three-segment form where a distinct object —
55
+ a policy — is acted on within the domain.
56
+
57
+ ## Explicit exclusions
58
+
59
+ **Routine automated retrieval is not governed.** A workload that reads its own credential at start-up
60
+ and refreshes it on a timer produces the highest-volume event a custody system emits. Requiring an
61
+ authorization decision, a classification, a material type and a justification on each of them would
62
+ add cost to that event in exchange for very little review value, and the requirement would be
63
+ switched off rather than met.
64
+
65
+ **The cryptographic data plane is not governed either.** `key.encrypt`, `key.decrypt`, `key.sign` and
66
+ `key.verify` are operations _with_ a key, not operations _on_ a key. They run millions of times a day
67
+ and they do not change the custody state of anything.
68
+
69
+ The exclusion is structural, not a matter of discipline: **no selector in this profile uses a bare
70
+ `secret.`, `key.` or `certificate.` prefix**. Every governed name is either listed exactly or sits
71
+ under a `.policy.` prefix, so `secret.retrieve` and `key.decrypt` match no rule and `check-profile`
72
+ reports them as not applicable. A test asserts that, because widening one prefix later would silently
73
+ start governing every secret read in a deployment.
74
+
75
+ Excluded does not mean unaudited. Those are conforming OpenAuditModel events, and
76
+ [data-access.md](../../semantic-conventions/data-access.md) covers recording reads. Where retrieval
77
+ volume is high, [configuration-and-change.md](../../semantic-conventions/configuration-and-change.md)
78
+ §6 is the relevant guidance: recording grants, rotations and out-of-pattern access is usually worth
79
+ more than recording every read.
80
+
81
+ ## Rules
82
+
83
+ | Rule | Applies to | Requires |
84
+ | ---------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
85
+ | `SECRET-CORE-001` | every governed event | `/authorization`, `/resource/classification` |
86
+ | `SECRET-CORE-002` | every governed event | `/metadata/secret/type` string |
87
+ | `SECRET-CORE-003` | every governed event | _recommends_ `/reason`, `/request/correlationId`, `/metadata/secret/provider` |
88
+ | `SECRET-LIFECYCLE-001` | `secret.create`, `key.generate`, `key.import`, `certificate.issue` | `/resource/ownerId` |
89
+ | `SECRET-ROTATE-001` | `secret.rotate`, `key.rotate`, `certificate.renew` | `/change`; recommends `/change/changedFields` |
90
+ | `SECRET-ACCESS-001` | `secret.reveal`, `secret.export`, `key.export` | `/authentication`, `/reason`; recommends `/approval` |
91
+ | `SECRET-ACCESS-002` | the same, **when** `/metadata/secret/emergencyAccess` is `true` | `/approval`, and `/authentication/mfa` equal to `true` |
92
+ | `SECRET-EXPORT-001` | `secret.export`, `key.export` | `/metadata/secret/destinationType` string |
93
+ | `SECRET-DESTROY-001` | `secret.revoke`, `secret.delete`, `key.disable`, `key.destroy`, `certificate.revoke`, `.delete` | `/reason`; recommends `/approval` |
94
+ | `SECRET-APPROVAL-001` | every governed event, **when** `/metadata/secret/approvalRequired` is `true` | `/approval` |
95
+ | `SECRET-POLICY-001` | `secret.policy.*`, `key.policy.*` | `/change`, `/reason`; recommends `/approval` |
96
+ | `SECRET-CERT-001` | `certificate.issue`, `certificate.renew` | `/metadata/secret/expiresAt` string; recommends `/metadata/secret/algorithm` |
97
+ | `SECRET-KEY-001` | `key.import` | `/reason`; recommends `/approval` |
98
+ | `SECRET-KEY-002` | `key.generate`, `key.import`, `key.rotate` | _recommends_ `/metadata/secret/algorithm`, `/metadata/secret/expiresAt` |
99
+
100
+ `SECRET-CORE-003` and `SECRET-KEY-002` are `warning` rules: they never fail conformance. Each rule's
101
+ full text and rationale is in [profile.json](profile.json).
102
+
103
+ ## Metadata namespace
104
+
105
+ Every requirement this profile places on `metadata` sits under `/metadata/secret/`. The namespace
106
+ names the **control domain** — secrets and key management — not only password-shaped material, so a
107
+ key and a certificate event use it too.
108
+
109
+ | Field | Type | Status | Records |
110
+ | ----------------------------------- | ------- | --------------------------------------------- | --------------------------------------------- |
111
+ | `/metadata/secret/type` | string | REQUIRED on every governed event | The kind of protected material |
112
+ | `/metadata/secret/provider` | string | RECOMMENDED | The kind of custody system holding it |
113
+ | `/metadata/secret/algorithm` | string | RECOMMENDED for keys and certificates | The cryptographic algorithm |
114
+ | `/metadata/secret/expiresAt` | string | RECOMMENDED on keys; REQUIRED on certificates | When the material stops being valid |
115
+ | `/metadata/secret/destinationType` | string | REQUIRED on export | The kind of destination material was moved to |
116
+ | `/metadata/secret/approvalRequired` | boolean | Producer declaration | That local policy requires approval |
117
+ | `/metadata/secret/emergencyAccess` | boolean | Producer declaration | That this was break-glass access |
118
+
119
+ Illustrative values, all open vocabularies: `type` — `database-credential`, `service-credential`,
120
+ `signing-key`, `encryption-key`, `certificate`; `provider` — `software-vault`,
121
+ `hardware-security-module`, `managed-key-service`, `internal-certificate-authority`;
122
+ `destinationType` — `hardware-security-module`, `managed-key-service`, `offline-backup`,
123
+ `operator-console`, `external-party`.
124
+
125
+ `provider` and `destinationType` record the **kind** of system, not a product name and not an
126
+ address. A product name dates the trail and an address is frequently sensitive itself; neither is
127
+ comparable across deployments.
128
+
129
+ Namespacing keeps two profiles from assigning different meanings to the same key when one event is
130
+ governed by both. `expiresAt` on a share, on a service account and on a certificate are not the same
131
+ fact.
132
+
133
+ **A scalar must never be placed at `/metadata/secret` itself.** The container is a descriptor; a
134
+ string there would be read as the secret, and `auditmodel lint-privacy` reports it as one.
135
+
136
+ ## Conditional policy fields
137
+
138
+ The rule language offers exactly one conditional mechanism: one path compared for equality against
139
+ one scalar. This profile spends it twice, and in both cases on a **producer declaration** rather than
140
+ on the profile's own guess.
141
+
142
+ - `/metadata/secret/approvalRequired` — `true` makes `/approval` required by `SECRET-APPROVAL-001`.
143
+ - `/metadata/secret/emergencyAccess` — `true` makes `/approval` and multi-factor authentication
144
+ required by `SECRET-ACCESS-002`.
145
+
146
+ Neither flag is itself required, and **an absent flag does not hold**: a rule whose condition path is
147
+ missing contributes nothing. The alternative — treating an absent flag as possibly true — would fail
148
+ every event that omitted a field the rule was never meant to govern. A recorded `false` is an answer,
149
+ not an absence, and the profile treats it as one.
150
+
151
+ ## Approval model
152
+
153
+ **Approval is never required unconditionally**, and a test enforces that no rule requires `/approval`
154
+ without a `when` condition.
155
+
156
+ Requiring approval for every rotation would describe one organization's process and be ignored by
157
+ everyone else: automated rotation and unattended certificate renewal are the healthy paths in this
158
+ domain, they run thousands of times a day without a human anywhere near them, and a profile that
159
+ punished them would push deployments back towards long-lived material. So the profile **recommends**
160
+ approval where it is often appropriate — reveal, export, destruction, policy change — and
161
+ **requires** it in exactly two places: where the producer's own policy declared it necessary, and
162
+ where the producer declared the access to be break-glass.
163
+
164
+ Both rules require the `/approval` **object**, not an `approved` status. Emergencies are frequently
165
+ approved retrospectively, and `status: pending` on a completed operation is an accurate record of a
166
+ control bypass — precisely what a post-incident review looks for. Making that unrepresentable would
167
+ encourage producers to suppress it, which is the outcome the model exists to prevent. See
168
+ [configuration-and-change.md](../../semantic-conventions/configuration-and-change.md) §4.
169
+
170
+ ## Privacy considerations
171
+
172
+ The following MUST NOT appear in any event governed by this profile, in any field, under any
173
+ property name:
174
+
175
+ 1. Secret values, old or new, whole or partial.
176
+ 2. Private or symmetric key material, wrapped or unwrapped.
177
+ 3. Recovery phrases, key shares, unwrapping keys and passphrases.
178
+ 4. Passwords, client secrets, tokens and API credentials.
179
+ 5. Connection strings, in `metadata`, in `change.before`/`change.after` or anywhere else.
180
+ 6. A hash or fingerprint of any of the above that would allow verification against a guess.
181
+
182
+ This is a requirement of the profile that **no rule in it can check**, and the profile says so rather
183
+ than approximating it with a rule that would be wrong. Checking values is
184
+ [`auditmodel lint-privacy`](../../specification/privacy.md), a separate and complementary command: a
185
+ profile says which fields must be present, the linter says which values must not. Every published
186
+ fixture here is required by test to pass both, and is additionally scanned field by field for a
187
+ scalar under any credential-shaped property name.
188
+
189
+ A rotation records `change.changedFields: ["secret", "version", "rotatedAt"]` and version identifiers
190
+ in `change.before` and `change.after`. The fact of the rotation is fully auditable; the material
191
+ appears nowhere. See [change-model.md](../../specification/change-model.md) §5.
192
+
193
+ No rule requires a personal identifier. `/resource/ownerId` is satisfied by an owning team or service
194
+ and needs no individual's name; `/metadata/secret/type` describes material, not people.
195
+
196
+ ## Known rule-language limitations
197
+
198
+ These are requirements or checks the v0.1 rule language cannot express. They are stated rather than
199
+ approximated.
200
+
201
+ 1. **No numeric comparison.** `SECRET-CERT-001` can require an expiry to be present and to be a
202
+ string. It cannot check that it is after the event time, that a certificate lifetime is below a
203
+ maximum, or that a rotation interval in a policy change did not get longer.
204
+ 2. **No cross-field comparison.** The profile cannot require that `change.before` and `change.after`
205
+ differ, so a rotation that changed nothing and a rotation that worked are indistinguishable to it.
206
+ 3. **No array-content predicates.** It cannot assert that `change.changedFields` names the member
207
+ that was replaced, that `approval.approvers` holds two distinct principals, or that
208
+ `receivedApprovals` reached `requiredApprovals`.
209
+ 4. **No disjunction.** "either an expiry or a declared non-expiring justification" is unexpressible;
210
+ the profile requires the strictly weaker thing and documents the rest here.
211
+ 5. **One condition per rule.** "emergency access to material classified `secret`" cannot be written;
212
+ `SECRET-ACCESS-002` fires on the emergency declaration alone.
213
+ 6. **Semantics are unverifiable.** A checker can confirm `/resource/id` is present. It cannot confirm
214
+ that it names the material acted on, that the actor is the operator rather than the custody
215
+ service, or that `/metadata/secret/type` is truthful.
216
+ 7. **Absence of material is unverifiable by a profile.** Rule evaluation is presence, JSON type and
217
+ scalar equality. Nothing in it could detect a secret placed in a field the profile requires.
218
+
219
+ ## Cross-profile overlaps
220
+
221
+ Four families in this repository touch secrets, and they are deliberately separated by **whose control
222
+ domain the event belongs to**, not by what the material is called.
223
+
224
+ | Event | Owned by | Why |
225
+ | ---------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
226
+ | `identity.credential.rotate` | [identity-and-access-management](../identity-and-access-management/) (`IAM-CRED-001`) | The credential belongs to a **principal**. The reviewable fact is whose access it grants. |
227
+ | `configuration.secret.rotate`, `.access` | configuration and change conventions | The secret is an application **setting**. The reviewable fact is that a deployed configuration changed. |
228
+ | `api-key.*` | [api-and-integration-management](../api-and-integration-management/) | The credential is an **interface grant** to a caller. The reviewable fact is who may call what. |
229
+ | `secret.*`, `key.*`, `certificate.*` | **this profile** | The material is held **in custody**. The reviewable fact is what the custodian did with material it holds for others. |
230
+
231
+ This profile **does not govern** `identity.credential.rotate`, `configuration.secret.*` or
232
+ `api-key.*`, and duplicates none of their rules. `IAM-CRED-001` already requires authorization, a
233
+ justification and `/metadata/credential/type` for principal credentials; adding a second rule over
234
+ the same name would mean two profiles disagreeing about one event. The separation is also structural:
235
+ no selector here uses a `key.` prefix, so `api-key.create` could not match this profile even by
236
+ accident.
237
+
238
+ A deployment whose secret store _is_ its configuration system should pick one domain and use it
239
+ consistently rather than emitting both. Where an operation genuinely belongs to two domains — a
240
+ custody rotation that also rotates a service account's credential — emit the event of the system that
241
+ performed it and reference the other through `relatedResources` or `/request/correlationId`.
242
+
243
+ There is one further overlap outside the profiles: [integrity.md](../../specification/integrity.md)
244
+ §8 (10) states that key management — generation, storage, rotation, revocation, distribution and
245
+ custody — is outside the core specification. That remains true. This profile governs **audit events
246
+ about** key management; it defines no key management behaviour, no algorithm requirement and no trust
247
+ model.
248
+
249
+ ## Fixtures
250
+
251
+ [examples/profiles/secrets-and-key-management/](../../examples/profiles/secrets-and-key-management/)
252
+ — twelve valid, twelve invalid, three not applicable.
253
+
254
+ | Fixture | Event | Rules exercised |
255
+ | ------------------------------ | ---------------------- | ------------------------------------------------------- |
256
+ | `secret-create.json` | `secret.create` | `CORE-001`, `CORE-002`, `LIFECYCLE-001` |
257
+ | `secret-rotate.json` | `secret.rotate` | `ROTATE-001`, unattended rotation with no justification |
258
+ | `secret-reveal.json` | `secret.reveal` | `ACCESS-001` |
259
+ | `secret-reveal-emergency.json` | `secret.reveal` | `ACCESS-002` with a retrospective approval |
260
+ | `secret-export.json` | `secret.export` | `ACCESS-001`, `EXPORT-001`, `APPROVAL-001` |
261
+ | `secret-revoke.json` | `secret.revoke` | `DESTROY-001` |
262
+ | `secret-policy-update.json` | `secret.policy.update` | `POLICY-001` |
263
+ | `key-generate.json` | `key.generate` | `LIFECYCLE-001`, `KEY-002` |
264
+ | `key-import.json` | `key.import` | `KEY-001`, `LIFECYCLE-001` |
265
+ | `key-destroy.json` | `key.destroy` | `DESTROY-001`, `APPROVAL-001` |
266
+ | `key-export.json` | `key.export` | `ACCESS-001`, `EXPORT-001` |
267
+ | `certificate-issue.json` | `certificate.issue` | `CERT-001`, `LIFECYCLE-001` |
268
+
269
+ Every enforceable rule has exactly one negative fixture, listed in the
270
+ [fixture README](../../examples/profiles/secrets-and-key-management/README.md). Each removes one
271
+ required value from a valid fixture and fails for one documented reason.
272
+
273
+ ### Not-applicable rationale
274
+
275
+ `secret-retrieve.json`, `secret-cache-refresh.json` and `key-decrypt.json` are ordinary,
276
+ well-formed audit events that this profile deliberately does not govern: a workload reading its own
277
+ credential at start-up, the same workload refreshing its cached copy, and a service decrypting a
278
+ record with a key it is permitted to use. `check-profile` reports each as not applicable, with exit
279
+ code 3.
280
+
281
+ They exist to hold the exclusion in place. If a future edit widened a selector to a bare `secret.` or
282
+ `key.` prefix, these fixtures would start conforming instead of being skipped and the test would fail
283
+ — which is the point, because that edit would silently impose an authorization decision, a
284
+ classification and a material type on every secret read and every cryptographic operation in a
285
+ production system.
286
+
287
+ ## Not required, and why
288
+
289
+ - **Trace identifiers.** `/request/traceId` presupposes a tracing discipline a conforming producer
290
+ may not have. `/request/correlationId` is recommended, never required, including for rotation.
291
+ - **A justification for scheduled rotation.** Automated rotation is the behaviour the profile wants
292
+ to encourage; a required `/reason` would make the safest path the most expensive one.
293
+ - **`/request/protocol`, `/request/ipAddress`, `/request/userAgent`.** Real signals for interactive
294
+ access, but they describe one access mechanism. A custody operation performed by a scheduler has
295
+ none of them.
296
+ - **Key fingerprints and material hashes.** They are exactly what an attacker needs to confirm a
297
+ guess. `/resource/id` identifies the material without describing it.
298
+ - **Quorum, key shares and split knowledge.** Genuine controls in high-assurance custody, but the
299
+ rule language cannot inspect `approval.approvers`, and requiring the object without being able to
300
+ check its contents would suggest a guarantee the tool does not give.
301
+ - **A `secret.approval.*` event family.** Approval is already modelled by `workflow.approval.*` in
302
+ [workflow-and-approval.md](../../semantic-conventions/workflow-and-approval.md) and by the core
303
+ `/approval` object.
304
+
305
+ ## Open questions
306
+
307
+ - Is a single `/metadata/secret/type` enough, or do keys need a separate usage field — signing,
308
+ encryption, key-wrapping — that a rotation cannot change? The profile currently folds usage into
309
+ the type vocabulary.
310
+ - Should `secret.reveal` by a service actor be treated differently from a reveal by a human? The
311
+ profile requires `/authentication` for both and lets `actor.type` carry the distinction, because
312
+ automated reveal is real in migration tooling and rejecting it would push producers to mislabel it.
313
+ - Should `key.export` require an approval unconditionally? It is the operation with the largest
314
+ irreversible consequence in the profile, but disaster-recovery replication performs it on a
315
+ schedule in deployments that are not doing anything wrong.
316
+ - Is `/metadata/secret/destinationType` the right granularity, or is an external-versus-internal
317
+ boolean the fact reviewers actually filter on, as the document profile's `recipientType` suggests?
318
+
319
+ ## Compatibility
320
+
321
+ The profile version is independent of the core specification version. `coreVersions` declares which
322
+ core versions the profile applies to; an event declaring any other `specVersion` is **not applicable**
323
+ rather than in violation. Adding a rule is a breaking change for producers, in the same sense as
324
+ adding a required core field.
325
+
326
+ ## Not a compliance statement
327
+
328
+ Conformance to this profile means an event carries the fields this profile requires. It is not
329
+ compliance with any law, regulation, standard, certification scheme or contract, and MUST NOT be
330
+ presented as evidence of one. Nothing here asserts that any cryptographic algorithm, key length,
331
+ rotation interval or custody arrangement is adequate.
@@ -0,0 +1,219 @@
1
+ {
2
+ "profileVersion": "0.1",
3
+ "name": "secrets-and-key-management",
4
+ "version": "0.1",
5
+ "status": "experimental",
6
+ "coreVersions": ["0.1"],
7
+ "title": "Secrets and Key Management Profile",
8
+ "description": "Additional conformance requirements for the custody of secrets, cryptographic keys and certificates: creation, rotation, revocation, destruction, policy change, and the privileged reveal and export operations that turn protected material into copied material. Every requirement adds to the OpenAuditModel Core Specification; none relaxes it. Routine automated retrieval of a secret and ordinary cryptographic operations against a key are deliberately not governed. An audit event records that a secret operation occurred; it never records the secret.",
9
+ "rules": [
10
+ {
11
+ "id": "SECRET-CORE-001",
12
+ "description": "Every governed secret, key or certificate operation records the authorization decision that permitted it and the classification of the material it acted on.",
13
+ "rationale": "Custody operations are the ones an attacker performs after gaining a foothold and before doing anything visible, so an operation with no recorded decision cannot be separated from one that bypassed policy. Classification is what makes the record triageable: a key protecting settlement records and a key in a sandbox produce identical events otherwise, and a reviewer who cannot tell them apart must treat every rotation as an incident or none of them.",
14
+ "severity": "error",
15
+ "events": [
16
+ "secret.create",
17
+ "secret.update",
18
+ "secret.rotate",
19
+ "secret.revoke",
20
+ "secret.delete",
21
+ "secret.reveal",
22
+ "secret.export",
23
+ "key.generate",
24
+ "key.import",
25
+ "key.rotate",
26
+ "key.enable",
27
+ "key.disable",
28
+ "key.destroy",
29
+ "key.export",
30
+ "certificate.issue",
31
+ "certificate.renew",
32
+ "certificate.revoke",
33
+ "certificate.delete"
34
+ ],
35
+ "eventPrefixes": ["secret.policy.", "key.policy."],
36
+ "requiredPaths": ["/authorization", "/resource/classification"]
37
+ },
38
+ {
39
+ "id": "SECRET-CORE-002",
40
+ "description": "Every governed operation records the kind of protected material it acted on.",
41
+ "rationale": "The kind of material is the blast radius. Replacing a read-only reporting credential and replacing the key that signs every payment instruction are the same event shape and entirely different incidents, and no external reader can recover the difference from an opaque identifier once the material is gone. The type is recorded rather than inferred because only the custodian knows it, and it is known at exactly the moment the event is emitted.",
42
+ "severity": "error",
43
+ "events": [
44
+ "secret.create",
45
+ "secret.update",
46
+ "secret.rotate",
47
+ "secret.revoke",
48
+ "secret.delete",
49
+ "secret.reveal",
50
+ "secret.export",
51
+ "key.generate",
52
+ "key.import",
53
+ "key.rotate",
54
+ "key.enable",
55
+ "key.disable",
56
+ "key.destroy",
57
+ "key.export",
58
+ "certificate.issue",
59
+ "certificate.renew",
60
+ "certificate.revoke",
61
+ "certificate.delete"
62
+ ],
63
+ "eventPrefixes": ["secret.policy.", "key.policy."],
64
+ "requiredMetadata": [{ "path": "/secret/type", "type": "string" }]
65
+ },
66
+ {
67
+ "id": "SECRET-CORE-003",
68
+ "description": "A governed operation should record why it happened, which custody system holds the material, and how it correlates with the wider operation.",
69
+ "rationale": "These are the fields a reviewer reaches for first and a producer omits most often. They are recommended rather than required because a scheduled rotation has no justification beyond the policy that scheduled it, because a deployment may have exactly one custody system and nothing to distinguish, and because correlation identifiers presuppose a tracing or workflow discipline that a conforming producer may not have. A missing answer here should prompt a question, not fail a build.",
70
+ "severity": "warning",
71
+ "events": [
72
+ "secret.create",
73
+ "secret.update",
74
+ "secret.rotate",
75
+ "secret.revoke",
76
+ "secret.delete",
77
+ "secret.reveal",
78
+ "secret.export",
79
+ "key.generate",
80
+ "key.import",
81
+ "key.rotate",
82
+ "key.enable",
83
+ "key.disable",
84
+ "key.destroy",
85
+ "key.export",
86
+ "certificate.issue",
87
+ "certificate.renew",
88
+ "certificate.revoke",
89
+ "certificate.delete"
90
+ ],
91
+ "eventPrefixes": ["secret.policy.", "key.policy."],
92
+ "recommendedPaths": ["/reason", "/request/correlationId", "/metadata/secret/provider"]
93
+ },
94
+ {
95
+ "id": "SECRET-LIFECYCLE-001",
96
+ "description": "Material brought into custody records who owns it.",
97
+ "rationale": "An unowned secret cannot be rotated and cannot be retired, because nobody knows which systems break when it changes. Ownership is knowable at exactly one moment — when the material is created, generated, imported or issued — and every later attempt to reconstruct it is archaeology across deployment manifests. Recording an owning team or service is not personal data and does not require naming an individual.",
98
+ "severity": "error",
99
+ "events": ["secret.create", "key.generate", "key.import", "certificate.issue"],
100
+ "requiredPaths": ["/resource/ownerId"]
101
+ },
102
+ {
103
+ "id": "SECRET-ROTATE-001",
104
+ "description": "Rotation records the transition it performed, naming what changed rather than what the material now is.",
105
+ "rationale": "Rotation is the healthy operation in this domain and the one most often recorded as a bare fact. Without `/change` the trail says something was replaced and nothing about which version succeeded which, so a consumer failing on a stale value cannot be tied to the rotation that caused it and a rotation that silently did nothing is indistinguishable from one that worked. `/change` is the core structure designed to carry that transition without carrying a value: `changedFields` names members, and `before` and `after` carry version identifiers, never key or secret material.",
106
+ "severity": "error",
107
+ "events": ["secret.rotate", "key.rotate", "certificate.renew"],
108
+ "requiredPaths": ["/change"],
109
+ "recommendedPaths": ["/change/changedFields"]
110
+ },
111
+ {
112
+ "id": "SECRET-ACCESS-001",
113
+ "description": "Revealing a stored value or exporting material records how the principal authenticated and why the access was needed.",
114
+ "rationale": "Reveal and export are the two operations that convert protected material into copied material: after them the custodian is no longer the only holder and no later control can undo that. The authentication context is what distinguishes a reviewed operator session from a replayed one, and the justification is the only thing a reviewer has months later, when the actor has changed teams and the copy is still valid. Recording the fact of the access never requires recording any part of the value.",
115
+ "severity": "error",
116
+ "events": ["secret.reveal", "secret.export", "key.export"],
117
+ "requiredPaths": ["/authentication", "/reason"],
118
+ "recommendedPaths": ["/approval"]
119
+ },
120
+ {
121
+ "id": "SECRET-ACCESS-002",
122
+ "description": "Access the producer has declared to be emergency or break-glass access is approved and performed from a multi-factor authenticated session.",
123
+ "rationale": "Break-glass access exists precisely to bypass the controls that normally apply, so it is the one path where the audit record is the only remaining control. `/approval` is required as a present object rather than as an approved decision, because emergencies are frequently approved retrospectively and the model can say so — `status: pending` on a successful access is an accurate record of a control bypass and exactly what a post-incident review looks for. Suppressing it would be the failure this rule exists to prevent. The rule fires only on the producer's own declaration, so the profile never has to guess what counts as an emergency in a given deployment.",
124
+ "severity": "error",
125
+ "events": ["secret.reveal", "secret.export", "key.export"],
126
+ "when": { "path": "/metadata/secret/emergencyAccess", "equals": true },
127
+ "requiredPaths": ["/approval"],
128
+ "requiredValues": [{ "path": "/authentication/mfa", "equals": true }]
129
+ },
130
+ {
131
+ "id": "SECRET-EXPORT-001",
132
+ "description": "An export records the kind of destination the material was moved to.",
133
+ "rationale": "Where exported material went is the single fact that determines the risk of the operation, and it is the fact least likely to survive in anyone's memory. Material copied into a second hardware custodian, written to offline backup media, displayed on an operator console and handed to an external party are four different exposures that produce identical events without this field. The profile requires the kind of destination rather than an address, because an address is often itself sensitive and is never comparable across deployments.",
134
+ "severity": "error",
135
+ "events": ["secret.export", "key.export"],
136
+ "requiredMetadata": [{ "path": "/secret/destinationType", "type": "string" }]
137
+ },
138
+ {
139
+ "id": "SECRET-DESTROY-001",
140
+ "description": "Revoking, disabling, destroying or deleting protected material is justified.",
141
+ "rationale": "These are the operations an audit trail is least able to reconstruct afterwards, because the evidence is what was removed. Destroying an encryption key destroys the readability of everything it protected, and revoking a certificate can take a service offline as effectively as an outage — so the difference between a planned decommissioning, an incident response and a mistake has to be stated at the time, by the only party who knows it. Approval is recommended rather than required because many deployments legitimately let a team retire its own test material, and a rule that forbade that would be switched off rather than met.",
142
+ "severity": "error",
143
+ "events": [
144
+ "secret.revoke",
145
+ "secret.delete",
146
+ "key.disable",
147
+ "key.destroy",
148
+ "certificate.revoke",
149
+ "certificate.delete"
150
+ ],
151
+ "requiredPaths": ["/reason"],
152
+ "recommendedPaths": ["/approval"]
153
+ },
154
+ {
155
+ "id": "SECRET-APPROVAL-001",
156
+ "description": "An operation the producer has declared to require approval records the approval.",
157
+ "rationale": "Approval requirements in this domain are set locally: one organization requires two signatures to destroy a key and another rotates certificates unattended thousands of times a day, and a profile that imposed either model on the other would describe one deployment and be ignored by the rest. So the producer declares the requirement and the profile enforces the consequence — an operation that its own policy says needs approval, and that carries none, is the control gap this rule makes visible. The rule requires the approval object, not an approved status, so that a rejected or still-pending approval on a completed operation is recordable rather than unrepresentable.",
158
+ "severity": "error",
159
+ "events": [
160
+ "secret.create",
161
+ "secret.update",
162
+ "secret.rotate",
163
+ "secret.revoke",
164
+ "secret.delete",
165
+ "secret.reveal",
166
+ "secret.export",
167
+ "key.generate",
168
+ "key.import",
169
+ "key.rotate",
170
+ "key.enable",
171
+ "key.disable",
172
+ "key.destroy",
173
+ "key.export",
174
+ "certificate.issue",
175
+ "certificate.renew",
176
+ "certificate.revoke",
177
+ "certificate.delete"
178
+ ],
179
+ "eventPrefixes": ["secret.policy.", "key.policy."],
180
+ "when": { "path": "/metadata/secret/approvalRequired", "equals": true },
181
+ "requiredPaths": ["/approval"]
182
+ },
183
+ {
184
+ "id": "SECRET-POLICY-001",
185
+ "description": "A change to a secret or key policy records the transition and why it was made.",
186
+ "rationale": "A policy change is where the controls over every piece of material in scope are switched off at once, and it usually leaves no trace anywhere else: lengthening a rotation interval, removing an approval requirement or widening who may reveal a value weakens thousands of secrets in a single event that looks like routine configuration. Recording the before and after state is what turns that event into evidence, and recording the reason is what separates a deliberate policy decision from an accident nobody noticed for a year.",
187
+ "severity": "error",
188
+ "eventPrefixes": ["secret.policy.", "key.policy."],
189
+ "requiredPaths": ["/change", "/reason"],
190
+ "recommendedPaths": ["/approval"]
191
+ },
192
+ {
193
+ "id": "SECRET-CERT-001",
194
+ "description": "Issuing or renewing a certificate records when it stops being valid.",
195
+ "rationale": "A certificate always has an end of validity, it is fixed at the moment of issue, and it is the fact that causes the outage when nobody wrote it down. This profile recommends an expiry everywhere else and requires it here for that reason: expiry is optional for material that may legitimately be indefinite, and mandatory for material that cannot be. The rule constrains presence and JSON type only; the v0.1 rule language cannot compare an expiry against the event time or against a maximum lifetime.",
196
+ "severity": "error",
197
+ "events": ["certificate.issue", "certificate.renew"],
198
+ "requiredMetadata": [{ "path": "/secret/expiresAt", "type": "string" }],
199
+ "recommendedPaths": ["/metadata/secret/algorithm"]
200
+ },
201
+ {
202
+ "id": "SECRET-KEY-001",
203
+ "description": "Importing key material generated outside the custody system is justified.",
204
+ "rationale": "Imported material existed somewhere else before the import, under procedures the custodian did not run and cannot attest to, and it may still exist there afterwards. That is the one property of a key that a custody system cannot establish for itself, so the reason for accepting external material has to be recorded by the party that decided to accept it. Generation inside the custodian carries no such requirement, which is deliberate: the profile should make the safer path the cheaper one.",
205
+ "severity": "error",
206
+ "events": ["key.import"],
207
+ "requiredPaths": ["/reason"],
208
+ "recommendedPaths": ["/approval"]
209
+ },
210
+ {
211
+ "id": "SECRET-KEY-002",
212
+ "description": "Key lifecycle events should record the algorithm the key uses and when it stops being valid.",
213
+ "rationale": "Algorithm and expiry are what make a key inventory answerable — which keys use a deprecated algorithm, which expire this quarter — and neither can be reconstructed after the key is destroyed. They are recommended rather than required because a custodian may legitimately treat the algorithm as an internal detail of a managed key service it does not control, and because a key held for as long as the data it protects has no expiry to record.",
214
+ "severity": "warning",
215
+ "events": ["key.generate", "key.import", "key.rotate"],
216
+ "recommendedPaths": ["/metadata/secret/algorithm", "/metadata/secret/expiresAt"]
217
+ }
218
+ ]
219
+ }