| @@ -0,0 +1,600 @@ |
| |
1 | # Framework Mapping |
| |
2 | |
| |
3 | This document is the rationale behind every control mapping in gh-attest: what |
| |
4 | each GitHub signal is, which compliance control it is offered as evidence for, |
| |
5 | and **why that mapping is defensible to an auditor**. It also explains what each |
| |
6 | control code (`A.8.32`, `CC8.1`, …) actually means in plain language, and how to |
| |
7 | add a new framework. |
| |
8 | |
| |
9 | It is the human-readable companion to the machine-readable mappings in |
| |
10 | [`migrations/`](../migrations). The two must agree — see |
| |
11 | [Keeping this document in sync](#keeping-this-document-in-sync). |
| |
12 | |
| |
13 | > **Scope of the claim.** gh-attest produces *evidence*, not a compliance |
| |
14 | > verdict. A "positive" row means a GitHub setting is in a state that supports a |
| |
15 | > control; it does not mean the control is satisfied — that judgment belongs to |
| |
16 | > the organization and its auditor. This document explains why each signal is |
| |
17 | > *relevant* to a control, and is honest about where a mapping is a strong fit |
| |
18 | > versus a defensible-but-debatable one. |
| |
19 | |
| |
20 | --- |
| |
21 | |
| |
22 | ## Contents |
| |
23 | |
| |
24 | - [How mapping works mechanically](#how-mapping-works-mechanically) |
| |
25 | - [The frameworks in one paragraph each](#the-frameworks-in-one-paragraph-each) |
| |
26 | - [The signals and their mappings](#the-signals-and-their-mappings) |
| |
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) |
| |
31 | - [Membership changes (webhook trail)](#5-membership-changes-webhook-trail) |
| |
32 | - [Membership & team inventory (polled)](#6-membership--team-inventory-polled) |
| |
33 | - [Repository inventory](#7-repository-inventory) |
| |
34 | - [Complete mapping reference](#complete-mapping-reference) |
| |
35 | - [Adding a new framework](#adding-a-new-framework) |
| |
36 | - [Keeping this document in sync](#keeping-this-document-in-sync) |
| |
37 | - [Sources](#sources) |
| |
38 | |
| |
39 | --- |
| |
40 | |
| |
41 | ## How mapping works mechanically |
| |
42 | |
| |
43 | Understanding the evidence output requires understanding four rules in the |
| |
44 | mapping engine. All four live in [`control_mappings`](../migrations/0002_control_mappings.sql) |
| |
45 | and [`buildEvidenceRows`](../src/exporter.ts). |
| |
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. |
| |
53 | |
| |
54 | **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. |
| |
66 | |
| |
67 | **4. `posture` is the auditor-facing verdict on a row**, one of: |
| |
68 | |
| |
69 | | Posture | Meaning | Example | |
| |
70 | | --- | --- | --- | |
| |
71 | | `positive` | State supports the control | Branch protection enabled | |
| |
72 | | `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 | |
| |
74 | |
| |
75 | Two more rules affect *which* snapshots become evidence at all: |
| |
76 | |
| |
77 | - **Unmapped states produce no evidence, in either direction.** A |
| |
78 | `branch_protection` status of `unavailable` (GitHub returned 403 — the feature |
| |
79 | 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 | - **"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). |
| |
86 | |
| |
87 | --- |
| |
88 | |
| |
89 | ## The frameworks in one paragraph each |
| |
90 | |
| |
91 | **SOC 2** is an attestation report defined by the AICPA's *Trust Services |
| |
92 | Criteria* (TSC). The criteria we map to are all in the **Common Criteria (CC)** |
| |
93 | series, which every SOC 2 report shares regardless of which trust categories are |
| |
94 | in scope. A code like `CC8.1` reads as *Common Criteria, category 8 (Change |
| |
95 | Management), criterion 1*. The CC categories used here: **CC6** — logical & |
| |
96 | physical access; **CC7** — system operations (detection & monitoring); **CC8** — |
| |
97 | change management. |
| |
98 | |
| |
99 | **ISO/IEC 27001:2022** is a certifiable ISMS standard. Its **Annex A** lists 93 |
| |
100 | controls grouped into four themes: **A.5** organizational (37), **A.6** people |
| |
101 | (8), **A.7** physical (14), **A.8** technological (34). A code like `A.8.32` |
| |
102 | reads as *Annex A, theme 8 (Technological), control 32*. Note this is the |
| |
103 | **2022** numbering — the 2013 edition used different numbers, so a mapping must |
| |
104 | state which edition it targets. |
| |
105 | |
| |
106 | --- |
| |
107 | |
| |
108 | ## The signals and their mappings |
| |
109 | |
| |
110 | Each section below states: what GitHub thing we read and how, the `(resource, |
| |
111 | status)` vocabulary we normalize it to, the control(s) we map it to with a |
| |
112 | plain-language explanation, the evidentiary argument, and an honest fit |
| |
113 | assessment. |
| |
114 | |
| |
115 | ### 1. Branch protection & repository rulesets |
| |
116 | |
| |
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). |
| |
125 | |
| |
126 | **Maps to:** |
| |
127 | |
| |
128 | | Framework | Control | Title / plain meaning | |
| |
129 | | --- | --- | --- | |
| |
130 | | SOC 2 | **CC8.1** | *Change management.* Changes to software/infrastructure must go through an authorized, controlled process — designed, tested, approved, and implemented — and unauthorized changes must be prevented. | |
| |
131 | | ISO 27001 | **A.8.32** | *Change management.* Changes to information systems must follow formal change-management procedures to prevent unauthorized or destabilizing changes. | |
| |
132 | |
| |
133 | **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`. |
| |
153 | |
| |
154 | **Maps to:** |
| |
155 | |
| |
156 | | Framework | Control | When | Posture | |
| |
157 | | --- | --- | --- | --- | |
| |
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" | |
| |
164 | |
| |
165 | **Control meanings.** |
| |
166 | - **CC7.1** — *Detection & monitoring.* The entity uses detection procedures to |
| |
167 | identify configuration changes that introduce new vulnerabilities, and |
| |
168 | susceptibilities to *newly discovered* vulnerabilities. Dependabot is a |
| |
169 | textbook example: it continuously matches your dependency tree against newly |
| |
170 | 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 |
| |
174 | 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}`. |
| |
208 | |
| |
209 | **Maps to:** |
| |
210 | |
| |
211 | | Framework | Control | When | Posture | |
| |
212 | | --- | --- | --- | --- | |
| |
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" | |
| |
217 | |
| |
218 | **Control meanings.** |
| |
219 | - **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. |
| |
223 | - **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; |
| |
225 | a remediated one is evidence the gap was closed. |
| |
226 | - **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. |
| |
229 | |
| |
230 | **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}`. |
| |
252 | |
| |
253 | **Maps to:** |
| |
254 | |
| |
255 | | Framework | Control | When | Posture | |
| |
256 | | --- | --- | --- | --- | |
| |
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" | |
| |
266 | |
| |
267 | **Control meanings.** |
| |
268 | - **CC6.6** — *Protection against external threats.* The entity implements |
| |
269 | logical-access security measures to protect against threats from **outside** |
| |
270 | its system boundaries. |
| |
271 | - **CC6.1** — *Logical access controls over protected assets.* The entity |
| |
272 | implements logical-access security software, infrastructure, and architectures |
| |
273 | over protected information assets to protect them from security events. A |
| |
274 | credential *is* such a control; its exposure is a failure of that control. |
| |
275 | - **A.5.17** — *Authentication information.* Allocation and management of |
| |
276 | authentication information (passwords, keys, tokens) must be controlled. A |
| |
277 | credential committed to a repository is exposed authentication information — |
| |
278 | exactly what this control governs. |
| |
279 | |
| |
280 | **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 |
| |
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). |
| |
290 | |
| |
291 | **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 |
| |
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, |
| |
296 | **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. |
| |
302 | |
| |
303 | ### 5. Membership changes (webhook trail) |
| |
304 | |
| |
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. |
| |
309 | |
| |
310 | **Maps to:** |
| |
311 | |
| |
312 | | Framework | Control | When | Posture | |
| |
313 | | --- | --- | --- | --- | |
| |
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" | |
| |
318 | |
| |
319 | **Control meanings.** |
| |
320 | - **CC6.2** — *Registration & authorization of new users.* Before credentials are |
| |
321 | 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. |
| |
324 | - **CC6.3** — *Authorize / modify / remove access.* Access is authorized, |
| |
325 | modified, or removed based on roles, least privilege, and segregation of |
| |
326 | duties. Member *removal* and *role change* are the modify/remove events here. |
| |
327 | - **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. |
| |
330 | |
| |
331 | **Why this holds & posture logic.** These are the *audit trail* of access |
| |
332 | 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. |
| |
337 | |
| |
338 | **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. |
| |
341 | |
| |
342 | ### 6. Membership & team inventory (polled) |
| |
343 | |
| |
344 | **What we collect.** The hourly poll writes the **full current set** of org |
| |
345 | members and team members (see [`pollOrgAccess`](../src/poller.ts)), distinct from |
| |
346 | the webhook change-trail above. `resource ∈ {org_member, team_member}`, |
| |
347 | `status` = the role. Each poll shares one `captured_at` so a batch is a coherent |
| |
348 | point-in-time snapshot — this is what powers the [access-review diff](../src/access-review.ts). |
| |
349 | |
| |
350 | **Maps to:** |
| |
351 | |
| |
352 | | Framework | Control | Resource | Posture | |
| |
353 | | --- | --- | --- | --- | |
| |
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" | |
| |
357 | |
| |
358 | **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 |
| |
360 | A.5.18 all expect. Org membership maps to CC6.2 (who is registered/authorized in |
| |
361 | the org); team membership maps to CC6.3 (role-/least-privilege-based access). |
| |
362 | A.5.18 explicitly names "reviewed" among its verbs, so both feed it. All rows |
| |
363 | are **informational**: an inventory does not pass or fail, it *enables* the |
| |
364 | review. |
| |
365 | |
| |
366 | **Fit assessment: strong, with one nuance.** The org→CC6.2 / team→CC6.3 split is |
| |
367 | 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 |
| |
369 | 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 |
| |
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. |
| |
374 | |
| |
375 | ### 7. Repository inventory |
| |
376 | |
| |
377 | **What we collect.** `repository` webhook events — repos created/deleted/renamed |
| |
378 | within the installation. `resource = repository`. |
| |
379 | |
| |
380 | **Maps to:** |
| |
381 | |
| |
382 | | Framework | Control | Posture | |
| |
383 | | --- | --- | --- | |
| |
384 | | ISO 27001 | **A.5.9** | informational — "asset inventory trail" | |
| |
385 | |
| |
386 | **Control meaning.** |
| |
387 | - **A.5.9** — *Inventory of information and other associated assets.* A complete, |
| |
388 | maintained inventory of information assets and their owners must exist. |
| |
389 | |
| |
390 | **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 |
| |
392 | it changes — exactly A.5.9's requirement. **Informational**: it is inventory, not |
| |
393 | a pass/fail condition. |
| |
394 | |
| |
395 | **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 |
| |
397 | that a full, owner-annotated asset register exists. If a future need is to attest |
| |
398 | a complete inventory, the polled repo list (already fetched in |
| |
399 | [`listInstallationRepos`](../src/poller.ts)) would be the better source than the |
| |
400 | webhook trail. |
| |
401 | |
| |
402 | --- |
| |
403 | |
| |
404 | ## Complete mapping reference |
| |
405 | |
| |
406 | 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 |
| |
409 | 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.** |
| |
413 | |
| |
414 | | Resource | Status | Framework | Control | Posture | Rationale | |
| |
415 | | --- | --- | --- | --- | --- | --- | |
| |
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 | |
| |
430 | | `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 | |
| |
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 | |
| |
440 | | `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 | |
| |
443 | | `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 | |
| |
446 | | `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 | |
| |
451 | | `team` | · | ISO 27001 | A.5.18 | informational | Access-rights change, audit trail | |
| |
452 | | `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 | |
| |
455 | | `team_member` | · | SOC 2 | CC6.3 | informational | Team-based access inventory | |
| |
456 | | `team_member` | · | ISO 27001 | A.5.18 | informational | Access-rights inventory | |
| |
457 | |
| |
458 | ### Control glossary |
| |
459 | |
| |
460 | | Code | Title (plain language) | |
| |
461 | | --- | --- | |
| |
462 | | **CC6.1** | Implement logical-access controls over protected information assets | |
| |
463 | | **CC6.2** | Register & authorize new users before granting access; remove credentials when access ends | |
| |
464 | | **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 | |
| |
466 | | **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 | |
| |
469 | | **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) | |
| |
471 | | **A.5.18** | Provision, review, modify, and remove access rights per policy | |
| |
472 | | **A.8.8** | Obtain, evaluate, and act on information about technical vulnerabilities | |
| |
473 | | **A.8.28** | Apply secure coding principles throughout development | |
| |
474 | | **A.8.29** | Run security testing within the development and acceptance lifecycle | |
| |
475 | | **A.8.32** | Subject system changes to formal change-management procedures | |
| |
476 | |
| |
477 | --- |
| |
478 | |
| |
479 | ## Adding a new framework |
| |
480 | |
| |
481 | The join engine is framework-agnostic — a new framework is **data, not code**. |
| |
482 | Adding one (e.g. NIST CSF 2.0, PCI DSS 4.0, CIS Controls) is a new migration that |
| |
483 | inserts `control_mappings` rows with a new `framework` value, plus a section in |
| |
484 | this document. No changes to the poller, exporter query, or webhook handler are |
| |
485 | needed. The only code touchpoints are the `Framework` type and the |
| |
486 | `normalizeFramework` allow-list — see [What the code needs](#what-the-code-needs). |
| |
487 | |
| |
488 | ### Methodology — how to map a signal to a control accurately |
| |
489 | |
| |
490 | Do this per `(resource, status)` you want to attest, and write the reasoning into |
| |
491 | this document as you go. The goal the user cares about is **100% defensibility**, |
| |
492 | so bias toward under-claiming. |
| |
493 | |
| |
494 | 1. **Start from the signal, not the control.** Name exactly what the GitHub state |
| |
495 | proves ("a merge gate exists on the default branch"), in one sentence, without |
| |
496 | reference to any framework. |
| |
497 | 2. **Find the control whose *intent* that sentence satisfies** — read the actual |
| |
498 | control text, not a blog summary. If the signal only partially satisfies the |
| |
499 | control, say so in the fit assessment; do not round up. |
| |
500 | 3. **Prefer one strong control over several weak ones.** A single defensible |
| |
501 | mapping is worth more to an auditor than three tenuous ones. Tenuous mappings |
| |
502 | erode trust in the whole evidence pack. |
| |
503 | 4. **Assign posture from the control's expectation, not the signal's sentiment:** |
| |
504 | - `positive` — the state is what the control wants. |
| |
505 | - `negative` — the state is a concrete gap the control would flag. |
| |
506 | - `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. |
| |
513 | 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. |
| |
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. |
| |
518 | |
| |
519 | ### What the code needs |
| |
520 | |
| |
521 | Three touchpoints, all small: |
| |
522 | |
| |
523 | - **`src/exporter.ts`** — add the new value to the `Framework` type |
| |
524 | (`"soc2" | "iso27001" | ...`). |
| |
525 | - **`src/index.ts`** — add it to `normalizeFramework` so `?framework=` and the |
| |
526 | export form accept it. |
| |
527 | - **`src/dashboard.ts`** — add it to the framework selector if it should be |
| |
528 | user-selectable. |
| |
529 | |
| |
530 | ### Checklist for a new framework |
| |
531 | |
| |
532 | - [ ] New migration `migrations/000N_<framework>_mappings.sql` inserting |
| |
533 | `control_mappings` rows with the new `framework` value. |
| |
534 | - [ ] Every mapping uses a `resource`/`status` the pipeline already produces (see |
| |
535 | [`extractFact`](../src/webhook.ts) and [`poller.ts`](../src/poller.ts)). If |
| |
536 | you need a signal that isn't collected yet, that is a collection change |
| |
537 | first — a mapping to a resource that is never written produces no evidence. |
| |
538 | - [ ] `rationale` on each row is a complete, auditor-readable sentence. |
| |
539 | - [ ] A new `### <Framework>` subsection here, or per-signal rows added to the |
| |
540 | existing sections, plus reference-table and glossary entries. |
| |
541 | - [ ] The framework edition/version is stated. |
| |
542 | - [ ] `Framework` type + `normalizeFramework` updated. |
| |
543 | - [ ] `npm run typecheck` passes; `npm run db:migrate:local` applies cleanly. |
| |
544 | |
| |
545 | ### Worked micro-example |
| |
546 | |
| |
547 | To map branch protection to **NIST CSF 2.0**, whose `PR.PS-06` covers a secure |
| |
548 | software development lifecycle: |
| |
549 | |
| |
550 | ```sql |
| |
551 | INSERT INTO control_mappings (resource, status, framework, control_id, posture, rationale) VALUES |
| |
552 | ('branch_protection', 'enabled', 'nistcsf', 'PR.PS-06', 'positive', 'SDLC change control — review required before merge to the default branch'), |
| |
553 | ('branch_protection', 'disabled', 'nistcsf', 'PR.PS-06', 'negative', 'SDLC change-control gap — direct pushes to the default branch possible'); |
| |
554 | ``` |
| |
555 | |
| |
556 | Then add `"nistcsf"` to the `Framework` type and `normalizeFramework`, and add a |
| |
557 | `### NIST CSF 2.0` subsection here documenting the reasoning and fit. |
| |
558 | |
| |
559 | --- |
| |
560 | |
| |
561 | ## Keeping this document in sync |
| |
562 | |
| |
563 | The mappings live in two places that must agree: the SQL seed rows in |
| |
564 | `migrations/` (the source of truth the engine reads) and the |
| |
565 | [reference table](#complete-mapping-reference) and per-signal sections here. |
| |
566 | **When you change a mapping, change both in the same PR** — the same discipline |
| |
567 | the README applies to retention periods. A mapping row with no rationale here, or |
| |
568 | a row here with no SQL, is a bug. |
| |
569 | |
| |
570 | This is enforced. [`scripts/check-mappings.mjs`](../scripts/check-mappings.mjs) |
| |
571 | applies every migration to an in-memory SQLite database, reads back |
| |
572 | `control_mappings`, and diffs the `(resource, status, framework, control_id, |
| |
573 | posture)` tuples against the rows parsed out of the |
| |
574 | [reference table](#complete-mapping-reference) above. It fails with a row-level |
| |
575 | diff if the two drift. Run it with: |
| |
576 | |
| |
577 | ```sh |
| |
578 | npm run test:mappings |
| |
579 | ``` |
| |
580 | |
| |
581 | It has no dependencies (Node's built-in `node:sqlite`) and is a good CI gate. It |
| |
582 | checks the **reference table** specifically — the per-signal tables and glossary |
| |
583 | are prose and are not parsed, so keep those consistent by hand. |
| |
584 | |
| |
585 | --- |
| |
586 | |
| |
587 | ## Sources |
| |
588 | |
| |
589 | Control *numbers and titles* are cited from the published standards; exact |
| |
590 | criterion text is paraphrased (the standards themselves are copyrighted). |
| |
591 | |
| |
592 | - ISO/IEC 27001:2022, Annex A — control titles confirmed via |
| |
593 | [ISMS.online, "ISO 27001:2022 Annex A Explained"](https://www.isms.online/iso-27001/annex-a-2022/). |
| |
594 | - AICPA *Trust Services Criteria* (TSP Section 100, 2017 criteria with 2022 |
| |
595 | revised points of focus) — CC category scope confirmed via |
| |
596 | [Linford & Co., "Trust Services Criteria"](https://linfordco.com/blog/trust-services-critieria-principles-soc-2/) |
| |
597 | and [Secureframe, "SOC 2 Common Criteria"](https://secureframe.com/hub/soc-2/common-criteria). |
| |
598 | - The authoritative text for both is the source standard: purchase ISO/IEC |
| |
599 | 27001:2022 from ISO, and the AICPA Trust Services Criteria from the AICPA. |
| |
600 | Verify any mapping against those before an audit. |