diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..25cd65b --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,26 @@ +name: Checks + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + security-policy: + name: Disclosure policy + runs-on: ubuntu-latest + steps: + # Pinned to a commit, not a tag, for the reason T59 gives: a tag moves and + # the reviewed build is not the published one. Same SHAs the initiative + # workflows already use. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 24 + # This repository has no package.json and needs none — the check is one + # script against one file, and a dependency would be a supply chain for a + # thing that reads Markdown. + - run: node scripts/check-security-policy.mjs diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cb0de5e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,48 @@ +# Reporting a security issue + +This is the organization-wide policy. It applies to every Morelitea repository +that does not carry its own `SECURITY.md`. + +## How to report + +Email **security@morelitea.com**. Please do not open a public issue. + +Include what you can: + +- what the issue is, and where +- how to reproduce it +- what an attacker gets — the part that decides how fast we move +- a suggested fix, if you have one + +## What to expect + +- Acknowledgement within 48 hours. +- An estimated timeline once we have reproduced it. +- Notice when it is resolved. +- Credit in the release notes, unless you would rather stay anonymous. + +## Which repositories this covers + +GitHub applies this policy to every Morelitea repository that has no policy of +its own. A repository-local `SECURITY.md` takes precedence and may provide a +more specific reporting route or support window. + +Reports about any non-archived repository are supported. Reports about an +archived repository are still welcome, but the resolution may be an archive +notice rather than a patch. + +## What we ask + +- Give us a chance to fix it before disclosing publicly. +- Do not access, change or keep data that is not yours while testing. +- Do not run denial-of-service tests or automated scanners against our hosted + services. Test against your own deployment — everything under `initiative` is + self-hostable, which makes that straightforward. + +## Scope + +First-party code, our deployment configuration, and our CI workflows. + +Vulnerabilities in third-party dependencies are out of scope as issues in their +own right, but we want to hear about a vulnerable transitive dependency reaching +one of our releases — that is our problem to fix even when the bug is not ours. diff --git a/scripts/check-security-policy.mjs b/scripts/check-security-policy.mjs new file mode 100644 index 0000000..aedfd61 --- /dev/null +++ b/scripts/check-security-policy.mjs @@ -0,0 +1,46 @@ +/** + * The organization-wide disclosure policy has to stay usable. + * + * This file is what GitHub shows on every repository that has no policy of its + * own, so a broken link or a missing reporting route is broken everywhere at + * once and shows up nowhere — no build consumes it, and nothing else reads it. + * + * Checks the things that make it a policy rather than a page: somewhere to + * report, a promise about what happens next, and links that resolve. + */ +import {readFileSync} from 'node:fs'; + +const text = readFileSync('SECURITY.md', 'utf8'); +const problems = []; + +// A reporting route. Without one the document is an essay. +if (!/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/.test(text) && !/security\/advisories\/new/.test(text)) { + problems.push('no reporting route: neither an email address nor a private advisory link'); +} + +// What a reporter is told to expect. The policy makes a commitment; if the +// section naming it disappears, the commitment disappears silently. +if (!/##\s*What to expect/i.test(text)) { + problems.push('no "What to expect" section — a reporter is told nothing about what follows'); +} + +// Scope. A policy without one invites testing nobody wants. +if (!/##\s*Scope/i.test(text)) { + problems.push('no "Scope" section'); +} + +// Relative links must resolve. An absolute one is somebody else's uptime. +for (const [, label, target] of text.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) { + if (/^(https?:|mailto:|#)/.test(target)) continue; + try { + readFileSync(target.split('#')[0], 'utf8'); + } catch { + problems.push(`link "${label}" points at ${target}, which is not in this repository`); + } +} + +if (problems.length) { + console.error('SECURITY.md problems:\n' + problems.map((p) => ` - ${p}`).join('\n')); + process.exit(1); +} +console.log('SECURITY.md: reporting route, expectations, scope and links all present');