| @@ -25,9 +25,9 @@ It is the human-readable companion to the machine-readable mappings in |
| 25 | 25 | - [The frameworks in one paragraph each](#the-frameworks-in-one-paragraph-each) |
| 26 | 26 | - [The signals and their mappings](#the-signals-and-their-mappings) |
| 27 | 27 | - [Branch protection & repository rulesets](#1-branch-protection--repository-rulesets) |
| 28 | | - [Dependabot alerts](#2-dependabot-alerts) |
| 29 | | - [Code scanning alerts](#3-code-scanning-alerts) |
| 30 | | - [Secret scanning alerts](#4-secret-scanning-alerts) |
| 28 | - [Dependabot](#2-dependabot) |
| 29 | - [Code scanning](#3-code-scanning) |
| 30 | - [Secret scanning](#4-secret-scanning) |
| 31 | 31 | - [Membership changes (webhook trail)](#5-membership-changes-webhook-trail) |
| 32 | 32 | - [Membership & team inventory (polled)](#6-membership--team-inventory-polled) |
| 33 | 33 | - [Repository inventory](#7-repository-inventory) |
| @@ -44,25 +44,27 @@ Understanding the evidence output requires understanding four rules in the |
| 44 | 44 | mapping engine. All four live in [`control_mappings`](../migrations/0002_control_mappings.sql) |
| 45 | 45 | and [`buildEvidenceRows`](../src/exporter.ts). |
| 46 | 46 | |
| 47 | | **1. A snapshot is a `(resource, status)` pair; a mapping is a row that attaches |
| 48 | | a control to one.** The poller and webhook handler both normalize GitHub events |
| 49 | | into a small vocabulary — `resource` (e.g. `branch_protection`, `dependabot_alert`) |
| 50 | | and `status` (e.g. `enabled`, `open`, `fixed`). See [`extractFact`](../src/webhook.ts) |
| 51 | | and [`poller.ts`](../src/poller.ts). Mapping happens as a **join at export time**, |
| 52 | | never at ingest, so a mapping can be corrected without re-ingesting history. |
| 47 | **1. A snapshot is a `(resource, status)` pair about a `subject`; a mapping is a |
| 48 | row that attaches a control to a `(resource, status)`.** The poller and webhook |
| 49 | handler both normalize GitHub events into a small vocabulary — `resource` |
| 50 | (e.g. `branch_protection`, `dependabot_alert`) and `status` (e.g. `enabled`, |
| 51 | `open`, `fixed`) — plus a `subject` identifying which entity within the repo or |
| 52 | org the fact is about (an alert number, a member login, a team slug). See |
| 53 | [`extractFact`](../src/webhook.ts) and [`poller.ts`](../src/poller.ts). Mapping |
| 54 | happens as a **join at export time**, never at ingest, so a mapping can be |
| 55 | corrected without re-ingesting history. |
| 53 | 56 | |
| 54 | 57 | **2. `status = NULL` in a mapping matches *any* status for that resource.** The |
| 55 | | join condition is `cm.status IS NULL OR cm.status = l.status`. This is how a |
| 56 | | "the tooling exists and is producing signal" fact is expressed independently of |
| 57 | | any individual finding's state. |
| 58 | | |
| 59 | | **3. Consequently, one snapshot can emit multiple evidence rows.** A single |
| 60 | | Dependabot alert with `status = 'open'` matches *both* the `NULL` mapping |
| 61 | | (CC7.1, "detection tooling is active", **positive**) *and* the `'open'` mapping |
| 62 | | (CC7.2, "unremediated vulnerability", **negative**). This is intentional: the |
| 63 | | existence of the scanner and the existence of an open finding are two different |
| 64 | | facts about two different control expectations. This behavior is called out |
| 65 | | per-signal below wherever it applies. |
| 58 | join condition is `cm.status IS NULL OR cm.status = l.status`. This is used for |
| 59 | trail/inventory resources (`team`, `repository`, `org_member`, `team_member`) |
| 60 | where every state is the same kind of informational fact. |
| 61 | |
| 62 | **3. One snapshot can emit multiple evidence rows.** A single secret-scanning |
| 63 | alert with `status = 'open'` matches the `open` mapping under **each** of |
| 64 | CC6.6, CC6.1 (SOC 2) and A.5.17 (ISO). This is intentional: the same fact is |
| 65 | legitimate evidence for more than one control expectation, and attesting all of |
| 66 | them lets the export serve whichever control the organization's narrative uses. |
| 67 | This behavior is called out per-signal below wherever it applies. |
| 66 | 68 | |
| 67 | 69 | **4. `posture` is the auditor-facing verdict on a row**, one of: |
| 68 | 70 | |
| @@ -70,19 +72,26 @@ per-signal below wherever it applies. |
| 70 | 72 | | --- | --- | --- | |
| 71 | 73 | | `positive` | State supports the control | Branch protection enabled | |
| 72 | 74 | | `negative` | State is a gap against the control | Branch protection disabled; open secret | |
| 73 | | | `informational` | Neither pass nor fail — an audit-trail / inventory fact | A member was added; a repo exists | |
| 75 | | `informational` | Neither pass nor fail — an audit-trail / inventory fact | A member was added; a finding was dismissed by a user | |
| 74 | 76 | |
| 75 | 77 | Two more rules affect *which* snapshots become evidence at all: |
| 76 | 78 | |
| 77 | 79 | - **Unmapped states produce no evidence, in either direction.** A |
| 78 | 80 | `branch_protection` status of `unavailable` (GitHub returned 403 — the feature |
| 79 | 81 | isn't on the repo's plan; see [`fetchBranchProtection`](../src/poller.ts)) has |
| 80 | | no mapping row, so it never counts as a pass *or* a fail. Same for raw |
| 81 | | `push` events. |
| 82 | no mapping row, so it never counts as a pass *or* a fail. The same applies to |
| 83 | the `disabled`/`unavailable` states of the detection-tooling resources |
| 84 | (`dependabot`, `code_scanning`, `secret_scanning`), to the trail-only |
| 85 | resources `branch_protection_rule_event` and `repository_ruleset_event` |
| 86 | (rule-scoped webhook events that can't be attributed to the default branch — |
| 87 | the poll is authoritative for state), and to raw `push` events. |
| 82 | 88 | - **"Latest row wins" per `(repo, subject, resource)`** gives point-in-time |
| 83 | | current posture from an append-only table. Access facts are special-cased so a |
| 84 | | member who lost access stops being attested — only the most recent poll batch |
| 85 | | counts. See the CTE in [`buildEvidenceRows`](../src/exporter.ts). |
| 89 | current posture from an append-only table. Because `subject` carries the |
| 90 | alert number / login / slug, this operates **per entity**: one alert being |
| 91 | fixed does not mask another alert that is still open in the same repo. Access |
| 92 | facts are further special-cased so a member who lost access stops being |
| 93 | attested — only the most recent poll batch counts. See the CTE in |
| 94 | [`buildEvidenceRows`](../src/exporter.ts). |
| 86 | 95 | |
| 87 | 96 | --- |
| 88 | 97 | |
| @@ -114,14 +123,22 @@ assessment. |
| 114 | 123 | |
| 115 | 124 | ### 1. Branch protection & repository rulesets |
| 116 | 125 | |
| 117 | | **What we collect.** For each repo's default branch, whether merge controls are |
| 118 | | in force — via the `branch_protection_rule` and `repository_ruleset` webhooks |
| 119 | | (change events) and an hourly poll of the branch-protection and rulesets APIs |
| 120 | | (baseline, for protection that predates the install). Normalized to |
| 121 | | `resource ∈ {branch_protection, repository_ruleset}`, `status ∈ {enabled, |
| 122 | | disabled}`. A ruleset in `evaluate` (monitor-only) mode counts as **not** |
| 123 | | enabled because it does not actually block anything — see |
| 124 | | [`fetchRulesets`](../src/poller.ts). |
| 126 | **What we collect.** For each repo's **default branch**, whether merge controls |
| 127 | are in force. The hourly poll is authoritative: the branch-protection API for |
| 128 | classic protection, and the `rules/branches/{default-branch}` API for rulesets — |
| 129 | which aggregates the rules from every *active* ruleset (repo- and org-level) |
| 130 | that actually applies to that branch, so evaluate-mode (monitor-only) rulesets |
| 131 | and rulesets targeting other branches correctly count as **not** enabled. |
| 132 | Normalized to `resource ∈ {branch_protection, repository_ruleset}`, `status ∈ |
| 133 | {enabled, disabled}`. |
| 134 | |
| 135 | Webhooks supplement the poll only where they are unambiguous: a |
| 136 | `branch_protection_rule` event whose rule pattern is exactly the default branch |
| 137 | updates state immediately. Any other rule event — and *every* |
| 138 | `repository_ruleset` event, since one ruleset's deletion says nothing about |
| 139 | whether other rulesets still cover the branch — is recorded as an unmapped |
| 140 | trail event (`branch_protection_rule_event`, `repository_ruleset_event`) and |
| 141 | the next poll settles the state. See [`extractFact`](../src/webhook.ts). |
| 125 | 142 | |
| 126 | 143 | **Maps to:** |
| 127 | 144 | |
| @@ -131,36 +148,46 @@ enabled because it does not actually block anything — see |
| 131 | 148 | | ISO 27001 | **A.8.32** | *Change management.* Changes to information systems must follow formal change-management procedures to prevent unauthorized or destabilizing changes. | |
| 132 | 149 | |
| 133 | 150 | **Why this holds.** Branch protection / rulesets are the technical enforcement of |
| 134 | | change control in a Git workflow: requiring pull-request review before merge, |
| 135 | | blocking direct pushes to the default branch, and requiring status checks to |
| 136 | | pass. That is exactly the "controlled process… stops unauthorized changes" |
| 137 | | language of both controls. Enabled → **positive**; disabled → **negative** |
| 138 | | ("direct pushes now possible" is a concrete change-control gap). |
| 139 | | |
| 140 | | **Fit assessment: strong.** This is the least ambiguous mapping in the system — |
| 141 | | both frameworks name "change management" explicitly, and branch protection is |
| 142 | | the canonical GitHub-native implementation of it. The one nuance an auditor will |
| 143 | | probe is *scope*: we check the **default branch** only, and "enabled" does not |
| 144 | | verify that the *specific* rules (required reviewers, etc.) match the |
| 145 | | organization's policy. The evidence attests that a change-control gate exists, |
| 146 | | not that its configuration is sufficient. |
| 147 | | |
| 148 | | ### 2. Dependabot alerts |
| 149 | | |
| 150 | | **What we collect.** `dependabot_alert` webhook events — known-vulnerability |
| 151 | | alerts against the repo's dependencies. `status` is the alert state: `open`, |
| 152 | | `fixed`, `dismissed`, `auto_dismissed`. |
| 151 | change control in a Git workflow. Enabled → **positive**; disabled → |
| 152 | **negative** ("direct pushes possible" is a concrete change-control gap). |
| 153 | |
| 154 | **Fit assessment: strong, with the scope stated in the evidence itself.** Both |
| 155 | frameworks name "change management" explicitly, and branch protection is the |
| 156 | canonical GitHub-native implementation of it. What the evidence attests is that |
| 157 | a change-control gate **exists on the default branch** — it does not verify that |
| 158 | the *specific* rules (required reviewers, status checks, …) match the |
| 159 | organization's policy, and the exported rationale says so explicitly ("rule |
| 160 | contents not verified") rather than claiming review is required. |
| 161 | |
| 162 | ### 2. Dependabot |
| 163 | |
| 164 | **What we collect.** Two distinct facts: |
| 165 | |
| 166 | - **Tooling state** (`resource = dependabot`, `status ∈ {enabled, disabled, |
| 167 | unavailable}`) — the hourly poll checks whether Dependabot alerts are enabled |
| 168 | on each repo (the alert-list API answering at all is the signal; see |
| 169 | [`pollRepoAlerts`](../src/poller.ts)). Only `enabled` is mapped; a disabled or |
| 170 | unavailable scanner is recorded but deliberately produces no evidence either |
| 171 | way, matching the branch-protection `unavailable` precedent. |
| 172 | - **Findings** (`resource = dependabot_alert`, `status ∈ {open, fixed, |
| 173 | dismissed, auto_dismissed}`, `subject` = alert number) — from |
| 174 | `dependabot_alert` webhooks, plus the same hourly poll re-recording every |
| 175 | *open* alert. The poll matters twice: alerts already open before the App was |
| 176 | installed never sent a webhook, and an open alert with no events for the |
| 177 | whole retention window would otherwise age out of evidence. |
| 153 | 178 | |
| 154 | 179 | **Maps to:** |
| 155 | 180 | |
| 156 | 181 | | Framework | Control | When | Posture | |
| 157 | 182 | | --- | --- | --- | --- | |
| 158 | | | SOC 2 | **CC7.1** | any alert (`status = NULL`) | positive — "detection tooling is active" | |
| 159 | | | SOC 2 | **CC7.2** | `open` | negative — "unremediated known vulnerability" | |
| 160 | | | SOC 2 | **CC7.2** | `fixed` / `dismissed` / `auto_dismissed` | positive — "remediated" | |
| 161 | | | ISO 27001 | **A.8.8** | any alert (`status = NULL`) | positive — "technical vulnerability management active" | |
| 162 | | | ISO 27001 | **A.8.8** | `open` | negative — "unremediated known vulnerability" | |
| 163 | | | ISO 27001 | **A.8.8** | `fixed` / `dismissed` / `auto_dismissed` | positive — "remediated" | |
| 183 | | SOC 2 | **CC7.1** | tooling `enabled` | positive — detection tooling is on | |
| 184 | | SOC 2 | **CC7.1** | `open` | negative — unremediated known vulnerability | |
| 185 | | SOC 2 | **CC7.1** | `fixed` / `auto_dismissed` | positive — remediated (machine-verified) | |
| 186 | | SOC 2 | **CC7.1** | `dismissed` | informational — human risk-acceptance, justification subject to review | |
| 187 | | ISO 27001 | **A.8.8** | tooling `enabled` | positive — vulnerability management active | |
| 188 | | ISO 27001 | **A.8.8** | `open` | negative — unremediated known vulnerability | |
| 189 | | ISO 27001 | **A.8.8** | `fixed` / `auto_dismissed` | positive — remediated (machine-verified) | |
| 190 | | ISO 27001 | **A.8.8** | `dismissed` | informational — human risk-acceptance, justification subject to review | |
| 164 | 191 | |
| 165 | 192 | **Control meanings.** |
| 166 | 193 | - **CC7.1** — *Detection & monitoring.* The entity uses detection procedures to |
| @@ -168,101 +195,97 @@ alerts against the repo's dependencies. `status` is the alert state: `open`, |
| 168 | 195 | susceptibilities to *newly discovered* vulnerabilities. Dependabot is a |
| 169 | 196 | textbook example: it continuously matches your dependency tree against newly |
| 170 | 197 | published CVEs. |
| 171 | | - **CC7.2** — *Anomaly monitoring.* The entity monitors system components for |
| 172 | | anomalies and analyzes them to determine whether they are security events. |
| 173 | 198 | - **A.8.8** — *Management of technical vulnerabilities.* Information about |
| 174 | 199 | technical vulnerabilities must be obtained, exposure evaluated, and |
| 175 | | appropriate measures taken. This is a single control spanning the whole |
| 176 | | vulnerability lifecycle — detect, evaluate, remediate. |
| 177 | | |
| 178 | | **Why this holds.** The *presence* of Dependabot alerts proves the detection |
| 179 | | capability required by CC7.1 exists and is running — hence the `NULL` mapping |
| 180 | | fires positive on any alert regardless of state. Each *individual* alert's |
| 181 | | lifecycle (open vs. remediated) is then evidence for CC7.2: an open alert is an |
| 182 | | unresolved condition, a fixed/dismissed one is a closed one. On the **ISO** side |
| 183 | | the entire story lands on a *single* control, A.8.8, because A.8.8 explicitly |
| 184 | | covers the full lifecycle — so every status maps to A.8.8 (open → negative, |
| 185 | | remediated → positive, tooling-active → positive). See |
| 186 | | [migration 0007](../migrations/0007_close_coverage_gaps.sql). |
| 187 | | |
| 188 | | **Fit assessment: CC7.1 strong; CC7.2 defensible but the weakest link in the |
| 189 | | system.** CC7.2's formal text is about anomalies "indicative of malicious acts, |
| 190 | | natural disasters, and errors" — i.e. runtime security events. An unpatched |
| 191 | | dependency is a *known vulnerability*, which sits more naturally in CC7.1's |
| 192 | | "susceptibility to newly discovered vulnerabilities" language than in CC7.2's |
| 193 | | anomaly-detection language. Many auditors keep the **entire** dependency story |
| 194 | | (detection *and* remediation tracking) under CC7.1. **Recommendation:** before |
| 195 | | you present this to an auditor, decide whether open/remediated Dependabot state |
| 196 | | belongs under CC7.1 or CC7.2 in your control narrative, and align the mapping to |
| 197 | | that decision. Both are defensible; the current split is a design choice, not a |
| 198 | | requirement. The **ISO A.8.8** mapping, by contrast, is a strong, clean fit — |
| 199 | | A.8.8 is purpose-built for technical-vulnerability management and absorbs the |
| 200 | | whole lifecycle without the CC7.1/CC7.2 ambiguity. It was added in migration 0007 |
| 201 | | to close a gap: before it, Dependabot produced no evidence at all in an ISO |
| 202 | | export. |
| 203 | | |
| 204 | | ### 3. Code scanning alerts |
| 205 | | |
| 206 | | **What we collect.** `code_scanning_alert` webhook events — SAST findings from |
| 207 | | CodeQL or a third-party analyzer. `status ∈ {open, fixed, dismissed}`. |
| 200 | appropriate measures taken — one control spanning the whole lifecycle. |
| 201 | |
| 202 | **Why this holds.** The tooling-`enabled` fact proves the detection capability |
| 203 | required by CC7.1/A.8.8 exists and is on *right now* — it comes from the |
| 204 | feature's own state, not (as in earlier versions) from inference off the latest |
| 205 | alert event, which kept attesting after a scanner was switched off and never |
| 206 | fired for a clean repo. Each individual alert's lifecycle is then finding-level |
| 207 | evidence under the same controls: an open alert is an unresolved known |
| 208 | vulnerability; a `fixed` (patched) or `auto_dismissed` (e.g. dependency |
| 209 | removed) alert is a machine-verified closure; a `dismissed` alert is a **human |
| 210 | decision** — it may be sound risk acceptance or may be rubber-stamping, and the |
| 211 | justification is exactly what an auditor samples, so it is recorded as |
| 212 | informational rather than claimed as remediation. |
| 213 | |
| 214 | **Fit assessment: strong on both frameworks.** The whole SOC 2 lifecycle sits |
| 215 | under CC7.1, whose "susceptibility to newly discovered vulnerabilities" |
| 216 | language covers known-CVE management directly. (Earlier versions split |
| 217 | open/remediated state to CC7.2; CC7.2's anomaly-monitoring text is about |
| 218 | runtime security events, which made it the weakest link in the system — that |
| 219 | split has been removed.) A.8.8 is purpose-built for this signal and absorbs the |
| 220 | whole lifecycle. |
| 221 | |
| 222 | ### 3. Code scanning |
| 223 | |
| 224 | **What we collect.** Same two-fact shape as Dependabot: tooling state |
| 225 | (`resource = code_scanning`, from the hourly poll; only `enabled` mapped) and |
| 226 | findings (`resource = code_scanning_alert`, `status ∈ {open, fixed, |
| 227 | dismissed}`, `subject` = alert number) from `code_scanning_alert` webhooks plus |
| 228 | the hourly re-record of open alerts. Findings are SAST results from CodeQL or a |
| 229 | third-party analyzer. |
| 208 | 230 | |
| 209 | 231 | **Maps to:** |
| 210 | 232 | |
| 211 | 233 | | Framework | Control | When | Posture | |
| 212 | 234 | | --- | --- | --- | --- | |
| 213 | | | ISO 27001 | **A.8.29** | any alert (`status = NULL`) | positive — "security testing in development is active" | |
| 214 | | | ISO 27001 | **A.8.28** | `open` | negative — "unremediated finding" | |
| 215 | | | ISO 27001 | **A.8.28** | `fixed` / `dismissed` | positive — "remediated" | |
| 216 | | | SOC 2 | **CC7.1** | any alert (`status = NULL`) | positive — "detection tooling is active" | |
| 235 | | ISO 27001 | **A.8.29** | tooling `enabled` | positive — security testing in development is on | |
| 236 | | ISO 27001 | **A.8.28** | `open` | negative — unremediated finding | |
| 237 | | ISO 27001 | **A.8.28** | `fixed` | positive — remediated | |
| 238 | | ISO 27001 | **A.8.28** | `dismissed` | informational — human risk-acceptance, justification subject to review | |
| 239 | | SOC 2 | **CC7.1** | tooling `enabled` | positive — detection tooling is on | |
| 240 | | SOC 2 | **CC7.1** | `open` | negative — unremediated finding | |
| 241 | | SOC 2 | **CC7.1** | `fixed` | positive — remediated | |
| 242 | | SOC 2 | **CC7.1** | `dismissed` | informational — human risk-acceptance, justification subject to review | |
| 217 | 243 | |
| 218 | 244 | **Control meanings.** |
| 219 | 245 | - **A.8.29** — *Security testing in development and acceptance.* Security testing |
| 220 | | processes must be defined and run within the development lifecycle so |
| 221 | | vulnerabilities are found before production. The existence of code scanning |
| 222 | | *is* that testing process. |
| 246 | processes must be defined and run within the development lifecycle. The |
| 247 | existence of code scanning *is* that testing process. |
| 223 | 248 | - **A.8.28** — *Secure coding.* Secure coding principles must be applied during |
| 224 | 249 | development. An open finding is evidence of a secure-coding gap in the source; |
| 225 | 250 | a remediated one is evidence the gap was closed. |
| 226 | 251 | - **CC7.1** — *Detection & monitoring.* (Same control as Dependabot's SOC 2 |
| 227 | | mapping.) Code scanning is detection tooling that surfaces vulnerabilities, so |
| 228 | | its presence satisfies the "detection procedures exist and run" expectation. |
| 252 | mapping.) |
| 229 | 253 | |
| 230 | 254 | **Why this holds.** On the **ISO** side this splits cleanly across two controls |
| 231 | | that map to two facts: *"a testing process exists"* (A.8.29, from the `NULL` |
| 232 | | mapping) versus *"the code itself is/ isn't secure"* (A.8.28, from each finding's |
| 233 | | state). On the **SOC 2** side (added in [migration 0007](../migrations/0007_close_coverage_gaps.sql)) |
| 234 | | only the tooling-active fact is mapped, to CC7.1 — mirroring how Dependabot's |
| 235 | | tooling-active fact maps to CC7.1. |
| 236 | | |
| 237 | | **Fit assessment: strong on ISO; SOC 2 intentionally partial.** The |
| 238 | | A.8.29-vs-A.8.28 split is clean — one control is about *having* the testing |
| 239 | | process, the other about the *code quality* it reveals — and both titles match |
| 240 | | the signal directly. The new SOC 2 CC7.1 mapping covers only detection-active, |
| 241 | | **not** finding-level state: code-scanning `open`/`fixed` rows are deliberately |
| 242 | | *not* routed to CC7.2, because whether the vulnerability lifecycle belongs under |
| 243 | | CC7.1 or CC7.2 is still an open decision (see the Dependabot fit assessment). Once |
| 244 | | that is settled, finding-level SOC 2 rows for code scanning can be added to match |
| 245 | | Dependabot. Until then a SOC 2 export shows code scanning as "detection active" |
| 246 | | only — which under-claims rather than over-claims, the safe direction. |
| 247 | | |
| 248 | | ### 4. Secret scanning alerts |
| 249 | | |
| 250 | | **What we collect.** `secret_scanning_alert` webhook events — detected |
| 251 | | credentials/tokens committed to the repo. `status ∈ {open, resolved}`. |
| 255 | that map to two facts: *"a testing process exists"* (A.8.29, from tooling |
| 256 | state) versus *"the code itself is / isn't secure"* (A.8.28, from each |
| 257 | finding's state). On the **SOC 2** side the full lifecycle mirrors Dependabot |
| 258 | under CC7.1 — the finding-level rows were previously deferred pending the |
| 259 | CC7.1-vs-CC7.2 decision, which is now settled in CC7.1's favor. |
| 260 | |
| 261 | **Fit assessment: strong on both.** The A.8.29-vs-A.8.28 split follows the |
| 262 | controls' own having-a-process vs. code-quality distinction, and the SOC 2 side |
| 263 | is now symmetric with Dependabot rather than intentionally partial. |
| 264 | |
| 265 | ### 4. Secret scanning |
| 266 | |
| 267 | **What we collect.** Tooling state (`resource = secret_scanning`, from the |
| 268 | hourly poll; only `enabled` mapped) and findings (`resource = |
| 269 | secret_scanning_alert`, `status ∈ {open, resolved}`, `subject` = alert number) |
| 270 | from `secret_scanning_alert` webhooks plus the hourly re-record of open alerts. |
| 271 | One payload quirk matters: unlike the other two alert payloads, the |
| 272 | secret-scanning webhook alert carries **no `state` field** — ingest derives |
| 273 | open/resolved from `alert.resolution`, which is set iff the alert is resolved |
| 274 | (see [`extractFact`](../src/webhook.ts)). |
| 252 | 275 | |
| 253 | 276 | **Maps to:** |
| 254 | 277 | |
| 255 | 278 | | Framework | Control | When | Posture | |
| 256 | 279 | | --- | --- | --- | --- | |
| 257 | | | SOC 2 | **CC6.6** | any alert (`status = NULL`) | positive — "leaked-credential detection is active" | |
| 258 | | | SOC 2 | **CC6.6** | `open` | negative — "live credential exposure" | |
| 259 | | | SOC 2 | **CC6.6** | `resolved` | positive — "exposure remediated" | |
| 260 | | | SOC 2 | **CC6.1** | any alert (`status = NULL`) | positive — "logical-access credential protection active" | |
| 261 | | | SOC 2 | **CC6.1** | `open` | negative — "exposed credential undermines logical access controls" | |
| 262 | | | SOC 2 | **CC6.1** | `resolved` | positive — "logical access control restored" | |
| 263 | | | ISO 27001 | **A.5.17** | any alert (`status = NULL`) | positive — "authentication-information protection active" | |
| 264 | | | ISO 27001 | **A.5.17** | `open` | negative — "exposed authentication information" | |
| 265 | | | ISO 27001 | **A.5.17** | `resolved` | positive — "exposure remediated" | |
| 280 | | SOC 2 | **CC6.6** | tooling `enabled` | positive — leaked-credential detection is on | |
| 281 | | SOC 2 | **CC6.6** | `open` | negative — live credential exposure | |
| 282 | | SOC 2 | **CC6.6** | `resolved` | informational — resolution reason subject to review | |
| 283 | | SOC 2 | **CC6.1** | tooling `enabled` | positive — logical-access credential protection is on | |
| 284 | | SOC 2 | **CC6.1** | `open` | negative — exposed credential undermines logical access controls | |
| 285 | | SOC 2 | **CC6.1** | `resolved` | informational — resolution reason subject to review | |
| 286 | | ISO 27001 | **A.5.17** | tooling `enabled` | positive — authentication-information protection is on | |
| 287 | | ISO 27001 | **A.5.17** | `open` | negative — exposed authentication information | |
| 288 | | ISO 27001 | **A.5.17** | `resolved` | informational — resolution reason subject to review | |
| 266 | 289 | |
| 267 | 290 | **Control meanings.** |
| 268 | 291 | - **CC6.6** — *Protection against external threats.* The entity implements |
| @@ -279,65 +302,70 @@ credentials/tokens committed to the repo. `status ∈ {open, resolved}`. |
| 279 | 302 | |
| 280 | 303 | **Why this holds.** A committed credential is relevant to all three controls at |
| 281 | 304 | once. For **CC6.6**, it is a direct path for an *external* attacker to cross the |
| 282 | | system boundary. For **CC6.1**, the credential is itself one of the logical-access |
| 283 | | keys the control is meant to safeguard, so a leak is a compromise of the access |
| 284 | | controls themselves. For **A.5.17**, the credential is authentication information |
| 285 | | whose confidentiality the control requires. In every case: scanning active → |
| 286 | | **positive** (a protective measure exists); open alert → **negative** (a live |
| 287 | | gap); resolved → **positive** (gap closed). See |
| 288 | | [migration 0006](../migrations/0006_secret_scanning_cc6_1.sql) (CC6.1) and |
| 289 | | [migration 0007](../migrations/0007_close_coverage_gaps.sql) (A.5.17). |
| 305 | system boundary. For **CC6.1**, the credential is itself one of the |
| 306 | logical-access keys the control is meant to safeguard. For **A.5.17**, the |
| 307 | credential is authentication information whose confidentiality the control |
| 308 | requires. Scanning enabled → **positive**; open alert → **negative**. A |
| 309 | `resolved` alert is **informational**, not positive: GitHub's resolution |
| 310 | reasons include `wont_fix`, so "resolved" may mean the credential was revoked |
| 311 | *or* that someone decided to leave it — the recorded reason is what the |
| 312 | reviewer must check. |
| 290 | 313 | |
| 291 | 314 | **Fit assessment: all three defensible.** CC6.6 is the external-threat framing, |
| 292 | 315 | CC6.1 the logical-access framing, A.5.17 the ISO authentication-information |
| 293 | | framing (added in migration 0007 to close a gap — before it, secret scanning |
| 294 | | produced no ISO evidence). Mapping to all three means the export satisfies |
| 295 | | whichever control the organization's narrative uses. A further SOC 2 framing, |
| 316 | framing. Mapping to all three means the export satisfies whichever control the |
| 317 | organization's narrative uses — at the cost of row multiplicity: one open |
| 318 | secret emits a negative under each of the three. A further SOC 2 framing, |
| 296 | 319 | **CC6.7** (restricting the transmission/movement of information), also touches |
| 297 | | this and could be added if an auditor prefers it. Note the multiplicity: one |
| 298 | | `open` secret now emits **six** rows — a positive ("scanner running") and a |
| 299 | | negative ("open exposure") under *each* of CC6.6, CC6.1 (SOC 2 export) and A.5.17 |
| 300 | | (ISO export). That is intended and reads correctly, but expect the row counts to |
| 301 | | scale accordingly. |
| 320 | this and could be added if an auditor prefers it. |
| 302 | 321 | |
| 303 | 322 | ### 5. Membership changes (webhook trail) |
| 304 | 323 | |
| 305 | | **What we collect.** `member`, `team`, and `repository` webhook events — the |
| 306 | | *change* events, recording that an access-related mutation happened. Normalized |
| 307 | | to `resource ∈ {member_access, team, repository}` with the GitHub action as |
| 308 | | status. |
| 324 | **What we collect.** The *change* events for access administration, each with a |
| 325 | `subject` so the trail keeps one latest row per person/team rather than one per |
| 326 | repo: |
| 327 | |
| 328 | - `member` webhook → `resource = member_access`, subject = the collaborator's |
| 329 | login. **Scope note: this event covers repository collaborators**, not org |
| 330 | members. |
| 331 | - `organization` webhook → `resource = org_membership`, subject = the member's |
| 332 | login (or the invitee's login/email). This is where org-level joins, removals |
| 333 | and invitations arrive. |
| 334 | - `team` webhook → `resource = team`, subject = the team slug. |
| 309 | 335 | |
| 310 | 336 | **Maps to:** |
| 311 | 337 | |
| 312 | 338 | | Framework | Control | When | Posture | |
| 313 | 339 | | --- | --- | --- | --- | |
| 314 | | | SOC 2 | **CC6.2** | `member_access` `added` | informational — "access grant, logged for review" | |
| 315 | | | SOC 2 | **CC6.3** | `member_access` `removed` | positive — "timely access removal" | |
| 316 | | | SOC 2 | **CC6.3** | `member_access` `edited` | informational — "access-level change, logged" | |
| 317 | | | ISO 27001 | **A.5.18** | `team` (any) | informational — "access-rights change, audit trail" | |
| 340 | | SOC 2 | **CC6.2** | `member_access` `added`, `org_membership` `member_added` / `member_invited` | informational — access grant / invitation, logged for review | |
| 341 | | SOC 2 | **CC6.3** | `member_access` `removed` / `edited`, `org_membership` `member_removed` | informational — modification / deprovisioning recorded | |
| 342 | | ISO 27001 | **A.5.18** | all of the above | informational — access-rights change, audit trail | |
| 343 | | ISO 27001 | **A.5.18** | `team` (any) | informational — access-rights change, audit trail | |
| 318 | 344 | |
| 319 | 345 | **Control meanings.** |
| 320 | 346 | - **CC6.2** — *Registration & authorization of new users.* Before credentials are |
| 321 | 347 | issued, new users are registered and authorized; credentials are removed when |
| 322 | | access is no longer authorized. A member being *added* is the provisioning |
| 323 | | event this criterion governs. |
| 348 | access is no longer authorized. A member being *added* or *invited* is the |
| 349 | provisioning event this criterion governs. |
| 324 | 350 | - **CC6.3** — *Authorize / modify / remove access.* Access is authorized, |
| 325 | 351 | modified, or removed based on roles, least privilege, and segregation of |
| 326 | | duties. Member *removal* and *role change* are the modify/remove events here. |
| 352 | duties. Removal and role change are the modify/remove events here. |
| 327 | 353 | - **A.5.18** — *Access rights.* Access rights are provisioned, reviewed, |
| 328 | | modified, and removed per the access-control policy. A team membership change |
| 329 | | is an access-rights mutation on that trail. |
| 354 | modified, and removed per the access-control policy. |
| 330 | 355 | |
| 331 | 356 | **Why this holds & posture logic.** These are the *audit trail* of access |
| 332 | 357 | administration — evidence that grants/changes are captured, which is what an |
| 333 | | auditor samples. Most are **informational** (an add or a role change is neither |
| 334 | | inherently good nor bad — it needs human review). The one exception is |
| 335 | | `removed` → **positive**, because timely de-provisioning is itself a control |
| 336 | | objective (CC6.3), so a captured removal is affirmative evidence. |
| 358 | auditor samples. **All rows are informational**, removals included: the event |
| 359 | proves a removal happened and when, but not that it was *timely* relative to an |
| 360 | offboarding trigger the system cannot see — so the rationale says |
| 361 | "deprovisioning recorded; timeliness subject to review" rather than claiming |
| 362 | timeliness as a positive. (Earlier versions claimed "timely access removal"; |
| 363 | that was rounding up.) |
| 337 | 364 | |
| 338 | 365 | **Fit assessment: strong on the CC6.2/CC6.3 split** (it follows the criteria's |
| 339 | | own provisioning-vs-modification language). The informational posture is the |
| 340 | | right call — this data feeds the access review, it does not pass/fail on its own. |
| 366 | own provisioning-vs-modification language), and honest about scope now that |
| 367 | repo-collaborator and org-member events are separate resources with separate |
| 368 | rationales. |
| 341 | 369 | |
| 342 | 370 | ### 6. Membership & team inventory (polled) |
| 343 | 371 | |
| @@ -351,9 +379,9 @@ point-in-time snapshot — this is what powers the [access-review diff](../src/a |
| 351 | 379 | |
| 352 | 380 | | Framework | Control | Resource | Posture | |
| 353 | 381 | | --- | --- | --- | --- | |
| 354 | | | SOC 2 | **CC6.2** | `org_member` | informational — "org access inventory, subject to periodic review" | |
| 355 | | | SOC 2 | **CC6.3** | `team_member` | informational — "team-based access inventory" | |
| 356 | | | ISO 27001 | **A.5.18** | `org_member`, `team_member` | informational — "access-rights inventory" | |
| 382 | | SOC 2 | **CC6.2** | `org_member` | informational — org access inventory, subject to periodic review | |
| 383 | | SOC 2 | **CC6.3** | `team_member` | informational — team-based access inventory | |
| 384 | | ISO 27001 | **A.5.18** | `org_member`, `team_member` | informational — access-rights inventory | |
| 357 | 385 | |
| 358 | 386 | **Why this holds.** A point-in-time roster of who has access is the raw material |
| 359 | 387 | of a periodic access review — the recurring auditor ask that CC6.2/CC6.3 and |
| @@ -368,20 +396,21 @@ reasonable but not the only defensible cut — the *review* of org membership is |
| 368 | 396 | arguably as much CC6.3 (appropriateness of access) as CC6.2 (registration). Since |
| 369 | 397 | these are informational inventory rows feeding a review, the exact CC6.2/CC6.3 |
| 370 | 398 | attribution is low-stakes; A.5.18 is unambiguous. Note the two resource families |
| 371 | | (`member_access`/`team` webhook trail vs. `org_member`/`team_member` polled |
| 372 | | inventory) are deliberately separate resource names so the change-trail and the |
| 373 | | current-state inventory don't collide. |
| 399 | (`member_access`/`org_membership`/`team` webhook trail vs. |
| 400 | `org_member`/`team_member` polled inventory) are deliberately separate resource |
| 401 | names so the change-trail and the current-state inventory don't collide. |
| 374 | 402 | |
| 375 | 403 | ### 7. Repository inventory |
| 376 | 404 | |
| 377 | 405 | **What we collect.** `repository` webhook events — repos created/deleted/renamed |
| 378 | | within the installation. `resource = repository`. |
| 406 | and visibility changes within the installation. `resource = repository`. |
| 379 | 407 | |
| 380 | 408 | **Maps to:** |
| 381 | 409 | |
| 382 | | | Framework | Control | Posture | |
| 383 | | | --- | --- | --- | |
| 384 | | | ISO 27001 | **A.5.9** | informational — "asset inventory trail" | |
| 410 | | Framework | Control | When | Posture | |
| 411 | | --- | --- | --- | --- | |
| 412 | | ISO 27001 | **A.5.9** | any action | informational — asset inventory trail | |
| 413 | | SOC 2 | **CC6.1** | `publicized` | informational — repo made public, flagged for review | |
| 385 | 414 | |
| 386 | 415 | **Control meaning.** |
| 387 | 416 | - **A.5.9** — *Inventory of information and other associated assets.* A complete, |
| @@ -389,8 +418,11 @@ within the installation. `resource = repository`. |
| 389 | 418 | |
| 390 | 419 | **Why this holds.** Repositories are information assets. The trail of repo |
| 391 | 420 | create/delete/rename events is evidence that the asset inventory is maintained as |
| 392 | | it changes — exactly A.5.9's requirement. **Informational**: it is inventory, not |
| 393 | | a pass/fail condition. |
| 421 | it changes — exactly A.5.9's requirement. One action gets an extra row: a repo |
| 422 | being **publicized** is a visibility change with direct confidentiality impact, |
| 423 | so it is additionally surfaced under CC6.1 rather than left as a generic |
| 424 | inventory tick. Both rows are **informational** — whether going public was |
| 425 | intended is a judgment the reviewer makes. |
| 394 | 426 | |
| 395 | 427 | **Fit assessment: strong for what it claims.** The honest caveat is completeness: |
| 396 | 428 | this is a *change trail*, so it evidences that inventory changes are captured, not |
| @@ -404,56 +436,72 @@ webhook trail. |
| 404 | 436 | ## Complete mapping reference |
| 405 | 437 | |
| 406 | 438 | This table is the authoritative human-readable copy of every row in |
| 407 | | [`control_mappings`](../migrations/0002_control_mappings.sql) after all migrations |
| 408 | | (0002 seeds most; 0003 replaces branch-protection/ruleset with the |
| 439 | [`control_mappings`](../migrations/0002_control_mappings.sql) after all |
| 440 | migrations (0002 seeds most; 0003 replaces branch-protection/ruleset with the |
| 409 | 441 | enabled/disabled vocabulary; 0005 adds the polled access inventory; 0006 adds |
| 410 | | the secret-scanning CC6.1 rows; 0007 closes the cross-framework coverage gaps — |
| 411 | | Dependabot→A.8.8, code scanning→CC7.1, secret scanning→A.5.17). **A "·" in |
| 412 | | Status means the mapping's `status` is `NULL` — it matches any status.** |
| 442 | the secret-scanning CC6.1 rows; 0007 closes cross-framework coverage gaps; 0008 |
| 443 | applies the mapping-review fixes — per-entity subjects, polled tooling state, |
| 444 | the CC7.1 consolidation, and informational postures for human dismissals). |
| 445 | **A "·" in Status means the mapping's `status` is `NULL` — it matches any |
| 446 | status.** The Rationale column is the exact auditor-facing string in the |
| 447 | database, and is CI-checked against it. |
| 413 | 448 | |
| 414 | 449 | | Resource | Status | Framework | Control | Posture | Rationale | |
| 415 | 450 | | --- | --- | --- | --- | --- | --- | |
| 416 | | | `branch_protection` | `enabled` | SOC 2 | CC8.1 | positive | Change management — review before merge | |
| 417 | | | `branch_protection` | `disabled` | SOC 2 | CC8.1 | negative | Change-control gap — direct pushes possible | |
| 418 | | | `branch_protection` | `enabled` | ISO 27001 | A.8.32 | positive | Change management | |
| 419 | | | `branch_protection` | `disabled` | ISO 27001 | A.8.32 | negative | Change-control gap — direct pushes possible | |
| 420 | | | `repository_ruleset` | `enabled` | SOC 2 | CC8.1 | positive | Change management — review before merge | |
| 421 | | | `repository_ruleset` | `disabled` | SOC 2 | CC8.1 | negative | Change-control gap — direct pushes possible | |
| 422 | | | `repository_ruleset` | `enabled` | ISO 27001 | A.8.32 | positive | Change management | |
| 423 | | | `repository_ruleset` | `disabled` | ISO 27001 | A.8.32 | negative | Change-control gap — direct pushes possible | |
| 424 | | | `dependabot_alert` | · | SOC 2 | CC7.1 | positive | Detection tooling is active | |
| 425 | | | `dependabot_alert` | `open` | SOC 2 | CC7.2 | negative | Unremediated known vulnerability | |
| 426 | | | `dependabot_alert` | `fixed` | SOC 2 | CC7.2 | positive | Remediated | |
| 427 | | | `dependabot_alert` | `dismissed` | SOC 2 | CC7.2 | positive | Remediated (risk accepted) | |
| 428 | | | `dependabot_alert` | `auto_dismissed` | SOC 2 | CC7.2 | positive | Remediated (e.g. dependency removed) | |
| 429 | | | `dependabot_alert` | · | ISO 27001 | A.8.8 | positive | Technical vulnerability management — detection active | |
| 451 | | `branch_protection` | `enabled` | SOC 2 | CC8.1 | positive | Change management — a protection rule is enforced on the default branch (rule contents not verified) | |
| 452 | | `branch_protection` | `disabled` | SOC 2 | CC8.1 | negative | Change-control gap — no protection on the default branch; direct pushes possible | |
| 453 | | `branch_protection` | `enabled` | ISO 27001 | A.8.32 | positive | Change management — a protection rule is enforced on the default branch (rule contents not verified) | |
| 454 | | `branch_protection` | `disabled` | ISO 27001 | A.8.32 | negative | Change-control gap — no protection on the default branch; direct pushes possible | |
| 455 | | `repository_ruleset` | `enabled` | SOC 2 | CC8.1 | positive | Change management — an active ruleset covers the default branch (rule contents not verified) | |
| 456 | | `repository_ruleset` | `disabled` | SOC 2 | CC8.1 | negative | Change-control gap — no active ruleset covers the default branch | |
| 457 | | `repository_ruleset` | `enabled` | ISO 27001 | A.8.32 | positive | Change management — an active ruleset covers the default branch (rule contents not verified) | |
| 458 | | `repository_ruleset` | `disabled` | ISO 27001 | A.8.32 | negative | Change-control gap — no active ruleset covers the default branch | |
| 459 | | `dependabot` | `enabled` | SOC 2 | CC7.1 | positive | Detection tooling — Dependabot alerts are enabled on the repository | |
| 460 | | `dependabot` | `enabled` | ISO 27001 | A.8.8 | positive | Technical vulnerability management — Dependabot alerts are enabled on the repository | |
| 461 | | `code_scanning` | `enabled` | SOC 2 | CC7.1 | positive | Detection tooling — code scanning is enabled on the repository | |
| 462 | | `code_scanning` | `enabled` | ISO 27001 | A.8.29 | positive | Security testing in development — code scanning is enabled on the repository | |
| 463 | | `secret_scanning` | `enabled` | SOC 2 | CC6.6 | positive | Leaked-credential detection — secret scanning is enabled on the repository | |
| 464 | | `secret_scanning` | `enabled` | SOC 2 | CC6.1 | positive | Logical-access credential protection — secret scanning is enabled on the repository | |
| 465 | | `secret_scanning` | `enabled` | ISO 27001 | A.5.17 | positive | Authentication-information protection — secret scanning is enabled on the repository | |
| 466 | | `dependabot_alert` | `open` | SOC 2 | CC7.1 | negative | Unremediated known vulnerability | |
| 467 | | `dependabot_alert` | `fixed` | SOC 2 | CC7.1 | positive | Vulnerability remediated | |
| 468 | | `dependabot_alert` | `dismissed` | SOC 2 | CC7.1 | informational | Dismissed by a user — risk-acceptance justification subject to review | |
| 469 | | `dependabot_alert` | `auto_dismissed` | SOC 2 | CC7.1 | positive | Auto-dismissed by GitHub (e.g. dependency removed) | |
| 430 | 470 | | `dependabot_alert` | `open` | ISO 27001 | A.8.8 | negative | Unremediated known technical vulnerability | |
| 431 | 471 | | `dependabot_alert` | `fixed` | ISO 27001 | A.8.8 | positive | Vulnerability remediated | |
| 432 | | | `dependabot_alert` | `dismissed` | ISO 27001 | A.8.8 | positive | Vulnerability remediated (risk accepted) | |
| 433 | | | `dependabot_alert` | `auto_dismissed` | ISO 27001 | A.8.8 | positive | Vulnerability remediated (e.g. dependency removed) | |
| 434 | | | `code_scanning_alert` | · | ISO 27001 | A.8.29 | positive | Security testing in development is active | |
| 435 | | | `code_scanning_alert` | `open` | ISO 27001 | A.8.28 | negative | Unremediated finding | |
| 436 | | | `code_scanning_alert` | `fixed` | ISO 27001 | A.8.28 | positive | Remediated | |
| 437 | | | `code_scanning_alert` | `dismissed` | ISO 27001 | A.8.28 | positive | Remediated (risk accepted) | |
| 438 | | | `code_scanning_alert` | · | SOC 2 | CC7.1 | positive | Detection tooling is active (findings unmapped in SOC 2) | |
| 439 | | | `secret_scanning_alert` | · | SOC 2 | CC6.6 | positive | Leaked-credential detection is active | |
| 472 | | `dependabot_alert` | `dismissed` | ISO 27001 | A.8.8 | informational | Dismissed by a user — risk-acceptance justification subject to review | |
| 473 | | `dependabot_alert` | `auto_dismissed` | ISO 27001 | A.8.8 | positive | Auto-dismissed by GitHub (e.g. dependency removed) | |
| 474 | | `code_scanning_alert` | `open` | SOC 2 | CC7.1 | negative | Unremediated static-analysis finding | |
| 475 | | `code_scanning_alert` | `fixed` | SOC 2 | CC7.1 | positive | Finding remediated | |
| 476 | | `code_scanning_alert` | `dismissed` | SOC 2 | CC7.1 | informational | Dismissed by a user — risk-acceptance justification subject to review | |
| 477 | | `code_scanning_alert` | `open` | ISO 27001 | A.8.28 | negative | Unremediated static-analysis finding | |
| 478 | | `code_scanning_alert` | `fixed` | ISO 27001 | A.8.28 | positive | Finding remediated | |
| 479 | | `code_scanning_alert` | `dismissed` | ISO 27001 | A.8.28 | informational | Dismissed by a user — risk-acceptance justification subject to review | |
| 440 | 480 | | `secret_scanning_alert` | `open` | SOC 2 | CC6.6 | negative | Live credential exposure | |
| 441 | | | `secret_scanning_alert` | `resolved` | SOC 2 | CC6.6 | positive | Exposure remediated | |
| 442 | | | `secret_scanning_alert` | · | SOC 2 | CC6.1 | positive | Logical-access credential protection — detection active | |
| 481 | | `secret_scanning_alert` | `resolved` | SOC 2 | CC6.6 | informational | Resolution recorded — reason (revoked vs. won't-fix) subject to review | |
| 443 | 482 | | `secret_scanning_alert` | `open` | SOC 2 | CC6.1 | negative | Exposed credential undermines logical access controls | |
| 444 | | | `secret_scanning_alert` | `resolved` | SOC 2 | CC6.1 | positive | Logical access control restored — exposure remediated | |
| 445 | | | `secret_scanning_alert` | · | ISO 27001 | A.5.17 | positive | Authentication-information protection — detection active | |
| 483 | | `secret_scanning_alert` | `resolved` | SOC 2 | CC6.1 | informational | Resolution recorded — reason (revoked vs. won't-fix) subject to review | |
| 446 | 484 | | `secret_scanning_alert` | `open` | ISO 27001 | A.5.17 | negative | Exposed authentication information | |
| 447 | | | `secret_scanning_alert` | `resolved` | ISO 27001 | A.5.17 | positive | Authentication-information exposure remediated | |
| 448 | | | `member_access` | `added` | SOC 2 | CC6.2 | informational | Access grant — logged for review | |
| 449 | | | `member_access` | `removed` | SOC 2 | CC6.3 | positive | Timely access removal | |
| 450 | | | `member_access` | `edited` | SOC 2 | CC6.3 | informational | Access-level change — logged for review | |
| 485 | | `secret_scanning_alert` | `resolved` | ISO 27001 | A.5.17 | informational | Resolution recorded — reason (revoked vs. won't-fix) subject to review | |
| 486 | | `member_access` | `added` | SOC 2 | CC6.2 | informational | Repository collaborator added — access grant logged for review | |
| 487 | | `member_access` | `removed` | SOC 2 | CC6.3 | informational | Repository collaborator removed — deprovisioning recorded; timeliness subject to review | |
| 488 | | `member_access` | `edited` | SOC 2 | CC6.3 | informational | Repository collaborator permission changed — logged for review | |
| 489 | | `member_access` | `added` | ISO 27001 | A.5.18 | informational | Repository collaborator added — access-rights change, audit trail | |
| 490 | | `member_access` | `removed` | ISO 27001 | A.5.18 | informational | Repository collaborator removed — access-rights change, audit trail | |
| 491 | | `member_access` | `edited` | ISO 27001 | A.5.18 | informational | Repository collaborator permission changed — access-rights change, audit trail | |
| 492 | | `org_membership` | `member_added` | SOC 2 | CC6.2 | informational | Organization member added — access grant logged for review | |
| 493 | | `org_membership` | `member_removed` | SOC 2 | CC6.3 | informational | Organization member removed — deprovisioning recorded; timeliness subject to review | |
| 494 | | `org_membership` | `member_invited` | SOC 2 | CC6.2 | informational | Organization invitation issued — logged for review | |
| 495 | | `org_membership` | `member_added` | ISO 27001 | A.5.18 | informational | Organization member added — access-rights change, audit trail | |
| 496 | | `org_membership` | `member_removed` | ISO 27001 | A.5.18 | informational | Organization member removed — access-rights change, audit trail | |
| 497 | | `org_membership` | `member_invited` | ISO 27001 | A.5.18 | informational | Organization invitation issued — access-rights change, audit trail | |
| 451 | 498 | | `team` | · | ISO 27001 | A.5.18 | informational | Access-rights change, audit trail | |
| 452 | 499 | | `repository` | · | ISO 27001 | A.5.9 | informational | Asset inventory trail | |
| 453 | | | `org_member` | · | SOC 2 | CC6.2 | informational | Org access inventory — subject to periodic review | |
| 454 | | | `org_member` | · | ISO 27001 | A.5.18 | informational | Access-rights inventory | |
| 500 | | `repository` | `publicized` | SOC 2 | CC6.1 | informational | Repository made public — visibility change affecting asset confidentiality, flagged for review | |
| 501 | | `org_member` | · | SOC 2 | CC6.2 | informational | Organization access inventory — subject to periodic access review | |
| 502 | | `org_member` | · | ISO 27001 | A.5.18 | informational | Access rights inventory | |
| 455 | 503 | | `team_member` | · | SOC 2 | CC6.3 | informational | Team-based access inventory | |
| 456 | | | `team_member` | · | ISO 27001 | A.5.18 | informational | Access-rights inventory | |
| 504 | | `team_member` | · | ISO 27001 | A.5.18 | informational | Access rights inventory | |
| 457 | 505 | |
| 458 | 506 | ### Control glossary |
| 459 | 507 | |
| @@ -464,7 +512,6 @@ Status means the mapping's `status` is `NULL` — it matches any status.** |
| 464 | 512 | | **CC6.3** | Authorize, modify, and remove access by role, with least privilege and segregation of duties | |
| 465 | 513 | | **CC6.6** | Protect against threats originating outside the system boundary | |
| 466 | 514 | | **CC7.1** | Detect configuration changes that introduce vulnerabilities, and susceptibility to newly discovered ones | |
| 467 | | | **CC7.2** | Monitor components for anomalies and analyze them as potential security events | |
| 468 | 515 | | **CC8.1** | Put changes through an authorized, controlled process; block unauthorized changes | |
| 469 | 516 | | **A.5.9** | Maintain an inventory of information and associated assets, with owners | |
| 470 | 517 | | **A.5.17** | Control the allocation and management of authentication information (passwords, keys, tokens) | |
| @@ -501,20 +548,23 @@ so bias toward under-claiming. |
| 501 | 548 | mapping is worth more to an auditor than three tenuous ones. Tenuous mappings |
| 502 | 549 | erode trust in the whole evidence pack. |
| 503 | 550 | 4. **Assign posture from the control's expectation, not the signal's sentiment:** |
| 504 | | - `positive` — the state is what the control wants. |
| 551 | - `positive` — the state is what the control wants, and the platform verified |
| 552 | it (not merely a human clicking "dismiss"). |
| 505 | 553 | - `negative` — the state is a concrete gap the control would flag. |
| 506 | 554 | - `informational` — the state is audit-trail/inventory that feeds a review but |
| 507 | | is not itself pass/fail. When in doubt, use `informational`. |
| 508 | | 5. **Decide detection-vs-finding.** If the signal is a scanner/alert stream, you |
| 509 | | usually want two mapping kinds: a `status = NULL` row for "the control's |
| 510 | | *tooling* exists" (positive), and per-status rows for individual findings. |
| 511 | | Remember rule 3 in [How mapping works](#how-mapping-works-mechanically): both |
| 512 | | fire on the same snapshot. |
| 555 | is not itself pass/fail. Human decisions (dismissals, resolutions with a |
| 556 | reason) belong here. When in doubt, use `informational`. |
| 557 | 5. **Separate tooling state from findings.** If the signal is a scanner/alert |
| 558 | stream, attest "the control's *tooling* is on" from the feature's own state |
| 559 | (polled), not from the existence of alerts — and attest each finding's |
| 560 | lifecycle per alert, with the alert number as `subject` so findings don't |
| 561 | mask each other. |
| 513 | 562 | 6. **Pin the edition.** State which version of the framework you mapped (e.g. |
| 514 | 563 | "PCI DSS v4.0.1", "NIST CSF 2.0") — control numbers move between editions. |
| 515 | | 7. **Write the rationale** in the `rationale` column *and* the fit assessment |
| 516 | | here. The `rationale` is what an auditor reads in the export; make it a |
| 517 | | complete thought, not a keyword. |
| 564 | 7. **Write the rationale** in the `rationale` column *and* copy it into the |
| 565 | reference table here verbatim — it is CI-checked. The `rationale` is what an |
| 566 | auditor reads in the export; make it a complete thought that claims no more |
| 567 | than the signal proves. |
| 518 | 568 | |
| 519 | 569 | ### What the code needs |
| 520 | 570 | |
| @@ -570,7 +620,7 @@ a row here with no SQL, is a bug. |
| 570 | 620 | This is enforced. [`scripts/check-mappings.mjs`](../scripts/check-mappings.mjs) |
| 571 | 621 | applies every migration to an in-memory SQLite database, reads back |
| 572 | 622 | `control_mappings`, and diffs the `(resource, status, framework, control_id, |
| 573 | | posture)` tuples against the rows parsed out of the |
| 623 | posture, rationale)` tuples against the rows parsed out of the |
| 574 | 624 | [reference table](#complete-mapping-reference) above. It fails with a row-level |
| 575 | 625 | diff if the two drift. Run it with: |
| 576 | 626 | |