Create test fixture directories and integration tests for Phase 7 profile validation exit gate: Invalid profiles (tests/fixtures/profiles/invalid/): - unknown-key.yaml: unrecognized extraction tuning key - bad-combinator.yaml: malformed match combinators (scalar not list) - secret-key.yaml: forbidden api_key (PROFILE_SECRETS_FORBIDDEN) - malformed.yaml: unclosed bracket (YAML syntax error) Valid profiles (tests/fixtures/profiles/valid/): - minimal.yaml: smallest valid profile (name, description, priority only) - invoice-minimal.yaml: simplified invoice with match and extraction Resolution fixtures (tests/fixtures/profiles/resolution/): - custom-invoice.yaml: priority 100 overrides built-in (priority 50) - README.md: documents resolution priority order Integration tests (tests/integration/advanced/profiles.rs): - test_invalid_profiles_rejected: validates all 4 invalid profiles fail - test_valid_profiles_accepted: validates all 2 valid profiles pass - test_profile_resolution_order: tests --profile-dir override behavior - test_invalid_fixture_error_types: validates specific error messages Acceptance criteria: ✓ pdftract profiles validate rejects all 4 invalid files ✓ pdftract profiles validate accepts all 2 valid files ✓ Profile resolution order test passes Closes bf-2ypn2
170 lines
3.2 KiB
Python
170 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate embedded-js.pdf fixture for TH-04 JavaScript detection testing.
|
|
|
|
This creates a minimal PDF with 3 JavaScript actions at different locations:
|
|
1. Catalog /OpenAction -> /JS containing app.alert("pwn")
|
|
2. Page 0 /AA -> /O (open action) -> /JS containing a second alert
|
|
3. Page 1 annotation /A -> /JS containing a third snippet
|
|
|
|
Generated PDF is valid and can be parsed by pdftract, but the JavaScript
|
|
is NEVER executed - only detected and reported.
|
|
"""
|
|
|
|
import struct
|
|
|
|
def write_pdf():
|
|
"""Generate a minimal PDF with embedded JavaScript actions."""
|
|
|
|
# PDF version 1.4
|
|
pdf_header = b"%PDF-1.4\n\n"
|
|
|
|
# Object 0: Pages root (dummy, will be overwritten)
|
|
# Object 1: Page 0 (with /AA /O JavaScript)
|
|
obj1 = """1 0 obj
|
|
<<
|
|
/Type/Page
|
|
/MediaBox[0 0 612 792]
|
|
/Parent 0 0 R
|
|
/Resources<<
|
|
/Font<<
|
|
/F1<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>
|
|
>>
|
|
>>
|
|
/Contents 2 0 R
|
|
/AA<<
|
|
/O<</S/JavaScript/JS(app.alert('page_open'))>>
|
|
>>
|
|
>>
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Object 2: Content stream for Page 0 (simple text)
|
|
obj2 = """2 0 obj
|
|
<<
|
|
/Length 44
|
|
>>
|
|
stream
|
|
BT
|
|
/F1 12 Tf
|
|
100 700 Td
|
|
(Page 0) Tj
|
|
ET
|
|
endstream
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Object 3: Page 1 (with annotation containing JavaScript)
|
|
obj3 = """3 0 obj
|
|
<<
|
|
/Type/Page
|
|
/MediaBox[0 0 612 792]
|
|
/Parent 0 0 R
|
|
/Resources<<
|
|
/Font<<
|
|
/F1<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>
|
|
>>
|
|
>>
|
|
/Contents 4 0 R
|
|
/Annots[5 0 R]
|
|
>>
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Object 4: Content stream for Page 1
|
|
obj4 = """4 0 obj
|
|
<<
|
|
/Length 44
|
|
>>
|
|
stream
|
|
BT
|
|
/F1 12 Tf
|
|
100 700 Td
|
|
(Page 1) Tj
|
|
ET
|
|
endstream
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Object 5: Annotation on Page 1 with JavaScript action
|
|
obj5 = """5 0 obj
|
|
<<
|
|
/Type/Annot
|
|
/Subtype/Link
|
|
/Rect[100 600 200 620]
|
|
/A<</S/JavaScript/JS(app.alert('annot_action'))>>
|
|
>>
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Object 6: Pages catalog
|
|
obj6 = """6 0 obj
|
|
<<
|
|
/Type/Pages
|
|
/Count 2
|
|
/Kids[1 0 R 3 0 R]
|
|
>>
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Object 7: Catalog with /OpenAction JavaScript
|
|
obj7 = """7 0 obj
|
|
<<
|
|
/Type/Catalog
|
|
/Pages 6 0 R
|
|
/OpenAction<</S/JavaScript/JS(app.alert("pwn"))>>
|
|
>>
|
|
endobj
|
|
|
|
"""
|
|
|
|
# Combine all objects
|
|
pdf_content = obj1 + obj2 + obj3 + obj4 + obj5 + obj6 + obj7
|
|
|
|
# Calculate offsets for xref table
|
|
offsets = []
|
|
current_offset = len(pdf_header)
|
|
|
|
# Split into individual objects to calculate offsets
|
|
objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]
|
|
|
|
for obj in objects:
|
|
offsets.append(current_offset)
|
|
current_offset += len(obj.encode('latin1'))
|
|
|
|
# Build xref table
|
|
xref = f"xref\n0 8\n0000000000 65535 f \n"
|
|
for i, offset in enumerate(offsets, start=1):
|
|
xref += f"{offset:010d} 00000 n \n"
|
|
|
|
# Trailer
|
|
trailer = f"""trailer
|
|
<<
|
|
/Size 8
|
|
/Root 7 0 R
|
|
>>
|
|
startxref
|
|
{current_offset}
|
|
%%
|
|
%%EOF
|
|
"""
|
|
|
|
# Write complete PDF
|
|
complete_pdf = pdf_header + pdf_content.encode('latin1') + xref.encode('latin1') + trailer.encode('latin1')
|
|
|
|
with open("tests/fixtures/security/embedded-js.pdf", "wb") as f:
|
|
f.write(complete_pdf)
|
|
|
|
print("Generated tests/fixtures/security/embedded-js.pdf")
|
|
print("JavaScript actions:")
|
|
print(" 1. Catalog /OpenAction: app.alert(\"pwn\")")
|
|
print(" 2. Page 0 /AA /O: app.alert('page_open')")
|
|
print(" 3. Page 1 annotation /A: app.alert('annot_action')")
|
|
|
|
if __name__ == "__main__":
|
|
write_pdf()
|