| @@ -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. |