195 lines
7.8 KiB
Python
195 lines
7.8 KiB
Python
"""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)
|
|
)
|