@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.
- package/LICENSE +215 -0
- package/README.md +647 -0
- package/dist/conformance/src/cli.d.ts +20 -0
- package/dist/conformance/src/cli.js +560 -0
- package/dist/conformance/src/cli.js.map +1 -0
- package/dist/conformance/src/format-errors.d.ts +20 -0
- package/dist/conformance/src/format-errors.js +116 -0
- package/dist/conformance/src/format-errors.js.map +1 -0
- package/dist/conformance/src/integrity/canonicalize.d.ts +25 -0
- package/dist/conformance/src/integrity/canonicalize.js +100 -0
- package/dist/conformance/src/integrity/canonicalize.js.map +1 -0
- package/dist/conformance/src/integrity/digest.d.ts +54 -0
- package/dist/conformance/src/integrity/digest.js +126 -0
- package/dist/conformance/src/integrity/digest.js.map +1 -0
- package/dist/conformance/src/integrity/types.d.ts +86 -0
- package/dist/conformance/src/integrity/types.js +36 -0
- package/dist/conformance/src/integrity/types.js.map +1 -0
- package/dist/conformance/src/integrity/verify-chain.d.ts +23 -0
- package/dist/conformance/src/integrity/verify-chain.js +235 -0
- package/dist/conformance/src/integrity/verify-chain.js.map +1 -0
- package/dist/conformance/src/integrity/verify-event.d.ts +29 -0
- package/dist/conformance/src/integrity/verify-event.js +140 -0
- package/dist/conformance/src/integrity/verify-event.js.map +1 -0
- package/dist/conformance/src/privacy/entropy.d.ts +14 -0
- package/dist/conformance/src/privacy/entropy.js +86 -0
- package/dist/conformance/src/privacy/entropy.js.map +1 -0
- package/dist/conformance/src/privacy/field-names.d.ts +36 -0
- package/dist/conformance/src/privacy/field-names.js +121 -0
- package/dist/conformance/src/privacy/field-names.js.map +1 -0
- package/dist/conformance/src/privacy/lint-event.d.ts +18 -0
- package/dist/conformance/src/privacy/lint-event.js +272 -0
- package/dist/conformance/src/privacy/lint-event.js.map +1 -0
- package/dist/conformance/src/privacy/rules.d.ts +44 -0
- package/dist/conformance/src/privacy/rules.js +161 -0
- package/dist/conformance/src/privacy/rules.js.map +1 -0
- package/dist/conformance/src/privacy/safe-formats.d.ts +32 -0
- package/dist/conformance/src/privacy/safe-formats.js +118 -0
- package/dist/conformance/src/privacy/safe-formats.js.map +1 -0
- package/dist/conformance/src/privacy/size-analysis.d.ts +36 -0
- package/dist/conformance/src/privacy/size-analysis.js +87 -0
- package/dist/conformance/src/privacy/size-analysis.js.map +1 -0
- package/dist/conformance/src/privacy/token-patterns.d.ts +24 -0
- package/dist/conformance/src/privacy/token-patterns.js +126 -0
- package/dist/conformance/src/privacy/token-patterns.js.map +1 -0
- package/dist/conformance/src/privacy/traverse.d.ts +33 -0
- package/dist/conformance/src/privacy/traverse.js +61 -0
- package/dist/conformance/src/privacy/traverse.js.map +1 -0
- package/dist/conformance/src/privacy/types.d.ts +58 -0
- package/dist/conformance/src/privacy/types.js +32 -0
- package/dist/conformance/src/privacy/types.js.map +1 -0
- package/dist/conformance/src/privacy/url-analysis.d.ts +40 -0
- package/dist/conformance/src/privacy/url-analysis.js +148 -0
- package/dist/conformance/src/privacy/url-analysis.js.map +1 -0
- package/dist/conformance/src/profiles/check-profile.d.ts +22 -0
- package/dist/conformance/src/profiles/check-profile.js +72 -0
- package/dist/conformance/src/profiles/check-profile.js.map +1 -0
- package/dist/conformance/src/profiles/evaluate-rule.d.ts +27 -0
- package/dist/conformance/src/profiles/evaluate-rule.js +87 -0
- package/dist/conformance/src/profiles/evaluate-rule.js.map +1 -0
- package/dist/conformance/src/profiles/load-profile.d.ts +30 -0
- package/dist/conformance/src/profiles/load-profile.js +95 -0
- package/dist/conformance/src/profiles/load-profile.js.map +1 -0
- package/dist/conformance/src/profiles/resolve-pointer.d.ts +44 -0
- package/dist/conformance/src/profiles/resolve-pointer.js +123 -0
- package/dist/conformance/src/profiles/resolve-pointer.js.map +1 -0
- package/dist/conformance/src/profiles/select-rules.d.ts +13 -0
- package/dist/conformance/src/profiles/select-rules.js +42 -0
- package/dist/conformance/src/profiles/select-rules.js.map +1 -0
- package/dist/conformance/src/profiles/types.d.ts +101 -0
- package/dist/conformance/src/profiles/types.js +31 -0
- package/dist/conformance/src/profiles/types.js.map +1 -0
- package/dist/conformance/src/profiles/validate-profile-definition.d.ts +12 -0
- package/dist/conformance/src/profiles/validate-profile-definition.js +47 -0
- package/dist/conformance/src/profiles/validate-profile-definition.js.map +1 -0
- package/dist/conformance/src/sources.d.ts +53 -0
- package/dist/conformance/src/sources.js +153 -0
- package/dist/conformance/src/sources.js.map +1 -0
- package/dist/conformance/src/validate-core.d.ts +30 -0
- package/dist/conformance/src/validate-core.js +62 -0
- package/dist/conformance/src/validate-core.js.map +1 -0
- package/dist/conformance/src/validate.d.ts +34 -0
- package/dist/conformance/src/validate.js +62 -0
- package/dist/conformance/src/validate.js.map +1 -0
- package/dist/conformance/src/validator-interface.d.ts +35 -0
- package/dist/conformance/src/validator-interface.js +19 -0
- package/dist/conformance/src/validator-interface.js.map +1 -0
- package/package.json +85 -0
- package/profiles/README.md +201 -0
- package/profiles/api-and-integration-management/README.md +271 -0
- package/profiles/api-and-integration-management/profile.json +258 -0
- package/profiles/backup-and-recovery/README.md +318 -0
- package/profiles/backup-and-recovery/profile.json +178 -0
- package/profiles/customer-and-account-management/README.md +340 -0
- package/profiles/customer-and-account-management/profile.json +237 -0
- package/profiles/deployment-and-change-management/README.md +359 -0
- package/profiles/deployment-and-change-management/profile.json +298 -0
- package/profiles/document-management/README.md +120 -0
- package/profiles/document-management/profile.json +170 -0
- package/profiles/financial-transaction-management/README.md +339 -0
- package/profiles/financial-transaction-management/profile.json +247 -0
- package/profiles/identity-and-access-management/README.md +112 -0
- package/profiles/identity-and-access-management/profile.json +120 -0
- package/profiles/incident-management/README.md +338 -0
- package/profiles/incident-management/profile.json +256 -0
- package/profiles/message-broker-management/README.md +344 -0
- package/profiles/message-broker-management/profile.json +399 -0
- package/profiles/profile-definition.schema.json +213 -0
- package/profiles/secrets-and-key-management/README.md +331 -0
- package/profiles/secrets-and-key-management/profile.json +219 -0
- package/schemas/v0.1/audit-event.schema.json +911 -0
- package/semantic-conventions/README.md +52 -0
- package/semantic-conventions/authentication.md +111 -0
- package/semantic-conventions/configuration-and-change.md +122 -0
- package/semantic-conventions/correlation-and-tracing.md +387 -0
- package/semantic-conventions/data-access.md +115 -0
- package/semantic-conventions/event-naming.md +114 -0
- package/semantic-conventions/identity-and-access.md +112 -0
- package/semantic-conventions/privileged-operations.md +110 -0
- package/semantic-conventions/workflow-and-approval.md +109 -0
- package/specification/actor-model.md +135 -0
- package/specification/approval-and-delegation.md +150 -0
- package/specification/authentication.md +116 -0
- package/specification/authorization.md +117 -0
- package/specification/change-model.md +134 -0
- package/specification/delivery.md +136 -0
- package/specification/design-principles.md +137 -0
- package/specification/event-model.md +387 -0
- package/specification/evidence-model.md +109 -0
- package/specification/extension-model.md +159 -0
- package/specification/integrity.md +314 -0
- package/specification/overview.md +172 -0
- package/specification/privacy.md +386 -0
- package/specification/resource-model.md +116 -0
- package/specification/terminology.md +135 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# API and Integration Management Profile
|
|
2
|
+
|
|
3
|
+
**Status: Experimental. Implemented in v0.1, 13 rules, 11 of them enforceable.**
|
|
4
|
+
|
|
5
|
+
The enforceable rules are in [profile.json](profile.json).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
auditmodel check-profile examples/profiles/api-and-integration-management/valid --profile api-and-integration-management
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
An integration is a standing hole in a trust boundary. Somebody opened it, somebody widened it, and
|
|
14
|
+
one day somebody will ask when. The events that answer that question — an API key issued, a webhook
|
|
15
|
+
pointed somewhere new, a connector authorized against a third party — are administrative events that
|
|
16
|
+
happen rarely and matter enormously, and they are routinely the thinnest events a platform emits,
|
|
17
|
+
because the engineering attention goes to the traffic rather than to the administration of it.
|
|
18
|
+
|
|
19
|
+
This profile states what those administrative events have to carry: who decided, how they were
|
|
20
|
+
authenticated, which integration point changed, which external party is on the other side, why a
|
|
21
|
+
withdrawal happened, and what changed in a reconfiguration. It states equally clearly what must never
|
|
22
|
+
appear in them — the key, the token, the client secret, the signing secret, the callback URL.
|
|
23
|
+
|
|
24
|
+
## Scope
|
|
25
|
+
|
|
26
|
+
Applications that issue and manage API credentials, that let an operator subscribe an external
|
|
27
|
+
destination to their events, or that connect to third-party systems: SaaS platforms, integration
|
|
28
|
+
platforms, developer portals, API gateways with a management plane, and any product with an
|
|
29
|
+
"Integrations" or "Developer" settings page.
|
|
30
|
+
|
|
31
|
+
The profile is vendor-neutral. It describes operations any integration surface performs. It assumes
|
|
32
|
+
no particular authorization protocol, no particular transport, no particular hosting model, and no
|
|
33
|
+
particular approval workflow. It does not require `/request/protocol`, `/request/traceId` or any
|
|
34
|
+
other field that presupposes a specific technology stack.
|
|
35
|
+
|
|
36
|
+
## Event families
|
|
37
|
+
|
|
38
|
+
| Family | Events | Governed |
|
|
39
|
+
| ------------------------ | --------------------------------------------------------------------------- | -------- |
|
|
40
|
+
| API credential lifecycle | `api-key.create`, `.rotate`, `.revoke`, `.delete` | yes |
|
|
41
|
+
| Webhook administration | `webhook.create`, `.update`, `.enable`, `.disable`, `.delete`, `.test` | yes |
|
|
42
|
+
| Integration lifecycle | `integration.connect`, `.disconnect`, `.enable`, `.disable`, `.reauthorize` | yes |
|
|
43
|
+
| Integration settings | `integration.configuration.*` | yes |
|
|
44
|
+
| Sync administration | `integration.sync.start`, `integration.sync.cancel` | yes |
|
|
45
|
+
| API traffic | `api.request` and every other data-plane request | **no** |
|
|
46
|
+
| Webhook delivery | `webhook.delivery.*` | **no** |
|
|
47
|
+
| Sync execution | `integration.sync.progress` and other per-page or polling events | **no** |
|
|
48
|
+
|
|
49
|
+
## Explicit exclusions
|
|
50
|
+
|
|
51
|
+
**The data plane is not governed, and cannot be governed by accident.**
|
|
52
|
+
|
|
53
|
+
An API gateway emits one event per request. A webhook dispatcher emits one per delivery attempt, and
|
|
54
|
+
retries multiply it. A polling connector emits one per page for as long as it runs. Requiring an
|
|
55
|
+
authorization decision, an integration classification and a justification on each of those would put
|
|
56
|
+
the profile's cost squarely on the highest-volume events in the system, in exchange for almost no
|
|
57
|
+
review value — and the requirement would be switched off rather than met.
|
|
58
|
+
|
|
59
|
+
The exclusion is **structural**, not a matter of discipline. Every selector in this profile is either
|
|
60
|
+
an exact event name or the single narrow prefix `integration.configuration.`. **No selector uses a
|
|
61
|
+
bare `api.`, `api-key.`, `webhook.` or `integration.` prefix**, so `api.request`,
|
|
62
|
+
`webhook.delivery.attempt`, `integration.sync.progress` and `api-key.verify` match no rule at all and
|
|
63
|
+
`check-profile` reports them as not applicable. Three fixtures and a test hold that boundary in
|
|
64
|
+
place, because widening one prefix later would silently start governing every request, delivery and
|
|
65
|
+
poll in a deployment.
|
|
66
|
+
|
|
67
|
+
Excluded does not mean unaudited. A delivery attempt is still a conforming OpenAuditModel event;
|
|
68
|
+
[data-access.md](../../semantic-conventions/data-access.md) and
|
|
69
|
+
[correlation-and-tracing.md](../../semantic-conventions/correlation-and-tracing.md) cover recording
|
|
70
|
+
traffic. This profile simply makes no additional demands of it.
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
| Rule | Applies to | Requires |
|
|
75
|
+
| ------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
76
|
+
| `INTEGRATION-CORE-001` | every governed event | `/authorization`, `/metadata/integration/type` |
|
|
77
|
+
| `INTEGRATION-CORE-002` | every governed event | _recommends_ `/reason`, `/approval`, correlation ID, provider |
|
|
78
|
+
| `INTEGRATION-CORE-003` | every governed event where the producer declared approval was required | `/approval`, `/approval/status` |
|
|
79
|
+
| `INTEGRATION-AUTHN-001` | credential and connection operations where `actor.type` is `user` | `/authentication` |
|
|
80
|
+
| `INTEGRATION-AUTHN-002` | the same operations where `actor.type` is `admin` | `/authentication` |
|
|
81
|
+
| `INTEGRATION-KEY-001` | `api-key.create`, `.rotate`, `.revoke`, `.delete` | `/metadata/integration/credentialReference` |
|
|
82
|
+
| `INTEGRATION-KEY-002` | `api-key.create`, `api-key.rotate` | _recommends_ scope, expiry, `/resource/ownerId` |
|
|
83
|
+
| `INTEGRATION-REVOKE-001` | revocations, deletions, disablements, disconnections, sync cancellation | `/reason` |
|
|
84
|
+
| `INTEGRATION-HOOK-001` | every webhook administration event | `/metadata/integration/webhookId`, `/metadata/integration/endpointClass` |
|
|
85
|
+
| `INTEGRATION-CONFIG-001` | `webhook.update`, `integration.configuration.*` | `/change`; recommends `/change/changedFields` |
|
|
86
|
+
| `INTEGRATION-CONN-001` | every `integration.*` governed event | `/metadata/integration/connectionId`, `/metadata/integration/provider` |
|
|
87
|
+
| `INTEGRATION-FLOW-001` | `integration.connect`, `.reauthorize`, `sync.start`, `sync.cancel` | `/request/correlationId` |
|
|
88
|
+
| `INTEGRATION-FAIL-001` | every governed event whose outcome is `failure` | `/event/error/type` |
|
|
89
|
+
|
|
90
|
+
`INTEGRATION-CORE-002` and `INTEGRATION-KEY-002` are `warning` rules: they never fail conformance.
|
|
91
|
+
The other eleven are `error` rules. Each rule's full text and rationale is in
|
|
92
|
+
[profile.json](profile.json).
|
|
93
|
+
|
|
94
|
+
## Metadata namespace
|
|
95
|
+
|
|
96
|
+
Everything this profile requires under `metadata` lives at `/metadata/integration/`. Namespacing is
|
|
97
|
+
not decoration: an event can be governed by two profiles at once, and `type`, `provider` and
|
|
98
|
+
`expiresAt` mean different things to a secrets profile and to this one. A root key such as
|
|
99
|
+
`/metadata/type` would be a collision waiting to happen.
|
|
100
|
+
|
|
101
|
+
| Field | Type | Meaning |
|
|
102
|
+
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `type` | string | Kind of integration point: `api-credential`, `outbound-webhook`, `service-connector`, `data-sync-connector` |
|
|
104
|
+
| `provider` | string | Stable logical name of the party on the other side. Never a URL, never a legal entity name |
|
|
105
|
+
| `connectionId` | string | Identifier of the connection instance |
|
|
106
|
+
| `webhookId` | string | Identifier of the webhook subscription |
|
|
107
|
+
| `credentialReference` | string | Non-secret handle for the credential material. **Never the credential** |
|
|
108
|
+
| `endpointClass` | string | Where a callback goes: `internal-service`, `partner-network`, `public-internet`, `unknown` |
|
|
109
|
+
| `approvalRequired` | boolean | Producer's declaration that local policy required approval for this change |
|
|
110
|
+
| `scope` | string | What a credential may do. Recommended at issuance |
|
|
111
|
+
| `expiresAt` | string | When a credential stops working. Recommended at issuance |
|
|
112
|
+
|
|
113
|
+
All vocabularies above are open. The profile requires that the field be present and be a string; it
|
|
114
|
+
never says which strings are legal, because an integration taxonomy is an operator's decision.
|
|
115
|
+
|
|
116
|
+
The table lists only the fields some rule requires or recommends. The namespace is not closed:
|
|
117
|
+
producers may add their own descriptive fields beside them, and the fixtures do — `eventSelection` on
|
|
118
|
+
a webhook, `syncMode` on a sync, `deliveryAttempt` and `pageNumber` on the ungoverned data-plane
|
|
119
|
+
examples. No rule constrains those, and none is needed for conformance.
|
|
120
|
+
|
|
121
|
+
## Conditional-policy fields
|
|
122
|
+
|
|
123
|
+
Four rules are conditional, and each fires on exactly one producer-set discriminator:
|
|
124
|
+
|
|
125
|
+
| Rule | Fires when |
|
|
126
|
+
| ----------------------- | -------------------------------------------------- |
|
|
127
|
+
| `INTEGRATION-CORE-003` | `/metadata/integration/approvalRequired` is `true` |
|
|
128
|
+
| `INTEGRATION-AUTHN-001` | `/actor/type` is `"user"` |
|
|
129
|
+
| `INTEGRATION-AUTHN-002` | `/actor/type` is `"admin"` |
|
|
130
|
+
| `INTEGRATION-FAIL-001` | `/event/outcome` is `"failure"` |
|
|
131
|
+
|
|
132
|
+
When a condition's path is absent, the condition does not hold and the rule contributes nothing. That
|
|
133
|
+
is the engine's defined behaviour and it is the reason `approvalRequired` should be recorded as
|
|
134
|
+
`false` rather than omitted: `false` is an answer, absence is silence.
|
|
135
|
+
|
|
136
|
+
## Approval model
|
|
137
|
+
|
|
138
|
+
**Approval is never universally required.** Whether an outbound webhook needs two sign-offs or none
|
|
139
|
+
is an operator's policy, not this specification's, and a profile that demanded approval for every
|
|
140
|
+
integration change would describe one organization's process and be ignored everywhere else.
|
|
141
|
+
|
|
142
|
+
The profile therefore takes the producer's word for it. `INTEGRATION-CORE-002` recommends `/approval`
|
|
143
|
+
on every governed event, so its absence is visible as a warning. `INTEGRATION-CORE-003` requires
|
|
144
|
+
`/approval` and `/approval/status` **only** when the producer has set
|
|
145
|
+
`/metadata/integration/approvalRequired` to `true` — the same shape as the IAM profile's privileged
|
|
146
|
+
flag and the document profile's external-share flag.
|
|
147
|
+
|
|
148
|
+
Recording `approval: { "status": "not-required" }` is the recommended way to say that no approval was
|
|
149
|
+
needed. It answers the reviewer's question instead of leaving a hole where the answer would be. Most
|
|
150
|
+
of the valid fixtures do exactly that.
|
|
151
|
+
|
|
152
|
+
## Privacy considerations
|
|
153
|
+
|
|
154
|
+
This is the profile with the highest chance of a producer putting a live credential in an audit
|
|
155
|
+
event, because the events are _about_ credentials. The following MUST NOT appear anywhere in an
|
|
156
|
+
event, in `metadata`, `extensions`, `change.before`, `change.after`, `resource.attributes`,
|
|
157
|
+
`event.summary`, `event.error.message` or `reason.text`:
|
|
158
|
+
|
|
159
|
+
- API key values, at issuance or rotation
|
|
160
|
+
- OAuth access tokens, refresh tokens, authorization codes and client secrets
|
|
161
|
+
- Webhook signing secrets and HMAC keys
|
|
162
|
+
- `Authorization` header values of any kind
|
|
163
|
+
- Signed or tokenized callback URLs, and any URL carrying a query string
|
|
164
|
+
|
|
165
|
+
The profile is built so that a producer never needs to. `credentialReference` exists so there is a
|
|
166
|
+
non-sensitive place to put "which key"; `endpointClass` exists so there is a non-sensitive place to
|
|
167
|
+
put "where the data goes". Requiring the full callback URL would have been the obvious design, and it
|
|
168
|
+
would have turned every conforming audit trail into a credential store, because delivery URLs
|
|
169
|
+
routinely carry shared secrets in their path or query string.
|
|
170
|
+
|
|
171
|
+
`INTEGRATION-CONFIG-001` requires `/change` but neither requires nor recommends `/change/before` or
|
|
172
|
+
`/change/after`, for the same reason: integration configuration is full of endpoints and headers.
|
|
173
|
+
Changed field names are the recommended form of the answer.
|
|
174
|
+
|
|
175
|
+
Every fixture in this profile — valid, invalid and not-applicable alike — is required by test to
|
|
176
|
+
pass `auditmodel lint-privacy`, and CI runs the command over `valid/`. A profile that accepted an event
|
|
177
|
+
carrying a credential would be worse than no profile at all. See
|
|
178
|
+
[specification/privacy.md](../../specification/privacy.md).
|
|
179
|
+
|
|
180
|
+
## Known rule-language limitations
|
|
181
|
+
|
|
182
|
+
The v0.1 rule language checks presence, JSON type and strict scalar equality against a single
|
|
183
|
+
condition. These are the places this profile wanted more and did not get it:
|
|
184
|
+
|
|
185
|
+
- **No disjunction.** `INTEGRATION-AUTHN-001` and `INTEGRATION-AUTHN-002` are the same requirement
|
|
186
|
+
written twice because `actor.type` cannot be compared against `"user"` **or** `"admin"` in one
|
|
187
|
+
rule. A deployment that models operators under some other core principal type is not caught.
|
|
188
|
+
- **No cross-field comparison.** The profile cannot assert that `approval.receivedApprovals` reaches
|
|
189
|
+
`approval.requiredApprovals`, nor that an event with `approvalRequired: true` does not carry
|
|
190
|
+
`approval.status: "not-required"` — a contradiction the engine cannot see.
|
|
191
|
+
- **No numeric ranges.** `requiredMetadata` checks that a value is a `number` or an `integer`. It
|
|
192
|
+
cannot require that an expiry is within a maximum lifetime, or that a retry count is bounded.
|
|
193
|
+
- **No string patterns.** The profile can require that `endpointClass` is a string. It cannot require
|
|
194
|
+
that it comes from a vocabulary, and it cannot reject a full URL stored there. The privacy linter
|
|
195
|
+
is the backstop for the second one, not the profile.
|
|
196
|
+
- **No array-content predicates.** `change.changedFields` cannot be required to contain a particular
|
|
197
|
+
entry, and `approval.approvers` cannot be required to exclude the actor — which is exactly how a
|
|
198
|
+
self-approval would be detected.
|
|
199
|
+
- **Absence and falsity are different, and only one is checkable.** A rule fires on
|
|
200
|
+
`approvalRequired: true` and stays silent on both `false` and absent. Recording `false` explicitly
|
|
201
|
+
is a documentation convention this profile recommends; it is not something the engine can compel.
|
|
202
|
+
|
|
203
|
+
Each of these is guidance in this README rather than a rule, deliberately. A profile that pretended
|
|
204
|
+
to enforce something it cannot check would be worse than one that says plainly where it stops.
|
|
205
|
+
|
|
206
|
+
## Cross-profile overlaps
|
|
207
|
+
|
|
208
|
+
**`secrets-and-key-management`.** The overlap is real and it is intentional. That profile governs the
|
|
209
|
+
**security lifecycle of secret and key material** — how material is generated, stored, rotated,
|
|
210
|
+
escrowed and destroyed. This profile governs the **lifecycle and configuration of APIs, webhooks and
|
|
211
|
+
external integrations** — which integration points exist, who opened them, where they point and why
|
|
212
|
+
they were closed.
|
|
213
|
+
|
|
214
|
+
An API key rotation sits in the intersection, and it may legitimately conform to either profile
|
|
215
|
+
depending on the producer's vocabulary and which control domain the producer intends the event to
|
|
216
|
+
serve. The two profiles deliberately do **not** use identical selectors, and neither claims the
|
|
217
|
+
event exclusively. If a producer wants the event to satisfy both, it can: both profiles only add
|
|
218
|
+
requirements, both namespace their metadata, and nothing in one contradicts the other. Check against
|
|
219
|
+
both and the event must satisfy the union.
|
|
220
|
+
|
|
221
|
+
**`identity-and-access-management`.** `identity.credential.rotate` is the IAM profile's view of a
|
|
222
|
+
credential belonging to a **principal**. `api-key.rotate` here is the view of a credential belonging
|
|
223
|
+
to an **integration**. Producers whose API keys are modelled as principal credentials should use the
|
|
224
|
+
IAM vocabulary; producers whose keys are modelled as integration configuration should use this one.
|
|
225
|
+
|
|
226
|
+
**`deployment-and-change-management`.** An integration configuration change made through a pipeline
|
|
227
|
+
is both a configuration change and a deployment. `/change/deploymentId` is the core field that links
|
|
228
|
+
them; this profile requires neither profile's fields of the other.
|
|
229
|
+
|
|
230
|
+
## Fixture matrix
|
|
231
|
+
|
|
232
|
+
[examples/profiles/api-and-integration-management/](../../examples/profiles/api-and-integration-management/)
|
|
233
|
+
— thirteen valid, twelve invalid, three not applicable.
|
|
234
|
+
|
|
235
|
+
| Kind | Count | Guarantee |
|
|
236
|
+
| ----------------- | ----- | ------------------------------------------------------------------------------- |
|
|
237
|
+
| `valid/` | 13 | Core-valid, privacy-clean, conforming (exit 0) |
|
|
238
|
+
| `invalid/` | 12 | Core-valid, privacy-clean, failing **exactly one** rule at a documented pointer |
|
|
239
|
+
| `not-applicable/` | 3 | Core-valid, privacy-clean, governed by no rule (exit 3) |
|
|
240
|
+
|
|
241
|
+
Every one of the eleven enforceable rules has at least one negative fixture;
|
|
242
|
+
`INTEGRATION-CORE-001` has two, one for each field it requires.
|
|
243
|
+
|
|
244
|
+
## Not-applicable rationale
|
|
245
|
+
|
|
246
|
+
Three fixtures exist to prove an exclusion rather than to demonstrate a requirement:
|
|
247
|
+
|
|
248
|
+
- `api-request.json` — an ordinary authenticated API call. The busiest event in an API platform, and
|
|
249
|
+
one this profile says nothing about.
|
|
250
|
+
- `webhook-delivery.json` — a successful delivery attempt. Governing this would mean requiring an
|
|
251
|
+
authorization decision on every outbound HTTP call a dispatcher makes, including retries.
|
|
252
|
+
- `integration-sync-progress.json` — a per-page progress event from a polling connector. The sync
|
|
253
|
+
that _started_ is governed; the pages it walks are not.
|
|
254
|
+
|
|
255
|
+
All three are perfectly good OpenAuditModel events. `check-profile` reports each as not applicable
|
|
256
|
+
with exit code 3, which is deliberately **not** conformance: an event no rule governs is out of
|
|
257
|
+
scope, never silently approved.
|
|
258
|
+
|
|
259
|
+
## Open questions
|
|
260
|
+
|
|
261
|
+
- Should `api-key.create` require an expiry rather than recommend one? The argument for is that a
|
|
262
|
+
non-expiring credential is the finding; the argument against is that many products legitimately
|
|
263
|
+
issue keys with no expiry and would fill the field with a sentinel.
|
|
264
|
+
- Is `endpointClass` the right abstraction, or should the profile require a destination host
|
|
265
|
+
identifier that a producer resolves internally? A class answers the risk question; a host
|
|
266
|
+
identifier answers the forensic one. There is no adoption evidence yet for either.
|
|
267
|
+
- Should `integration.sync.start` require the sync scope — full or incremental, and over what — as
|
|
268
|
+
`INTEGRATION-CONN-001` requires the connection? The vocabulary differs enough between connectors
|
|
269
|
+
that a required field would likely be filled inconsistently.
|
|
270
|
+
- Should a self-approval be expressible? Detecting one needs an array-content predicate the rule
|
|
271
|
+
language does not have.
|
|
@@ -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
|
+
}
|