189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
"""The fixture corpus is anonymised, and this test is what makes that a fact
|
|
rather than an intention (plan Step 7).
|
|
|
|
This repository has a PUBLIC remote. A fixture carrying the operator's real
|
|
name, home address, employer or a real job-board URL would be published the
|
|
moment it is pushed, and no later cleanup un-publishes it. So the corpus is
|
|
scanned on every run, before it grows past the point where anyone reads it
|
|
whole.
|
|
|
|
The scan reports its denominator, and that is the point rather than a
|
|
courtesy. "Found nothing" is a measurement, and a measurement without a
|
|
denominator is indistinguishable from a scan that ran over an empty
|
|
directory, matched nothing because its patterns were wrong, or never ran at
|
|
all. So every run says how many files and how many bytes it looked at, and
|
|
`test_the_scan_can_actually_find` seeds a canary into a temporary corpus and
|
|
requires the scan to catch it -- a scanner that cannot find is not evidence of
|
|
absence.
|
|
|
|
Two deliberate deviations from the plan text, both stated rather than
|
|
smuggled:
|
|
|
|
1. The operator's home address is not hard-coded here. Writing a real home
|
|
address into a deny list on a public remote would publish the very thing
|
|
the list exists to keep out. Private tokens go in the environment variable
|
|
`JOBBSOK_HYGIENE_DENY_EXTRA` (newline-separated) instead, and the scan
|
|
picks them up if it is set. The name and employer ARE listed literally,
|
|
because both already appear in every commit's author metadata on the same
|
|
public remote -- listing them costs nothing that is not already public and
|
|
buys a check that actually runs.
|
|
2. The provenance-header rule covers the directories this step seeds.
|
|
`cowork-probe/` is exempt by name: it is the Step 1 probe rig, machine
|
|
config with no person in it, and it predates this rule.
|
|
|
|
Style note: this file follows tests/test_helpers_selfcheck.py.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import warnings
|
|
|
|
FIXTURES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures")
|
|
|
|
#: Directories seeded by this step, and the ones the provenance rule governs.
|
|
CORPUS_DIRS = ("profiles", "listings")
|
|
|
|
#: Exempt from the provenance rule, with the reason in the module docstring.
|
|
PROVENANCE_EXEMPT = ("cowork-probe",)
|
|
|
|
#: Literal tokens that must never appear. Real employer names and real job
|
|
#: boards; the person tokens are already public in the commit metadata.
|
|
DENY_TOKENS = (
|
|
"Kjell Tore",
|
|
"Guttormsen",
|
|
"Statens vegvesen",
|
|
"vegvesen",
|
|
"finn.no",
|
|
"arbeidsplassen.nav.no",
|
|
"jobbnorge.no",
|
|
"linkedin.com",
|
|
"indeed.com",
|
|
)
|
|
|
|
#: Extra deny tokens supplied at run time, one per line. This is where a home
|
|
#: address belongs -- checked, never committed.
|
|
DENY_EXTRA_ENV = "JOBBSOK_HYGIENE_DENY_EXTRA"
|
|
|
|
#: Every host and mail domain in the corpus must sit under the reserved
|
|
#: `.example` TLD (RFC 2606), so nothing in here can resolve to a real place.
|
|
SAFE_TLD = ".example"
|
|
|
|
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,})")
|
|
URL_HOST_RE = re.compile(r"https?://([A-Za-z0-9.:-]+)")
|
|
|
|
PROVENANCE_MARKER = "Proveniens:"
|
|
|
|
|
|
class KorpusSkannet(UserWarning):
|
|
"""Not a defect: the denominator the hygiene scan actually looked at.
|
|
|
|
Emitted as a warning because pytest shows warnings even under ``-q``,
|
|
while a passing test's stdout is captured and never reaches the operator.
|
|
"""
|
|
|
|
|
|
def deny_tokens():
|
|
tokens = list(DENY_TOKENS)
|
|
extra = os.environ.get(DENY_EXTRA_ENV, "")
|
|
tokens.extend(line.strip() for line in extra.splitlines() if line.strip())
|
|
return tokens
|
|
|
|
|
|
def corpus_files(root):
|
|
found = []
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames.sort()
|
|
for name in sorted(filenames):
|
|
found.append(os.path.join(dirpath, name))
|
|
return found
|
|
|
|
|
|
def scan(root):
|
|
"""Scan every file under ``root`` and report findings AND the denominator."""
|
|
tokens = deny_tokens()
|
|
findings = []
|
|
files = corpus_files(root)
|
|
total_bytes = 0
|
|
|
|
for path in files:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
text = handle.read()
|
|
total_bytes += len(text.encode("utf-8"))
|
|
relative = os.path.relpath(path, root)
|
|
|
|
lowered = text.lower()
|
|
for token in tokens:
|
|
if token.lower() in lowered:
|
|
findings.append((relative, "deny-token %r" % token))
|
|
for domain in EMAIL_RE.findall(text):
|
|
if not domain.lower().endswith(SAFE_TLD):
|
|
findings.append((relative, "mail domain %r is not %s" % (domain, SAFE_TLD)))
|
|
for host in URL_HOST_RE.findall(text):
|
|
if not host.lower().split(":")[0].endswith(SAFE_TLD):
|
|
findings.append((relative, "url host %r is not %s" % (host, SAFE_TLD)))
|
|
|
|
return {"files": len(files), "bytes": total_bytes, "findings": findings}
|
|
|
|
|
|
def report(result, root):
|
|
message = "hygiene scan: %d files, %d bytes scanned under %s; %d finding(s)" % (
|
|
result["files"],
|
|
result["bytes"],
|
|
os.path.basename(root.rstrip(os.sep)),
|
|
len(result["findings"]),
|
|
)
|
|
print(message)
|
|
warnings.warn(message, KorpusSkannet)
|
|
return message
|
|
|
|
|
|
def test_every_corpus_fixture_carries_a_provenance_header():
|
|
checked = 0
|
|
for name in CORPUS_DIRS:
|
|
directory = os.path.join(FIXTURES, name)
|
|
assert os.path.isdir(directory), "corpus directory %r is missing" % name
|
|
files = corpus_files(directory)
|
|
assert files, "corpus directory %r is empty" % name
|
|
for path in files:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
text = handle.read()
|
|
assert PROVENANCE_MARKER in text, "%s has no %r header" % (path, PROVENANCE_MARKER)
|
|
checked += 1
|
|
# Eight profiles and eight listings, per the plan. A rule that checked
|
|
# zero files would pass just as quietly.
|
|
assert checked == 16, "expected 16 corpus fixtures, provenance-checked %d" % checked
|
|
|
|
|
|
def test_no_deny_listed_token_appears_anywhere_in_the_corpus():
|
|
result = scan(FIXTURES)
|
|
assert result["findings"] == [], "hygiene findings: %r" % (result["findings"],)
|
|
|
|
|
|
def test_the_scan_states_the_denominator_it_looked_at():
|
|
result = scan(FIXTURES)
|
|
message = report(result, FIXTURES)
|
|
assert result["files"] >= 16
|
|
assert result["bytes"] > 0
|
|
assert str(result["files"]) in message and "bytes scanned" in message
|
|
|
|
|
|
def test_the_scan_can_actually_find(tmp_path):
|
|
# A canary corpus, deliberately dirty. It lives in tmp_path and never in
|
|
# tests/fixtures, because a canary committed to a public remote is the
|
|
# leak it is supposed to warn about.
|
|
(tmp_path / "cv.md").write_text(
|
|
"Proveniens: kanarifugl.\nTidligere hos Statens vegvesen.\n", encoding="utf-8"
|
|
)
|
|
(tmp_path / "kontakt.md").write_text(
|
|
"Proveniens: kanarifugl.\nSkriv til rekruttering@fjordtek.no\n", encoding="utf-8"
|
|
)
|
|
(tmp_path / "lenke.md").write_text(
|
|
"Proveniens: kanarifugl.\nSe https://www.finn.no/job/123\n", encoding="utf-8"
|
|
)
|
|
|
|
result = scan(str(tmp_path))
|
|
reasons = " ".join(reason for _path, reason in result["findings"])
|
|
assert result["files"] == 3
|
|
assert "vegvesen" in reasons
|
|
assert "fjordtek.no" in reasons
|
|
assert "finn.no" in reasons
|