UnzipFile

Unzip a file using fileName or entryID to specify a file. Unzipped files will be loaded to the War Room and names will be put into the context.

python · Common Scripts

Source

import os
import shutil
import sys
import zipfile as z
from os.path import isdir, isfile
from pathlib import Path
from subprocess import PIPE, Popen
from tempfile import mkdtemp

import demistomock as demisto
from CommonServerPython import *

from CommonServerUserPython import *

MAX_FILENAME_SIZE_BYTES = 255
SLICE_FILENAME_SIZE_BYTES = 235


def get_zip_path(args):
    """
    :param args: arg from demisto
    :return: path of zip file
    """
    file_entry_id = ""
    if args.get("fileName") or args.get("lastZipFileInWarroom"):
        entries = demisto.executeCommand("getEntries", {})
        for entry in entries:
            fn = demisto.get(entry, "File")

            # We check the python version to prevent encoding issues. Effects Demisto 4.5+
            is_text = type(fn) is str  # pylint: disable=E0602

            is_correct_file = args.get("fileName", "").lower() == fn.lower()
            is_zip = fn.lower().endswith(".zip")

            if is_text and is_zip:
                if args.get("fileName") and is_correct_file:
                    file_entry_id = entry["ID"]
                    break
                if args.get("lastZipFileInWarroom"):
                    file_entry_id = entry["ID"]

        # after the for loop above checks if a entry was found
        if not file_entry_id:
            if args.get("fileName"):
                demisto.results(
                    {
                        "Type": entryTypes["error"],
                        "ContentsFormat": formats["text"],
                        "Contents": args.get("fileName", "") + " not such file in war room",
                    }
                )
            if args.get("lastZipFileInWarroom"):
                demisto.results(
                    {"Type": entryTypes["error"], "ContentsFormat": formats["text"], "Contents": "Not found zip file in war room"}
                )
            sys.exit(0)
    if "entryID" in args:
        file_entry_id = args.get("entryID")  # type: ignore

    if not file_entry_id:
        return_error("You must set entryID or fileName or lastZipFileInWarroom=true when executing Unzip script")

    res = demisto.executeCommand("getFilePath", {"id": file_entry_id})
    if res[0]["Type"] == entryTypes["error"]:
        demisto.results(
            {
                "Type": entryTypes["error"],
                "ContentsFormat": formats["text"],
                "Contents": "Failed to get the file path for entry: " + file_entry_id,
            }
        )
        sys.exit(0)

    return res[0]["Contents"]


def extract(file_info, dir_path, zip_tool="7z", password=None):
    """
    :param file_info: The file data.
    :param dir_path: directory that the file will be extract to
    :param zip_tool: The tool to extract files with (7z or zipfile).
    :param password: password if the zip file is encrypted
    :return:
        excluded_dirs: the excluded dirs which are in dir_path
        excluded_files: the excludedfiles which are in dir_path
    """
    # remembering which files and dirs we currently have so we add them later as newly extracted files.
    file_path = file_info["path"]
    file_name = file_info["name"]
    excluded_files = [f for f in os.listdir(".") if isfile(f)]
    excluded_dirs = [d for d in os.listdir(".") if isdir(d)]
    # extracting the zip file
    """
    We check the python version to ensure the docker image contains the necessary packages. 4.5+
    use the new docker image.
    """
    stdout = ""
    if ".rar" in file_name and sys.version_info > (3, 0):
        stdout = extract_using_unrar(file_path, dir_path, password=password)
    elif ".tar" in file_name:
        stdout = extract_using_tarfile(file_path, dir_path, file_name)
    else:
        if zip_tool == "7z":
            stdout = extract_using_7z(file_path, dir_path, password=password)
        elif zip_tool == "zipfile":
            if password:
                password = bytes(password, "utf-8")
            extract_using_zipfile(file_path, dir_path, password=password)
        else:
            return_error(f"There is no zipTool named: {zip_tool}")

    if "Wrong password?" in stdout:
        demisto.debug(stdout)
        return_error("Data Error in encrypted file. Wrong password?")

    return excluded_dirs, excluded_files


def extract_using_unrar(file_path, dir_path, password=None):
    """
    :param file_path: The file path.
    :param dir_path: directory that the file will be extract to
    :param password: password if the zip file is encrypted
    """
    if password:
        cmd = ["unrar", "x", f"-p{password}", file_path, dir_path]
    else:
        cmd = ["unrar", "x", "-p-", file_path, dir_path]
    process = Popen(cmd, stdout=PIPE, stderr=PIPE)
    # process = Popen([cmd], shell=True, stdout=PIPE, stderr=PIPE)
    stdout, stderr = process.communicate()
    stdout = str(stdout)
    if stderr:
        if "Incorrect password" in str(stderr):
            return_error("The .rar file provided requires a password.")
        else:
            return_error(str(stderr))
    return stdout


def extract_using_tarfile(file_path: str, dir_path: str, file_name: str) -> str:
    if ".tar.gz" in file_name:
        cmd = ["tar", "-xzvf", file_path, "-C", dir_path, "--no-same-owner", "--no-same-permissions"]
    elif file_name.endswith(".tar"):
        cmd = ["tar", "-xf", file_path, "-C", dir_path, "--no-same-owner", "--no-same-permissions"]
    else:
        cmd = []
        demisto.debug(f"{file_name=} didn't match any condition. {cmd=}")
    process = Popen(cmd, stdout=PIPE, stderr=PIPE)
    stdout, stderr = process.communicate()
    stdout = str(stdout)
    if stderr:
        demisto.info(str(stderr))
    return stdout


def extract_using_7z(file_path, dir_path, password=None):
    """
    :param file_path: The file path.
    :param dir_path: directory that the file will be extract to
    :param password: password if the zip file is encrypted
    """
    cmd = ["7z", "x", f"-p{password}", f"-o{dir_path}", file_path]
    process = Popen(cmd, stdout=PIPE, stderr=PIPE)
    # process = Popen([cmd], shell=True, stdout=PIPE, stderr=PIPE)
    stdout, stderr = process.communicate()
    stdout = str(stdout)

    if "Errors" in stdout:
        return_error(
            "7z couldn't extract this file - try using zipTool=zipfile\n"
            "If you already tried both zipfile and 7z check that the zip file is valid."
        )
    return stdout


def extract_using_zipfile(file_path, dir_path, password=None):
    """
    :param file_path: The file path.
    :param dir_path: directory that the file will be extract to
    :param password: password if the zip file is encrypted
    """
    try:
        with z.ZipFile(file_path, "r") as given_zip:
            for f in given_zip.filelist:
                full_filename = f.filename
                filename_length = len(full_filename.encode("utf-8"))
                if filename_length > MAX_FILENAME_SIZE_BYTES:
                    file_name_splited = full_filename.rsplit(".", 1)
                    file_name = file_name_splited[0]
                    extension = file_name_splited[1]
                    slice_object = slice(SLICE_FILENAME_SIZE_BYTES)
                    file_name_bytes = file_name.encode("utf-8")[slice_object]
                    # Rename the filename to a shorten filename.
                    f.filename = f"{file_name_bytes.decode('utf-8', errors='ignore')}_shortened_.{extension}"
                    demisto.results(f"The filename {full_filename} is too long - change file name to:\n{f.filename}")
            for filename in given_zip.filelist:
                given_zip.extract(filename, path=dir_path, pwd=password)
        given_zip.close()
    except Exception as e:
        return_error(
            f"zipfile couldn't extract this file - try using zipTool=7z\n"
            f"If you already tried both zipfile and 7z check that the zip file is valid. {e}"
        )


def upload_files(excluded_dirs, excluded_files, dir_path):
    """
    :param excluded_dirs: excluded dirs
    :param excluded_files: excluded files
    :param dir_path: dir path for the files
    :return:
    """
    filenames = []  # type: ignore
    file_entry_id = dir_path
    # recursive call over the file system top down
    for root, directories, files in os.walk(dir_path):
        # removing the previously existing dirs from the search
        directories[:] = [d for d in directories if d not in excluded_dirs]
        for f in files:
            # skipping previously existing files and verifying that the current file is a file and
            # then adding it to the extracted files list
            if f not in excluded_files and isfile(os.path.join(root, f)):
                filenames.append(os.path.join(root, f))

    if len(filenames) == 0:
        demisto.results(
            {"Type": entryTypes["error"], "ContentsFormat": formats["text"], "Contents": "Could not find files in archive"}
        )
    else:
        results = []
        # extracted files can be in sub directories so we save the base names of
        # the files and also the full path of the file
        files_dic = {file_path: os.path.basename(file_path) for file_path in filenames}
        # Filter out symlinks and out-of-tree entries before building outputs
        safe_base = os.path.realpath(dir_path) + os.sep
        safe_files_dic = {}
        for file_path, file_name in files_dic.items():
            real = os.path.realpath(file_path)
            if Path(file_path).is_symlink() or not real.startswith(safe_base):
                demisto.debug(f"Skipping out-of-tree entry: {file_path} -> {real}")
                continue
            safe_files_dic[file_path] = file_name
            with open(file_path, "rb") as _file:
                demisto.results(fileResult(file_name, _file.read()))
        # Build outputs from the filtered (safe) set only
        files_base_names = list(safe_files_dic.values())  # noqa: F812
        results.append(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["json"],
                "Contents": {"extractedFiles": files_base_names},
                "EntryContext": {"ExtractedFiles": files_base_names, 'File(val.EntryID=="' + file_entry_id + '").Unzipped': True},
                "ReadableContentsFormat": formats["markdown"],
                "HumanReadable": tableToMarkdown(
                    "Extracted Files",
                    [{"name": file_name, "path": file_path} for file_path, file_name in safe_files_dic.items()],
                ),
            }
        )

        demisto.results(results)


def get_password(args):
    """
    Get the file's password argument inserted by the user. The password can be inserted either in the sensitive
    argument (named 'password') or nonsensitive argument (named 'nonsensitive_password). This function asserts these
    arguments are used properly and raises an error if both are inserted and have a different value.
    so this
    Args:
        args: script's arguments

    Returns:
        the password given for the file.
    """
    sensitive_password = args.get("password")
    nonsensitive_password = args.get("nonsensitive_password")
    if sensitive_password and nonsensitive_password and sensitive_password != nonsensitive_password:
        raise ValueError("Please use either the password argument or the non_sensitive_password argument, and not both.")

    return sensitive_password or nonsensitive_password


def main():
    dir_path = mkdtemp()
    try:
        args = demisto.args()
        zip_tool = args.get("zipTool", "7z")
        file_info = get_zip_path(args)
        password = get_password(args)
        excluded_dirs, excluded_files = extract(file_info=file_info, dir_path=dir_path, password=password, zip_tool=zip_tool)
        # Remove symlinks before uploading - use pure Python to avoid dependency on 'find' command
        for root, dirs, files_in_dir in os.walk(dir_path):
            for name in files_in_dir + dirs:
                full_path = Path(root) / name
                if full_path.is_symlink():
                    try:
                        full_path.unlink()
                        demisto.debug(f"Removed symlink: {full_path}")
                    except OSError:
                        demisto.debug(f"Failed to remove symlink: {full_path}")
        upload_files(excluded_dirs, excluded_files, dir_path)

    except Exception as e:
        return_error(str(e))
    finally:
        shutil.rmtree(dir_path)


if __name__ in ("__builtin__", "builtins"):
    main()

README

Unzip a file using fileName or entryID to specify a file. Unzipped files will be loaded to the War Room and names will be put into the context.

Script Data


Name Description
Script Type python3
Tags Utility, file
Cortex XSOAR Version 5.0.0

Used In


Sample usage of this script can be found in the following playbooks and scripts.

  • Comprehensive PAN-OS Best Practice Assessment
  • Cortex XDR - Retrieve File by sha256
  • CrowdStrike Falcon - Retrieve File
  • Get File Sample By Hash - Cylance Protect
  • Local Analysis alert Investigation
  • MDE - Retrieve File
  • PS Remote Get File Sample From Path
  • PS-Remote Get MFT
  • PS-Remote Get Registry
  • Pull Request Creation - Generic

Inputs


Argument Name Description
fileName The file name.
password Password to protect the ZIP file.
nonsensitive_password Password to protect the ZIP file, inserted as a non sensative argument.
entryID The entry ID of the attached ZIPp file in the War Room.
lastZipFileInWarroom Enter ‘yes’ (or any other value) if the ZIP file is last ZIP file in the War Room.
zipTool Tool to extract zip

Outputs


Path Description Type
ExtractedFiles A list of file names that were extracted from the ZIP file. Unknown