feat(active-content,okf): bound the last two detection surfaces
`scan_active_content` called directly and `okf.link_graph` were the two surfaces still reading attacker-supplied text with no cap — the first reached by an adapter that wants the active-content classes alone, the second running a `findall` over every body in a bundle. Both are detection-shaped, so they truncate and flag rather than raise the way the transform surfaces do: what a detector shortens is its own coverage, not the caller's content. Truncation is only honest if it is visible, so neither goes quiet: the scanner emits `active:oversize-input` (LLM10), and `link_graph` records `(from_id, body_length)` in `LinkGraphResult.truncated` — the field that lets a caller tell "no links past here" from "no links read past here". Reached through `scan_output`, the text is already under that surface's cap and `max_scan_chars` is now passed down, so the flag is raised once, there.
This commit is contained in:
parent
0bf07295c2
commit
b90233481a
6 changed files with 111 additions and 22 deletions
|
|
@ -60,12 +60,13 @@ from urllib.parse import urlsplit
|
|||
from .calibration import (
|
||||
ACTIVE_CONTENT_ORDINARY_SEVERITY as _ORDINARY_SEVERITY,
|
||||
ACTIVE_CONTENT_SEVERITY as _SEVERITY,
|
||||
MAX_SCAN_CHARS,
|
||||
URL_OPAQUE_ENTROPY_H as _OPAQUE_H,
|
||||
URL_OPAQUE_HEX_MIN_LEN as _OPAQUE_HEX_LEN,
|
||||
URL_OPAQUE_MIN_LEN as _OPAQUE_MIN_LEN,
|
||||
)
|
||||
from .entropy import is_hex_blob, shannon_entropy, try_decode_base64
|
||||
from .report import Finding, Report, Source
|
||||
from .report import Finding, Report, Severity, Source
|
||||
|
||||
# --- URL defang (shared primitive) -------------------------------------------
|
||||
# Rewrite a URL to a form no renderer will resolve, while keeping it readable.
|
||||
|
|
@ -267,15 +268,35 @@ def is_ordinary_url(url: str) -> bool:
|
|||
# shares them.
|
||||
|
||||
|
||||
def scan_active_content(text: str, source: Source = Source.OUTPUT) -> Report:
|
||||
def scan_active_content(
|
||||
text: str,
|
||||
source: Source = Source.OUTPUT,
|
||||
max_scan_chars: int = MAX_SCAN_CHARS,
|
||||
) -> Report:
|
||||
"""Report active-content constructs with an external target in ``text``.
|
||||
|
||||
Report-only (design principles 3 & 4): the input is never mutated and no
|
||||
disposition is rendered here. Labels are ``active:<class>``; severities
|
||||
mirror ``neutralize``'s (image / raw-html / data-uri HIGH, links MEDIUM).
|
||||
|
||||
Self-safety (OWASP LLM10): the scanned length is capped once, and an
|
||||
``active:oversize-input`` finding announces that the tail went unread. It
|
||||
truncates rather than raising the way the transform surfaces do — what a
|
||||
detector shortens is its own coverage, not the caller's content. Reached
|
||||
through :func:`~llm_ingestion_guard.output.scan_output` the text is already
|
||||
under that surface's cap, so the flag is raised once, there.
|
||||
"""
|
||||
report = Report()
|
||||
|
||||
if len(text) > max_scan_chars:
|
||||
report.add(Finding(
|
||||
label="active:oversize-input", severity=Severity.MEDIUM,
|
||||
source=source, detector="active_content", count=len(text),
|
||||
owasp="LLM10",
|
||||
evidence=f"input {len(text)} chars exceeds cap {max_scan_chars}; scanned prefix only",
|
||||
))
|
||||
text = text[:max_scan_chars]
|
||||
|
||||
def _flag(cls: str, hits: list[tuple[str, bool]]) -> None:
|
||||
"""Report one finding for ``cls``, graded by its *worst* member.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue