docs(m1): add security model and complete the readme first screen
This commit is contained in:
parent
0e3295f52b
commit
a9153e043e
5 changed files with 338 additions and 8 deletions
195
tests/test_public_surface.py
Normal file
195
tests/test_public_surface.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"""What this repository publishes, and what it must never publish (plan Step 14).
|
||||
|
||||
The remote is PUBLIC. That single fact decides both halves of this file.
|
||||
|
||||
The first half is presentation: a README first screen that says what the plugin
|
||||
is, an install block that covers both surfaces it actually runs on -- Claude
|
||||
Code and Claude Cowork, which install it in completely different ways -- and a
|
||||
`SECURITY.md` that states the trust model instead of implying it.
|
||||
|
||||
The second half is the one that matters more, and it is deliberately wider than
|
||||
the two files this step writes. The scan reads **every tracked file**, because
|
||||
the leak this repository is exposed to does not live in the README. It lives in
|
||||
`docs/cowork-probe.md`, which is committed here and is appended to by six later
|
||||
steps with measured facts from the operator's own machine and browser (risk
|
||||
H1). A two-file scan would never look at it. The first run of this scan found
|
||||
a real job-ad identifier there, published, which is the whole argument for
|
||||
scanning wide compressed into one finding.
|
||||
|
||||
Three deliberate scopings, stated rather than smuggled, because a rule that
|
||||
cannot be stated is a rule that will be quietly widened later:
|
||||
|
||||
1. **An absolute home path means a literal one.** `/Users/<name>/` and
|
||||
`/home/<name>/` are refused; `$HOME/Library/...` is not, because that is the
|
||||
correct way to write the same path and `scripts/cowork_probe_check.sh` has
|
||||
to name a macOS directory to read a log out of it.
|
||||
2. **Infrastructure hosts are allowed by name.** The Forgejo host this
|
||||
repository lives on and the badge service in the README are addresses the
|
||||
published repository cannot function without. Every other host in a URL that
|
||||
points at a resource must be under `.example` (RFC 2606).
|
||||
3. **`tests/test_fixture_hygiene.py` is exempt from the URL rule**, and only
|
||||
from that one. It seeds a synthetic job-board URL as a canary to prove its
|
||||
own scan can find, and a scanner cannot scan another scanner's canary
|
||||
without one of the two being wrong.
|
||||
|
||||
The scan reports its denominator on every run, and proves it can find before it
|
||||
reports that it found nothing. "Found nothing" is a measurement; without a
|
||||
denominator and a positive control it is indistinguishable from a scan whose
|
||||
patterns were wrong.
|
||||
|
||||
Style note: this file follows tests/test_fixture_hygiene.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import warnings
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
README = os.path.join(REPO, "README.md")
|
||||
SECURITY = os.path.join(REPO, "SECURITY.md")
|
||||
|
||||
|
||||
class OffentligFlateSkannet(UserWarning):
|
||||
"""Carries the denominator into the run's output, where a reader sees it."""
|
||||
|
||||
|
||||
#: A literal home directory with a user name in it. Written as a pattern rather
|
||||
#: than a literal so this file does not match itself.
|
||||
HJEMMESTI = re.compile(r"/(?:Users|home)/[A-Za-z0-9._-]+/")
|
||||
|
||||
#: A URL pointing at a resource -- with a path, so a bare domain named in prose
|
||||
#: is not a finding. Both spellings the corpus uses are covered.
|
||||
ADRESSE = re.compile(r"(?:https?://|\bwww\.)([A-Za-z0-9.-]+\.[A-Za-z]{2,})(/[^\s`'\")\]]*)")
|
||||
|
||||
#: Hosts the published repository cannot function without.
|
||||
INFRASTRUKTUR = ("git.fromaitochitta.com", "img.shields.io")
|
||||
|
||||
#: Exempt from the URL rule only, and for the reason in this module's docstring.
|
||||
URL_UNNTAK = ("tests/test_fixture_hygiene.py",)
|
||||
|
||||
#: The invariants that are live at M1. M3 and M6 extend the list as their
|
||||
#: surfaces land; these four are true from the first commit.
|
||||
IKKE_FORHANDLINGSBART = (
|
||||
"read-only",
|
||||
"credential",
|
||||
"local",
|
||||
"guard",
|
||||
)
|
||||
|
||||
|
||||
def sporede_filer():
|
||||
ut = subprocess.run(
|
||||
["git", "-C", REPO, "ls-files"], capture_output=True, text=True
|
||||
)
|
||||
assert ut.returncode == 0, ut.stderr
|
||||
return [p for p in ut.stdout.split("\n") if p]
|
||||
|
||||
|
||||
def skann(rot, filer):
|
||||
"""Return (findings, files read, bytes read) for ``filer`` under ``rot``."""
|
||||
funn = []
|
||||
lest = 0
|
||||
bytes_lest = 0
|
||||
for relativ in filer:
|
||||
full = os.path.join(rot, relativ)
|
||||
if not os.path.isfile(full):
|
||||
continue
|
||||
try:
|
||||
with open(full, "r", encoding="utf-8") as handle:
|
||||
tekst = handle.read()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
lest += 1
|
||||
bytes_lest += len(tekst.encode("utf-8"))
|
||||
|
||||
for treff in HJEMMESTI.finditer(tekst):
|
||||
funn.append((relativ, "absolutt hjemmesti", treff.group(0)))
|
||||
|
||||
if relativ in URL_UNNTAK:
|
||||
continue
|
||||
for vert, _sti in ADRESSE.findall(tekst):
|
||||
if vert in INFRASTRUKTUR:
|
||||
continue
|
||||
if vert.endswith(".example"):
|
||||
continue
|
||||
funn.append((relativ, "adresse utenfor .example", vert))
|
||||
return funn, lest, bytes_lest
|
||||
|
||||
|
||||
def test_the_readme_install_block_covers_both_surfaces():
|
||||
with open(README, "r", encoding="utf-8") as handle:
|
||||
tekst = handle.read()
|
||||
|
||||
assert "## Install" in tekst, "the README has no install section"
|
||||
installer = tekst.split("## Install", 1)[1]
|
||||
installer = installer.split("\n## ", 1)[0]
|
||||
|
||||
assert "Claude Code" in installer, (
|
||||
"the install block does not name Claude Code, the development surface"
|
||||
)
|
||||
assert "Cowork" in installer, (
|
||||
"the install block does not name Cowork, which installs by uploading an "
|
||||
"archive and not by a marketplace command -- a reader following the "
|
||||
"Claude Code lines there gets nowhere"
|
||||
)
|
||||
assert "scripts/package_plugin.sh" in installer, (
|
||||
"the Cowork route needs the packaging command; never a recursive zip "
|
||||
"of the repository root"
|
||||
)
|
||||
assert "scripts/bootstrap.sh" in installer, (
|
||||
"without the bootstrap there is no environment for the host MCP server "
|
||||
"to run on in an installed copy"
|
||||
)
|
||||
# The honesty clause: the guard is installed from a self-hosted host a
|
||||
# third party may not reach. A public install block that fails at its first
|
||||
# step for everyone but its author has to say so.
|
||||
assert "git.fromaitochitta.com" in tekst
|
||||
assert "## Non-goals" in tekst, "the README states no non-goals"
|
||||
|
||||
|
||||
def test_security_md_states_the_trust_model():
|
||||
assert os.path.isfile(SECURITY), "SECURITY.md does not exist"
|
||||
with open(SECURITY, "r", encoding="utf-8") as handle:
|
||||
tekst = handle.read()
|
||||
lav = tekst.lower()
|
||||
for begrep in IKKE_FORHANDLINGSBART:
|
||||
assert begrep in lav, (
|
||||
"SECURITY.md never mentions %r; it is one of the four invariants "
|
||||
"that are live at M1" % begrep
|
||||
)
|
||||
# The trust boundary is the write, not the read. Saying only "we use a
|
||||
# guard" leaves the reader guessing where it sits.
|
||||
assert "guard_ingest.py" in tekst
|
||||
assert "M3" in tekst or "M6" in tekst, (
|
||||
"SECURITY.md must say which surfaces are not live yet, or it reads as "
|
||||
"a description of a finished system"
|
||||
)
|
||||
|
||||
|
||||
def test_no_tracked_file_carries_a_home_path_or_a_real_address(tmp_path):
|
||||
# Prove the scan can find before believing that it found nothing.
|
||||
kanari = tmp_path / "kanari.md"
|
||||
kanari.write_text(
|
||||
"Se /Users/enperson/hemmelig/mappe/ og https://ekte-jobbportal.no/annonse/12345\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
kanarifunn, kanarilest, _ = skann(str(tmp_path), ["kanari.md"])
|
||||
assert kanarilest == 1
|
||||
assert len(kanarifunn) == 2, (
|
||||
"the scan cannot find what it exists to find; it reported %r"
|
||||
% (kanarifunn,)
|
||||
)
|
||||
|
||||
filer = sporede_filer()
|
||||
funn, lest, bytes_lest = skann(REPO, filer)
|
||||
warnings.warn(
|
||||
"public surface scan: %d tracked files, %d bytes read; %d finding(s)"
|
||||
% (lest, bytes_lest, len(funn)),
|
||||
OffentligFlateSkannet,
|
||||
)
|
||||
assert lest > 0, "the scan read no files; git ls-files returned %d paths" % len(filer)
|
||||
assert funn == [], (
|
||||
"tracked files carry paths or addresses that must not be published:\n%s"
|
||||
% "\n".join(" %s: %s -- %s" % rad for rad in funn)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue