From 807919f15c98d7756c26cea851ec107775f56589 Mon Sep 17 00:00:00 2001
From: S'Bussiso Dube <80188685+Sbussiso@users.noreply.github.com>
Date: Wed, 9 Sep 2026 19:01:42 -0700
Subject: [PATCH] =?UTF-8?q?Every=20URL=20we=20publish=20now=20resolves=20?=
=?UTF-8?q?=E2=80=94=20seven=20didn't?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reviewed the docs the way a launch does: by following the links rather
than reading them. Seven were broken, and the two that matter are not
documentation problems.
security.txt advertised a policy page that has never existed.
Policy: https://sentinel-command.com/security#vulnerability-disclosure
404, along with every plausible variant. security.txt is machine-read
under RFC 9116 — a researcher fetches it, follows Policy:, and decides
whether they are covered before touching anything. Ours led to nothing,
so we published no scope and, more importantly, no safe harbour. The
authorisation language that keeps good-faith research out of CFAA
territory was unreachable at the exact moment someone needed it.
SECURITY.md compounded it by calling that page "canonical when the two
disagree", so the complete policy sitting in this repo was formally
subordinate to a 404. It now points at itself, and security.txt points
at it.
The test that should have caught this asserted the Policy: line was
PRESENT. A well-formed pointer at nothing passes that. Worse, its
docstring described an id="vulnerability-disclosure" section on a page
that was never built — a test pinning a fiction. It now pins the
specific target, so moving it is deliberate and comes with a test to
update.
The launch checklist gave operators a webhook URL that 404s.
Clerk and Resend were to be configured against
sentinel-command.com/api/webhooks/*. The apex is the marketing site; the
API is on app.sentinel-command.com. Configuring what was written means
every membership, billing and delivery webhook fails silently — nothing
errors, events simply never arrive — and it fails at launch, during the
window where org and billing state matters most.
Same apex/app confusion hit the on-call runbook's Quick reference table,
where /api/health is the first thing a responder runs, and the disaster
recovery runbook's readiness probe. Eight URLs corrected across four
files; all verified 200 (or 405 on a POST-only webhook, which is the
endpoint answering correctly).
Also fixed in SECURITY.md, a customer-facing document: it called the
product "SourceBox Sentry" twice, two brand names ago, and told
reporters /api/health returns a commit. It returns a version.
864 tests pass, ruff clean.
Co-Authored-By: Claude Opus 5
---
README.md | 4 ++--
SECURITY.md | 12 +++++-------
backend/app/api/well_known.py | 22 ++++++++++++++++++----
backend/tests/test_security_txt.py | 25 +++++++++++++++++++------
docs/LAUNCH_HANDOFF.md | 4 ++--
docs/runbooks/DISASTER_RECOVERY.md | 2 +-
docs/runbooks/ON_CALL.md | 6 +++---
7 files changed, 50 insertions(+), 25 deletions(-)
diff --git a/README.md b/README.md
index b300afda..d8727a51 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
► Try the live app
·
- Documentation
+ Documentation
·
CameraNode
@@ -93,7 +93,7 @@ CameraNode captures and encodes video on your network, then pushes it **outbound
| If you want to… | Go to |
|-----------------|-------|
-| **Use Sentinel** — set up cameras, recording, notifications, integrations | The in-app [Documentation](https://sentinel-command.com/docs) |
+| **Use Sentinel** — set up cameras, recording, notifications, integrations | The in-app [Documentation](https://app.sentinel-command.com/docs) |
| **See how the whole system fits together** — every repo, every deployed service, the paths between them | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) |
| **Understand the code** — architecture, API, data models, configuration | [AGENTS.md](AGENTS.md) |
| **Operate it** — decision records, runbooks, legal templates | [docs/](docs/) |
diff --git a/SECURITY.md b/SECURITY.md
index a49878c4..301af40e 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,12 +1,10 @@
# Security Policy
-SourceBox Sentry is a security-focused application and we take vulnerabilities seriously.
+Sentinel by SourceBox is a security-focused product and we take vulnerabilities seriously.
-The full policy — scope, response timelines, safe-harbour terms, and the standard machine-readable [`security.txt`](https://app.sentinel-command.com/.well-known/security.txt) — lives at:
+**This file is the policy.** Scope, response timelines, and safe-harbour terms are all below, and the machine-readable [`security.txt`](https://app.sentinel-command.com/.well-known/security.txt) points here.
-**https://sentinel-command.com/security#vulnerability-disclosure**
-
-This file is the GitHub-standard summary; the deployed page above is canonical when the two disagree.
+It previously deferred to a page at `sentinel-command.com/security` and called that page canonical. That page does not exist and never has — so the canonical policy was a 404, and `security.txt` sent researchers there. Corrected 2026-09-09. If a hosted policy page is published later, point `security.txt` at it and say so here.
## Reporting a vulnerability
@@ -24,7 +22,7 @@ Two channels, use whichever you prefer:
- Description of the issue and its impact
- Steps to reproduce (URLs, payloads, screenshots)
-- Version / commit you tested against — surfaced by `GET /api/health`
+- Version you tested against — `GET /api/health` returns it (e.g. `{"version": "2.1.2"}`)
- Optional suggested fix or mitigation
### Response timeline
@@ -67,7 +65,7 @@ If you make a good-faith effort to comply with this policy:
## Bug bounty
-There is no monetary bug bounty today — SourceBox Sentry is pre-PMF. We're upfront about that so you can decide whether to invest the time. If we ever launch one, prior reporters will be at the front of the line.
+There is no monetary bug bounty today — Sentinel is pre-PMF. We're upfront about that so you can decide whether to invest the time. If we ever launch one, prior reporters will be at the front of the line.
## Security updates
diff --git a/backend/app/api/well_known.py b/backend/app/api/well_known.py
index 93e04f3e..de028e9e 100644
--- a/backend/app/api/well_known.py
+++ b/backend/app/api/well_known.py
@@ -73,10 +73,24 @@ def _build_security_txt() -> str:
datetime.now(tz=UTC) + timedelta(days=_EXPIRY_DAYS)
).strftime("%Y-%m-%dT%H:%M:%SZ")
- # The security policy page now lives on the standalone website
- # (sentinel-command.com), not this app's frontend. The Policy: URL
- # must point there so researchers land on the actual disclosure page.
- policy_url = "https://sentinel-command.com/security#vulnerability-disclosure"
+ # Policy: MUST resolve. This is machine-read (RFC 9116) by scanners
+ # and by researchers deciding whether they are covered by safe
+ # harbour before they touch anything — a 404 here means no published
+ # scope and no published authorisation.
+ #
+ # It pointed at https://sentinel-command.com/security#vulnerability-disclosure
+ # on the assumption the page had moved to the standalone site. It had
+ # not: that URL, and every plausible variant of it, returns 404
+ # (checked 2026-09-09). SECURITY.md in this repository is the only
+ # place the full policy — scope, timelines, safe harbour — actually
+ # exists, so it is what we point at.
+ #
+ # If the standalone site ever publishes the page, move this back and
+ # update the matching assertion in tests/test_security_txt.py.
+ policy_url = (
+ "https://github.com/SourceBox-LLC/Sentinel-Command"
+ "/blob/master/SECURITY.md"
+ )
# Order follows RFC 9116 §2.5 examples for readability. Comments
# at the top help human readers; scanners ignore them. Contact
diff --git a/backend/tests/test_security_txt.py b/backend/tests/test_security_txt.py
index 22d63e3b..4758eb8d 100644
--- a/backend/tests/test_security_txt.py
+++ b/backend/tests/test_security_txt.py
@@ -168,16 +168,29 @@ def test_security_txt_has_canonical_and_policy(unauthenticated_client):
assert any(line.startswith("Policy:") for line in body.splitlines())
-def test_security_txt_policy_anchor_matches_security_page(unauthenticated_client):
- """Pin the anchor — the security page on sentinel-command.com renders an
- ``id="vulnerability-disclosure"`` section that this URL deep-links to.
- A regression that renames the section would leave every scanner+researcher
- landing on the page header instead of the policy text."""
+def test_security_txt_policy_points_at_a_document_that_exists(
+ unauthenticated_client,
+):
+ """Pin the Policy target to the document that actually holds the policy.
+
+ This previously asserted ``sentinel-command.com/security#vulnerability-
+ disclosure``, and its docstring claimed that page rendered an
+ ``id="vulnerability-disclosure"`` section. It never did — that URL and
+ every plausible variant returned 404 (checked 2026-09-09), so the test
+ was pinning a fiction while researchers following RFC 9116 found no
+ scope and no safe-harbour terms.
+
+ Asserting *presence* of a Policy line, as the test above does, is not
+ enough: a well-formed pointer at nothing still passes. This pins the
+ specific target so moving it is a deliberate act with a test to update.
+ """
body = unauthenticated_client.get("/.well-known/security.txt").text
policy_line = [
line for line in body.splitlines() if line.startswith("Policy:")
][0]
- assert "sentinel-command.com/security#vulnerability-disclosure" in policy_line
+ assert "github.com/SourceBox-LLC/Sentinel-Command/blob/master/SECURITY.md" in (
+ policy_line
+ )
# ── Public + cacheable for scanners ────────────────────────────────
diff --git a/docs/LAUNCH_HANDOFF.md b/docs/LAUNCH_HANDOFF.md
index 6e52e717..d3aa5945 100644
--- a/docs/LAUNCH_HANDOFF.md
+++ b/docs/LAUNCH_HANDOFF.md
@@ -47,7 +47,7 @@ charges don't post, the dev-mode badge shows in the UI, and the
-a sentinel-command
```
4. Verify the Clerk webhook endpoint
- `https://sentinel-command.com/api/webhooks/clerk` is
+ `https://app.sentinel-command.com/api/webhooks/clerk` is
registered in the production Clerk app and signing secret is set
(`CLERK_WEBHOOK_SECRET`). Test by upgrading a test org and
confirming the `Setting(org_plan="pro")` row shows up.
@@ -96,7 +96,7 @@ the answer for those.
the pre-rename brand and is *not* a verified sending domain — setting
`EMAIL_FROM_ADDRESS` to it would have failed every send.
3. Configure a webhook in Resend → endpoint
- `https://sentinel-command.com/api/webhooks/resend`. Copy the
+ `https://app.sentinel-command.com/api/webhooks/resend`. Copy the
signing secret (starts with `whsec_`).
4. Set the four Fly secrets:
```
diff --git a/docs/runbooks/DISASTER_RECOVERY.md b/docs/runbooks/DISASTER_RECOVERY.md
index 620c9e60..33341c5e 100644
--- a/docs/runbooks/DISASTER_RECOVERY.md
+++ b/docs/runbooks/DISASTER_RECOVERY.md
@@ -268,7 +268,7 @@ bash /app/scripts/restore_db.sh /data/backups/sentinel-.dump
# 4. Start the app and verify BEFORE deleting the pre-restore dump.
exit
fly machine start -a sentinel-command
-curl -fsS https://sentinel-command.com/api/health/ready
+curl -fsS https://app.sentinel-command.com/api/health/ready
```
Then sanity-check in the dashboard: an org loads, cameras list, a known
diff --git a/docs/runbooks/ON_CALL.md b/docs/runbooks/ON_CALL.md
index d5052465..f3de7e46 100644
--- a/docs/runbooks/ON_CALL.md
+++ b/docs/runbooks/ON_CALL.md
@@ -39,8 +39,8 @@ That last row matters: this runbook is for "the service is broken." If the *data
| Tool / link | Why |
|---|---|
-| https://sentinel-command.com/api/health | Liveness — is the process up? |
-| https://sentinel-command.com/api/health/detailed | DB ping latency, cache + queue depths |
+| https://app.sentinel-command.com/api/health | Liveness — is the process up? |
+| https://app.sentinel-command.com/api/health/detailed | DB ping latency, cache + queue depths |
| `fly logs -a sentinel-command` | Application stderr/stdout |
| `fly status -a sentinel-command` | Machine health + last deploy |
| `fly ssh console -a sentinel-command` | Shell into the live machine |
@@ -243,7 +243,7 @@ self-hosted section applies to them, not this scenario.
Check the "events" timeline for recent restarts.
2. **`fly status -a sentinel-postgres`** — is the *database* up? This is
the check that did not exist before the migration.
-3. `curl https://sentinel-command.com/api/health/detailed` —
+3. `curl https://app.sentinel-command.com/api/health/detailed` —
look at:
- `checks.database.status` and `latency_ms`
- `checks.disk.percent_used` and `checks.disk.status` (segments now,