<commit_before>package xyz.docbleach.module.pdf;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.io.RandomAccessBufferedFileInputStream;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.io.ScratchFile;
import org.apache.pdfbox.pdfparser.PDFParser;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDDestinationOrAction;
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.pdmodel.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.docbleach.api.BleachSession;
import xyz.docbleach.api.bleach.Bleach;
import xyz.docbleach.api.exception.BleachException;
import xyz.docbleach.api.threat.Threat;
import xyz.docbleach.api.threat.ThreatAction;
import xyz.docbleach.api.threat.ThreatSeverity;
import xyz.docbleach.api.threat.ThreatType;
import xyz.docbleach.api.util.StreamUtils;

import java.io.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * PDF parsing is a bit tricky: everything may or may not be linked to additional actions, so we
 * need to treat each and every elements.
 */
public class PdfBleach implements Bleach {
    private static final Logger LOGGER = LoggerFactory.getLogger(PdfBleach.class);
    private static final byte[] PDF_MAGIC = new byte[]{37, 80, 68, 70};
    private static final String[] COMMON_PASSWORDS = new String[]{
            null, "", "test", "example", "sample", "malware", "infected", "password"
    };
    private static final MemoryUsageSetting MEMORY_USAGE_SETTING = MemoryUsageSetting.setupMixed(1024 * 100);

    @Override
    public boolean handlesMagic(InputStream stream) {
        return StreamUtils.hasHeader(stream, PDF_MAGIC);
    }

    @Override
    public String getName() {
        return "PDF Bleach";
    }

    @Override
    public void sanitize(InputStream inputStream, OutputStream outputStream, BleachSession session) throws BleachException {
        try (ScratchFile scratchFile = new ScratchFile(MEMORY_USAGE_SETTING);
             RandomAccessRead source = new RandomAccessBufferedFileInputStream(inputStream)
        ) {
            sanitize(scratchFile, source, outputStream, session);
        } catch (IOException e) {
            throw new BleachException(e);
        }
    }

    private void sanitize(ScratchFile scratchFile, RandomAccessRead source, OutputStream outputStream, BleachSession session) throws IOException, BleachException {
        final PDDocument doc = getDocument(scratchFile, source);

        // Can't remember why I used this, but this method doesn't know the passord anymore
        // So this is just impossible to achieve.
        // doc.protect(new StandardProtectionPolicy(password, password, doc.getCurrentAccessPermission()));
        // @TODO: find out. :D

        final PDDocumentCatalog docCatalog = doc.getDocumentCatalog();

        sanitizeNamed(session, doc, docCatalog.getNames());
        sanitizeOpenAction(session, docCatalog);
        sanitizeDocumentActions(session, docCatalog.getActions());
        sanitizePageActions(session, docCatalog.getPages());
        sanitizeAcroFormActions(session, docCatalog.getAcroForm());
        sanitizeObjects(session, doc.getDocument().getObjects());

        doc.save(outputStream);
        doc.close();
    }<commit_msg>:sparkles: PDF: Sanitise document outline actions<commit_after>package xyz.docbleach.module.pdf;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.io.RandomAccessBufferedFileInputStream;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.io.ScratchFile;
import org.apache.pdfbox.pdfparser.PDFParser;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDDestinationOrAction;
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.pdmodel.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.docbleach.api.BleachSession;
import xyz.docbleach.api.bleach.Bleach;
import xyz.docbleach.api.exception.BleachException;
import xyz.docbleach.api.threat.Threat;
import xyz.docbleach.api.threat.ThreatAction;
import xyz.docbleach.api.threat.ThreatSeverity;
import xyz.docbleach.api.threat.ThreatType;
import xyz.docbleach.api.util.StreamUtils;

import java.io.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * PDF parsing is a bit tricky: everything may or may not be linked to additional actions, so we
 * need to treat each and every elements.
 */
public class PdfBleach implements Bleach {
    private static final Logger LOGGER = LoggerFactory.getLogger(PdfBleach.class);
    private static final byte[] PDF_MAGIC = new byte[]{37, 80, 68, 70};
    private static final String[] COMMON_PASSWORDS = new String[]{
            null, "", "test", "example", "sample", "malware", "infected", "password"
    };
    private static final MemoryUsageSetting MEMORY_USAGE_SETTING = MemoryUsageSetting.setupMixed(1024 * 100);

    @Override
    public boolean handlesMagic(InputStream stream) {
        return StreamUtils.hasHeader(stream, PDF_MAGIC);
    }

    @Override
    public String getName() {
        return "PDF Bleach";
    }

    @Override
    public void sanitize(InputStream inputStream, OutputStream outputStream, BleachSession session) throws BleachException {
        try (ScratchFile scratchFile = new ScratchFile(MEMORY_USAGE_SETTING);
             RandomAccessRead source = new RandomAccessBufferedFileInputStream(inputStream)
        ) {
            sanitize(scratchFile, source, outputStream, session);
        } catch (IOException e) {
            throw new BleachException(e);
        }
    }

    private void sanitize(ScratchFile scratchFile, RandomAccessRead source, OutputStream outputStream, BleachSession session) throws IOException, BleachException {
        final PDDocument doc = getDocument(scratchFile, source);

        // Can't remember why I used this, but this method doesn't know the passord anymore
        // So this is just impossible to achieve.
        // doc.protect(new StandardProtectionPolicy(password, password, doc.getCurrentAccessPermission()));
        // @TODO: find out. :D

        final PDDocumentCatalog docCatalog = doc.getDocumentCatalog();

        sanitizeNamed(session, doc, docCatalog.getNames());
        sanitizeOpenAction(session, docCatalog);
        sanitizeDocumentActions(session, docCatalog.getActions());
        sanitizePageActions(session, docCatalog.getPages());
        sanitizeAcroFormActions(session, docCatalog.getAcroForm());
        sanitizeDocumentOutline(session, doc.getDocumentCatalog().getDocumentOutline());

        sanitizeObjects(session, doc.getDocument().getObjects());

        doc.save(outputStream);
        doc.close();
    }

    private void sanitizeDocumentOutline(BleachSession session, PDDocumentOutline documentOutline) {
        if (documentOutline == null)
            return;
        if (!documentOutline.hasChildren())
            return;
        documentOutline.children().forEach(item -> sanitizeDocumentOutlineItem(session, item));
    }

    private void sanitizeDocumentOutlineItem(BleachSession session, PDOutlineItem item) {
        if (item.getAction() == null)
            return;
        LOGGER.debug("Found&removed action on outline item (was {})", item.getAction());
        item.setAction(null);
        recordJavascriptThreat(session, "DocumentOutline Item Action", "Action");
    }
{'fraction_non_alphanumeric': 0.09832341806381828, 'fraction_numerical': 0.003677663601946998, 'mean_word_length': 4.4196951934349356, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 4, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6817750', 'n_tokens_mistral': 2742, 'n_tokens_neox': 2680, 'n_words': 554}