audit-labs/gh-attest

GitHub Audit Evidence Extractor

clone: git clone https://gitbay.org/audit-labs/gh-attest.git

main: docs/framework-mapping.md · raw

  1# Framework Mapping
  2
  3This document is the rationale behind every control mapping in gh-attest: what
  4each GitHub signal is, which compliance control it is offered as evidence for,
  5and **why that mapping is defensible to an auditor**. It also explains what each
  6control code (`A.8.32`, `CC8.1`, …) actually means in plain language, and how to
  7add a new framework.
  8
  9It 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](#2-dependabot)
 29  - [Code scanning](#3-code-scanning)
 30  - [Secret scanning](#4-secret-scanning)
 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
 43Understanding the evidence output requires understanding four rules in the
 44mapping engine. All four live in [`control_mappings`](../migrations/0002_control_mappings.sql)
 45and [`buildEvidenceRows`](../src/exporter.ts).
 46
 47**1. A snapshot is a `(resource, status)` pair about a `subject`; a mapping is a
 48row that attaches a control to a `(resource, status)`.** The poller and webhook
 49handler both normalize GitHub events into a small vocabulary — `resource`
 50(e.g. `branch_protection`, `dependabot_alert`) and `status` (e.g. `enabled`,
 51`open`, `fixed`) — plus a `subject` identifying which entity within the repo or
 52org 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
 54happens as a **join at export time**, never at ingest, so a mapping can be
 55corrected without re-ingesting history.
 56
 57**2. `status = NULL` in a mapping matches *any* status for that resource.** The
 58join condition is `cm.status IS NULL OR cm.status = l.status`. This is used for
 59trail/inventory resources (`team`, `repository`, `org_member`, `team_member`)
 60where every state is the same kind of informational fact.
 61
 62**3. One snapshot can emit multiple evidence rows.** A single secret-scanning
 63alert with `status = 'open'` matches the `open` mapping under **each** of
 64CC6.6, CC6.1 (SOC 2) and A.5.17 (ISO). This is intentional: the same fact is
 65legitimate evidence for more than one control expectation, and attesting all of
 66them lets the export serve whichever control the organization's narrative uses.
 67This behavior is called out per-signal below wherever it applies.
 68
 69**4. `posture` is the auditor-facing verdict on a row**, one of:
 70
 71| Posture | Meaning | Example |
 72| --- | --- | --- |
 73| `positive` | State supports the control | Branch protection enabled |
 74| `negative` | State is a gap against the control | Branch protection disabled; open secret |
 75| `informational` | Neither pass nor fail — an audit-trail / inventory fact | A member was added; a finding was dismissed by a user |
 76
 77Two more rules affect *which* snapshots become evidence at all:
 78
 79- **Unmapped states produce no evidence, in either direction.** A
 80  `branch_protection` status of `unavailable` (GitHub returned 403 — the feature
 81  isn't on the repo's plan; see [`fetchBranchProtection`](../src/poller.ts)) has
 82  no mapping row, so it never counts as a pass *or* a fail. The same applies to
 83  the `disabled`/`unavailable` states of the detection-tooling resources
 84  (`dependabot`, `code_scanning`, `secret_scanning`), to the trail-only
 85  resources `branch_protection_rule_event` and `repository_ruleset_event`
 86  (rule-scoped webhook events that can't be attributed to the default branch —
 87  the poll is authoritative for state), and to raw `push` events.
 88- **"Latest row wins" per `(repo, subject, resource)`** gives point-in-time
 89  current posture from an append-only table. Because `subject` carries the
 90  alert number / login / slug, this operates **per entity**: one alert being
 91  fixed does not mask another alert that is still open in the same repo. Access
 92  facts are further special-cased so a member who lost access stops being
 93  attested — only the most recent poll batch counts. See the CTE in
 94  [`buildEvidenceRows`](../src/exporter.ts).
 95
 96---
 97
 98## The frameworks in one paragraph each
 99
100**SOC 2** is an attestation report defined by the AICPA's *Trust Services
101Criteria* (TSC). The criteria we map to are all in the **Common Criteria (CC)**
102series, which every SOC 2 report shares regardless of which trust categories are
103in scope. A code like `CC8.1` reads as *Common Criteria, category 8 (Change
104Management), criterion 1*. The CC categories used here: **CC6** — logical &
105physical access; **CC7** — system operations (detection & monitoring); **CC8**106change management.
107
108**ISO/IEC 27001:2022** is a certifiable ISMS standard. Its **Annex A** lists 93
109controls grouped into four themes: **A.5** organizational (37), **A.6** people
110(8), **A.7** physical (14), **A.8** technological (34). A code like `A.8.32`
111reads as *Annex A, theme 8 (Technological), control 32*. Note this is the
112**2022** numbering — the 2013 edition used different numbers, so a mapping must
113state which edition it targets.
114
115---
116
117## The signals and their mappings
118
119Each section below states: what GitHub thing we read and how, the `(resource,
120status)` vocabulary we normalize it to, the control(s) we map it to with a
121plain-language explanation, the evidentiary argument, and an honest fit
122assessment.
123
124### 1. Branch protection & repository rulesets
125
126**What we collect.** For each repo's **default branch**, whether merge controls
127are in force. The hourly poll is authoritative: the branch-protection API for
128classic protection, and the `rules/branches/{default-branch}` API for rulesets —
129which aggregates the rules from every *active* ruleset (repo- and org-level)
130that actually applies to that branch, so evaluate-mode (monitor-only) rulesets
131and rulesets targeting other branches correctly count as **not** enabled.
132Normalized to `resource ∈ {branch_protection, repository_ruleset}`, `status ∈
133{enabled, disabled}`.
134
135Webhooks supplement the poll only where they are unambiguous: a
136`branch_protection_rule` event whose rule pattern is exactly the default branch
137updates state immediately. Any other rule event — and *every*
138`repository_ruleset` event, since one ruleset's deletion says nothing about
139whether other rulesets still cover the branch — is recorded as an unmapped
140trail event (`branch_protection_rule_event`, `repository_ruleset_event`) and
141the next poll settles the state. See [`extractFact`](../src/webhook.ts).
142
143**Maps to:**
144
145| Framework | Control | Title / plain meaning |
146| --- | --- | --- |
147| 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. |
148| ISO 27001 | **A.8.32** | *Change management.* Changes to information systems must follow formal change-management procedures to prevent unauthorized or destabilizing changes. |
149
150**Why this holds.** Branch protection / rulesets are the technical enforcement of
151change control in a Git workflow. Enabled → **positive**; disabled →
152**negative** ("direct pushes possible" is a concrete change-control gap).
153
154**One row per repo, not two.** The two are alternative implementations of the
155same control, so the evidence query collapses them to a single row per
156(framework, control, repo), keeping whichever mechanism is actually in force —
157a repo covered by an active ruleset is not a change-control gap merely because
158classic protection is off. Both snapshots are still recorded; the collapse
159happens at query time in [`collapseChangeControl`](../src/exporter.ts).
160
161**Fit assessment: strong, with the scope stated in the evidence itself.** Both
162frameworks name "change management" explicitly, and branch protection is the
163canonical GitHub-native implementation of it. What the evidence attests is that
164a change-control gate **exists on the default branch** — it does not verify that
165the *specific* rules (required reviewers, status checks, …) match the
166organization's policy, and the exported rationale says so explicitly ("rule
167contents not verified") rather than claiming review is required.
168
169### 2. Dependabot
170
171**What we collect.** Two distinct facts:
172
173- **Tooling state** (`resource = dependabot`, `status ∈ {enabled, disabled,
174  unavailable}`) — the hourly poll checks whether Dependabot alerts are enabled
175  on each repo (the alert-list API answering at all is the signal; see
176  [`pollRepoAlerts`](../src/poller.ts)). Only `enabled` is mapped; a disabled or
177  unavailable scanner is recorded but deliberately produces no evidence either
178  way, matching the branch-protection `unavailable` precedent.
179- **Findings** (`resource = dependabot_alert`, `status ∈ {open, fixed,
180  dismissed, auto_dismissed}`, `subject` = alert number) — from
181  `dependabot_alert` webhooks, plus the same hourly poll re-recording every
182  *open* alert. The poll matters twice: alerts already open before the App was
183  installed never sent a webhook, and an open alert with no events for the
184  whole retention window would otherwise age out of evidence.
185
186**Maps to:**
187
188| Framework | Control | When | Posture |
189| --- | --- | --- | --- |
190| SOC 2 | **CC7.1** | tooling `enabled` | positive — detection tooling is on |
191| SOC 2 | **CC7.1** | `open` | negative — unremediated known vulnerability |
192| SOC 2 | **CC7.1** | `fixed` / `auto_dismissed` | positive — remediated (machine-verified) |
193| SOC 2 | **CC7.1** | `dismissed` | informational — human risk-acceptance, justification subject to review |
194| ISO 27001 | **A.8.8** | tooling `enabled` | positive — vulnerability management active |
195| ISO 27001 | **A.8.8** | `open` | negative — unremediated known vulnerability |
196| ISO 27001 | **A.8.8** | `fixed` / `auto_dismissed` | positive — remediated (machine-verified) |
197| ISO 27001 | **A.8.8** | `dismissed` | informational — human risk-acceptance, justification subject to review |
198
199**Control meanings.**
200- **CC7.1** — *Detection & monitoring.* The entity uses detection procedures to
201  identify configuration changes that introduce new vulnerabilities, and
202  susceptibilities to *newly discovered* vulnerabilities. Dependabot is a
203  textbook example: it continuously matches your dependency tree against newly
204  published CVEs.
205- **A.8.8** — *Management of technical vulnerabilities.* Information about
206  technical vulnerabilities must be obtained, exposure evaluated, and
207  appropriate measures taken — one control spanning the whole lifecycle.
208
209**Why this holds.** The tooling-`enabled` fact proves the detection capability
210required by CC7.1/A.8.8 exists and is on *right now* — it comes from the
211feature's own state, not (as in earlier versions) from inference off the latest
212alert event, which kept attesting after a scanner was switched off and never
213fired for a clean repo. Each individual alert's lifecycle is then finding-level
214evidence under the same controls: an open alert is an unresolved known
215vulnerability; a `fixed` (patched) or `auto_dismissed` (e.g. dependency
216removed) alert is a machine-verified closure; a `dismissed` alert is a **human
217decision** — it may be sound risk acceptance or may be rubber-stamping, and the
218justification is exactly what an auditor samples, so it is recorded as
219informational rather than claimed as remediation.
220
221**Fit assessment: strong on both frameworks.** The whole SOC 2 lifecycle sits
222under CC7.1, whose "susceptibility to newly discovered vulnerabilities"
223language covers known-CVE management directly. (Earlier versions split
224open/remediated state to CC7.2; CC7.2's anomaly-monitoring text is about
225runtime security events, which made it the weakest link in the system — that
226split has been removed.) A.8.8 is purpose-built for this signal and absorbs the
227whole lifecycle.
228
229### 3. Code scanning
230
231**What we collect.** Same two-fact shape as Dependabot: tooling state
232(`resource = code_scanning`, from the hourly poll; only `enabled` mapped) and
233findings (`resource = code_scanning_alert`, `status ∈ {open, fixed,
234dismissed}`, `subject` = alert number) from `code_scanning_alert` webhooks plus
235the hourly re-record of open alerts. Findings are SAST results from CodeQL or a
236third-party analyzer.
237
238**Maps to:**
239
240| Framework | Control | When | Posture |
241| --- | --- | --- | --- |
242| ISO 27001 | **A.8.29** | tooling `enabled` | positive — security testing in development is on |
243| ISO 27001 | **A.8.28** | `open` | negative — unremediated finding |
244| ISO 27001 | **A.8.28** | `fixed` | positive — remediated |
245| ISO 27001 | **A.8.28** | `dismissed` | informational — human risk-acceptance, justification subject to review |
246| SOC 2 | **CC7.1** | tooling `enabled` | positive — detection tooling is on |
247| SOC 2 | **CC7.1** | `open` | negative — unremediated finding |
248| SOC 2 | **CC7.1** | `fixed` | positive — remediated |
249| SOC 2 | **CC7.1** | `dismissed` | informational — human risk-acceptance, justification subject to review |
250
251**Control meanings.**
252- **A.8.29** — *Security testing in development and acceptance.* Security testing
253  processes must be defined and run within the development lifecycle. The
254  existence of code scanning *is* that testing process.
255- **A.8.28** — *Secure coding.* Secure coding principles must be applied during
256  development. An open finding is evidence of a secure-coding gap in the source;
257  a remediated one is evidence the gap was closed.
258- **CC7.1** — *Detection & monitoring.* (Same control as Dependabot's SOC 2
259  mapping.)
260
261**Why this holds.** On the **ISO** side this splits cleanly across two controls
262that map to two facts: *"a testing process exists"* (A.8.29, from tooling
263state) versus *"the code itself is / isn't secure"* (A.8.28, from each
264finding's state). On the **SOC 2** side the full lifecycle mirrors Dependabot
265under CC7.1 — the finding-level rows were previously deferred pending the
266CC7.1-vs-CC7.2 decision, which is now settled in CC7.1's favor.
267
268**Fit assessment: strong on both.** The A.8.29-vs-A.8.28 split follows the
269controls' own having-a-process vs. code-quality distinction, and the SOC 2 side
270is now symmetric with Dependabot rather than intentionally partial.
271
272### 4. Secret scanning
273
274**What we collect.** Tooling state (`resource = secret_scanning`, from the
275hourly poll; only `enabled` mapped) and findings (`resource =
276secret_scanning_alert`, `status ∈ {open, resolved}`, `subject` = alert number)
277from `secret_scanning_alert` webhooks plus the hourly re-record of open alerts.
278One payload quirk matters: unlike the other two alert payloads, the
279secret-scanning webhook alert carries **no `state` field** — ingest derives
280open/resolved from `alert.resolution`, which is set iff the alert is resolved
281(see [`extractFact`](../src/webhook.ts)).
282
283**Maps to:**
284
285| Framework | Control | When | Posture |
286| --- | --- | --- | --- |
287| SOC 2 | **CC6.6** | tooling `enabled` | positive — leaked-credential detection is on |
288| SOC 2 | **CC6.6** | `open` | negative — live credential exposure |
289| SOC 2 | **CC6.6** | `resolved` | informational — resolution reason subject to review |
290| SOC 2 | **CC6.1** | tooling `enabled` | positive — logical-access credential protection is on |
291| SOC 2 | **CC6.1** | `open` | negative — exposed credential undermines logical access controls |
292| SOC 2 | **CC6.1** | `resolved` | informational — resolution reason subject to review |
293| ISO 27001 | **A.5.17** | tooling `enabled` | positive — authentication-information protection is on |
294| ISO 27001 | **A.5.17** | `open` | negative — exposed authentication information |
295| ISO 27001 | **A.5.17** | `resolved` | informational — resolution reason subject to review |
296
297**Control meanings.**
298- **CC6.6** — *Protection against external threats.* The entity implements
299  logical-access security measures to protect against threats from **outside**
300  its system boundaries.
301- **CC6.1** — *Logical access controls over protected assets.* The entity
302  implements logical-access security software, infrastructure, and architectures
303  over protected information assets to protect them from security events. A
304  credential *is* such a control; its exposure is a failure of that control.
305- **A.5.17** — *Authentication information.* Allocation and management of
306  authentication information (passwords, keys, tokens) must be controlled. A
307  credential committed to a repository is exposed authentication information —
308  exactly what this control governs.
309
310**Why this holds.** A committed credential is relevant to all three controls at
311once. For **CC6.6**, it is a direct path for an *external* attacker to cross the
312system boundary. For **CC6.1**, the credential is itself one of the
313logical-access keys the control is meant to safeguard. For **A.5.17**, the
314credential is authentication information whose confidentiality the control
315requires. Scanning enabled → **positive**; open alert → **negative**. A
316`resolved` alert is **informational**, not positive: GitHub's resolution
317reasons include `wont_fix`, so "resolved" may mean the credential was revoked
318*or* that someone decided to leave it — the recorded reason is what the
319reviewer must check.
320
321**Fit assessment: all three defensible.** CC6.6 is the external-threat framing,
322CC6.1 the logical-access framing, A.5.17 the ISO authentication-information
323framing. Mapping to all three means the export satisfies whichever control the
324organization's narrative uses — at the cost of row multiplicity: one open
325secret emits a negative under each of the three. A further SOC 2 framing,
326**CC6.7** (restricting the transmission/movement of information), also touches
327this and could be added if an auditor prefers it.
328
329### 5. Membership changes (webhook trail)
330
331**What we collect.** The *change* events for access administration, each with a
332`subject` so the trail keeps one latest row per person/team rather than one per
333repo:
334
335- `member` webhook → `resource = member_access`, subject = the collaborator's
336  login. **Scope note: this event covers repository collaborators**, not org
337  members.
338- `organization` webhook → `resource = org_membership`, subject = the member's
339  login (or the invitee's login/email). This is where org-level joins, removals
340  and invitations arrive.
341- `team` webhook → `resource = team`, subject = the team slug.
342
343**Maps to:**
344
345| Framework | Control | When | Posture |
346| --- | --- | --- | --- |
347| SOC 2 | **CC6.2** | `member_access` `added`, `org_membership` `member_added` / `member_invited` | informational — access grant / invitation, logged for review |
348| SOC 2 | **CC6.3** | `member_access` `removed` / `edited`, `org_membership` `member_removed` | informational — modification / deprovisioning recorded |
349| ISO 27001 | **A.5.18** | all of the above | informational — access-rights change, audit trail |
350| ISO 27001 | **A.5.18** | `team` (any) | informational — access-rights change, audit trail |
351
352**Control meanings.**
353- **CC6.2** — *Registration & authorization of new users.* Before credentials are
354  issued, new users are registered and authorized; credentials are removed when
355  access is no longer authorized. A member being *added* or *invited* is the
356  provisioning event this criterion governs.
357- **CC6.3** — *Authorize / modify / remove access.* Access is authorized,
358  modified, or removed based on roles, least privilege, and segregation of
359  duties. Removal and role change are the modify/remove events here.
360- **A.5.18** — *Access rights.* Access rights are provisioned, reviewed,
361  modified, and removed per the access-control policy.
362
363**Why this holds & posture logic.** These are the *audit trail* of access
364administration — evidence that grants/changes are captured, which is what an
365auditor samples. **All rows are informational**, removals included: the event
366proves a removal happened and when, but not that it was *timely* relative to an
367offboarding trigger the system cannot see — so the rationale says
368"deprovisioning recorded; timeliness subject to review" rather than claiming
369timeliness as a positive. (Earlier versions claimed "timely access removal";
370that was rounding up.)
371
372**Fit assessment: strong on the CC6.2/CC6.3 split** (it follows the criteria's
373own provisioning-vs-modification language), and honest about scope now that
374repo-collaborator and org-member events are separate resources with separate
375rationales.
376
377### 6. Membership & team inventory (polled)
378
379**What we collect.** The hourly poll writes the **full current set** of org
380members and team members (see [`pollOrgAccess`](../src/poller.ts)), distinct from
381the webhook change-trail above. `resource ∈ {org_member, team_member}`,
382`status` = the role. Each poll shares one `captured_at` so a batch is a coherent
383point-in-time snapshot — this is what powers the [access-review diff](../src/access-review.ts).
384
385**Maps to:**
386
387| Framework | Control | Resource | Posture |
388| --- | --- | --- | --- |
389| SOC 2 | **CC6.2** | `org_member` | informational — org access inventory, subject to periodic review |
390| SOC 2 | **CC6.3** | `team_member` | informational — team-based access inventory |
391| ISO 27001 | **A.5.18** | `org_member`, `team_member` | informational — access-rights inventory |
392
393**Why this holds.** A point-in-time roster of who has access is the raw material
394of a periodic access review — the recurring auditor ask that CC6.2/CC6.3 and
395A.5.18 all expect. Org membership maps to CC6.2 (who is registered/authorized in
396the org); team membership maps to CC6.3 (role-/least-privilege-based access).
397A.5.18 explicitly names "reviewed" among its verbs, so both feed it. All rows
398are **informational**: an inventory does not pass or fail, it *enables* the
399review.
400
401**Fit assessment: strong, with one nuance.** The org→CC6.2 / team→CC6.3 split is
402reasonable but not the only defensible cut — the *review* of org membership is
403arguably as much CC6.3 (appropriateness of access) as CC6.2 (registration). Since
404these are informational inventory rows feeding a review, the exact CC6.2/CC6.3
405attribution is low-stakes; A.5.18 is unambiguous. Note the two resource families
406(`member_access`/`org_membership`/`team` webhook trail vs.
407`org_member`/`team_member` polled inventory) are deliberately separate resource
408names so the change-trail and the current-state inventory don't collide.
409
410### 7. Repository inventory
411
412**What we collect.** `repository` webhook events — repos created/deleted/renamed
413and visibility changes within the installation. `resource = repository`.
414
415**Maps to:**
416
417| Framework | Control | When | Posture |
418| --- | --- | --- | --- |
419| ISO 27001 | **A.5.9** | any action | informational — asset inventory trail |
420| SOC 2 | **CC6.1** | `publicized` | informational — repo made public, flagged for review |
421
422**Control meaning.**
423- **A.5.9** — *Inventory of information and other associated assets.* A complete,
424  maintained inventory of information assets and their owners must exist.
425
426**Why this holds.** Repositories are information assets. The trail of repo
427create/delete/rename events is evidence that the asset inventory is maintained as
428it changes — exactly A.5.9's requirement. One action gets an extra row: a repo
429being **publicized** is a visibility change with direct confidentiality impact,
430so it is additionally surfaced under CC6.1 rather than left as a generic
431inventory tick. Both rows are **informational** — whether going public was
432intended is a judgment the reviewer makes.
433
434**Fit assessment: strong for what it claims.** The honest caveat is completeness:
435this is a *change trail*, so it evidences that inventory changes are captured, not
436that a full, owner-annotated asset register exists. If a future need is to attest
437a complete inventory, the polled repo list (already fetched in
438[`listInstallationRepos`](../src/poller.ts)) would be the better source than the
439webhook trail.
440
441---
442
443## Complete mapping reference
444
445This table is the authoritative human-readable copy of every row in
446[`control_mappings`](../migrations/0002_control_mappings.sql) after all
447migrations (0002 seeds most; 0003 replaces branch-protection/ruleset with the
448enabled/disabled vocabulary; 0005 adds the polled access inventory; 0006 adds
449the secret-scanning CC6.1 rows; 0007 closes cross-framework coverage gaps; 0008
450applies the mapping-review fixes — per-entity subjects, polled tooling state,
451the CC7.1 consolidation, and informational postures for human dismissals).
452**A "·" in Status means the mapping's `status` is `NULL` — it matches any
453status.** The Rationale column is the exact auditor-facing string in the
454database, and is CI-checked against it.
455
456| Resource | Status | Framework | Control | Posture | Rationale |
457| --- | --- | --- | --- | --- | --- |
458| `branch_protection` | `enabled` | SOC 2 | CC8.1 | positive | Change management — a protection rule is enforced on the default branch (rule contents not verified) |
459| `branch_protection` | `disabled` | SOC 2 | CC8.1 | negative | Change-control gap — no protection on the default branch; direct pushes possible |
460| `branch_protection` | `enabled` | ISO 27001 | A.8.32 | positive | Change management — a protection rule is enforced on the default branch (rule contents not verified) |
461| `branch_protection` | `disabled` | ISO 27001 | A.8.32 | negative | Change-control gap — no protection on the default branch; direct pushes possible |
462| `repository_ruleset` | `enabled` | SOC 2 | CC8.1 | positive | Change management — an active ruleset covers the default branch (rule contents not verified) |
463| `repository_ruleset` | `disabled` | SOC 2 | CC8.1 | negative | Change-control gap — no active ruleset covers the default branch |
464| `repository_ruleset` | `enabled` | ISO 27001 | A.8.32 | positive | Change management — an active ruleset covers the default branch (rule contents not verified) |
465| `repository_ruleset` | `disabled` | ISO 27001 | A.8.32 | negative | Change-control gap — no active ruleset covers the default branch |
466| `dependabot` | `enabled` | SOC 2 | CC7.1 | positive | Detection tooling — Dependabot alerts are enabled on the repository |
467| `dependabot` | `enabled` | ISO 27001 | A.8.8 | positive | Technical vulnerability management — Dependabot alerts are enabled on the repository |
468| `code_scanning` | `enabled` | SOC 2 | CC7.1 | positive | Detection tooling — code scanning is enabled on the repository |
469| `code_scanning` | `enabled` | ISO 27001 | A.8.29 | positive | Security testing in development — code scanning is enabled on the repository |
470| `secret_scanning` | `enabled` | SOC 2 | CC6.6 | positive | Leaked-credential detection — secret scanning is enabled on the repository |
471| `secret_scanning` | `enabled` | SOC 2 | CC6.1 | positive | Logical-access credential protection — secret scanning is enabled on the repository |
472| `secret_scanning` | `enabled` | ISO 27001 | A.5.17 | positive | Authentication-information protection — secret scanning is enabled on the repository |
473| `dependabot_alert` | `open` | SOC 2 | CC7.1 | negative | Unremediated known vulnerability |
474| `dependabot_alert` | `fixed` | SOC 2 | CC7.1 | positive | Vulnerability remediated |
475| `dependabot_alert` | `dismissed` | SOC 2 | CC7.1 | informational | Dismissed by a user — risk-acceptance justification subject to review |
476| `dependabot_alert` | `auto_dismissed` | SOC 2 | CC7.1 | positive | Auto-dismissed by GitHub (e.g. dependency removed) |
477| `dependabot_alert` | `open` | ISO 27001 | A.8.8 | negative | Unremediated known technical vulnerability |
478| `dependabot_alert` | `fixed` | ISO 27001 | A.8.8 | positive | Vulnerability remediated |
479| `dependabot_alert` | `dismissed` | ISO 27001 | A.8.8 | informational | Dismissed by a user — risk-acceptance justification subject to review |
480| `dependabot_alert` | `auto_dismissed` | ISO 27001 | A.8.8 | positive | Auto-dismissed by GitHub (e.g. dependency removed) |
481| `code_scanning_alert` | `open` | SOC 2 | CC7.1 | negative | Unremediated static-analysis finding |
482| `code_scanning_alert` | `fixed` | SOC 2 | CC7.1 | positive | Finding remediated |
483| `code_scanning_alert` | `dismissed` | SOC 2 | CC7.1 | informational | Dismissed by a user — risk-acceptance justification subject to review |
484| `code_scanning_alert` | `open` | ISO 27001 | A.8.28 | negative | Unremediated static-analysis finding |
485| `code_scanning_alert` | `fixed` | ISO 27001 | A.8.28 | positive | Finding remediated |
486| `code_scanning_alert` | `dismissed` | ISO 27001 | A.8.28 | informational | Dismissed by a user — risk-acceptance justification subject to review |
487| `secret_scanning_alert` | `open` | SOC 2 | CC6.6 | negative | Live credential exposure |
488| `secret_scanning_alert` | `resolved` | SOC 2 | CC6.6 | informational | Resolution recorded — reason (revoked vs. won't-fix) subject to review |
489| `secret_scanning_alert` | `open` | SOC 2 | CC6.1 | negative | Exposed credential undermines logical access controls |
490| `secret_scanning_alert` | `resolved` | SOC 2 | CC6.1 | informational | Resolution recorded — reason (revoked vs. won't-fix) subject to review |
491| `secret_scanning_alert` | `open` | ISO 27001 | A.5.17 | negative | Exposed authentication information |
492| `secret_scanning_alert` | `resolved` | ISO 27001 | A.5.17 | informational | Resolution recorded — reason (revoked vs. won't-fix) subject to review |
493| `member_access` | `added` | SOC 2 | CC6.2 | informational | Repository collaborator added — access grant logged for review |
494| `member_access` | `removed` | SOC 2 | CC6.3 | informational | Repository collaborator removed — deprovisioning recorded; timeliness subject to review |
495| `member_access` | `edited` | SOC 2 | CC6.3 | informational | Repository collaborator permission changed — logged for review |
496| `member_access` | `added` | ISO 27001 | A.5.18 | informational | Repository collaborator added — access-rights change, audit trail |
497| `member_access` | `removed` | ISO 27001 | A.5.18 | informational | Repository collaborator removed — access-rights change, audit trail |
498| `member_access` | `edited` | ISO 27001 | A.5.18 | informational | Repository collaborator permission changed — access-rights change, audit trail |
499| `org_membership` | `member_added` | SOC 2 | CC6.2 | informational | Organization member added — access grant logged for review |
500| `org_membership` | `member_removed` | SOC 2 | CC6.3 | informational | Organization member removed — deprovisioning recorded; timeliness subject to review |
501| `org_membership` | `member_invited` | SOC 2 | CC6.2 | informational | Organization invitation issued — logged for review |
502| `org_membership` | `member_added` | ISO 27001 | A.5.18 | informational | Organization member added — access-rights change, audit trail |
503| `org_membership` | `member_removed` | ISO 27001 | A.5.18 | informational | Organization member removed — access-rights change, audit trail |
504| `org_membership` | `member_invited` | ISO 27001 | A.5.18 | informational | Organization invitation issued — access-rights change, audit trail |
505| `team` | · | ISO 27001 | A.5.18 | informational | Access-rights change, audit trail |
506| `repository` | · | ISO 27001 | A.5.9 | informational | Asset inventory trail |
507| `repository` | `publicized` | SOC 2 | CC6.1 | informational | Repository made public — visibility change affecting asset confidentiality, flagged for review |
508| `org_member` | · | SOC 2 | CC6.2 | informational | Organization access inventory — subject to periodic access review |
509| `org_member` | · | ISO 27001 | A.5.18 | informational | Access rights inventory |
510| `team_member` | · | SOC 2 | CC6.3 | informational | Team-based access inventory |
511| `team_member` | · | ISO 27001 | A.5.18 | informational | Access rights inventory |
512
513### Control glossary
514
515| Code | Title (plain language) |
516| --- | --- |
517| **CC6.1** | Implement logical-access controls over protected information assets |
518| **CC6.2** | Register & authorize new users before granting access; remove credentials when access ends |
519| **CC6.3** | Authorize, modify, and remove access by role, with least privilege and segregation of duties |
520| **CC6.6** | Protect against threats originating outside the system boundary |
521| **CC7.1** | Detect configuration changes that introduce vulnerabilities, and susceptibility to newly discovered ones |
522| **CC8.1** | Put changes through an authorized, controlled process; block unauthorized changes |
523| **A.5.9** | Maintain an inventory of information and associated assets, with owners |
524| **A.5.17** | Control the allocation and management of authentication information (passwords, keys, tokens) |
525| **A.5.18** | Provision, review, modify, and remove access rights per policy |
526| **A.8.8** | Obtain, evaluate, and act on information about technical vulnerabilities |
527| **A.8.28** | Apply secure coding principles throughout development |
528| **A.8.29** | Run security testing within the development and acceptance lifecycle |
529| **A.8.32** | Subject system changes to formal change-management procedures |
530
531---
532
533## Adding a new framework
534
535The join engine is framework-agnostic — a new framework is **data, not code**.
536Adding one (e.g. NIST CSF 2.0, PCI DSS 4.0, CIS Controls) is a new migration that
537inserts `control_mappings` rows with a new `framework` value, plus a section in
538this document. No changes to the poller, exporter query, or webhook handler are
539needed. The only code touchpoints are the `Framework` type and the
540`normalizeFramework` allow-list — see [What the code needs](#what-the-code-needs).
541
542### Methodology — how to map a signal to a control accurately
543
544Do this per `(resource, status)` you want to attest, and write the reasoning into
545this document as you go. The goal the user cares about is **100% defensibility**,
546so bias toward under-claiming.
547
5481. **Start from the signal, not the control.** Name exactly what the GitHub state
549   proves ("a merge gate exists on the default branch"), in one sentence, without
550   reference to any framework.
5512. **Find the control whose *intent* that sentence satisfies** — read the actual
552   control text, not a blog summary. If the signal only partially satisfies the
553   control, say so in the fit assessment; do not round up.
5543. **Prefer one strong control over several weak ones.** A single defensible
555   mapping is worth more to an auditor than three tenuous ones. Tenuous mappings
556   erode trust in the whole evidence pack.
5574. **Assign posture from the control's expectation, not the signal's sentiment:**
558   - `positive` — the state is what the control wants, and the platform verified
559     it (not merely a human clicking "dismiss").
560   - `negative` — the state is a concrete gap the control would flag.
561   - `informational` — the state is audit-trail/inventory that feeds a review but
562     is not itself pass/fail. Human decisions (dismissals, resolutions with a
563     reason) belong here. When in doubt, use `informational`.
5645. **Separate tooling state from findings.** If the signal is a scanner/alert
565   stream, attest "the control's *tooling* is on" from the feature's own state
566   (polled), not from the existence of alerts — and attest each finding's
567   lifecycle per alert, with the alert number as `subject` so findings don't
568   mask each other.
5696. **Pin the edition.** State which version of the framework you mapped (e.g.
570   "PCI DSS v4.0.1", "NIST CSF 2.0") — control numbers move between editions.
5717. **Write the rationale** in the `rationale` column *and* copy it into the
572   reference table here verbatim — it is CI-checked. The `rationale` is what an
573   auditor reads in the export; make it a complete thought that claims no more
574   than the signal proves.
575
576### What the code needs
577
578Three touchpoints, all small:
579
580- **`src/exporter.ts`** — add the new value to the `Framework` type
581  (`"soc2" | "iso27001" | ...`).
582- **`src/index.ts`** — add it to `normalizeFramework` so `?framework=` and the
583  export form accept it.
584- **`src/dashboard.ts`** — add it to the framework selector if it should be
585  user-selectable.
586
587### Checklist for a new framework
588
589- [ ] New migration `migrations/000N_<framework>_mappings.sql` inserting
590      `control_mappings` rows with the new `framework` value.
591- [ ] Every mapping uses a `resource`/`status` the pipeline already produces (see
592      [`extractFact`](../src/webhook.ts) and [`poller.ts`](../src/poller.ts)). If
593      you need a signal that isn't collected yet, that is a collection change
594      first — a mapping to a resource that is never written produces no evidence.
595- [ ] `rationale` on each row is a complete, auditor-readable sentence.
596- [ ] A new `### <Framework>` subsection here, or per-signal rows added to the
597      existing sections, plus reference-table and glossary entries.
598- [ ] The framework edition/version is stated.
599- [ ] `Framework` type + `normalizeFramework` updated.
600- [ ] `npm run typecheck` passes; `npm run db:migrate:local` applies cleanly.
601
602### Worked micro-example
603
604To map branch protection to **NIST CSF 2.0**, whose `PR.PS-06` covers a secure
605software development lifecycle:
606
607```sql
608INSERT INTO control_mappings (resource, status, framework, control_id, posture, rationale) VALUES
609  ('branch_protection', 'enabled',  'nistcsf', 'PR.PS-06', 'positive', 'SDLC change control — review required before merge to the default branch'),
610  ('branch_protection', 'disabled', 'nistcsf', 'PR.PS-06', 'negative', 'SDLC change-control gap — direct pushes to the default branch possible');
611```
612
613Then add `"nistcsf"` to the `Framework` type and `normalizeFramework`, and add a
614`### NIST CSF 2.0` subsection here documenting the reasoning and fit.
615
616---
617
618## Keeping this document in sync
619
620The mappings live in two places that must agree: the SQL seed rows in
621`migrations/` (the source of truth the engine reads) and the
622[reference table](#complete-mapping-reference) and per-signal sections here.
623**When you change a mapping, change both in the same PR** — the same discipline
624the README applies to retention periods. A mapping row with no rationale here, or
625a row here with no SQL, is a bug.
626
627This is enforced. [`scripts/check-mappings.mjs`](../scripts/check-mappings.mjs)
628applies every migration to an in-memory SQLite database, reads back
629`control_mappings`, and diffs the `(resource, status, framework, control_id,
630posture, rationale)` tuples against the rows parsed out of the
631[reference table](#complete-mapping-reference) above. It fails with a row-level
632diff if the two drift. Run it with:
633
634```sh
635npm run test:mappings
636```
637
638It has no dependencies (Node's built-in `node:sqlite`) and is a good CI gate. It
639checks the **reference table** specifically — the per-signal tables and glossary
640are prose and are not parsed, so keep those consistent by hand.
641
642---
643
644## Sources
645
646Control *numbers and titles* are cited from the published standards; exact
647criterion text is paraphrased (the standards themselves are copyrighted).
648
649- ISO/IEC 27001:2022, Annex A — control titles confirmed via
650  [ISMS.online, "ISO 27001:2022 Annex A Explained"](https://www.isms.online/iso-27001/annex-a-2022/).
651- AICPA *Trust Services Criteria* (TSP Section 100, 2017 criteria with 2022
652  revised points of focus) — CC category scope confirmed via
653  [Linford & Co., "Trust Services Criteria"](https://linfordco.com/blog/trust-services-critieria-principles-soc-2/)
654  and [Secureframe, "SOC 2 Common Criteria"](https://secureframe.com/hub/soc-2/common-criteria).
655- The authoritative text for both is the source standard: purchase ISO/IEC
656  27001:2022 from ISO, and the AICPA Trust Services Criteria from the AICPA.
657  Verify any mapping against those before an audit.