diff --git a/Governance/policies/CERTIFICATE_MANAGEMENT.md b/Governance/policies/CERTIFICATE_MANAGEMENT.md new file mode 100644 index 0000000..7960a95 --- /dev/null +++ b/Governance/policies/CERTIFICATE_MANAGEMENT.md @@ -0,0 +1,180 @@ +# Certificate Management Policy + +## Purpose + +This policy defines how certificates used by TeachLink Mobile are issued, renewed, +monitored for expiry, and revoked when they should no longer be trusted. A certificate +that expires takes a feature down with it; one that stays trusted after it should have +been revoked is a way in. Both failures are preventable by treating certificates as +tracked assets with an owner and a date, rather than as configuration somebody renews +when they notice. + +## Scope + +This policy covers every certificate in the project's lifecycle: + +| Certificate | Purpose | Where it lives | +| --- | --- | --- | +| iOS distribution certificate (`.cer` / `.p12`) | Signs the iOS app distributed through the App Store | EAS credential store | +| iOS provisioning profile (`.mobileprovision`) | Binds the distribution certificate to the App ID and devices | EAS credential store | +| TLS server certificates | Terminates HTTPS for the API and WebSocket endpoints | Infrastructure provider | +| Certificate pinning material | Backup pins for app-to-server trust | App configuration, per the [Encryption Policy](ENCRYPTION.md) | +| Client and mTLS certificates, where used | Authenticates CI or a service to an internal endpoint | Secret store | + +It does not cover code-signing or commit-signing keys as such: the iOS distribution +certificate is included because it is a certificate, while the private key behind it +and the Android upload keystore are governed by the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md) and the +[Key Management Policy](KEY_MANAGEMENT.md). + +## Issuance Rules + +Certificates are created from a controlled source so that there is always one record of +what was issued and by whom. + +- **Issued for a stated purpose.** Every certificate records what it is for, which + system consumes it, and the period it is valid for. A certificate issued "just in + case" is not tracked and eventually becomes the one nobody recognised. +- **Shortest viable validity.** Validity is set to what the job needs. A server + certificate valid for a year is preferred over one valid for ten, because the renewal + process is then exercised regularly and revocation is quicker to complete. +- **Controlled issuers.** Certificates are issued from the project's own accounts or + providers. A certificate obtained from a public free-CA service, or one whose private + key was generated on an untrusted machine, is not accepted. +- **Private keys generated in the controlled environment.** The private key is created + where it will be stored, or transferred by an approved secure channel, and never + generated on a personal machine and moved. +- **No shared certificates.** Each environment and purpose has its own certificate. A + staging certificate is not reused in production. +- **Recorded at issuance.** The certificate, its purpose, its issuer, its not-before and + not-after dates, and its owner are entered in the certificate inventory below + immediately, not when someone next thinks to update it. +- **Store approval required.** A new certificate on an Apple or Google identity, or on + a production hostname, is issued only with the approval required by the + [PR Approval Requirements](APPROVAL_REQUIREMENTS.md). + +## Ownership + +| Certificate group | Accountable owner | Responsible for performing | +| --- | --- | --- | +| iOS distribution certificate and provisioning profile | [Release Manager](../roles/RELEASE_MANAGER.md) | Performing renewal; releasing the new profile into EAS | +| TLS certificates for production endpoints | [Security Officer](../roles/SECURITY_OFFICER.md) with the infrastructure maintainer | Approving issuance; the infrastructure maintainer performs it | +| Pinning material | [Security Officer](../roles/SECURITY_OFFICER.md) | Approving a pin change, which requires two maintainers under the [CI/CD Governance Policy](CICD_GOVERNANCE.md) | +| Client and mTLS certificates for CI | [Security Officer](../roles/SECURITY_OFFICER.md) | Approving; the requesting maintainer performs it | +| Test and preview certificates | The maintainer running the preview environment | Performing renewal | + +Rules that hold for every certificate: + +- Ownership is a role, not a person, so the responsibility survives a handover. +- The owner is someone who can actually renew the certificate without needing the + previous owner. A certificate whose only knowledgeable holder is one person is a + single point of failure and is flagged for a second owner. +- Two people must be able to complete any renewal, matching the rule in the + [Secret Rotation Policy](SECRET_ROTATION.md). +- A certificate with no identified owner is treated as unmanaged: it is renewed and + given an owner, or revoked. + +## Renewal Rules + +Renewal starts early enough that expiry is never the reason a build fails. + +| Requirement | Detail | +| --- | --- | +| Renewal lead time | Production certificates are renewed **at least 30 days** before `notAfter`, and 60 days before for anything Apple-issued, because profile regeneration is not always immediate | +| Renewal changes nothing else | A renewal replaces the certificate, not the identity. Changing the subject, the SANs, or the key alongside the renewal is a separate change with its own review | +| Provisioning profile follows the certificate | An iOS renewal regenerates the provisioning profile bound to the new certificate; renewing one without the other leaves the pair mismatched | +| Verification before promotion | A renewed certificate is validated by a `preview` build or a staging request before it is used in production, per the promotion path in the [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md) | +| Retain the previous certificate | The superseded certificate is kept, in the secure store only, until a production build or request has been accepted with the new one, so a failed renewal can be reversed without an outage | +| No reuse of the old private key | A renewal generates a fresh key. Reusing the previous private key defeats much of the purpose of renewing | +| Retired certificates are revoked | Once the successor is in production, the previous certificate is revoked at the issuer rather than merely deleted locally, per the [Key Management Policy](KEY_MANAGEMENT.md#revocation) | +| Record it | Each renewal records the date, the owner, the certificate identifier, the new expiry, and the verification result | + +## Expiry Monitoring + +Expiry is monitored so that renewal is a scheduled task rather than an outage. The +owner of each certificate group is responsible for the following: + +| Control | Requirement | +| --- | --- | +| Expiry date recorded | Every certificate's `notAfter` is in the inventory, so the remaining life is visible without opening the certificate | +| Calendar entry | A renewal date is scheduled per certificate, with a reminder 14 days ahead of the renewal date and a second reminder at 30 days before `notAfter` | +| Monthly check | The owner confirms the calendar matches the actual certificates in the store. A certificate present in the store but absent from the inventory is treated as a finding | +| Automated expiry alert | Where the provider or platform offers expiry notification, it is enabled rather than relied upon alone, so that a rotation does not silence the alert | +| Escalation on approach | If a certificate is within 14 days of expiry and not renewed, the owner escalates through the [Escalation Path](../processes/ESCALATION_PATH.md) rather than waiting for it to lapse | +| Post-expiry check | After each renewal the owner confirms the old certificate no longer appears as current anywhere, including in the EAS credential list and in any local `.mobileprovision` left on a build machine | + +An expired certificate fails in the least obvious way: an iOS distribution certificate +blocks submission with a message that does not always name expiry, and a lapsed TLS +certificate produces an untrusted-connection screen for users. Both are more expensive +than a calendar entry. + +## Revocation + +A certificate is revoked at its issuer immediately when: + +- its private key is suspected or confirmed exposed; +- its validity period ends and it has no successor; +- it is issued to a person or machine that leaves the project, under + [Contributor Offboarding](../processes/OFFBOARDING.md); +- it is found configured for a purpose it was not issued for; or +- a provider or store announces that it has been superseded. + +Revocation removes trust at the source; deleting the local copy is not revocation. The +revocation is recorded with its date and reason, and where the certificate was a +signing identity, the exposure is reported through the +[Security Policy](../SECURITY_POLICY.md) and tracked in the +[Incident Log](../registers/INCIDENT_LOG.md). + +## Certificate Inventory + +| Certificate | Issuer | Purpose | Owner | `notAfter` | Status | +| --- | --- | --- | --- | --- | --- | +| iOS distribution certificate | Apple Developer | App Store distribution | [Release Manager](../roles/RELEASE_MANAGER.md) | Recorded in the EAS credential store | Managed | +| iOS provisioning profile | Apple Developer | Binds certificate to App ID | [Release Manager](../roles/RELEASE_MANAGER.md) | Regenerated with the certificate | Managed | +| Production TLS certificate | Infrastructure provider | `api.teachlink.com` | [Security Officer](../roles/SECURITY_OFFICER.md) | Provider-managed | Managed | +| Backup pin set | App configuration | App-to-server trust | [Security Officer](../roles/SECURITY_OFFICER.md) | Set with a documented expiry | Managed | + +Where a certificate's expiry date is held by a provider rather than by the project, the +inventory records who to ask. Where a value is not yet known, the cell names the source +of truth instead of being left blank, because a blank cell is indistinguishable from an +unmanaged certificate. + +## Enforcement Status + +- Expiry monitoring is a human activity driven by the inventory and the calendar. No + workflow currently fails on an approaching certificate expiry. +- Apple's platform enforces its own expiry: an expired distribution certificate blocks + submission, and an expired profile blocks installation of the build it belongs to. + This is a backstop, not a control; it is detected at the worst possible moment. +- Pinning material, if introduced, is the certificate with the largest expiry risk, + because a missed rotation becomes an outage rather than a warning. A pin set without + a backup pin is rejected under the [Encryption Policy](ENCRYPTION.md). + +## Exceptions + +A certificate that cannot be renewed within the lead time, or that must remain valid +past its intended expiry, is recorded in the +[Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) with an approver +holding the authority to grant it, the compensating control, and an expiry no more than +12 months ahead. An exception to "renew before expiry" is rarely acceptable for a +signing certificate; for a test-environment certificate it may be. + +## Ownership and Review + +The Security Officer owns this policy, with the Release Manager responsible for the +Apple-issued certificates. It is reviewed when a new certificate type is introduced, +when a provider changes its issuance model, and at least once every 12 months. Changes +are proposed in a pull request that touches only the `Governance/` folder. + +## Related Documentation + +- [Key Management Policy](KEY_MANAGEMENT.md): the private key lifecycle behind each + certificate +- [Secret Rotation Policy](SECRET_ROTATION.md): credential cadence and verification +- [Encryption Policy](ENCRYPTION.md): transport requirements and pinning rules +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): iOS and Android signing + custody and promotion +- [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md): the promotion path a + renewal follows +- [Contributor Offboarding](../processes/OFFBOARDING.md): revocation on departure +- [Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) diff --git a/Governance/policies/KEY_MANAGEMENT.md b/Governance/policies/KEY_MANAGEMENT.md new file mode 100644 index 0000000..666a769 --- /dev/null +++ b/Governance/policies/KEY_MANAGEMENT.md @@ -0,0 +1,189 @@ +# Key Management Policy + +## Purpose + +This policy defines the lifecycle of every cryptographic key TeachLink Mobile depends +on: how one is generated, where it is stored, who may reach it, when it is rotated, and +how it is revoked. A key that nobody can enumerate is a key nobody can rotate, and a key +that is never revoked stays useful to anyone who copied it. This document exists so +that the answer to "which keys does this project hold, and who owns each one?" is +written down rather than reconstructed from a build configuration. + +## Scope + +This policy covers all cryptographic keys, from creation to destruction, including: + +- symmetric data-encryption keys generated on device for the encrypted feature cache, + as in `src/utils/encryptedStorage.ts`; +- asymmetric keys and certificates used for app signing, submission, and update + delivery, whose custody is detailed in the + [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md); +- TLS server certificates and any pinning material, under the + [Certificate Management Policy](CERTIFICATE_MANAGEMENT.md); +- commit and tag signing keys, under the + [Commit Signing Policy](COMMIT_SIGNING.md); +- keys issued to CI automation and third-party integrations, whose rotation cadence is + set by the [Secret Rotation Policy](SECRET_ROTATION.md); and +- service-account keys, under the [Service Accounts Policy](SERVICE_ACCOUNTS.md). + +This policy governs the **key**; the [Encryption Policy](ENCRYPTION.md) governs the +**algorithm**, and the two are read together because a key is only as strong as the +primitive protecting it. + +## Key Inventory + +Every key the project holds is listed with an owner. A key that exists and is not on +this list is unmanaged, and the first action is to find its owner or destroy it. + +| Key group | Where it lives | Accountable owner | Lifecycle policy | +| --- | --- | --- | --- | +| iOS distribution certificate and private key | EAS credential store | [Release Manager](../roles/RELEASE_MANAGER.md) | [App Signing Keys](../domains/APP_SIGNING_KEYS.md) | +| iOS provisioning profile | EAS credential store | [Release Manager](../roles/RELEASE_MANAGER.md) | [Certificate Management](CERTIFICATE_MANAGEMENT.md) | +| App Store Connect API key (`.p8`) | EAS and secret store | [Release Manager](../roles/RELEASE_MANAGER.md) | [Secret Rotation](SECRET_ROTATION.md) | +| Android upload keystore (`.jks`) | EAS credential store | [Release Manager](../roles/RELEASE_MANAGER.md) | [App Signing Keys](../domains/APP_SIGNING_KEYS.md) | +| Google Play service-account key JSON | Secret store | [Security Officer](../roles/SECURITY_OFFICER.md) | [Service Accounts](SERVICE_ACCOUNTS.md) | +| Commit and tag signing keys | Each contributor's own device or hardware token | The signer, verified by the [Security Officer](../roles/SECURITY_OFFICER.md) | [Commit Signing](COMMIT_SIGNING.md) | +| On-device feature-cache data key | iOS Keychain / Android Keystore via `expo-secure-store` | Generated and destroyed on device; no human owner | This policy | +| CI and integration API tokens | GitHub Actions secrets, provider secret stores | [Security Officer](../roles/SECURITY_OFFICER.md) | [Secret Rotation](SECRET_ROTATION.md) | + +Recording a new key group is part of creating it. A key introduced by a change lands in +this table, or in the register the policy it belongs to, in that same change. + +## Key Generation + +- Keys are generated by a cryptographically secure random number generator, never by a + general-purpose one. `encryptedStorage.ts` generates its AES key with `expo-crypto`; + a change that substitutes `Math.random()` is a defect. +- Key length and algorithm follow the + [approved algorithms](ENCRYPTION.md#approved-algorithms): AES-GCM 256 for symmetric + data keys, RSA at 2048 bits or above or ECDSA P-256 and above for asymmetric keys. +- A key is not generated from a passphrase-derived value unless a KDF per the approved + list is applied. Deriving a key from a password with a bare hash is not permitted. +- Each key has a purpose. A key used for two purposes must be split, because the + compromise of a shared key compromises both uses at once. +- Keys are generated as late as possible and destroyed as early as possible. An + on-device cache key is created when the feature first needs it rather than at install + time. + +## Storage and Access Rules + +| Rule | Detail | +| --- | --- | +| Never in the repository | No key, private half, keystore password, or passphrase is committed, including in examples, tests, fixtures, and screenshots | +| Approved stores only | Protected storage (`expo-secure-store`, i.e. Keychain and Android Keystore), the EAS credential store, or the platform secret store | +| Never beside the ciphertext | A data key is not stored in the same place as the data it protects. The feature-cache key lives in protected storage while the ciphertext lives in `AsyncStorage` | +| Narrow scope | A key is scoped to the single store, workflow, or environment that needs it | +| Individual custody | Private keys are personal and non-transferable; a shared key defeats attribution and is not permitted | +| No sharing | A private key is never pasted into an issue, pull request, chat, or log | +| Least privilege for access | Only the named owner can export or replace a key; contributors and reviewers may request a build but may not read or copy key material | +| Minimum exposure | A key is never held in memory longer than the operation requires, and never written to a crash report or telemetry event | +| Backup exclusion | Stored key material is excluded from device backup, so a restored device cannot resurrect a session or key | + +Where a decision requires a judgement about who should hold a key, the +[Decision Rights Matrix](../matrices/DECISION_RIGHTS_MATRIX.md) resolves it. + +## Rotation + +Rotation replaces a key with a new one before the old one becomes a liability, on the +cadence set by the policy that owns the key: + +| Key group | Cadence | Rotation policy | +| --- | --- | --- | +| Signing and submission material | At minimum annually | [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#rotation-process) | +| Platform tokens and service-account keys | 90 days | [Secret Rotation Policy](SECRET_ROTATION.md) | +| Third-party and CI integration keys | 180 days | [Secret Rotation Policy](SECRET_ROTATION.md) | +| Commit signing keys | On expiry or suspicion of compromise | [Commit Signing Policy](COMMIT_SIGNING.md#rotation-expiry-and-revocation) | +| On-device feature-cache keys | On compromise signal, or when the protected storage is reset | This policy | + +Rotation is immediate, regardless of schedule, when a key is suspected or confirmed +exposed, when a holder leaves the team or loses the device holding it, or when there is +evidence of an unauthorised build, upload, or use. + +Order of operations matters. Revoke or retire the old key at the source **before** +returning to service, then verify the new key works. For signing material the +superseded key is retained in the secure store only until the store accepts a build +signed with the new key, which is the rollback position described in the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#rotation-process). + +Previously signed artefacts stay valid. History is not rewritten to re-sign commits or +re-issue builds that users already have; the affected window is reviewed instead. + +## Revocation + +Revocation is the action that actually removes trust, and replacing a secret without +revoking it leaves the old value usable. + +- Revocation happens **at the source**: the certificate is revoked, the token is + deleted, the keystore is disabled, or the signing key is removed from the account. + Overwriting a stored value is not revocation. +- Revocation is recorded. The revoked key identifier, the date, the reason, and the + person who performed it are written to the relevant register or issue, so a later + reader can tell a deliberate revocation from a lost record. +- Revoked keys are never deleted from the inventory. They are marked revoked, with the + date, because the record of what was trusted is part of the audit trail. +- A revoked key that is later found in use indicates a compromise, not a mistake, and + is handled as an incident through the [Security Policy](../SECURITY_POLICY.md) and + recorded in the [Incident Log](../registers/INCIDENT_LOG.md). +- Revocation is complete when the key can no longer authenticate or validate anything. + A key that is revoked but still present in a cached build configuration is not + revoked. + +## Destruction + +A retired key is destroyed once the retention requirement is met and no build, tag, or +commit still depends on it. Destruction means the key material is no longer +recoverable from the store, from backups, and from the devices of anyone who held it. +On a user's device, destroying the protected-storage entry destroys the data it +protects, which is why the feature cache is designed to be re-fetchable rather than +irreplaceable. + +## Verification + +Key controls are verified rather than assumed: + +| Verification | How | +| --- | --- | +| A key is reachable only by its owner | The [Access Register](../registers/ACCESS_REGISTER.md) entry matches the exportable set | +| A key is not in the repository | `.gitignore` covers the key file types, and the ignore-rule audit in the [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#enforcement-checks) runs before a release | +| A rotation worked | The new key authenticates and the old one is verifiably rejected at the source, per the [Secret Rotation Policy](SECRET_ROTATION.md#verification-requirement) | +| A revocation worked | An attempt to use the revoked key fails, and the result is recorded | +| A key is in inventory | Each key group in the inventory table has an owner who confirms it still exists | + +## Enforcement Status + +- Key custody is enforced by platform storage guarantees, `.gitignore` rules, review of + diffs touching `eas.json`, `app.config.ts`, and `.github/workflows/`, and the + pre-release ignore-rule audit. No automated check inspects a key store. +- Key rotation age is not machine-checked. Cadences are tracked on the calendar the + [Secret Rotation Policy](SECRET_ROTATION.md#scheduling-and-reminders) describes. +- The inventory above is a maintained document; it is only useful if it is corrected + when a key group changes. + +## Exceptions + +A deviation — an unrotated key past its cadence, a key stored outside an approved +store, or shared custody — is recorded in the +[Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) with an approver +holding the authority to grant it, the compensating control, and an expiry no more than +12 months ahead. Where children's data is involved, the +[Children's Data Policy](../compliance/CHILDRENS_DATA.md) applies. + +## Ownership and Review + +The Security Officer owns this policy with the release-management team, who hold the +material. It is reviewed whenever a key group is added or removed, when a provider +changes its key model, and at least once every 12 months. Changes are proposed in a +pull request that touches only the `Governance/` folder. + +## Related Documentation + +- [Encryption Policy](ENCRYPTION.md): approved algorithms and what must be encrypted +- [Secret Rotation Policy](SECRET_ROTATION.md): rotation cadence and verification +- [Certificate Management Policy](CERTIFICATE_MANAGEMENT.md): certificate lifecycle +- [Service Accounts Policy](SERVICE_ACCOUNTS.md): machine identities holding keys +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): build and store signing +- [Commit Signing Policy](COMMIT_SIGNING.md): commit and tag keys +- [Security Policy](../SECURITY_POLICY.md) and + [Incident Log](../registers/INCIDENT_LOG.md) +- [Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) and + [Access Register](../registers/ACCESS_REGISTER.md) diff --git a/Governance/policies/PATCH_MANAGEMENT.md b/Governance/policies/PATCH_MANAGEMENT.md new file mode 100644 index 0000000..031391d --- /dev/null +++ b/Governance/policies/PATCH_MANAGEMENT.md @@ -0,0 +1,180 @@ +# Patch Management Policy + +## Purpose + +This policy defines how patches are brought into TeachLink Mobile, how they are +prioritised against other work, and how an emergency patch is delivered faster than the +normal release process. A patch process that only describes the routine case leaves the +project improvising exactly when a patch matters most, so the routine cadence, the +prioritisation rules, and the emergency path are all specified here. + +## Scope + +This policy covers every update that changes shipped code or a shipped dependency: + +- application source changes that fix a defect or a security issue; +- dependency and transitive-dependency updates, driven by Renovate and by `audit.yml`; +- base image, toolchain, and build-dependency updates, including Expo SDK and React + Native upgrades; +- Android and iOS platform dependency updates shipped with the app; and +- patches to vendored or generated code committed to the repository. + +It does not govern the content of a security remediation deadline, which is the +[Vulnerability Remediation SLA Policy](VULN_SLA.md), nor the mechanics of producing and +signing a build, which are in the +[EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md) and the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md). + +## Patch Cadence + +| Patch class | Target window | Delivery | +| --- | --- | --- | +| Security patch, Critical or High severity | Per the [remediation SLA](VULN_SLA.md#remediation-targets-by-severity) | Expedited release, outside the normal cadence | +| Security patch, Medium or Low severity | 30 and 90 calendar days respectively | Next regular release | +| Routine dependency update, no advisory | Within **14 days** of the update becoming available, or in the next release, whichever is sooner | Next regular release | +| Security-adjacent tooling update, such as a CI action | Within **7 days** | Next release; immediately if it fixes an exploitable workflow flaw | +| Base image or toolchain patch | Within **30 days** | Next release, with build verification | +| Major version or framework upgrade | Planned work, not a patch | Milestone, following the [Versioning Policy](VERSIONING.md) | +| Platform SDK adoption | Planned work | Milestone, following the [OS Version Support Policy](../domains/OS_VERSION_SUPPORT.md) | + +Two rules apply across all classes: + +- **A patch is not a feature.** A patch lands on its own, is reviewed on its own, and is + not bundled with unrelated changes. Bundling makes a patch unreviewable, which is the + main reason bundled hotfixes take longer than they should. +- **Verification is part of the patch.** A patch ships only after the checks in + [PR Approval Requirements](APPROVAL_REQUIREMENTS.md#required-checks) pass. A patch + that cannot pass the checks is escalated, not merged with the checks relaxed. + +## Prioritisation + +When more patches are available than the release can carry, they are ordered by this +rule set, applied in order. + +| Priority | Criterion | Example | +| --- | --- | --- | +| 1 | Actively exploited, or a flaw in the release-signing or update-delivery chain | Compromised signing credential; an advisory with a known exploit in the wild | +| 2 | Critical or High severity affecting users' data, accounts, or payment | Authentication bypass; token exposure | +| 3 | Severity for which no workaround or mitigation is available to users | A crash or data-loss defect on the current release line | +| 4 | Severity for which a workaround exists | Medium severity with a configuration mitigation | +| 5 | Patch that removes an exception, or closes a security finding | Clearing a recorded `security` finding | +| 6 | Routine dependency update removing a deprecated or vulnerable package | Removing a package flagged by `audit.yml` | +| 7 | Toolchain or build-dependency update with no user-visible effect | Developer-experience and build-stability updates | + +Rules that resolve ties and edge cases: + +- **User exposure outranks internal risk.** A defect affecting a teacher's data ranks + above a defect affecting only maintainer tooling, at the same severity. +- **Reachability decides, not the advisory.** An advisory for a code path the app never + reaches is not automatically low priority; it is first assessed for reachability, and + the assessment is recorded. Severity is only lowered with a written argument, which is + the adjustment the [Vulnerability Remediation SLA Policy](VULN_SLA.md) describes. +- **Unblocking counts.** A patch that unblocks a release or removes a flakiness that is + consuming maintainer time moves up. +- **A patch is never deprioritised for being inconvenient.** "Large diff", "touching + many files", and "risky to merge" are reasons to review carefully and split into + reviewable pieces, not reasons to postpone indefinitely. +- **Batch size is bounded.** A routine update release carries a limited number of + changes, so that a regression can be attributed. When the limit is reached, the + remainder waits for the next release rather than growing an unreviewable batch. + +## Emergency-Patch Path + +The emergency path exists for a defect or security issue that cannot wait for the normal +release cadence. It shortens the process; it does not remove the controls. + +1. **Declare.** The Release Manager or Security Officer declares the emergency, states + the user impact, and opens an issue labelled `emergency`. +2. **Scope minimally.** The patch contains only what is needed to resolve the emergency. + Additional improvements wait for the next release, which is what keeps the patch + reviewable. +3. **Fix on a dedicated branch.** The work happens on a branch from the current release + tag or `main`, so it does not inherit unrelated in-flight changes. +4. **Review.** Two maintainers review, at least one of whom is the Release Manager. For + a security fix the Security Officer reviews as well. The emergency override in + [PR Approval Requirements](APPROVAL_REQUIREMENTS.md#emergency-overrides) allows + fewer checks, never fewer approvals, and the reason is recorded. +5. **Verify locally as far as the emergency allows.** The full suite is still run; where + a non-blocking check cannot complete in time, it is recorded and re-run afterwards + rather than assumed. +6. **Ship.** The build follows the promotion path in the + [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md): `development`, then + `preview` with a real device, then `production`. A signed `preview` build is + confirmed before promotion. +7. **Recover.** Within 24 hours the full test suite is run against the shipped change. + Where it fails, the emergency patch is reverted on the next release and the cause is + recorded, following the override's post-merge requirements. +8. **Follow up.** A post-incident review is filed for the cause and for anything the + emergency deferred, so the gap is closed rather than remembered. + +An emergency patch that changes the release-signing chain additionally follows the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md), including the requirement to +validate with a `preview` build before a production submission. + +Emergency status is used for user impact, not for a missed internal deadline. If it is +being used for the second thing, the deadline is the problem to fix, and that is +recorded as a governance item. + +## Release Cadence Relationship + +Patches flow through the +[Release Cadence](../processes/RELEASE_CADENCE.md) and the +[Release Branching Policy](RELEASE_BRANCHING.md). A regular release draws from +`main` and carries the routine batch; an expedited release is cut from the emergency +branch and is reconciled into `main` afterwards, so `main` always reflects what shipped. +Every patch appears in `CHANGELOG.md` and the release notes under the +[Changelog Policy](CHANGELOG_POLICY.md), with the issue it resolves referenced. + +## Verification + +| Check | Requirement | +| --- | --- | +| Checks pass | The required checks pass on the patch, or a recorded exception applies | +| No unrelated change | The diff contains only the patch, as reviewers confirm | +| Reproduces and fixes | Where the defect is reproducible, the patch is shown to fix it and not to change the behaviour under test | +| Rehearsed on a real build | For a platform or dependency patch, a `preview` build installs and launches | +| Attributable | The change is one patch per release entry, so a regression can be traced to it | +| Reverted cleanly if wrong | A revert is possible without a coordinated rollback, which is checked before shipping | + +## Enforcement Status + +- `audit.yml` runs on push and weekly and fails on high and critical dependency + advisories; Renovate proposes routine updates. This is the automated part of the + cadence. +- The 14-day routine window, the prioritisation order, and the emergency declaration + are human processes. No automation enforces a patch age or the batch-size bound. +- Reverting is deliberately easy because patches land on their own branches; that is a + design property, not a control. + +## Exceptions + +A patch that cannot be applied within its window — a dependency that breaks the build, +a vendor release that regresses a tested path, a platform fix unavailable to the project +— is recorded in the +[Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) with the reason, +the compensating control, an approver with authority, and an expiry no more than 12 +months ahead. An exception says why a patch is late; it does not make the late patch +optional, and the patch is still applied as soon as the blocker is resolved. + +## Ownership and Review + +The Release Manager owns this policy, with the Security Officer responsible for the +security-patch classes. It is reviewed after every emergency patch, when the release +cadence changes, and at least once every 12 months. Changes are proposed in a pull +request that touches only the `Governance/` folder. + +## Related Documentation + +- [Vulnerability Remediation SLA Policy](VULN_SLA.md): remediation deadlines by severity +- [Release Cadence](../processes/RELEASE_CADENCE.md) and + [Release Branching Policy](RELEASE_BRANCHING.md): how patches reach users +- [PR Approval Requirements](APPROVAL_REQUIREMENTS.md): checks and the emergency + override +- [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md): the promotion path +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): patches that touch signing +- [Versioning Policy](VERSIONING.md): where a major upgrade sits relative to a patch +- [Bot and Automation Governance Policy](BOT_AUTOMATION.md): Renovate and automated + updates +- [Dependency Register](../registers/DEPENDENCY_REGISTER.md) and + [Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) +- [Release Manager role](../roles/RELEASE_MANAGER.md) diff --git a/Governance/policies/VULN_SLA.md b/Governance/policies/VULN_SLA.md new file mode 100644 index 0000000..e98649a --- /dev/null +++ b/Governance/policies/VULN_SLA.md @@ -0,0 +1,210 @@ +# Vulnerability Remediation SLA Policy + +## Purpose + +This policy sets a remediation deadline for every known vulnerability that affects +TeachLink Mobile, measured from the moment the project becomes aware of it, and defines +what happens when a deadline is missed. Acknowledging a report quickly is of little +value if nothing is fixed for a year; this document is about the fix, and about being +honest when the fix will be late. + +## Scope + +This policy covers remediation of vulnerabilities that are known to the project. It +begins when the project becomes aware of a vulnerability through any channel — a +private report, an automated advisory, a dependency alert, a store notice, a public +disclosure, or a maintainer's own finding — and ends when users are no longer exposed, +either because a fix has shipped or because the exposure is formally closed. + +It covers vulnerabilities in: + +- TeachLink Mobile's own application code; +- dependencies in the [Dependency Register](../registers/DEPENDENCY_REGISTER.md), + including transitive ones reported by `audit.yml`; +- the build, release, and update-delivery chain, where a compromise of signing material + is treated as Critical per the + [Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md); +- the mobile platform, such as an OS or SDK flaw affecting the app; and +- third-party services the app depends on, in coordination with the provider. + +It does not govern how a vulnerability is reported. That is the +[Security Policy](../SECURITY_POLICY.md) and the +[Vulnerability Disclosure Process](../processes/VULN_DISCLOSURE.md). + +## Remediation Targets by Severity + +Severity is assigned using the +[Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md#severity-levels), so that +the same level means the same thing everywhere. A project-specific adjustment is +permissible where the rubric does not fit, for example when a library flaw has no +reachable call path in the app; the adjustment and its reasoning are recorded with the +finding rather than applied silently. + +| Severity | Fix or mitigation deployed | Update available to users | Notes | +| --- | --- | --- | --- | +| **Critical** | **24 hours** from awareness | 7 calendar days | Remote exploitation without interaction, account takeover, data loss, or compromise of the release-signing chain | +| **High** | **7 calendar days** from awareness | 14 calendar days | Limited-interaction or limited-privilege exploitation of user data, payment, or authentication | +| **Medium** | **30 calendar days** from awareness | Next regular release | Meaningful interaction or unusual conditions required; targeted exposure or denial of service | +| **Low** | **90 calendar days** from awareness | Next regular release | Limited impact, self-inflicted harm, or best-practice hardening | +| **Informational** | No fixed deadline | Not applicable | Recorded, and acted on only if it becomes part of a larger fix | + +Two rules qualify every row: + +- **Mitigation counts only if it removes the exposure.** A change that reduces severity + without removing the vulnerable path is not a mitigation. A workaround that blocks + the exploitation path, such as disabling the affected feature behind a flag, counts; + a comment, a note, or a monitoring rule does not. +- **A fix with no release path is not a fix.** The deadline that matters to a user is + the one in the "available to users" column. Where app-store review gates a release, + the target includes that lead time, and a delay caused by store review is recorded as + such rather than counted against the team. + +The rubric's acknowledgement targets — 4 business hours for Critical, 1 business day +for High — continue to apply unchanged and are measured separately. + +## Measurement + +Every vulnerability is tracked as an issue so that the clock is visible and auditable. + +### What starts the clock + +The clock starts at the **time of awareness**, which is the earliest of: + +- the moment a private report reaches the project, recorded by the + [Security Officer](../roles/SECURITY_OFFICER.md); +- the moment an automated advisory matching an affected dependency or image is published; +- the moment a store or platform vendor notifies the project; or +- the moment a maintainer identifies the issue in their own work. + +Awareness time, not publication time, is the reference. A vulnerability published +yesterday and discovered today starts today, and the days it was public before +discovery count against the provider only if the provider was in a position to notify +the project. + +### What stops the clock + +The clock stops when the exposure ends for users: + +- a release containing the fix has **shipped and is available**; or +- the vulnerability is formally closed because it is not exploitable in this app, which + requires a documented reachability argument, not an absence of evidence of exploit. + +A fix merged to `main` but not yet released does not stop the clock. The gap between +merge and release is the project's responsibility and is planned for, not treated as +waiting time. + +### What is recorded + +| Field | Content | +| --- | --- | +| Awareness timestamp | With time zone, and the source that triggered it | +| Severity and rationale | The level, and why it matches the rubric | +| Affected versions | Which released app versions and which dependency versions are affected | +| Exposure scope | Which users and which data, per the [Data Processing Register](../registers/DATA_PROCESSING_REGISTER.md) | +| Owner | The maintainer accountable for the fix | +| Milestones | Reproduce, fix merged, fix released, advisory published | +| Actual times | The real timestamps for each milestone, used to compute the variance | + +These fields match the issue templates and the +[Incident Log](../registers/INCIDENT_LOG.md), so a vulnerability that becomes an +incident is not re-described from scratch. + +### Tracking the targets + +- Open vulnerabilities are listed with the days remaining against their targets at the + monthly security review, which is the security counterpart of the reporting in the + [Code Review Service Level Agreement](REVIEW_SLA.md). +- A vulnerability approaching 80 percent of its window is flagged to the Security + Officer and the Release Manager. +- The measurement method is deliberately simple and stated here so that it cannot be + quietly adjusted per finding: start at awareness, stop at release, count calendar + days, and record both. + +## Escalation When a Target Is Missed + +A missed target is a signal to change the plan, not to relabel the severity. + +1. **Declare it.** Within one business day of the target being missed, the owner marks + the issue as missed and states why: not started, in progress, blocked, awaiting + review, awaiting store review, or awaiting a third party. +2. **Escalate immediately** for Critical and High. The [Escalation Path](../processes/ESCALATION_PATH.md) + applies, and the Release Manager decides whether the fix ships as an expedited + release outside the normal cadence. A security fix may not wait for a milestone that + is weeks away. +3. **Tell the users when the exposure is user-facing.** If a released version is + affected and no fix is available, the disclosure decision follows the coordinated + process in the + [Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md#coordinated-disclosure), + including notifying store listings if that is what reaches users. +4. **Record the cause.** A missed target without a stated cause is treated as an + incomplete record and is raised at the next security review. +5. **Fix the system, not just the instance.** Where the cause is structural — no + automated dependency alert, a test that does not cover the affected path, a single + maintainer who alone understands the area — a follow-up task is created to remove + that dependency, so the next occurrence is less likely to be late. +6. **Do not repeat silently.** A second missed target for the same vulnerability, or a + pattern of misses in one severity band, is escalated to the maintainers as a + governance item. + +Missing a target is not itself a fault, provided it is declared, escalated, and +explained. Hiding it, or silently extending the deadline, is. + +## Dependencies and Third Parties + +- An automated advisory with no fix available is remediated by mitigation: the affected + dependency is removed, replaced, or disabled, and the exposure is recorded with the + provider's response time. +- A fix is merged within the target window even when the provider has not shipped one, + by pinning to a safe version, backporting the fix, or removing the feature path. +- A third-party service vulnerability that the project cannot mitigate is recorded as + accepted risk in the [Risk Register](../registers/RISK_REGISTER.md) with the + compensating control, and is reported to users if their data is affected. +- Renovate and `audit.yml` handle routine dependency updates; a high or critical + advisory bypasses the normal update cadence and is treated as a Critical or High + finding by severity, not by convenience. + +## Exceptions + +A remediation target that cannot be met for a documented reason — a platform vendor +with no fix and no workaround, a store review outside the project's control, a +dependency that cannot be removed without breaking the app — is recorded in the +[Security Exceptions Register](../registers/SECURITY_EXCEPTIONS_REGISTER.md), with the +compensating control and an expiry no more than 12 months ahead. An exception is not +available for "we did not get to it". + +## Enforcement Status + +- `audit.yml` runs on push and on a weekly schedule and fails on high and critical + dependency advisories, which is the automated half of dependency remediation. Its + checks are the repository's real defence for the dependency case. +- Advisory matching, exposure analysis, and user notification are manual. Nothing + currently pages anyone when an advisory appears or when a target is missed, so + measurement depends on a maintainer reading the weekly audit run. +- Automation for advisory matching and for target tracking is a reasonable follow-up and + would be introduced under the + [Bot and Automation Governance Policy](BOT_AUTOMATION.md). + +## Ownership and Review + +The Security Officer owns this policy. It is reviewed after any missed Critical or High +target, when the severity rubric changes, when a new automated advisory source is +added, and at least once every 12 months. Changes are proposed in a pull request that +touches only the `Governance/` folder. + +## Related Documentation + +- [Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md): severity levels and + acknowledgement SLAs +- [Security Policy](../SECURITY_POLICY.md): how to report a vulnerability +- [Vulnerability Disclosure Process](../processes/VULN_DISCLOSURE.md): coordination with + reporters +- [Escalation Path](../processes/ESCALATION_PATH.md): what happens when a target is + missed +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): compromise of the + release-signing chain +- [Dependency Register](../registers/DEPENDENCY_REGISTER.md) and + [Risk Register](../registers/RISK_REGISTER.md) +- [Incident Log](../registers/INCIDENT_LOG.md) and + [Security Exceptions Register](../registers/SECURITY_EXCEPTIONS_REGISTER.md) +- [Security Officer role](../roles/SECURITY_OFFICER.md) and + [Release Manager role](../roles/RELEASE_MANAGER.md)