ReadPDFFileV2

Load a PDF file's content and metadata into context. Supports extraction of hashes, urls, and emails when available.

python · Common Scripts

Source

import contextlib
import glob
import html
import io
import json
import os
import re
import shutil
import stat
import subprocess

import demistomock as demisto
import PyPDF2
from CommonServerPython import *
from pikepdf import PasswordError, Pdf

URL_EXTRACTION_REGEX = (
    r"(?:(?:https?|ftp|hxxps?):\/\/|www\[?\.\]?|ftp\[?\.\]?)(?:[-\w\d]+\[?\.\]?)+"
    r"[-\w\d]+(?::\d+)?(?:(?:\/|\?)[-\w\d+&@#\/%=~_$?!\-:,.\(\);]*[\w\d+&@#\/%=~_$\(\);])?"
)
INTEGRATION_NAME = "ReadPDFFileV2"
DEFAULT_NUM_IMAGES = 20


class PdfPermissionsException(Exception):
    """
    Every exception class that is in charge of catching errors that occur when trying to
    extract data from the PDF must inherit this class
    """


class PdfCopyingProtectedException(PdfPermissionsException):
    """
    This class is in charge of catching errors that occur when we try to extract data from
    a `copy-protected` file (Copy-protected files are files that prevent us from copy its content)
    This is relevant since we run a command that copies the content of the pdf file into a text file.
    """


class PdfInvalidCredentialsException(PdfPermissionsException):
    """
    This class is in charge of catching errors that occur when we try to decrypt an encrypted
    pdf file with the wrong password.
    """


# Error class for shell errors
class ShellException(Exception):
    pass


try:
    ROOT_PATH = os.getcwd()
except OSError:
    return_error(
        "The script failed to access the current working directory. This might happen if your docker isn't "
        "set up correctly. Please contact customer support"
    )

EMAIL_REGXEX = "[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_.]+"
# Documentation claims png is enough for pdftohtml, but through testing we found jpg can be generated as well
IMG_FORMATS = ["jpg", "jpeg", "png", "gif"]


def handle_error_read_only(fun, path, exp) -> None:
    """
    Handling errors that can be encountered in `shutil.rmtree()` execution.
    """
    demisto.debug(exp)

    # Checking if the file is Read-Only
    if not os.access(path, os.W_OK):
        demisto.debug(f"The {path} file is read-only")
        # Change the file permission to the writing
        try:
            os.chmod(path, stat.S_IWUSR)
            fun(path)
        except Exception as e:
            raise ValueError(str(e))
    else:
        raise ValueError(str(exp))


def create_file_instance(entry_id: str, path: str, file_name: str, score: int | None) -> Common.File:
    dbot_score = Common.DBotScore(
        indicator=entry_id,
        indicator_type=DBotScoreType.FILE,
        integration_name="PDFx",
        score=score,
    )
    file = Common.File(
        dbot_score=dbot_score,
        extension="pdf",
        entry_id=entry_id,
        name=file_name,
        path=path,
    )
    return file


def mark_suspicious(suspicious_reason: str, entry_id: str, path: str, file_name: str) -> None:
    """Missing EOF, file may be corrupted or suspicious file"""

    human_readable = f"{suspicious_reason}\nFile marked as suspicious for entry id: {entry_id}"
    file_instance = create_file_instance(
        entry_id=entry_id,
        path=path,
        file_name=file_name,
        score=Common.DBotScore.SUSPICIOUS,
    )
    return_warning(message=human_readable, outputs=file_instance.to_context())


def run_shell_command(command: str, *args) -> bytes:
    """Runs shell command and returns the result if not encountered an error"""
    cmd = [command] + list(args)
    demisto.debug(f"Running the shell command {cmd=}")
    completed_process = subprocess.run(  # noqa: UP022
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    exit_codes = completed_process.returncode
    error_string = completed_process.stderr.decode("utf-8")
    demisto.debug(f"Got the following error: {exit_codes=},  {error_string=}")
    if exit_codes != 0:
        if "PDF file is damaged" in error_string or "Couldn't read xref table" in error_string:
            raise ShellException("PDf file is damaged/corrupted.")
        elif "Incorrect password" in error_string:
            raise PdfInvalidCredentialsException("Incorrect password. Please provide the correct password.")
        elif "Copying of text from this document is not allowed" in error_string:
            raise PdfCopyingProtectedException("Copying is not permitted")
        raise ShellException(f"Failed with the following error code: {exit_codes}.\n Error: {error_string}")
    elif error_string:
        demisto.debug(f"ReadPDFFilev2: exec of [{cmd}] completed with warnings: {error_string}")
    return completed_process.stdout


def get_files_names_in_path(path: str, name_of_file: str, full_path: bool = False) -> list:
    """Returns a list[str] of file names in path, will return full path if given full_path=True"""
    os.chdir(ROOT_PATH)
    os.chdir(path)
    res = []
    for file_path in glob.glob(name_of_file):
        if full_path:
            file_path = f"{path}/{file_path}"
        res.append(file_path)
    return res


def get_images_paths_in_path(path: str) -> list[str]:
    """Gets images paths from path"""
    res: list[str] = []
    for img_type in IMG_FORMATS:
        img_format = f"*.{img_type}"
        res.extend(get_files_names_in_path(path, img_format, True))
    return res


def get_pdf_metadata(file_path: str, user_or_owner_password: str | None = None) -> dict:
    """Gets the metadata from the pdf as a dictionary"""
    if user_or_owner_password:
        try:
            demisto.debug("Trying password as user password, using the [upw] flag")
            metadata_txt = run_shell_command("pdfinfo", "-upw", user_or_owner_password, file_path)
        except PdfInvalidCredentialsException:
            demisto.debug("Trying password as owner password, using the [opw] flag")
            metadata_txt = run_shell_command("pdfinfo", "-opw", user_or_owner_password, file_path)
        demisto.debug("PDF file has been successfully opened. Metadata has been retrieved.")
    else:
        metadata_txt = run_shell_command("pdfinfo", "-enc", "UTF-8", file_path)
    metadata = {}
    metadata_str = metadata_txt.decode("utf8", "replace")
    for line in metadata_str.split("\n"):
        # split to [key, value...]
        line_arr = line.split(":")
        if len(line_arr) > 1:
            key = line_arr[0]
            # camelize key
            if " " in key:
                if "PDF" in key:
                    key = key.title().replace("Pdf", "PDF").replace(" ", "")

                else:
                    key = key.title().replace(" ", "")

            # Handle values with and without ':'
            value = ""
            for i in range(1, len(line_arr)):
                value += line_arr[i].strip() + ":"
            # remove redundant ':'
            value = value[:-1]
            metadata[key] = value
    return metadata


def bypass_copy_protected_limitations(pdf_file: str) -> None:
    """
    This function is in charge of handling the situation when a pdf is `copy-protected`.
    Copy protected files prevent us from extracting content from the file, therefore we need a way to bypass this limitation.
    """
    with Pdf.open(pdf_file, allow_overwriting_input=True) as pdf:
        pdf.save(pdf_file)


def get_pdf_text(file_path: str, pdf_text_output_path: str) -> str:
    """Creates a txt file from the pdf in the pdf_text_output_path and returns the content of the txt file"""
    try:
        run_shell_command("pdftotext", file_path, pdf_text_output_path)
    except PdfCopyingProtectedException:
        bypass_copy_protected_limitations(pdf_file=file_path)
        run_shell_command("pdftotext", file_path, pdf_text_output_path)
    text = ""
    with open(pdf_text_output_path, "rb") as f:
        for line in f:
            text += line.decode("utf-8")
    return text


def get_pdf_htmls_content(pdf_path: str, output_folder: str, unescape_url: bool = True) -> str:
    """Creates an html file and images from the pdf in output_folder and returns the text content of the html files"""
    pdf_html_output_path = f"{output_folder}/PDF_html"
    try:
        run_shell_command("pdftohtml", pdf_path, pdf_html_output_path)
    except PdfCopyingProtectedException:
        bypass_copy_protected_limitations(pdf_file=pdf_path)
        run_shell_command("pdftohtml", pdf_path, pdf_html_output_path)
    html_file_names = get_files_names_in_path(output_folder, "*.html")
    html_content = ""
    for file_name in html_file_names:
        with open(file_name, "rb") as f:
            for line in f:
                html_content += html.unescape(str(line)) if unescape_url else str(line)
    return html_content


def build_readpdf_entry_object(
    entry_id: str,
    metadata: dict,
    text: str,
    urls: list,
    emails: list,
    images: list[str],
    max_images: int,
    hash_contexts: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
    """Builds an entry object for the main script flow"""
    pdf_file = {"EntryID": entry_id}
    # Add Text to file entity
    pdf_file["Text"] = text

    # Add Metadata to file entity
    for k in metadata:
        pdf_file[k] = metadata[k]

    md = "### Metadata\n"
    md += "* " if metadata else ""
    md += "\n* ".join([f"{k}: {v}" for k, v in metadata.items()])

    md += "\n### URLs\n"
    md += "* " if urls else ""
    md += "\n* ".join([f'{k["Data"]!s}' for k in urls])

    md += "\n### Text"
    md += f"\n{text}"
    results = [
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["markdown"],
            "Contents": md,
            "HumanReadable": md,
            "EntryContext": {"File(val.EntryID == obj.EntryID)": pdf_file, "URL": urls},
        }
    ]
    if images:
        results[0]["HumanReadable"] = f"{results[0]['HumanReadable']}\n### Images"
        os.chdir(ROOT_PATH)
        for i, img in enumerate(images):
            if i >= max_images:
                break
            file = file_result_existing_file(img)
            results.append(file)
    all_pdf_data = ""
    if metadata:
        for _, v in metadata.items():
            all_pdf_data += str(v)
    if text:
        all_pdf_data += text
    if urls:
        for u in urls:
            u = u["Data"] + " "
            all_pdf_data += u

    # Extract indicators (omitting context output, letting auto-extract work)
    try:
        indicators_map = demisto.executeCommand("extractIndicators", {"text": all_pdf_data})[0]["Contents"]
        indicators_map = json.loads(indicators_map)
        if emails:
            indicators_map["Email"] = emails
        if hash_contexts:
            indicators_map["Hashes"] = hash_contexts
    except json.JSONDecodeError:
        pass
    ec = build_readpdf_entry_context(indicators_map)
    results.append(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": indicators_map,
            "HumanReadable": indicators_map,
            "EntryContext": ec,
        }
    )
    return results


def build_readpdf_entry_context(indicators_map: Any) -> dict:
    ec = {}
    if isinstance(indicators_map, dict):
        if "URL" in indicators_map:
            ec_url = []
            for url in indicators_map["URL"]:
                ec_url.append({"Data": url})
            ec["URL"] = ec_url
        if "Email" in indicators_map:
            ec_email = []
            for email in indicators_map["Email"]:
                ec_email.append({"Email": email})
            ec["Account"] = ec_email
        if "Hashes" in indicators_map:
            ec["Hashes"] = indicators_map["Hashes"]
    return ec


def get_urls_from_binary_file(file_path: str) -> set:
    """Reading from the binary pdf in the pdf_text_output_path and returns a list of the urls in the file"""
    with open(file_path, "rb") as file:
        # the urls usually appear in the form: '/URI (url)'
        urls = re.findall(r"/URI ?\((.*?)\)", str(file.read()))
    binary_file_urls = set()
    # make sure the urls match the url regex
    for url in urls:
        mached_url = re.findall(URL_EXTRACTION_REGEX, url)
        if len(mached_url) != 0:
            binary_file_urls.add(mached_url[0])
    return binary_file_urls


def get_urls_and_emails_from_pdf_html_content(
    cpy_file_path: str, output_folder: str, unescape_url: bool = True
) -> tuple[set, set]:
    """
    Extract the URLs and emails from the pdf html content.

    Args:
        cpy_file_path (str): the path of the PDF file.
        output_folder (str): the folder output to get the HTML files from.
    Returns:
        tuple[set, set]: The URLs and emails that were found.
    """
    pdf_html_content = get_pdf_htmls_content(cpy_file_path, output_folder, unescape_url)
    return set(re.findall(URL_EXTRACTION_REGEX, pdf_html_content)), set(re.findall(EMAIL_REGXEX, pdf_html_content))


def extract_url_from_annot_object(annot_object: Any):
    """
    Extracts the URLs from the Annot object (under key: '/A').

    Args:
        annot_object (PyPDF2.generic.DictionaryObject): An object contains annotations of a PDF.

    Returns:
         (PyPDF2.generic.TextStringObject): The extracted url if exists, else - None.

    """

    # Extracts the URLs from the Annot object (under key: '/A'):
    if a := annot_object.get("/A"):
        if isinstance(a, PyPDF2.generic.IndirectObject):
            a = a.get_object()

        if url := a.get("/URI"):
            if isinstance(url, PyPDF2.generic.IndirectObject):
                url = url.get_object()
            return url
        return None
    return None


def extract_url(extracted_object: Any):
    """
    Extracts URL (if exists) from the extracted object, according to the URL_EXTRACTION_REGEX.

    Args:
        extracted_object (PyPDF2.generic.TextStringObject): A TextStringObject object contains a url or an email.

    Returns:
         (str): The extracted url.
    """
    match = ""
    matched_url = re.findall(URL_EXTRACTION_REGEX, extracted_object)
    if len(matched_url) != 0:
        match = matched_url[0]

    return match


def extract_email(extracted_object: Any):
    """
    Extracts Email (if exists) from the extracted object, according to the EMAIL_REGXEX.

    Args:
        extracted_object (PyPDF2.generic.TextStringObject): A TextStringObject object contains a url or an email.

    Returns:
         (str): The extracted email.
    """
    match = ""
    matched_email = re.findall(EMAIL_REGXEX, extracted_object)
    if len(matched_email) != 0:
        match = matched_email[0]

    return match


def extract_urls_and_emails_from_annot_objects(annot_objects: list | Any):
    """
    Extracts URLs and Emails from the Annot objects, and separate them into two different sets.

    Args:
        annot_objects (List): A list of objects that contain annotations of a PDF.

    Returns:
         Tuple[set, set]: A set includes the extracted urls, A set includes the extracted emails.

    """

    urls = set()
    emails = set()

    for annot_object in annot_objects:
        if isinstance(annot_object, PyPDF2.generic.IndirectObject):
            try:
                annot_object = annot_object.get_object()
            except Exception as e:
                if "Could not find object" in str(e):
                    demisto.error(f"annot.get_object() encountered an error: {e}.\n Skipping without failure.")
                    continue
                else:
                    demisto.error(f"annot.get_object() encountered an error: {e}.")

        extracted_object = extract_url_from_annot_object(annot_object)
        # Separates URLs and Emails:
        if extracted_object:
            if isinstance(extracted_object, bytes):
                extracted_object = extracted_object.decode()
            if url := extract_url(extracted_object):
                urls.add(url)
            if email := extract_email(extracted_object):
                emails.add(email)

    return urls, emails


def get_urls_and_emails_from_pdf_annots(file_path: str) -> tuple[set, set]:
    """
    Extracts the URLs and Emails from the pdf's Annots (Annotations and Commenting) using PyPDF2 package.
    Args:
        file_path (str): The path of the PDF file.
    Returns:
        Tuple[set, set]: A set includes the URLs that were found, A set includes the Emails that were found.
    """
    all_urls: set[str] = set()
    all_emails: set[str] = set()
    output_capture = io.StringIO()
    with open(file_path, "rb") as pdf_file:
        # The following context manager was added so we could redirect error messages to the server logs since
        # PyPDF2 would sometimes return warnings on some files (warnings and not errors because strict=False), and these warnings
        # would be flushed to stderr, and therefore they would be returned as an error message to the user instead of being
        # flushed to the server logs.
        with contextlib.redirect_stderr(output_capture):
            pdf = PyPDF2.PdfReader(pdf_file, strict=False)
            pages_len = len(pdf.pages)

            # Goes over the PDF, page by page, and extracts urls and emails:
            for page in range(pages_len):
                page_sliced = pdf.pages[page]
                page_object = page_sliced.get_object()

                # Extracts the PDF's Annots (Annotations and Commenting):
                if annots := page_object.get("/Annots"):  # type: ignore[union-attr]
                    if not isinstance(annots, PyPDF2.generic.ArrayObject):
                        annots = [annots]

                    for annot in annots:
                        annot_objects = annot.get_object()
                        if not isinstance(annot_objects, PyPDF2.generic.ArrayObject):
                            annot_objects = [annot_objects]

                        # Extracts URLs and Emails:
                        urls_set, emails_set = extract_urls_and_emails_from_annot_objects(annot_objects)
                        all_urls = all_urls.union(urls_set)
                        all_emails = all_emails.union(emails_set)
        demisto.debug(output_capture.getvalue())
    # Logging:
    if len(all_urls) == 0:
        demisto.debug("No URLs were extracted from the PDF.")
    if len(all_emails) == 0:
        demisto.debug("No Emails were extracted from the PDF.")

    return all_urls, all_emails


def extract_urls_and_emails_from_pdf_file(file_path: str, output_folder: str, unescape_url: bool = True) -> tuple[list, list]:
    """
    Extract URLs and Emails from the PDF file.
    Args:
        file_path (str): The path of the PDF file.
        output_folder (str): The output folder for html files.
    Returns:
        tuple[set, set]: A set including the URLs and emails that were found, A set including only emails that were
         extracted from the html content.
    """

    # Get urls from the binary file:
    binary_file_urls = get_urls_from_binary_file(file_path)

    # Get URLS + emails:
    annots_urls, annots_emails = get_urls_and_emails_from_pdf_annots(file_path)
    html_urls, html_emails = get_urls_and_emails_from_pdf_html_content(file_path, output_folder, unescape_url)

    # This url might be generated with the pdf html file, if so, we remove it
    html_urls.discard("http://www.w3.org/1999/xhtml")

    # Unify urls:
    urls_set = annots_urls.union(html_urls, binary_file_urls)
    emails_set = annots_emails.union(html_emails)

    urls_ec = []
    emails_ec = []
    for url in urls_set:
        urls_ec.append({"Data": url})
    for email in emails_set:
        emails_ec.append(email)

    return urls_ec, emails_ec


def extract_hash_contexts_from_pdf_file(file_text: str) -> list[dict[str, Any]]:
    """Extracts the hashes from the file's text, and converts them to hash contexts.

    Args:
        file_text (str): The text extracted from the PDF.

    Returns:
        list[dict[str, Any]]: A list of hash contexts.
    """
    hash_contexts: list[dict[str, Any]] = []
    hashes_in_file = get_hashes_from_file(file_text)
    for hash_type, hashes in hashes_in_file.items():
        if hashes:
            hash_contexts.extend(convert_hash_to_context(hash_type, hashes))
    return hash_contexts


def convert_hash_to_context(hash_type: str, hashes: set[Any]) -> list[dict[str, Any]]:
    """Converts the given hashes to hash contexts

    Args:
        hash_type (str): The hash type of the given hashes.
        hashes (set[Any]): The set of hashes.

    Returns:
        list[dict[str, Any]]: A list of hash contexts that have the same hash type.
    """
    hash_context: list[dict[str, Any]] = [{"type": hash_type, "value": hash} for hash in hashes]
    return hash_context


def get_hashes_from_file(file_text: str) -> dict[str, set[Any]]:
    """Extracts all the hashes found in the file's text.

    Args:
        file_text (str): The file's text.

    Returns:
        dict[str, set[Any]]: A dictionary that holds the hash types as keys, and each key
        holds the set of hashes corresponding to that hash type.
    """
    demisto.debug("Extracting hashes from file")
    hashes: dict[str, set[Any]] = {}
    hashes["SHA1"] = set(re.findall(sha1Regex, file_text))
    hashes["SHA256"] = set(re.findall(sha256Regex, file_text))
    hashes["SHA512"] = set(re.findall(sha512Regex, file_text))
    hashes["MD5"] = set(re.findall(md5Regex, file_text))

    return hashes


def handling_pdf_credentials(cpy_file_path: str, dec_file_path: str, encrypted: str = "", user_password: str = "") -> str:
    """
    This function decrypts the pdf if needed.
    """
    try:
        if user_password or "yes" in encrypted:
            with Pdf.open(cpy_file_path, allow_overwriting_input=True, password=user_password) as pdf:
                pdf.save(dec_file_path)
                return dec_file_path
    except PasswordError:
        raise PdfInvalidCredentialsException("Incorrect password. Please provide the correct password.")
    return cpy_file_path


def extract_data_from_pdf(
    path: str, user_password: str, entry_id: str, max_images: int | None, working_dir: str, unescape_url: bool = True
) -> None:
    max_images = max_images if max_images else DEFAULT_NUM_IMAGES
    if path:
        cpy_file_path = f"{working_dir}/WorkingReadPDF.pdf"
        shutil.copy(path, cpy_file_path)
        metadata = get_pdf_metadata(path, user_password)
        encrypted = metadata.get("Encrypted", "")

        cpy_file_path = handling_pdf_credentials(
            cpy_file_path=cpy_file_path,
            dec_file_path=f"{working_dir}/DecWorkingReadPDF.pdf",
            encrypted=encrypted,
            user_password=user_password,
        )

        # Get text:
        pdf_text_output_path = f"{working_dir}/PDFText.txt"
        text = get_pdf_text(cpy_file_path, pdf_text_output_path)

        # Get hash contexts
        hash_contexts = extract_hash_contexts_from_pdf_file(text)

        # Get URLS + emails:
        urls_ec, emails_ec = extract_urls_and_emails_from_pdf_file(cpy_file_path, working_dir, unescape_url)

        # Get images:
        images = get_images_paths_in_path(working_dir)
        readpdf_entry_object = build_readpdf_entry_object(
            entry_id, metadata, text, urls_ec, emails_ec, images, max_images=max_images, hash_contexts=hash_contexts
        )

        return_results(readpdf_entry_object)
    else:
        raise Exception(f"EntryID {entry_id} path could not be found")


def main():  # pragma: no cover
    args = demisto.args()
    unescape_url: bool = argToBoolean(args.get("unescape_url", "true"))
    working_dir = "ReadPDFTemp"
    try:
        if not os.path.exists(working_dir):
            """ Check if the working directory does not exist and create it """
            os.makedirs(working_dir)
        entry_id = args.get("entryID")
        user_password = str(args.get("userPassword", ""))
        max_images = arg_to_number(args.get("maxImages", None))
        path = demisto.getFilePath(entry_id).get("path")

        extract_data_from_pdf(
            path=path,
            user_password=user_password,
            entry_id=entry_id,
            max_images=max_images,
            working_dir=working_dir,
            unescape_url=unescape_url,
        )
    except PdfPermissionsException as e:
        return_warning(str(e))
    except ShellException as e:
        file_name = demisto.getFilePath(entry_id).get("name")
        mark_suspicious(
            suspicious_reason=f"The script {INTEGRATION_NAME} failed due to an error\n{e!s}",
            entry_id=entry_id,
            path=path,
            file_name=file_name,
        )
    except Exception as e:
        demisto.error(traceback.format_exc())
        return_error(str(e))
    finally:
        os.chdir(ROOT_PATH)
        shutil.rmtree(working_dir, onerror=handle_error_read_only)


if __name__ in ["__main__", "__builtin__", "builtins"]:
    main()

README

Load a PDF file’s content and metadata into context. Supports extraction of hashes, urls, and emails when available.

Script Data


Name Description
Script Type python3
Tags Utility, ingestion
Cortex XSOAR Version 4.1.0+

Inputs


Argument Name Description
entryID The War Room entryID of the file to read.
userPassword The password for the file, if encrypted.
maxImages The maximum number of images to extract from the PDF file.
unescape_url To unescape URLs that have been escaped as part of the URLs extraction. Invalid characters will be ignored. Default is true.

Outputs


Path Description Type
URL.Data The list of URLs that were extracted from the PDF file. String
File.Text The text that was extracted from the PDF file. String
File.Producer The producer of the PDF file. String
File.Title The title of the PDF file. String
File.Author The author of the PDF file. String
File.ModDate The ModDate of the PDF file. Date
File.CreationDate The CreationDate of the PDF file. Date
File.Pages The number of pages in the PDF file. String
File.Size The file size in bytes. Number
File.Form The PDF form type. String
File.Encrypted Whether the file is encrypted. String
File.FileSize The file size in bytes. String
File.SHA1 The SHA1 file hash of the file. String
File.PageRot The page rotation of the PDF file. String
File.Optimized Whether the page has been optimized. String
File.SHA256 The SHA256 file hash of the file. String
File.PDFVersion The PDF version. String
File.Name The name of the PDF file. String
File.Creator The creator of the PDF file. String
File.Tagged Whether the file has tagged meta-information. String
File.SSDeep The SSDeep hash of the file. String
File.EntryID The Entry ID of the file. String
File.JavaScript Whether the file is in JavaScript. String
File.Info The additional information about the file. String
File.PageSize The PDF file page size. String
File.Type The file type. String
File.Suspects Indicates the presence of tag suspects. String
File.MD5 The MD5 file hash of the file. String
File.UserProperties Indicates the presence of the structure elements that contain user properties attributes. String
File.Extension The file’s extension. String
Account.Email The email address of the account. String
Hashes.type The hash type extracted from the PDF file. String
Hashes.value The hash value extracted from the PDF file. String