audit-labs/gh-attest

GitHub Audit Evidence Extractor

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

v1.0.3: 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 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
 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; a mapping is a row that attaches
 48a control to one.** The poller and webhook handler both normalize GitHub events
 49into a small vocabulary — `resource` (e.g. `branch_protection`, `dependabot_alert`)
 50and `status` (e.g. `enabled`, `open`, `fixed`). See [`extractFact`](../src/webhook.ts)
 51and [`poller.ts`](../src/poller.ts). Mapping happens as a **join at export time**,
 52never 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
 55join 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
 57any individual finding's state.
 58
 59**3. Consequently, one snapshot can emit multiple evidence rows.** A single
 60Dependabot 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
 63existence of the scanner and the existence of an open finding are two different
 64facts about two different control expectations. This behavior is called out
 65per-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
 75Two 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
 92Criteria* (TSC). The criteria we map to are all in the **Common Criteria (CC)**
 93series, which every SOC 2 report shares regardless of which trust categories are
 94in scope. A code like `CC8.1` reads as *Common Criteria, category 8 (Change
 95Management), criterion 1*. The CC categories used here: **CC6** — logical &
 96physical access; **CC7** — system operations (detection & monitoring); **CC8** 97change management.
 98
 99**ISO/IEC 27001:2022** is a certifiable ISMS standard. Its **Annex A** lists 93
100controls 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`
102reads 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
104state which edition it targets.
105
106---
107
108## The signals and their mappings
109
110Each section below states: what GitHub thing we read and how, the `(resource,
111status)` vocabulary we normalize it to, the control(s) we map it to with a
112plain-language explanation, the evidentiary argument, and an honest fit
113assessment.
114
115### 1. Branch protection & repository rulesets
116
117**What we collect.** For each repo's default branch, whether merge controls are
118in 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,
122disabled}`. A ruleset in `evaluate` (monitor-only) mode counts as **not**
123enabled 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
134change control in a Git workflow: requiring pull-request review before merge,
135blocking direct pushes to the default branch, and requiring status checks to
136pass. That is exactly the "controlled process… stops unauthorized changes"
137language 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 —
141both frameworks name "change management" explicitly, and branch protection is
142the canonical GitHub-native implementation of it. The one nuance an auditor will
143probe is *scope*: we check the **default branch** only, and "enabled" does not
144verify that the *specific* rules (required reviewers, etc.) match the
145organization's policy. The evidence attests that a change-control gate exists,
146not that its configuration is sufficient.
147
148### 2. Dependabot alerts
149
150**What we collect.** `dependabot_alert` webhook events — known-vulnerability
151alerts 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
179capability required by CC7.1 exists and is running — hence the `NULL` mapping
180fires positive on any alert regardless of state. Each *individual* alert's
181lifecycle (open vs. remediated) is then evidence for CC7.2: an open alert is an
182unresolved condition, a fixed/dismissed one is a closed one. On the **ISO** side
183the entire story lands on a *single* control, A.8.8, because A.8.8 explicitly
184covers the full lifecycle — so every status maps to A.8.8 (open → negative,
185remediated → 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
189system.** CC7.2's formal text is about anomalies "indicative of malicious acts,
190natural disasters, and errors" — i.e. runtime security events. An unpatched
191dependency is a *known vulnerability*, which sits more naturally in CC7.1's
192"susceptibility to newly discovered vulnerabilities" language than in CC7.2's
193anomaly-detection language. Many auditors keep the **entire** dependency story
194(detection *and* remediation tracking) under CC7.1. **Recommendation:** before
195you present this to an auditor, decide whether open/remediated Dependabot state
196belongs under CC7.1 or CC7.2 in your control narrative, and align the mapping to
197that decision. Both are defensible; the current split is a design choice, not a
198requirement. The **ISO A.8.8** mapping, by contrast, is a strong, clean fit —
199A.8.8 is purpose-built for technical-vulnerability management and absorbs the
200whole lifecycle without the CC7.1/CC7.2 ambiguity. It was added in migration 0007
201to close a gap: before it, Dependabot produced no evidence at all in an ISO
202export.
203
204### 3. Code scanning alerts
205
206**What we collect.** `code_scanning_alert` webhook events — SAST findings from
207CodeQL 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
231that map to two facts: *"a testing process exists"* (A.8.29, from the `NULL`
232mapping) versus *"the code itself is/ isn't secure"* (A.8.28, from each finding's
233state). On the **SOC 2** side (added in [migration 0007](../migrations/0007_close_coverage_gaps.sql))
234only the tooling-active fact is mapped, to CC7.1 — mirroring how Dependabot's
235tooling-active fact maps to CC7.1.
236
237**Fit assessment: strong on ISO; SOC 2 intentionally partial.** The
238A.8.29-vs-A.8.28 split is clean — one control is about *having* the testing
239process, the other about the *code quality* it reveals — and both titles match
240the 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
243CC7.1 or CC7.2 is still an open decision (see the Dependabot fit assessment). Once
244that is settled, finding-level SOC 2 rows for code scanning can be added to match
245Dependabot. Until then a SOC 2 export shows code scanning as "detection active"
246only — 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
251credentials/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
281once. For **CC6.6**, it is a direct path for an *external* attacker to cross the
282system boundary. For **CC6.1**, the credential is itself one of the logical-access
283keys the control is meant to safeguard, so a leak is a compromise of the access
284controls themselves. For **A.5.17**, the credential is authentication information
285whose confidentiality the control requires. In every case: scanning active →
286**positive** (a protective measure exists); open alert → **negative** (a live
287gap); 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,
292CC6.1 the logical-access framing, A.5.17 the ISO authentication-information
293framing (added in migration 0007 to close a gap — before it, secret scanning
294produced no ISO evidence). Mapping to all three means the export satisfies
295whichever control the organization's narrative uses. A further SOC 2 framing,
296**CC6.7** (restricting the transmission/movement of information), also touches
297this 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
299negative ("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
301scale 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
307to `resource ∈ {member_access, team, repository}` with the GitHub action as
308status.
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
332administration — evidence that grants/changes are captured, which is what an
333auditor samples. Most are **informational** (an add or a role change is neither
334inherently good nor bad — it needs human review). The one exception is
335`removed`**positive**, because timely de-provisioning is itself a control
336objective (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
339own provisioning-vs-modification language). The informational posture is the
340right 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
345members and team members (see [`pollOrgAccess`](../src/poller.ts)), distinct from
346the 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
348point-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
359of a periodic access review — the recurring auditor ask that CC6.2/CC6.3 and
360A.5.18 all expect. Org membership maps to CC6.2 (who is registered/authorized in
361the org); team membership maps to CC6.3 (role-/least-privilege-based access).
362A.5.18 explicitly names "reviewed" among its verbs, so both feed it. All rows
363are **informational**: an inventory does not pass or fail, it *enables* the
364review.
365
366**Fit assessment: strong, with one nuance.** The org→CC6.2 / team→CC6.3 split is
367reasonable but not the only defensible cut — the *review* of org membership is
368arguably as much CC6.3 (appropriateness of access) as CC6.2 (registration). Since
369these are informational inventory rows feeding a review, the exact CC6.2/CC6.3
370attribution 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
372inventory) are deliberately separate resource names so the change-trail and the
373current-state inventory don't collide.
374
375### 7. Repository inventory
376
377**What we collect.** `repository` webhook events — repos created/deleted/renamed
378within 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
391create/delete/rename events is evidence that the asset inventory is maintained as
392it changes — exactly A.5.9's requirement. **Informational**: it is inventory, not
393a pass/fail condition.
394
395**Fit assessment: strong for what it claims.** The honest caveat is completeness:
396this is a *change trail*, so it evidences that inventory changes are captured, not
397that a full, owner-annotated asset register exists. If a future need is to attest
398a complete inventory, the polled repo list (already fetched in
399[`listInstallationRepos`](../src/poller.ts)) would be the better source than the
400webhook trail.
401
402---
403
404## Complete mapping reference
405
406This 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
409enabled/disabled vocabulary; 0005 adds the polled access inventory; 0006 adds
410the secret-scanning CC6.1 rows; 0007 closes the cross-framework coverage gaps —
411Dependabot→A.8.8, code scanning→CC7.1, secret scanning→A.5.17). **A "·" in
412Status 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
481The join engine is framework-agnostic — a new framework is **data, not code**.
482Adding one (e.g. NIST CSF 2.0, PCI DSS 4.0, CIS Controls) is a new migration that
483inserts `control_mappings` rows with a new `framework` value, plus a section in
484this document. No changes to the poller, exporter query, or webhook handler are
485needed. 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
490Do this per `(resource, status)` you want to attest, and write the reasoning into
491this document as you go. The goal the user cares about is **100% defensibility**,
492so bias toward under-claiming.
493
4941. **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.
4972. **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.
5003. **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.
5034. **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`.
5085. **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.
5136. **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.
5157. **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
521Three 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
547To map branch protection to **NIST CSF 2.0**, whose `PR.PS-06` covers a secure
548software development lifecycle:
549
550```sql
551INSERT 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
556Then 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
563The 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
567the README applies to retention periods. A mapping row with no rationale here, or
568a row here with no SQL, is a bug.
569
570This is enforced. [`scripts/check-mappings.mjs`](../scripts/check-mappings.mjs)
571applies every migration to an in-memory SQLite database, reads back
572`control_mappings`, and diffs the `(resource, status, framework, control_id,
573posture)` tuples against the rows parsed out of the
574[reference table](#complete-mapping-reference) above. It fails with a row-level
575diff if the two drift. Run it with:
576
577```sh
578npm run test:mappings
579```
580
581It has no dependencies (Node's built-in `node:sqlite`) and is a good CI gate. It
582checks the **reference table** specifically — the per-signal tables and glossary
583are prose and are not parsed, so keep those consistent by hand.
584
585---
586
587## Sources
588
589Control *numbers and titles* are cited from the published standards; exact
590criterion 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.