From 44148615e2cad2250fc1f5be82e6ee2272acfd0d Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sun, 16 Aug 2026 18:30:39 -0400 Subject: [PATCH] docs(security): add SECURITY.md and a private reporting channel keel holds exchange credentials and places live orders; once the repo is discoverable, the only routes were a public issue or silence (#279). SECURITY.md names the private channel (GitHub private vulnerability reporting, now enabled in repo settings), an honest solo-maintainer SLA, and the scope: a bypassable rail is a security issue, not merely a bug. tests/test_security_policy.py pins all of it. --- SECURITY.md | 47 ++++++++++++++++++ tests/test_security_policy.py | 92 +++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 SECURITY.md create mode 100644 tests/test_security_policy.py diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..fdd12e1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security Policy + +## Reporting a vulnerability + +**Do not open a public issue for anything you believe is a security flaw.** Use GitHub's +**private vulnerability reporting** — the **Report a vulnerability** button under this +repository's *Security* tab. It is enabled, and it reaches the maintainer without disclosing +the finding to anyone else. There is no separate email address; the button is the channel, +precisely because it cannot be accidentally CC'd. + +Please include what you found, how you found it, and — if you have it — a reproduction. If +you have already run it against live funds, say so; that changes the urgency, not the +welcome. + +## What counts as a vulnerability + +keel holds exchange API credentials and places live orders, so the defining class of defect +is anything that can make it trade what it should not, or expose what it holds: + +- **A rail that can be bypassed.** The guards in `keel/execution/guards.py` are the product. + A way around the halal allowlist, the spend or exposure caps, the drawdown breakers or the + kill-switch is a **security issue, not merely a bug** — whether it is a race, a parsing + quirk, or a default that fails open. If a rail can be made not to run, report it here. +- **Exposure of credentials or data**: anything that can leak API keys, secrets from + configuration, or database contents to a place they should not reach. +- **Corruption of the audit trail**: the attestation and decision records are what make the + engine auditable; anything that can rewrite or forge them silently is in scope. + +## Out of scope + +Please use a regular (public) issue, or no issue at all, for: + +- **Strategy performance and market losses.** A rule that loses money is a bad rule, not a + vulnerability; the backtest and experiments docs are where that conversation lives. +- **Your own key handling.** A key you pasted into a world-readable file, committed, or sent + to the wrong place is a compromise of your handling, not of keel. +- The exchange's own outages, or losses caused by market movement. + +## What to expect + +This project has **one maintainer**, and the timings below are sized to stay honest rather +than to sound corporate: + +- **Acknowledge within 3 days** of a private report. +- A severity call and a plan (fix, or mitigation with a timeline) **within 14 days**. +- Fixes are released as soon as they are verified; disclosure is coordinated with the + reporter, and credit is given unless you ask not to be named. diff --git a/tests/test_security_policy.py b/tests/test_security_policy.py new file mode 100644 index 0000000..ecf87d6 --- /dev/null +++ b/tests/test_security_policy.py @@ -0,0 +1,92 @@ +"""SECURITY.md: a private channel must exist before strangers read the source. + +keel holds exchange API credentials and places live orders. The day the repo is discoverable, +someone will eventually find a way to make it misbehave, and the only reporting routes today +are a public issue -- which discloses to everyone simultaneously -- or silence. A +vulnerability with no private channel is one that either burns the reporter's good will or +burns every operator at once; both end with the flaw living unpatched in the code that moves +money. + +This pins #279's acceptance into the repo itself, for the same reason the licence and +governance tests exist: the failure mode is a file that LOOKS configured and is not. A +SECURITY.md that names no working channel, promises a response a solo maintainer cannot +deliver, or stays silent on what counts as a vulnerability here is worse than none, because +it is trusted. + +The one part of #279 pytest cannot pin is the GitHub-side setting -- enabling private +vulnerability reporting in the repo's settings is a mutation outside the tree, and it is +verified against the live repo (`gh api .../security_and_analysis`) at issue-close time, not +in CI. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +_ROOT = Path(__file__).resolve().parents[1] + + +def _policy() -> str: + path = _ROOT / "SECURITY.md" + assert path.is_file(), ( + "SECURITY.md does not exist at the repo root -- there is no stated route for reporting " + "a vulnerability privately (#279)" + ) + return path.read_text() + + +def test_the_policy_names_a_private_channel(): + """A reporter's first question is 'where?', and the answer must already be written down. + + The channel is GitHub's private vulnerability reporting (the *Report a vulnerability* + button under the Security tab), which had to be ENABLED in the repo's settings for the + sentence to be true -- the file alone gives no channel. + """ + text = _policy().lower() + assert "report a vulnerability" in text or "private vulnerability reporting" in text, ( + "SECURITY.md must point at a real private channel (GitHub private vulnerability " + "reporting / the 'Report a vulnerability' button), not just an email-shaped promise" + ) + + +def test_the_policy_states_a_response_expectation_a_solo_maintainer_can_meet(): + """An acknowledgement window, stated in days, that matches one maintainer's reality. + + 'We will respond promptly' is not a commitment; a number is. The test accepts any stated + day-count precisely because the honest number is the maintainer's to size -- what it pins + is that a number was committed to at all. + """ + text = _policy().lower() + assert re.search(r"within \d+ (business )?days", text), ( + "SECURITY.md must state a concrete acknowledgement window ('within N days') -- " + "honestly sized for a solo maintainer, but concrete" + ) + + +def test_a_bypassable_rail_is_named_as_a_security_issue(): + """A rail that can be bypassed is a security issue here, not merely a bug. + + The rails ARE the product: an allowlist that can be talked around or a kill-switch that + can be raced past is the worst class of defect this codebase can carry, and a reporter + who thinks 'it is just a logic quirk, not security' will file it as a regular public bug. + """ + text = _policy().lower() + assert "rail" in text and "bypass" in text, ( + "SECURITY.md must say explicitly that a rail that can be bypassed is a security issue" + ) + + +def test_out_of_scope_reports_are_named(): + """What will NOT be treated as a vulnerability, so the channel stays usable. + + Strategy performance, market losses, and a user's own key handling are the three floods + a trading-repo security channel receives; each belongs in a public issue or nowhere at + all, and a policy that does not say so trains reporters to ping the private channel with + them anyway. + """ + text = _policy().lower() + assert "strategy" in text and "market" in text, ( + "SECURITY.md must name strategy performance and market losses as out of scope" + ) + assert "key" in text, "SECURITY.md must name a user's own key handling as out of scope"