1
0
Fork 0

feat(inbox): .pptx extraction — speaker notes, off-slide box, alt-text (stage 2e)

The pptx payload hides where an audience watching the slides does not look. The
extractor surfaces all three regions into the concept text so the stage-2 scan
catches the injection:

- speaker notes (slide.notes_slide.notes_text_frame.text);
- off-slide (off-canvas) text boxes (shape.text_frame.text, position-agnostic);
- image/shape alt-text (cNvPr@descr, read off the XML — python-pptx 1.0.2 has no
  stable public accessor across shape types).

Detach-proof: same visible slide without notes ADMITs. python-pptx imported
lazily (dev/showcase-scoped, not a core dep). Tests 305 -> 310.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-06 11:16:59 +02:00
commit d8a95e465f
2 changed files with 113 additions and 0 deletions

View file

@ -284,3 +284,82 @@ def test_docx_hidden_run_detach_proof(tmp_path):
fp = _make_docx(tmp_path, body="A normal paragraph.")
_extracted, _result, verdict = receive([fp])
assert verdict == "ADMIT"
# --- slice 2e: .pptx (python-pptx) ------------------------------------------
# The payload hides where an audience watching the slides does not look: speaker
# notes, an off-slide text box, or an image's alt-text. The extractor surfaces
# all three so the stage-2 scan catches them.
def _set_alt_text(shape, text):
# alt-text lives on the shape's non-visual props (cNvPr@descr); python-pptx
# 1.0.2 has no stable public accessor across shape types, so set it on the XML.
for el in shape._element.iter():
if el.tag.endswith("}cNvPr"):
el.set("descr", text)
return
def _make_pptx(tmp_path, name="deck.pptx", *, body=None, notes=None, offslide=None, alt=None):
import io
from pptx import Presentation
from pptx.util import Emu
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
if body is not None:
tb = slide.shapes.add_textbox(Emu(0), Emu(0), Emu(3000000), Emu(500000))
tb.text_frame.text = body
if offslide is not None:
tb = slide.shapes.add_textbox(Emu(-3000000), Emu(0), Emu(1000000), Emu(400000))
tb.text_frame.text = offslide # positioned off the canvas
if alt is not None:
from PIL import Image
buf = io.BytesIO()
Image.new("RGB", (2, 2), (255, 255, 255)).save(buf, "PNG")
buf.seek(0)
pic = slide.shapes.add_picture(buf, Emu(0), Emu(0), Emu(500000), Emu(500000))
_set_alt_text(pic, alt)
if notes is not None:
slide.notes_slide.notes_text_frame.text = notes
fp = tmp_path / name
prs.save(str(fp))
return fp
def test_pptx_speaker_notes_injection_is_caught(tmp_path):
fp = _make_pptx(tmp_path, body="Slide one.", notes=_INJECTION)
extracted, result, verdict = receive([fp])
assert extracted.provenance[0].source_type == "pptx"
assert result.disposition is Disposition.FAIL_SECURE
assert verdict == "REJECT"
def test_pptx_offslide_textbox_injection_is_caught(tmp_path):
fp = _make_pptx(tmp_path, body="Slide one.", offslide=_INJECTION)
_extracted, _result, verdict = receive([fp])
assert verdict == "REJECT"
def test_pptx_image_alt_text_injection_is_caught(tmp_path):
fp = _make_pptx(tmp_path, body="Slide one.", alt=_INJECTION)
_extracted, _result, verdict = receive([fp])
assert verdict == "REJECT"
def test_clean_pptx_admits(tmp_path):
fp = _make_pptx(tmp_path, name="clean.pptx", body="A routine slide. No behavior change.")
extracted, _result, verdict = receive([fp])
assert "uploads/clean.md" in extracted.bundle
assert verdict == "ADMIT"
def test_pptx_notes_detach_proof(tmp_path):
# Same visible slide, no speaker notes -> ADMIT: the notes region is what
# caught it, not merely "a pptx".
fp = _make_pptx(tmp_path, body="A routine slide.")
_extracted, _result, verdict = receive([fp])
assert verdict == "ADMIT"