Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ jobs:
# never show a stale version/date/DOI.
- name: Check docs metadata in sync with CITATION.cff
run: python3 scripts/build_docs.py --check
# Committed, no-secret leak guard: fail if the published page links to an
# external host not on the allowlist. A stray client/vendor/product domain
# reaching the published page is the leak shape this catches — without
# needing the private client-name list (Option A in the proposal, deferred).
- name: Check published page has no stray external domains
run: |
python3 scripts/check_external_domains.py --self-test
python3 scripts/check_external_domains.py
- name: Run conformance suite
run: python3 conformance/run.py
- name: Run conformance suite (strict PEG)
Expand Down
11 changes: 11 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ pre-commit:
if [ -x .venv/bin/python ]; then PY=.venv/bin/python; else PY=python3; fi
"$PY" scripts/build_docs.py --check

# Committed, no-secret leak guard mirrored in .github/workflows/ci.yml:
# block a staged docs/index.html that links to a non-allowlisted external
# host (a stray client/vendor/product domain — the leak shape, not a name).
# The private client-name list (Option A in the deny-scan proposal) is a
# separate, deferred layer; this one needs no secret and covers fresh clones.
external-domains:
glob: "docs/index.html"
run: |
if [ -x .venv/bin/python ]; then PY=.venv/bin/python; else PY=python3; fi
"$PY" scripts/check_external_domains.py
Comment on lines +59 to +63

# Public repo: scan staged content for AI-consultation process language.
# This is a backstop — the primary defense is agent awareness in the
# containing workspace. Workspace-specific codename and path patterns
Expand Down
100 changes: 100 additions & 0 deletions scripts/check_external_domains.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Guard the published spec page against stray external domains.

`docs/index.html` is published to GitHub Pages. A client / engagement / vendor
/ product domain reaching that page is a confidentiality leak — exactly the
class of leak that put `hellobernard.ai` live before anyone caught it.

This is the *committed, no-secret* leak defense: it fails if the published page
links to any external host not in ``ALLOWED_HOSTS``. It deliberately does NOT
try to catch arbitrary client *names* (a surname or codename matches no
structural shape, and the spec's own vocabulary — "collection", "provenance",
"gallery" — would false-positive on any art-market word list). Catching the
*shape* that actually leaks — a stray external domain — is the part that can
live safely in a public repo with zero dependence on a gitignored list.

Allowlist discipline: add a host here ONLY if it belongs on a public,
vendor-neutral standards page. A new host appearing in the page that you did
not deliberately allowlist is the signal this guard exists to raise.

python3 scripts/check_external_domains.py # check the real page
python3 scripts/check_external_domains.py --self-test # regression check
"""

from __future__ import annotations

import pathlib
import re
import sys

# Hosts the published page is allowed to link to. Matched case-insensitively,
# exact host or any subdomain (so `www.w3.org` matches `w3.org`).
#
# `hellobernard.ai` is the author's product domain, allowlisted here to reflect
# the page's *current* state — not an endorsement of keeping the bio link. If
# that link is ever dropped to keep the spec brand-neutral, remove the host
# here too so the guard re-tightens.
ALLOWED_HOSTS = frozenset(
{
"w3.org",
"creativecommons.org",
"doi.org",
"github.com",
"orcid.org",
"tymofiy.github.io",
"hellobernard.ai",
}
)

PAGE = pathlib.Path(__file__).resolve().parent.parent / "docs" / "index.html"
_HOST_RE = re.compile(r"https?://([A-Za-z0-9._-]+)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize scheme-relative external links

The guard promises to fail any non-allowlisted external host in docs/index.html, but this regex only matches URLs that explicitly include http:// or https://. In the CI path inspected in .github/workflows/ci.yml, the script is run directly, so a valid published link such as <a href="//client.example/path"> is still an external browser navigation while violations(...) returns [], letting CI and the pre-commit guard pass. Include scheme-relative URLs, or parse link attributes with a URL parser before applying the allowlist.

Useful? React with 👍 / 👎.



def _allowed(host: str) -> bool:
h = host.lower().rstrip(".")
return any(h == a or h.endswith("." + a) for a in ALLOWED_HOSTS)


def violations(text: str) -> list[str]:
"""External hosts in *text* that are not on the allowlist (sorted, unique)."""
return sorted({h.lower() for h in _HOST_RE.findall(text) if not _allowed(h.lower())})


def _self_test() -> int:
# A stray external domain must be flagged...
bad = violations('<a href="https://acme-auctioneers.example/x">x</a>')
assert bad == ["acme-auctioneers.example"], f"self-test: expected catch, got {bad}"
# ...while every allowlisted host (incl. a www. subdomain) must pass.
ok = violations(
"https://www.w3.org https://doi.org/10.x https://github.com/o/r "
"https://orcid.org/0000 https://tymofiy.github.io https://hellobernard.ai "
"https://creativecommons.org/licenses"
)
assert ok == [], f"self-test: allowlist over-flagged {ok}"
print("check_external_domains: self-test OK")
return 0


def main(argv: list[str]) -> int:
if "--self-test" in argv:
return _self_test()
if not PAGE.exists():
print(f"::error::{PAGE} not found")
return 1
bad = violations(PAGE.read_text(encoding="utf-8"))
Comment on lines +81 to +84
if bad:
# The offending host is already in the committed page, so naming it in
# the log leaks nothing new — it helps the author fix it fast.
print(
"::error::Published page links to non-allowlisted external "
f"domain(s): {', '.join(bad)}. If the link is intentional and "
"public-safe, add the host to ALLOWED_HOSTS in "
"scripts/check_external_domains.py; otherwise remove it — it may be "
"a client / vendor / engagement leak."
)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))