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