Skip to content
Open
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
33 changes: 14 additions & 19 deletions loopx/capabilities/decision_context/packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
import hashlib
import json
import math
import re
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any

from ...control_plane.runtime.public_safety import (
REMOTE_LOCATION_SURFACE_PATTERN,
SECRET_LIKE_SURFACE_PATTERN,
find_public_safe_local_path,
)
from ...public_safe_text import CREDENTIAL_CATEGORIES
from ...public_safe_text import COMPACT_TOKEN_PATTERN as _TOKEN_RE
from ...public_safe_text import classify_private_text

DECISION_EVIDENCE_PACKET_SCHEMA_VERSION = "decision_evidence_packet_v0"
DECISION_PROPOSAL_SCHEMA_VERSION = "decision_proposal_v0"
Expand Down Expand Up @@ -45,22 +45,13 @@
# by find_public_safe_local_path; this site keeps its own rejection message and
# length limit for whatever the owner recognizes.
#
# Local threshold policy only: the credential *shapes* are decided once by
# SECRET_LIKE_SURFACE_PATTERN, which this site consults in addition to this list.
_CREDENTIAL_RE = re.compile(
"(?i)("
+ "|".join(
[
"Author" + "ization:",
"Bear" + r"er\s+[A-Za-z0-9._-]+",
"api" + r"[_-]?key",
"pass" + "word",
"sec" + "ret",
"begin " + r"(?:rsa |open)?private key",
]
)
+ ")"
)
# Refs #5136, direction 1: one categorized call decides the credential question.
# The alternation list this site kept beside SECRET_LIKE_SURFACE_PATTERN was a
# shape list rather than the "local threshold policy" its comment claimed, and it
# was byte-identical to the copy in material_lifecycle/_validation.py. The owner
# covers the one spelling that list reached and its category arms do not -- a
# credential label glued into a field name -- through the
# ``include_compound_field_assignment`` opt-in this face passes.
_UNSAFE_FIELDS = {
"api_key",
"content",
Expand All @@ -85,7 +76,11 @@ def _compact_text(value: Any, *, field: str, max_len: int = 320) -> str:
raise ValueError(f"{field} must not contain a local path")
if REMOTE_LOCATION_SURFACE_PATTERN.search(text):
raise ValueError(f"{field} must use an opaque source reference, not a raw URL")
if SECRET_LIKE_SURFACE_PATTERN.search(text) or _CREDENTIAL_RE.search(text):
if classify_private_text(
text,
categories=CREDENTIAL_CATEGORIES,
include_compound_field_assignment=True,
):
raise ValueError(f"{field} contains a credential-like value")
return text

Expand Down
32 changes: 13 additions & 19 deletions loopx/capabilities/material_lifecycle/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,28 @@

import hashlib
import json
import re
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any

from ...control_plane.runtime.public_safety import (
REMOTE_LOCATION_SURFACE_PATTERN,
SECRET_LIKE_SURFACE_PATTERN,
find_public_safe_local_path,
)
from ...public_safe_text import CREDENTIAL_CATEGORIES
from ...public_safe_text import COMPACT_TOKEN_PATTERN as _TOKEN_RE
from ...public_safe_text import classify_private_text

# Refs #5136, direction 3: "does this text carry a local path?" is decided once
# by find_public_safe_local_path; this site keeps its own rejection message and
# length limit for whatever the owner recognizes.
#
# Local threshold policy only: the credential *shapes* are decided once by
# SECRET_LIKE_SURFACE_PATTERN, which this site consults in addition to this list.
_CREDENTIAL_RE = re.compile(
"(?i)("
+ "|".join(
[
"Author" + "ization:",
"Bear" + r"er\s+[A-Za-z0-9._-]+",
"api" + r"[_-]?key",
"pass" + "word",
"sec" + "ret",
"begin " + r"(?:rsa |open)?private key",
]
)
+ ")"
)
# Refs #5136, direction 1: one categorized call decides the credential question.
# The alternation list this site kept beside SECRET_LIKE_SURFACE_PATTERN was a
# shape list rather than the "local threshold policy" its comment claimed, and it
# was byte-identical to the copy in decision_context/packets.py. The owner covers
# the one spelling that list reached and its category arms do not -- a credential
# label glued into a field name -- via ``include_compound_field_assignment``.
UNSAFE_FIELDS = {
"api_key",
"content",
Expand All @@ -61,7 +51,11 @@ def compact_text(value: Any, *, field: str, max_len: int = 320) -> str:
raise ValueError(f"{field} must not contain a local path")
if REMOTE_LOCATION_SURFACE_PATTERN.search(text):
raise ValueError(f"{field} must use an opaque reference, not a raw URL")
if SECRET_LIKE_SURFACE_PATTERN.search(text) or _CREDENTIAL_RE.search(text):
if classify_private_text(
text,
categories=CREDENTIAL_CATEGORIES,
include_compound_field_assignment=True,
):
raise ValueError(f"{field} contains a credential-like value")
return text

Expand Down
54 changes: 53 additions & 1 deletion loopx/public_safe_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,37 @@
re.I,
)

# A credential label reached inside a field name rather than as a free-standing
# word: ``db_password = 'S3cret!value'``, ``password_hash=Qwerty1234567890``,
# ``client_secret: abcdef123456``. Every label arm above anchors the label with
# ``\\b``, and ``_`` is a word character, so a label glued to a field-name prefix
# or suffix is invisible to all of them -- the two capability faces caught that
# spelling with a substring rule of their own (Refs #5136, direction 1).
#
# The value carries no test, which is what the free-standing assignment arm above
# already does: an operator beside a credential label states an assignment, so a
# short or quoted value such as ``client_secret="hunter"`` cannot be released by a
# digit or word-length accident (Refs #5136, direction 2; those rows are the
# direction-4 counterexamples the migrated callers still lacked). The residual
# runs the other way: the field-name suffix also absorbs prose that ends on the
# label's plural before an operator, ``secrets:`` included, which the
# free-standing arm's ``\\b`` does not reach. That is why this arm stays an
# opt-in rather than a member of the ``credential`` category.
_COMPOUND_LABEL_SOURCE = "pass" + r"word|sec" + r"ret|api" + r"[_-]?key"
COMPOUND_CREDENTIAL_FIELD_ASSIGNMENT_PATTERN = re.compile(
r"[A-Za-z0-9_]*(?:" + _COMPOUND_LABEL_SOURCE + r")[A-Za-z0-9_]*[\"']?\s*[:=]",
re.IGNORECASE,
)
# The credential half of the rule, on its own, named so the two capability faces
# that ask it share one definition instead of each restating a category pair. Both
# categories are listed rather than subtracted from `ALL_CATEGORIES`:
# `credential_word` is one of the two, and a policy written as a difference would
# drop it -- loosening a face that never asked to be loosened -- the next time a
# category is added.
CREDENTIAL_CATEGORIES: frozenset[str] = frozenset(
{CATEGORY_CREDENTIAL, CATEGORY_CREDENTIAL_WORD}
)

# Refs #5136: relocated here from control_plane/runtime/public_safety.py so a
# single owner defines each shape. public_safety re-exports these names, so its
# ~8 direct importers and 30+ recursive-validation callers are unchanged. This
Expand Down Expand Up @@ -514,6 +545,7 @@ def classify_private_text(
*,
categories: frozenset[str] = ALL_CATEGORIES,
include_path_gaps: bool = False,
include_compound_field_assignment: bool = False,
) -> PrivateTextMatch | None:
"""Return the first recognized private-text match within ``categories``.

Expand All @@ -527,6 +559,12 @@ def classify_private_text(
home-relative (``~/``) and ``path:``-prefixed local references. It defaults
to False so this consolidation does not silently tighten any surface that
has not chosen the wider policy.

``include_compound_field_assignment`` opts a surface into
``COMPOUND_CREDENTIAL_FIELD_ASSIGNMENT_PATTERN``. It is an opt-in rather than
a ``credential`` category member for the reason stated on that constant: a
category addition would widen the four migrated text owners and the
publication tier by absence, which is a per-face decision nobody has made.
"""

if not value:
Expand All @@ -543,6 +581,16 @@ def classify_private_text(
return PrivateTextMatch(
CATEGORY_LOCAL_PATH, "local path behind a relative/prefixed form", pattern
)
if (
include_compound_field_assignment
and CATEGORY_CREDENTIAL in categories
and COMPOUND_CREDENTIAL_FIELD_ASSIGNMENT_PATTERN.search(value)
):
return PrivateTextMatch(
CATEGORY_CREDENTIAL,
"credential field name behind an assignment operator",
COMPOUND_CREDENTIAL_FIELD_ASSIGNMENT_PATTERN,
)
return None


Expand All @@ -551,12 +599,16 @@ def matches_private_text_policy(
*,
categories: frozenset[str] = ALL_CATEGORIES,
include_path_gaps: bool = False,
include_compound_field_assignment: bool = False,
) -> bool:
"""True when ``value`` is recognized within the named policy's categories."""

return (
classify_private_text(
value, categories=categories, include_path_gaps=include_path_gaps
value,
categories=categories,
include_path_gaps=include_path_gaps,
include_compound_field_assignment=include_compound_field_assignment,
)
is not None
)
Loading