mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
Merge f-0213-secure (fix 0855796e6) into f-0213-int
Conflicts in strata_cli/bench_facts.py (the redactor), three hunks: kept every side. The patterns stay QATUI's (the key and the names are matched as whole tokens, a hyphen next to the key does not hide it, the names are not touched inside a label the function wrote), each entry carries SECURE's third field (is a login or host name) for redact_document, and the host names come from socket.gethostname() and sysinfo.node() (LIFE), both marked as names.
This commit is contained in:
commit
792b626980
3 files changed
+68
-11
No files matched your search
@@ -53,9 +53,9 @@ def redactor(key: str = ""):
|
||||
The key is replaced only where it stands as a whole token and in its own case (#4483): a short key is also a common word or a part of a hash. A hyphen
|
||||
next to it does not make it part of a longer word (`pack-<key>`, `<key>-1`: the key was a leak there); letters, digits and the underscore do.
|
||||
The names (user, host) are not touched inside a label this function wrote: redacting a text twice gives the same text (a user called "user" gave `<<user>>`)."""
|
||||
subs: list[tuple[re.Pattern, str]] = []
|
||||
subs: list[tuple[re.Pattern, str, bool]] = [] # (pattern, label, is a login or host name)
|
||||
|
||||
def add(value, label, word=False, exact=False):
|
||||
def add(value, label, word=False, exact=False, name=False):
|
||||
if value and len(str(value)) >= 4:
|
||||
text = str(value)
|
||||
for form in sorted({text, text.replace("\\", "/"), text.replace("/", "\\")}, key=len, reverse=True):
|
||||
@@ -64,17 +64,17 @@ def redactor(key: str = ""):
|
||||
pattern = r"(?<!\w)" + pattern + r"(?!\w)"
|
||||
elif word:
|
||||
pattern = r"(?<![\w<-])" + pattern + r"(?![\w>-])"
|
||||
subs.append((re.compile(pattern, 0 if exact else re.I), label))
|
||||
subs.append((re.compile(pattern, 0 if exact else re.I), label, name))
|
||||
|
||||
add(ROOT, "<strata>")
|
||||
add(Path.home(), "<home>")
|
||||
add(tempfile.gettempdir(), "<tmp>")
|
||||
try:
|
||||
add(getpass.getuser(), "<user>", True)
|
||||
add(getpass.getuser(), "<user>", True, name=True)
|
||||
except Exception: # noqa: BLE001 - no user name is not an error
|
||||
pass
|
||||
add(socket.gethostname(), "<host>", True)
|
||||
add(sysinfo.node(), "<host>", True)
|
||||
add(socket.gethostname(), "<host>", True, name=True)
|
||||
add(sysinfo.node(), "<host>", True, name=True)
|
||||
add(usable_key(key), "<key>", word=True, exact=True)
|
||||
subs.sort(key=lambda item: -len(item[0].pattern))
|
||||
path_user = re.compile(r"(?i)([A-Za-z]:)?([\\/])(Users|home)([\\/])(?!<)[^\\/\s\"':;,]+")
|
||||
@@ -82,17 +82,52 @@ def redactor(key: str = ""):
|
||||
r"|169\.254)\.\d{1,3}\.\d{1,3}\b")
|
||||
bearer = re.compile(r"(?i)(bearer\s+)[A-Za-z0-9._~+/=-]{8,}")
|
||||
|
||||
def clean(text):
|
||||
def clean(text, names=True):
|
||||
"""names=False leaves a login or host name that is also an ordinary word ("user", "test", "admin") alone: for fixed
|
||||
schema text, where the word is not the machine's name; paths, addresses and the key are still replaced."""
|
||||
if not isinstance(text, str):
|
||||
return text
|
||||
for pattern, label in subs:
|
||||
text = pattern.sub(label, text)
|
||||
for pattern, label, is_name in subs:
|
||||
if names or not is_name:
|
||||
text = pattern.sub(label, text)
|
||||
text = path_user.sub(lambda m: f"{m.group(1) or ''}{m.group(2)}{m.group(3)}{m.group(4)}<user>", text)
|
||||
text = bearer.sub(r"\1<key>", text)
|
||||
return private_ip.sub("<ip>", text)
|
||||
clean.structural = lambda text: clean(text, names=False)
|
||||
return clean
|
||||
|
||||
|
||||
def redact_values(value, clean):
|
||||
"""redact_object for free text: the strings are replaced, the keys stay."""
|
||||
if isinstance(value, dict):
|
||||
return {k: redact_values(v, clean) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [redact_values(v, clean) for v in value]
|
||||
return clean(value) if isinstance(value, str) else value
|
||||
|
||||
|
||||
def redact_document(doc: dict, clean) -> dict:
|
||||
"""The benchmark report as results.json carries it (#4484): the text an operator or a server wrote (hardware, model and configuration
|
||||
descriptions, error messages, engine options, request checks) gets the full redaction; everything else is fixed schema text (field
|
||||
names, enum values, status reasons) and only loses paths, addresses and the key, so a login called "user" or "test" does not turn
|
||||
the report's own words into <user>."""
|
||||
structural = getattr(clean, "structural", clean)
|
||||
out = redact_object(doc, structural)
|
||||
for block in ("hardware", "model", "configuration"):
|
||||
if block in doc:
|
||||
out[block] = redact_values(doc[block], clean)
|
||||
for i, record in enumerate(doc.get("requests") or []):
|
||||
if isinstance(record, dict) and isinstance(record.get("error"), dict):
|
||||
out["requests"][i]["error"] = {**out["requests"][i]["error"],
|
||||
"message": redact_values(record["error"].get("message"), clean)}
|
||||
extensions = doc.get("extensions")
|
||||
if isinstance(extensions, dict):
|
||||
for field in ("other_engine_options", "request_checks"):
|
||||
if field in extensions:
|
||||
out["extensions"][field] = redact_values(extensions[field], clean)
|
||||
return out
|
||||
|
||||
|
||||
def redact_object(value, clean):
|
||||
if isinstance(value, dict):
|
||||
return {clean(k) if isinstance(k, str) else k: redact_object(v, clean) for k, v in value.items()}
|
||||
|
||||
@@ -1007,7 +1007,7 @@ def run(target: Target, out: Path | None, *, out_root: Path | None = None, optio
|
||||
"request_checks": {rid: v for rid, v in extras.items()},
|
||||
"optional_stage_skipped": skipped_optional,
|
||||
"api_key_sent": bool(target.key)}}
|
||||
doc = facts.redact_object(doc, redact) # results.json gets what the README and the other files get (#4484)
|
||||
doc = facts.redact_document(doc, redact) # results.json gets what the README and the other files get (#4484)
|
||||
if doc["run"]["optional_duration_s"] is None:
|
||||
why.add("/run/optional_duration_s", "the optional concurrency stage was not selected" if not optional_concurrency
|
||||
else "the optional concurrency stage did not run")
|
||||
|
||||
@@ -580,7 +580,9 @@ class RunTests(StandardBase):
|
||||
self.server.script["performance-prompt-4k-r2"] = {"cached": spec["input_tokens_frozen"] - 10}
|
||||
# the shared template prefix alone is allowed
|
||||
self.server.script["performance-prompt-4k-r1"] = {"cached": 40}
|
||||
doc, _ = self.run_standard()
|
||||
# whatever the login of the machine running the test is: "user" is an ordinary word of the report, not a name here
|
||||
with mock.patch.object(bench_facts.getpass, "getuser", return_value="no-such-login"):
|
||||
doc, _ = self.run_standard()
|
||||
self.assertEqual(doc["run"]["status"], "nonconforming")
|
||||
row = self.summary(doc, "prompt-4k", "prompt_tps")
|
||||
self.assertEqual(row["n"], 2)
|
||||
@@ -859,6 +861,26 @@ class CommandLineTests(StandardBase):
|
||||
self.assertIn("<ip>", doc["configuration"]["track_notes"])
|
||||
self.assertEqual(doc["request_counts"]["performance"]["planned"] > 0, True) # counters are untouched
|
||||
|
||||
def test_a_login_that_is_an_ordinary_word_does_not_rewrite_the_reports_own_words(self):
|
||||
"""INT3 on l620, where the login is exactly "user": the report's fixed text (status reasons, enum values, field names) came out as
|
||||
"<user>", while the operator's own text still loses the name."""
|
||||
spec = corpus_specs()["performance-prompt-4k-r2"]
|
||||
out = self.root / "wordlogin"
|
||||
with mock.patch.object(bench_facts.getpass, "getuser", return_value="user"), \
|
||||
mock.patch.object(bench_facts.socket, "gethostname", return_value="summaries"), \
|
||||
mock.patch.object(bench_facts.platform, "node", return_value="summaries"):
|
||||
self.server.script["performance-prompt-4k-r2"] = {"cached": spec["input_tokens_frozen"] - 10}
|
||||
code, _, errors = self.invoke("--url", self.server.url, "--out", str(out), "--track", "custom",
|
||||
"--track-notes", "ask the user at summaries time")
|
||||
self.assertEqual(code, 6, errors)
|
||||
doc = bs.load(out)
|
||||
self.assertEqual(bs.validate(doc), [])
|
||||
self.assertTrue(any("unexpected reuse of the user payload in performance-prompt-4k-r2" in r
|
||||
for r in doc["extensions"]["status_reasons"]), doc["extensions"]["status_reasons"])
|
||||
self.assertEqual(doc["configuration"]["track_notes"], "ask the <user> at <host> time")
|
||||
text = (out / "results.json").read_text(encoding="utf-8")
|
||||
self.assertIn('"summaries"', text) # a field name that equals the host name stays a field name
|
||||
|
||||
def test_a_credential_of_none_is_no_key_and_the_word_is_left_alone(self):
|
||||
"""#4483: STRATA_KEY=none was sent as "Bearer none" and the word was replaced wherever it occurred."""
|
||||
out = self.root / "nonekey"
|
||||
|
||||
Reference in new issue
Block a user