@openauditmodel/cli 0.3.0 → 0.4.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 (30) hide show
  1. package/README.md +60 -4
  2. package/dist/conformance/src/cli.js +113 -6
  3. package/dist/conformance/src/cli.js.map +1 -1
  4. package/dist/conformance/src/privacy/types.d.ts +19 -0
  5. package/dist/conformance/src/privacy/types.js +8 -0
  6. package/dist/conformance/src/privacy/types.js.map +1 -1
  7. package/dist/conformance/src/profiles/coverage.d.ts +62 -0
  8. package/dist/conformance/src/profiles/coverage.js +114 -0
  9. package/dist/conformance/src/profiles/coverage.js.map +1 -0
  10. package/package.json +7 -2
  11. package/profiles/README.md +42 -0
  12. package/profiles/api-and-integration-management/0.1/profile.json +258 -0
  13. package/profiles/backup-and-recovery/0.1/profile.json +178 -0
  14. package/profiles/customer-and-account-management/0.1/profile.json +237 -0
  15. package/profiles/deployment-and-change-management/0.1/profile.json +298 -0
  16. package/profiles/document-management/0.1/profile.json +170 -0
  17. package/profiles/financial-transaction-management/0.1/profile.json +247 -0
  18. package/profiles/identity-and-access-management/0.1/profile.json +120 -0
  19. package/profiles/incident-management/0.1/profile.json +256 -0
  20. package/profiles/incident-management/0.2/profile.json +287 -0
  21. package/profiles/incident-management/README.md +39 -21
  22. package/profiles/incident-management/profile.json +37 -6
  23. package/profiles/message-broker-management/0.1/profile.json +399 -0
  24. package/profiles/secrets-and-key-management/0.1/profile.json +219 -0
  25. package/semantic-conventions/README.md +15 -10
  26. package/semantic-conventions/backup-and-recovery.md +124 -0
  27. package/semantic-conventions/customer-and-account.md +133 -0
  28. package/semantic-conventions/financial-transactions.md +128 -0
  29. package/semantic-conventions/message-brokers.md +128 -0
  30. package/semantic-conventions/secrets-and-keys.md +132 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../../../conformance/src/profiles/coverage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openauditmodel/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Conformance toolchain for OpenAuditModel: validate audit events against the canonical JSON Schema, verify tamper-evidence, lint for leaked secrets and check domain profiles. Offline and deterministic.",
5
5
  "keywords": [
6
6
  "audit",
@@ -52,7 +52,12 @@
52
52
  "check:profile": "npm run build && node dist/conformance/src/cli.js check-profile examples/profiles/identity-and-access-management/valid --profile identity-and-access-management && node dist/conformance/src/cli.js check-profile examples/profiles/document-management/valid --profile document-management && node dist/conformance/src/cli.js check-profile examples/profiles/incident-management/valid --profile incident-management && node dist/conformance/src/cli.js check-profile examples/profiles/message-broker-management/valid --profile message-broker-management && node dist/conformance/src/cli.js check-profile examples/profiles/deployment-and-change-management/valid --profile deployment-and-change-management && node dist/conformance/src/cli.js check-profile examples/profiles/financial-transaction-management/valid --profile financial-transaction-management && node dist/conformance/src/cli.js check-profile examples/profiles/secrets-and-key-management/valid --profile secrets-and-key-management && node dist/conformance/src/cli.js check-profile examples/profiles/customer-and-account-management/valid --profile customer-and-account-management && node dist/conformance/src/cli.js check-profile examples/profiles/backup-and-recovery/valid --profile backup-and-recovery && node dist/conformance/src/cli.js check-profile examples/profiles/api-and-integration-management/valid --profile api-and-integration-management",
53
53
  "fixtures:integrity": "npm run build && node dist/conformance/tools/generate-integrity-fixtures.js && prettier --write examples/integrity",
54
54
  "fixtures:check": "npm run build && node dist/conformance/tools/generate-integrity-fixtures.js --check",
55
- "verify": "npm run format:check && npm run lint && npm run test && npm run validate:valid && npm run verify:integrity && npm run lint:privacy && npm run check:profile && npm run fixtures:check && npm run mcp:check-generated && npm run package:verify && npm run package:smoke",
55
+ "kit:build": "npm run build && node dist/conformance/tools/generate-kit.js && prettier --write conformance-kit",
56
+ "kit:check": "npm run build && node dist/conformance/tools/generate-kit.js --check",
57
+ "profiles:lint": "npm run build && node dist/conformance/tools/lint-profiles.js",
58
+ "profiles:archive": "npm run build && node dist/conformance/tools/archive-profile-versions.js",
59
+ "profiles:check-archive": "npm run build && node dist/conformance/tools/archive-profile-versions.js --check",
60
+ "verify": "npm run format:check && npm run lint && npm run test && npm run validate:valid && npm run verify:integrity && npm run lint:privacy && npm run check:profile && npm run fixtures:check && npm run profiles:check-archive && npm run profiles:lint && npm run kit:check && npm run mcp:check-generated && npm run package:verify && npm run package:smoke",
56
61
  "mcp:generate": "npm run generate --workspace mcp",
57
62
  "mcp:check-generated": "npm run generate:check --workspace mcp",
58
63
  "mcp:start": "npm run build && node dist/mcp/src/index.js",
@@ -75,6 +75,48 @@ documents**; it is not part of the canonical audit event schema and never constr
75
75
  `coreVersions` lists the core versions the profile applies to; an event declaring any other
76
76
  `specVersion` is **not applicable** rather than in violation.
77
77
 
78
+ ### Versions are filed, and a filed version never changes
79
+
80
+ **Changing a profile's rules MUST bump its `version`.** [ADR 0008](../decisions/0008-declarative-profile-conformance.md)
81
+ records why: adding a rule is a breaking change for producers, in the same sense as adding a required
82
+ core field, because events that conformed may stop conforming.
83
+
84
+ Each version is filed at `profiles/<name>/<version>/profile.json` when it is published, and a filed
85
+ copy is never edited afterwards. The site publishes every filed version, so revising a profile adds
86
+ an address rather than withdrawing the one consumers already have — the site serves these documents
87
+ with a year-long `immutable` cache lifetime, and a URL that answered yesterday must not 404 today.
88
+
89
+ The archive is what enforces the bump. Change a rule without changing `version` and the working
90
+ document no longer matches the copy filed under that version, which fails `npm run verify`. There are
91
+ two ways to make it pass: bump the version, or revert the change.
92
+
93
+ ```bash
94
+ npm run profiles:archive # file the current version of every profile
95
+ npm run profiles:check-archive # compare each profile with its filed copy
96
+ npm run profiles:lint # report defects the definition schema cannot express
97
+ ```
98
+
99
+ ### What the lint checks
100
+
101
+ The definition schema decides whether a profile document is well formed. It cannot decide whether the
102
+ document says what its author meant, and three of the ways it can fail to are silent:
103
+
104
+ | | |
105
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
106
+ | A rule id declared twice | Selection deduplicates by id, so the later rule is never evaluated and its requirements vanish. The event is reported **conforming** |
107
+ | A rule that states no requirement | An event is `not-applicable` only while no rule selects it. One requirement-free rule makes it governed and satisfied |
108
+ | A condition on a path no rule requires | An absent condition path means the condition does not hold, so a producer that never writes the flag escapes the requirement in silence |
109
+
110
+ The lint also rejects pointers that can never resolve — an empty reference token, and a
111
+ `requiredMetadata` path beginning with `/metadata`, which is concatenated into `/metadata/metadata/…`
112
+ — and requirements that contradict each other. Those are errors. Redundant selectors and unreachable
113
+ prefixes are warnings, and warnings never fail the build.
114
+
115
+ A version bump is not complete until three things move together: `version` in `profile.json`, a filed
116
+ copy under the new version, and the profile's allowlist entry in
117
+ `mcp/scripts/generate-resource-manifest.mjs`, which carries the version in the resource URI it serves.
118
+ A test holds each of them in place.
119
+
78
120
  ### Rules
79
121
 
80
122
  Each rule has an `id` and a `description`, and MAY have a `rationale` and a `severity`.
@@ -0,0 +1,258 @@
1
+ {
2
+ "profileVersion": "0.1",
3
+ "name": "api-and-integration-management",
4
+ "version": "0.1",
5
+ "status": "experimental",
6
+ "coreVersions": ["0.1"],
7
+ "title": "API and Integration Management Profile",
8
+ "description": "Additional conformance requirements for the administration of API credentials, webhook subscriptions and third-party integrations: issuing and revoking API keys, creating and reconfiguring webhooks, connecting, reauthorizing and disconnecting external systems, and starting or cancelling integration syncs. Every requirement adds to the OpenAuditModel Core Specification; none relaxes it. Data-plane traffic is deliberately not governed: ordinary API requests, webhook deliveries and routine integration polling match no rule in this profile.",
9
+ "rules": [
10
+ {
11
+ "id": "INTEGRATION-CORE-001",
12
+ "description": "Every governed API, webhook or integration administration event records the authorization decision that permitted it and the kind of integration point it acted on.",
13
+ "rationale": "An integration is a standing hole in a trust boundary, and the events that open, widen or close one are the only record that the hole was opened deliberately. Without a recorded decision nothing distinguishes a change a policy allowed from one that bypassed policy, and without the kind of integration point a reviewer cannot tell whether the event concerns a credential someone holds, a callback that pushes data outward, or a connector that pulls data in — three operations with entirely different blast radii that otherwise look identical in a report.",
14
+ "severity": "error",
15
+ "events": [
16
+ "api-key.create",
17
+ "api-key.rotate",
18
+ "api-key.revoke",
19
+ "api-key.delete",
20
+ "webhook.create",
21
+ "webhook.update",
22
+ "webhook.enable",
23
+ "webhook.disable",
24
+ "webhook.delete",
25
+ "webhook.test",
26
+ "integration.connect",
27
+ "integration.disconnect",
28
+ "integration.enable",
29
+ "integration.disable",
30
+ "integration.reauthorize",
31
+ "integration.sync.start",
32
+ "integration.sync.cancel"
33
+ ],
34
+ "eventPrefixes": ["integration.configuration."],
35
+ "requiredPaths": ["/authorization"],
36
+ "requiredMetadata": [{ "path": "/integration/type", "type": "string" }]
37
+ },
38
+ {
39
+ "id": "INTEGRATION-CORE-002",
40
+ "description": "A governed integration event should record why it happened, whether it was approved, which external party it concerns, and how it correlates with the wider operation.",
41
+ "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 business justification beyond the schedule, because most integration changes legitimately need no approval, and because a first-party API key has no external counterparty to name. A missing one of these should prompt a question, not fail a build.",
42
+ "severity": "warning",
43
+ "events": [
44
+ "api-key.create",
45
+ "api-key.rotate",
46
+ "api-key.revoke",
47
+ "api-key.delete",
48
+ "webhook.create",
49
+ "webhook.update",
50
+ "webhook.enable",
51
+ "webhook.disable",
52
+ "webhook.delete",
53
+ "webhook.test",
54
+ "integration.connect",
55
+ "integration.disconnect",
56
+ "integration.enable",
57
+ "integration.disable",
58
+ "integration.reauthorize",
59
+ "integration.sync.start",
60
+ "integration.sync.cancel"
61
+ ],
62
+ "eventPrefixes": ["integration.configuration."],
63
+ "recommendedPaths": [
64
+ "/reason",
65
+ "/approval",
66
+ "/request/correlationId",
67
+ "/metadata/integration/provider"
68
+ ]
69
+ },
70
+ {
71
+ "id": "INTEGRATION-CORE-003",
72
+ "description": "When the producer declares that local policy required approval for this change, the event carries the approval and its state.",
73
+ "rationale": "Approval requirements for integration changes are set by the operator, not by this specification: one organization gates every outbound webhook, another gates none. So the profile does not guess. When a producer has declared that approval was required, an event that omits the approval record is evidence that the control was skipped or that the trail cannot show it was honoured — and those two are indistinguishable after the fact, which is exactly the ambiguity an audit trail exists to remove. The status is required alongside the object because an approval with no state answers nothing.",
74
+ "severity": "error",
75
+ "events": [
76
+ "api-key.create",
77
+ "api-key.rotate",
78
+ "api-key.revoke",
79
+ "api-key.delete",
80
+ "webhook.create",
81
+ "webhook.update",
82
+ "webhook.enable",
83
+ "webhook.disable",
84
+ "webhook.delete",
85
+ "webhook.test",
86
+ "integration.connect",
87
+ "integration.disconnect",
88
+ "integration.enable",
89
+ "integration.disable",
90
+ "integration.reauthorize",
91
+ "integration.sync.start",
92
+ "integration.sync.cancel"
93
+ ],
94
+ "eventPrefixes": ["integration.configuration."],
95
+ "when": { "path": "/metadata/integration/approvalRequired", "equals": true },
96
+ "requiredPaths": ["/approval", "/approval/status"]
97
+ },
98
+ {
99
+ "id": "INTEGRATION-AUTHN-001",
100
+ "description": "A credential or connection-authorization operation performed by a human user records how that user was authenticated.",
101
+ "rationale": "Issuing, rotating or revoking an API key and authorizing or tearing down a connection are the operations an attacker performs to establish or remove persistence. When a person did it, the strength of the session behind the action is the fact that separates a routine administrative change from a change made through a stolen cookie, and it can never be reconstructed later. The requirement is conditional on the actor being a person because a scheduled rotation worker has no interactive session to describe, and a rule that demanded one would be switched off rather than met.",
102
+ "severity": "error",
103
+ "events": [
104
+ "api-key.create",
105
+ "api-key.rotate",
106
+ "api-key.revoke",
107
+ "api-key.delete",
108
+ "integration.connect",
109
+ "integration.reauthorize",
110
+ "integration.disconnect"
111
+ ],
112
+ "when": { "path": "/actor/type", "equals": "user" },
113
+ "requiredPaths": ["/authentication"]
114
+ },
115
+ {
116
+ "id": "INTEGRATION-AUTHN-002",
117
+ "description": "A credential or connection-authorization operation performed by an administrator records how that administrator was authenticated.",
118
+ "rationale": "This states the same requirement as INTEGRATION-AUTHN-001 for the other human principal type in the core model. It exists as a separate rule because the v0.1 rule language permits exactly one equality condition per rule and has no disjunction, so `user` and `admin` cannot be expressed in one condition. Splitting the rule is the honest way to cover both; collapsing them by dropping the condition would demand an interactive session from every automated rotation worker.",
119
+ "severity": "error",
120
+ "events": [
121
+ "api-key.create",
122
+ "api-key.rotate",
123
+ "api-key.revoke",
124
+ "api-key.delete",
125
+ "integration.connect",
126
+ "integration.reauthorize",
127
+ "integration.disconnect"
128
+ ],
129
+ "when": { "path": "/actor/type", "equals": "admin" },
130
+ "requiredPaths": ["/authentication"]
131
+ },
132
+ {
133
+ "id": "INTEGRATION-KEY-001",
134
+ "description": "An API key lifecycle event names the credential it acted on through a non-secret reference.",
135
+ "rationale": "A key rotation that does not say which key rotated cannot be tied to the calls made before and after it, so neither the exposure window nor the callers that broke can be established. The reference is a handle the producer can resolve; it is never the key. The profile requires the reference precisely so that producers have somewhere non-sensitive to put the answer. Nothing enforces that discipline: the privacy linter catches published credential formats and high-entropy values, but a producer who stores a real key here may well pass it, so this is a requirement on the producer rather than a check.",
136
+ "severity": "error",
137
+ "events": ["api-key.create", "api-key.rotate", "api-key.revoke", "api-key.delete"],
138
+ "requiredMetadata": [{ "path": "/integration/credentialReference", "type": "string" }]
139
+ },
140
+ {
141
+ "id": "INTEGRATION-KEY-002",
142
+ "description": "Issuing or rotating an API key should record what the key may do and when it stops working.",
143
+ "rationale": "Scope and expiry are what turn a credential into a reviewable one: an unbounded, never-expiring key is the finding, and the only moment the answer is known is issuance. Both are recommended rather than required because plenty of legitimate deployments issue keys with no expiry by design, and because scope vocabularies differ enough between products that a required field would be filled with a placeholder.",
144
+ "severity": "warning",
145
+ "events": ["api-key.create", "api-key.rotate"],
146
+ "recommendedPaths": [
147
+ "/metadata/integration/scope",
148
+ "/metadata/integration/expiresAt",
149
+ "/resource/ownerId"
150
+ ]
151
+ },
152
+ {
153
+ "id": "INTEGRATION-REVOKE-001",
154
+ "description": "Revoking, deleting, disabling or disconnecting an API credential, webhook or integration is justified.",
155
+ "rationale": "Withdrawal breaks something that was working for somebody, and the three explanations — planned decommission, incident containment, and mistake — produce identical events unless the reason is recorded. This is also the class of operation most often performed under pressure, when the person who could explain it is busy, so the justification has to be captured at the time or not at all.",
156
+ "severity": "error",
157
+ "events": [
158
+ "api-key.revoke",
159
+ "api-key.delete",
160
+ "webhook.disable",
161
+ "webhook.delete",
162
+ "integration.disconnect",
163
+ "integration.disable",
164
+ "integration.sync.cancel"
165
+ ],
166
+ "requiredPaths": ["/reason"]
167
+ },
168
+ {
169
+ "id": "INTEGRATION-HOOK-001",
170
+ "description": "A webhook administration event identifies the subscription and classifies the destination the callback is delivered to.",
171
+ "rationale": "A webhook is an instruction to push data out of the system on an ongoing basis, so the single fact that determines its risk is where the data goes. The profile requires a classification rather than the callback URL on purpose: delivery URLs routinely carry signed access parameters or shared secrets in their path or query string, so recording one turns the audit trail into a credential store. A class such as an internal service, a partner network or the public internet answers the reviewer's question without carrying anything an attacker can use.",
172
+ "severity": "error",
173
+ "events": [
174
+ "webhook.create",
175
+ "webhook.update",
176
+ "webhook.enable",
177
+ "webhook.disable",
178
+ "webhook.delete",
179
+ "webhook.test"
180
+ ],
181
+ "requiredMetadata": [
182
+ { "path": "/integration/webhookId", "type": "string" },
183
+ { "path": "/integration/endpointClass", "type": "string" }
184
+ ]
185
+ },
186
+ {
187
+ "id": "INTEGRATION-CONFIG-001",
188
+ "description": "Reconfiguring a webhook subscription or an integration records the change itself.",
189
+ "rationale": "An update event that says only that something changed is not reviewable: retry behaviour, event selection, field mapping and destination are all reached through the same operation, and they are not equally consequential. The core `/change` object is where the difference belongs. The changed field names are recommended alongside it because they are the cheapest useful form of the answer and they carry no configuration values; before and after states are neither required nor recommended, because integration configuration frequently contains endpoints and headers that must not be copied into an audit event.",
190
+ "severity": "error",
191
+ "events": ["webhook.update"],
192
+ "eventPrefixes": ["integration.configuration."],
193
+ "requiredPaths": ["/change"],
194
+ "recommendedPaths": ["/change/changedFields"]
195
+ },
196
+ {
197
+ "id": "INTEGRATION-CONN-001",
198
+ "description": "An integration lifecycle, configuration or sync event identifies the connection instance and the external party on the other side of it.",
199
+ "rationale": "Organizations run many connections to the same kind of system and several to the same provider, so neither the provider name nor the resource identifier alone locates the integration that changed. Recording both is what allows a reviewer to answer the question that actually gets asked after a third-party compromise — which of our connections to that party were live, and who changed them — without needing the producer's internal topology.",
200
+ "severity": "error",
201
+ "events": [
202
+ "integration.connect",
203
+ "integration.disconnect",
204
+ "integration.enable",
205
+ "integration.disable",
206
+ "integration.reauthorize",
207
+ "integration.sync.start",
208
+ "integration.sync.cancel"
209
+ ],
210
+ "eventPrefixes": ["integration.configuration."],
211
+ "requiredMetadata": [
212
+ { "path": "/integration/connectionId", "type": "string" },
213
+ { "path": "/integration/provider", "type": "string" }
214
+ ]
215
+ },
216
+ {
217
+ "id": "INTEGRATION-FLOW-001",
218
+ "description": "Connecting, reauthorizing, starting a sync or cancelling a sync records the correlation identifier that ties the steps of the workflow together.",
219
+ "rationale": "These four operations are never a single event. A connection is a redirect, a consent, a callback and a token exchange; a sync is a start, a run and an end. Reconstructing what happened means reassembling those steps, and a producer-assigned correlation identifier is the only thing that makes that possible. It is required here and merely recommended elsewhere because elsewhere there is nothing to correlate with. This is deliberately not a trace identifier: correlation is a value the producer stamps on related events, and requiring `traceId` would make conformance depend on distributed tracing being deployed.",
220
+ "severity": "error",
221
+ "events": [
222
+ "integration.connect",
223
+ "integration.reauthorize",
224
+ "integration.sync.start",
225
+ "integration.sync.cancel"
226
+ ],
227
+ "requiredPaths": ["/request/correlationId"]
228
+ },
229
+ {
230
+ "id": "INTEGRATION-FAIL-001",
231
+ "description": "A failed integration operation classifies the failure, not only names it.",
232
+ "rationale": "The core model already requires a failure code. A code alone is a producer-defined string, so a reviewer looking at a burst of failures cannot tell whether an integration is rejecting credentials, being denied permission, timing out or hitting a quota without learning that producer's vocabulary first. The coarse classification is what makes 'is this a credential problem or a network problem?' answerable across products, and that question decides whether a failure burst is an outage or an attack.",
233
+ "severity": "error",
234
+ "events": [
235
+ "api-key.create",
236
+ "api-key.rotate",
237
+ "api-key.revoke",
238
+ "api-key.delete",
239
+ "webhook.create",
240
+ "webhook.update",
241
+ "webhook.enable",
242
+ "webhook.disable",
243
+ "webhook.delete",
244
+ "webhook.test",
245
+ "integration.connect",
246
+ "integration.disconnect",
247
+ "integration.enable",
248
+ "integration.disable",
249
+ "integration.reauthorize",
250
+ "integration.sync.start",
251
+ "integration.sync.cancel"
252
+ ],
253
+ "eventPrefixes": ["integration.configuration."],
254
+ "when": { "path": "/event/outcome", "equals": "failure" },
255
+ "requiredPaths": ["/event/error/type"]
256
+ }
257
+ ]
258
+ }
@@ -0,0 +1,178 @@
1
+ {
2
+ "profileVersion": "0.1",
3
+ "name": "backup-and-recovery",
4
+ "version": "0.1",
5
+ "status": "experimental",
6
+ "coreVersions": ["0.1"],
7
+ "title": "Backup and Recovery Profile",
8
+ "description": "Additional conformance requirements for backup, snapshot, restore, recovery and failover audit events: creating and verifying recovery points, deleting and expiring them, restoring data, moving service between sites, and changing the policy that decides what is protected. Every requirement adds to the OpenAuditModel Core Specification; none relaxes it. Data-plane events such as chunk writes, progress reports and replication heartbeats are deliberately not governed.",
9
+ "rules": [
10
+ {
11
+ "id": "BACKUP-CORE-001",
12
+ "description": "Every governed backup, snapshot, restore, recovery or backup-policy operation records the authorization decision that permitted it.",
13
+ "rationale": "Backup and recovery tooling holds standing access to the most complete copy of an organization's data and to the switch that moves production between sites. Without a recorded decision, nothing in the trail separates an operation a policy permitted from one that ran because the backup service could. An operation performed by a scheduler is not exempt: it records the decision the schedule represents, or `not-applicable` where the producer made no decision at all, so that the absence of a control is stated rather than inferred from a missing field.",
14
+ "severity": "error",
15
+ "events": [
16
+ "backup.create",
17
+ "backup.complete",
18
+ "backup.verify",
19
+ "backup.delete",
20
+ "backup.expire",
21
+ "snapshot.create",
22
+ "snapshot.delete",
23
+ "restore.start",
24
+ "restore.complete",
25
+ "recovery.start",
26
+ "recovery.complete",
27
+ "recovery.failover",
28
+ "recovery.failback"
29
+ ],
30
+ "eventPrefixes": ["backup.policy."],
31
+ "requiredPaths": ["/authorization"]
32
+ },
33
+ {
34
+ "id": "BACKUP-CORE-002",
35
+ "description": "A governed operation should record why it happened and how it correlates with the wider recovery workflow.",
36
+ "rationale": "Backup and recovery operations are multi-stage by nature: a restore is a start and a completion, a failover is a sequence across storage, database and traffic layers, and each stage is usually emitted by a different component. A correlation identifier is what turns those events back into one operation. It is recommended rather than required because correlation infrastructure is not universal, and an appliance that emits events with no request context would otherwise be pushed to invent one. A justification is recommended everywhere and required on the operations that destroy or move data.",
37
+ "severity": "warning",
38
+ "events": [
39
+ "backup.create",
40
+ "backup.complete",
41
+ "backup.verify",
42
+ "backup.delete",
43
+ "backup.expire",
44
+ "snapshot.create",
45
+ "snapshot.delete",
46
+ "restore.start",
47
+ "restore.complete",
48
+ "recovery.start",
49
+ "recovery.complete",
50
+ "recovery.failover",
51
+ "recovery.failback"
52
+ ],
53
+ "eventPrefixes": ["backup.policy."],
54
+ "recommendedPaths": ["/reason", "/request/correlationId"]
55
+ },
56
+ {
57
+ "id": "BACKUP-SET-001",
58
+ "description": "A backup lifecycle event identifies the backup copy it acted on and the kind of copy it is.",
59
+ "rationale": "An identifier is what ties the event to the copy it describes; without it the trail records that a backup happened but not which one, and a later question about a specific recovery point has no answer. The kind of copy matters just as much, because a full, incremental, differential or transaction-log copy carry different consequences: deleting a full copy can invalidate every incremental that depends on it, while deleting one incremental usually does not. A reviewer cannot infer either fact from the event name.",
60
+ "severity": "error",
61
+ "events": [
62
+ "backup.create",
63
+ "backup.complete",
64
+ "backup.verify",
65
+ "backup.delete",
66
+ "backup.expire"
67
+ ],
68
+ "requiredMetadata": [
69
+ { "path": "/backup/backupId", "type": "string" },
70
+ { "path": "/backup/backupType", "type": "string" }
71
+ ],
72
+ "recommendedPaths": ["/metadata/backup/retentionClass"]
73
+ },
74
+ {
75
+ "id": "BACKUP-SET-002",
76
+ "description": "A backup run that completed successfully records the point in time the copy represents.",
77
+ "rationale": "A backup is only meaningful as a position on a timeline. Without the recovery point, the trail can say a copy exists but not what state it holds, and the question every recovery review begins with — how much data would be lost by restoring this copy — is unanswerable. The requirement is conditional on a successful outcome because a failed run produces no recovery point, and a rule that demanded one would force producers to fabricate a value for the failure case. The profile checks that the field is present and is a string; it cannot check that it is a valid timestamp or that it precedes the event time.",
78
+ "severity": "error",
79
+ "events": ["backup.complete"],
80
+ "when": { "path": "/event/outcome", "equals": "success" },
81
+ "requiredMetadata": [{ "path": "/backup/recoveryPoint", "type": "string" }],
82
+ "recommendedPaths": ["/metadata/backup/retentionClass"]
83
+ },
84
+ {
85
+ "id": "BACKUP-VERIFY-001",
86
+ "description": "A backup verification event records the verdict it reached about the copy.",
87
+ "rationale": "`event.outcome` says whether the verification ran; it does not say what the verification found. A check that completed successfully and proved the copy restorable, and a check that completed successfully and found it corrupt, are both `outcome: success`, and conflating them turns a verification programme into a record that jobs executed. The separate verdict is the only field that distinguishes an organization that tests its backups from one that tests whether its test harness runs. The profile does not fix the vocabulary of verdicts, because what verification means ranges from a checksum comparison to a full restore rehearsal.",
88
+ "severity": "error",
89
+ "events": ["backup.verify"],
90
+ "requiredMetadata": [{ "path": "/backup/verificationStatus", "type": "string" }],
91
+ "recommendedPaths": ["/evidence", "/metadata/backup/recoveryPoint"]
92
+ },
93
+ {
94
+ "id": "BACKUP-DELETE-001",
95
+ "description": "Deleting a backup copy or a snapshot before its retention has elapsed is justified.",
96
+ "rationale": "Deleting a recovery point removes the only means of returning to the state it held, and it is the operation an audit trail is least able to reconstruct afterwards, because the evidence is precisely what was removed. It is also the operation an attacker performs before the one they want to hide. A stated justification is what separates a deliberate disposal from destruction of evidence, and it must be recorded at the moment of deletion because there is nothing left to ask afterwards. Approval is recommended rather than required: many deployments legitimately let an owner remove a failed or superseded copy.",
97
+ "severity": "error",
98
+ "events": ["backup.delete", "snapshot.delete"],
99
+ "requiredPaths": ["/reason"],
100
+ "recommendedPaths": ["/approval", "/metadata/backup/retentionClass"]
101
+ },
102
+ {
103
+ "id": "BACKUP-EXPIRE-001",
104
+ "description": "A retention-driven expiry records the retention class that disposed of the copy.",
105
+ "rationale": "An expiry claims that a copy was removed because a policy said its time was up, and that claim is the entire justification for the event. Recording the retention class is what makes the claim checkable: it lets a reviewer test the disposal against the policy that supposedly drove it, and it makes a deletion recorded under the wrong name visible, because an operator-initiated removal dressed as an expiry has no class to point at. Retention metadata is recommended across the rest of this profile and required only here, where it is the operation's reason for existing.",
106
+ "severity": "error",
107
+ "events": ["backup.expire"],
108
+ "requiredMetadata": [{ "path": "/backup/retentionClass", "type": "string" }]
109
+ },
110
+ {
111
+ "id": "BACKUP-SNAPSHOT-001",
112
+ "description": "A snapshot event identifies the snapshot it created or removed.",
113
+ "rationale": "Snapshots are the recovery point people actually reach for, because they are cheap enough to take before every risky change and fast enough to restore from. That also makes them numerous, and a snapshot event without an identifier is indistinguishable from every other snapshot event on the same resource. The identifier is what allows a restore to be traced back to the copy it used, and a deletion to be traced to the copy that is now gone.",
114
+ "severity": "error",
115
+ "events": ["snapshot.create", "snapshot.delete"],
116
+ "requiredMetadata": [{ "path": "/backup/snapshotId", "type": "string" }],
117
+ "recommendedPaths": ["/metadata/backup/recoveryPoint", "/metadata/backup/retentionClass"]
118
+ },
119
+ {
120
+ "id": "BACKUP-RESTORE-001",
121
+ "description": "A restore records why it was performed, which operation it belongs to, which copy it read from and the point in time it restored to.",
122
+ "rationale": "A restore overwrites live data with older data, so it is simultaneously a data-loss event and a recovery: everything written between the recovery point and the restore is discarded. Three facts make it reviewable and none can be reconstructed later. The source names the copy that was trusted, which is what an investigation needs when the copy turns out to have been compromised. The recovery point bounds what was lost. The justification separates a disaster recovery from a quiet reversal of someone else's work — the single most common way a restore is misused. The restore identifier ties the start and the completion into one operation across the components that emit them.",
123
+ "severity": "error",
124
+ "events": ["restore.start", "restore.complete"],
125
+ "requiredPaths": ["/reason"],
126
+ "requiredMetadata": [
127
+ { "path": "/backup/restoreId", "type": "string" },
128
+ { "path": "/backup/sourceId", "type": "string" },
129
+ { "path": "/backup/recoveryPoint", "type": "string" }
130
+ ],
131
+ "recommendedPaths": ["/approval", "/request/correlationId"]
132
+ },
133
+ {
134
+ "id": "BACKUP-RECOVERY-001",
135
+ "description": "A recovery or failover event identifies the recovery operation it belongs to.",
136
+ "rationale": "A recovery is never one event. Declaring it, moving storage, promoting a replica, redirecting traffic and standing the original site back up are separate operations, usually in separate systems, often hours apart and frequently performed by different people under pressure. The recovery identifier is what makes them one operation rather than an unexplained sequence, and it is the field a post-incident review depends on to reconstruct the order in which decisions were taken.",
137
+ "severity": "error",
138
+ "events": ["recovery.start", "recovery.complete", "recovery.failover", "recovery.failback"],
139
+ "requiredMetadata": [{ "path": "/backup/recoveryId", "type": "string" }],
140
+ "recommendedPaths": ["/request/correlationId", "/metadata/backup/recoveryPoint"]
141
+ },
142
+ {
143
+ "id": "BACKUP-FAILOVER-001",
144
+ "description": "A failover or failback records where service was moved to and why it was moved.",
145
+ "rationale": "A failover moves production service somewhere else. Without the destination the event says service moved but not where, which is useless during the incident and worse afterwards, when the question is which site was serving traffic at a given moment. Without a reason, a rehearsed drill, a planned maintenance move and an emergency response to an outage are the same event, and they demand entirely different scrutiny. The destination is recorded as a logical scope — a site, region, cluster or replica name — never as an endpoint, URL or connection string.",
146
+ "severity": "error",
147
+ "events": ["recovery.failover", "recovery.failback"],
148
+ "requiredPaths": ["/reason"],
149
+ "requiredMetadata": [{ "path": "/backup/targetScope", "type": "string" }],
150
+ "recommendedPaths": ["/approval", "/request/correlationId"]
151
+ },
152
+ {
153
+ "id": "BACKUP-APPROVAL-001",
154
+ "description": "When the producer declares that local policy required approval, the operation carries the approval record.",
155
+ "rationale": "This profile does not decide which deployments need an approval before a destructive deletion, a production restore or a failover; that is an organizational judgement, and a rule that imposed one answer would describe a single company's process and be switched off everywhere else. The producer declares the answer, and the profile enforces the consequence: an event that states approval was required and carries no approval record documents an operation performed without the control its own system said was needed. That gap is exactly what a review is looking for, and it is invisible unless the declaration and the record are held to each other.",
156
+ "severity": "error",
157
+ "events": [
158
+ "backup.delete",
159
+ "snapshot.delete",
160
+ "restore.start",
161
+ "recovery.failover",
162
+ "recovery.failback"
163
+ ],
164
+ "when": { "path": "/metadata/backup/approvalRequired", "equals": true },
165
+ "requiredPaths": ["/approval"]
166
+ },
167
+ {
168
+ "id": "BACKUP-POLICY-001",
169
+ "description": "A change to a backup policy records the policy, what changed and why.",
170
+ "rationale": "A backup policy decides what is protected, how often and for how long, so editing it changes future recoverability without touching any data. The damage surfaces only when a recovery is attempted, which may be months later, and by then the event that caused it is one line in a change log. Recording the transition in `change` is what makes it possible to say what protection was reduced rather than merely that a policy was edited, and the justification is what distinguishes a deliberate cost decision from a silent weakening of the organization's ability to recover.",
171
+ "severity": "error",
172
+ "eventPrefixes": ["backup.policy."],
173
+ "requiredPaths": ["/change", "/reason"],
174
+ "requiredMetadata": [{ "path": "/backup/policyId", "type": "string" }],
175
+ "recommendedPaths": ["/approval", "/metadata/backup/retentionClass"]
176
+ }
177
+ ]
178
+ }