Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions scripts/check-security-policy.mjs
Original file line number Diff line number Diff line change
@@ -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');