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:
parent
26a231d6c4
commit
d8a95e465f
2 changed files with 113 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue