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

Details

IDUnzipFile
Languagepython
From Version5.0.0
Docker Imagedemisto/unzip:1.0.0.11009641
TagsUtility file

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
import os
from tempfile import mkdtemp
from unittest.mock import patch, MagicMock

import pytest
from UnzipFile import *

data_test_unzip_no_password = ["testZip.yml", "ScanSummary.txt", "item.png"]


@pytest.mark.parametrize("file_name", data_test_unzip_no_password)
def test_unzip_no_password(file_name):
    """
    Given
    - valid zip file - no password required
    - empty folder _dir
    When
    - run extract on that zip file and export the internal files to _dir
    Then
    - ensure zip file content have be saved at _dir directory with the original filename
    - ensure that the saved file has expected content
    """
    # Given
    # - valid zip file - no password required
    main_dir = "/".join(__file__.split("/")[0:-1])
    expected_file_unzipped = os.path.join(main_dir + "/test_data", file_name)
    zipped_file_path = expected_file_unzipped + ".zip"
    # Creation of file object
    zipped_file_object = {"name": "testFile", "path": zipped_file_path}
    # - empty folder _di
    _dir = mkdtemp()
    # When
    # - run extract on that zip file and export the internal files to _dir
    extract(zipped_file_object, _dir)
    # Then
    # - ensure zip file content have been saved at _dir directory with the original filename
    with open(_dir + "/" + file_name, "rb") as f:
        actual_file_data = f.read()
    with open(expected_file_unzipped, "rb") as f:
        expected_data = f.read()
    shutil.rmtree(_dir)
    # - ensure that the saved file has expected content data
    assert expected_data.splitlines() == actual_file_data.splitlines(), f"failed extracting {zipped_file_path}"


@pytest.mark.parametrize("zip_tool", ("7z", "zipfile"))
def test_unzip_with_password(zip_tool: str):
    """
    Given
    - valid zip file - with password required
    - empty folder _dir
    - the tool to extract files
    When
    - run extract on that zip file and export the internal files to _dir
    Then
    - ensure zip file content have be saved at _dir directory with the original filename
    - ensure that the saved file has expected content
    """
    # Given
    # - valid zip file - no password required
    file_name = "fix_unzip.png"
    password = "demisto"
    main_dir = "/".join(__file__.split("/")[0:-1])
    expected_file_unzipped = os.path.join(main_dir + "/test_data", file_name)
    zipped_file_path = expected_file_unzipped + ".zip"
    # Creation of file object
    zipped_file_object = {"name": "testFile", "path": zipped_file_path}
    # - empty folder _dir
    _dir = mkdtemp()
    # When
    # - run extract on that zip file and export the internal files to _dir
    extract(zipped_file_object, _dir, password=password, zip_tool=zip_tool)
    # Then
    # - ensure zip file content have been saved at _dir directory with the original filename
    with open(_dir + "/" + file_name, "rb") as f:
        actual_file_data = f.read()
    with open(expected_file_unzipped, "rb") as f:
        expected_data = f.read()
    shutil.rmtree(_dir)
    # - ensure that the saved file has expected content data
    assert expected_data == actual_file_data, "failed unzipping file: " + zipped_file_path + " with password: " + password


long_file_name = os.urandom(256)
data_test_unzip_long_file_name = ["long_filename_zip.zip"]


@pytest.mark.parametrize("file_name", data_test_unzip_long_file_name)
def test_unzip_long_filename(file_name, mocker):
    """
    Given
    - valid zip file - includes a file with long filename
    - empty folder _dir
    When
    - run extract on that zip file and export the internal files to _dir
    Then
    - ensure zip file content have be saved at _dir directory with the new filename
    """
    import UnzipFile as unzip

    # Given
    # - valid zip file - includes a file with long filename
    main_dir = "/".join(__file__.split("/")[0:-1])
    zip_file_path = os.path.join(main_dir + "/test_data", file_name)
    # Creation of file object
    zipped_file_object = {"name": "testFile", "path": zip_file_path}
    # - empty folder _dir
    _dir = mkdtemp()
    # When
    # - run extract on that zip file and export the internal files to _dir
    mocker.patch.object(unzip, "SLICE_FILENAME_SIZE_BYTES", return_value=100)
    extract(zipped_file_object, _dir, zip_tool="zipfile")
    # Then
    # - ensure zip file content have been saved at _dir directory with the new filename
    files_list = os.listdir(_dir)

    shutil.rmtree(_dir)
    assert files_list[0].endswith("_shortened_.rtf") is True


def test_unrar_no_password():
    """
    Given
    - valid rar file - no password required
    - empty folder _dir
    When
    - run extract on the rar file and export the internal files to _dir
    Then
    - ensure rar file content has been saved at _dir directory with the original filename
    - ensure that the saved file has expected content
    """
    file_name = "Untitled_document.pdf"
    main_dir = "/".join(__file__.split("/")[0:-1])
    expected_file_unzipped = os.path.join(main_dir + "/test_data", file_name)
    zipped_file_path = expected_file_unzipped + ".rar"
    # Creation of file object
    zipped_file_object = {"name": "Untitled_document.pdf.rar", "path": zipped_file_path}
    # - empty folder _di
    _dir = mkdtemp()
    # When
    # - run extract on that zip file and export the internal files to _dir
    extract(zipped_file_object, _dir)
    # Then
    # - ensure rar file content have been saved at _dir directory with the original filename
    with open(_dir + "/" + file_name, "rb") as f:
        actual_file_data = f.read()
    with open(expected_file_unzipped, "rb") as f:
        expected_data = f.read()
    shutil.rmtree(_dir)
    # - ensure that the saved file has expected content data
    assert expected_data == actual_file_data, "failed extracting " + zipped_file_path


def test_extract_tarfile():
    """
    Given
    - valid tar.gz file
    - empty folder _dir
    When
    - run extract on the tar file and export the internal files to _dir
    Then
    - ensure tar file content has been saved at _dir directory with the original filename
    - ensure that the saved file has expected content
    """
    file_name = "test_file.txt"
    main_dir = "/".join(__file__.split("/")[0:-1])
    expected_file_unzipped = os.path.join(main_dir + "/test_data", file_name)
    zipped_file_path = expected_file_unzipped + ".tar.gz"
    # Creation of file object
    zipped_file_object = {"name": "test_file.tar.gz", "path": zipped_file_path}
    # - empty folder _di
    _dir = mkdtemp()
    # When
    # - run extract on that zip file and export the internal files to _dir
    extract(zipped_file_object, _dir)
    # Then
    # - ensure tar file content have been saved at _dir directory with the original filename
    with open(_dir + "/" + file_name, "rb") as f:
        actual_file_data = f.read()
    with open(expected_file_unzipped, "rb") as f:
        expected_data = f.read()
    shutil.rmtree(_dir)
    # - ensure that the saved file has expected content data
    assert expected_data == actual_file_data, "failed extracting " + zipped_file_path


ARGS_BOTH_PASSWORDS_IDENTICAL = {"password": "aa", "nonsensitive_password": "aa"}
ARGS_BOTH_PASSWORDS_NOT_IDENTICAL = {"password": "aa", "nonsensitive_password": "bb"}
ARGS_ONLY_PASSWORD = {"password": "aa"}
ARGS_ONLY_NONSENSITIVE_PASSWORD = {"nonsensitive_password": "aa"}


@pytest.mark.parametrize("args", [ARGS_BOTH_PASSWORDS_IDENTICAL, ARGS_ONLY_NONSENSITIVE_PASSWORD, ARGS_ONLY_PASSWORD])
def test_get_password_valid(args):
    """
    Given
    - arguments for the script
    When
    - running the script on a password locked file
    Then
    - ensure that only one of the arguments 'password' or 'nonsensitive_password' is given or if they are identical.
    """
    assert get_password(args) == "aa"


def test_get_password_invalid():
    """
    Given
    - arguments for the script
    When
    - running the script on a password locked file
    Then
    - ensure that only one of the arguments 'password' or 'nonsensitive_password' is given or if they are identical.
    """
    with pytest.raises(ValueError) as e:
        get_password(ARGS_BOTH_PASSWORDS_NOT_IDENTICAL)
        if not e:
            raise AssertionError


def test_archive_with_slash_in_path():
    """
    Given
    - valid tar.gz file with slash in path
    - empty folder _dir
    When
    - run extract on the tar file and export the internal files to _dir
    Then
    - ensure no error was returned
    """
    zipped_file_object = {"name": "Archive_with_slash_in_path.tar.gz", "path": "test_data/Archive_with_slash_in_path.tar.gz"}
    # - empty folder _dir
    _dir = mkdtemp()
    # When
    # - run extract on that zip file and export the internal files to _dir
    excluded_dirs, excluded_files = extract(zipped_file_object, _dir)
    # Then
    assert excluded_dirs


@pytest.fixture
def mock_popen():
    with patch("UnzipFile.Popen") as mock:
        yield mock


def test_extract_with_errors_in_stdout(mock_popen):
    """
    Given:
    - A valid tar.gz file with a file name that contains the word "Errors".
    - A temporary directory for extraction.

    When:
    - Extracting the tar file into the directory.

    Then:
    - Ensure extraction completes successfully.
    - Ensure extracted files exist in the directory.
    - Ensure no unexpected errors occur.
    """

    # Prepare the mock to simulate the command's output
    mock_process = MagicMock()
    mock_process.communicate.return_value = (
        b"Hello_World_Errors.yml\nHello_World.yml",  # stdout
        b"",  # stderr (no error)
    )

    # Mock the Popen constructor to return our mock process
    mock_popen.return_value = mock_process

    # Setup test inputs
    file_path = "/test_data/Archive_with_Errors.tar.gz"
    dir_path = "/tmp/extracted_files"
    file_name = "Archive_with_Errors.tar.gz"

    # Run the extraction function and assert it raises the expected exception

    result = extract_using_tarfile(file_path, dir_path, file_name)
    # Assert the stdout contains both filenames
    assert "Hello_World_Errors.yml" in result
    assert "Hello_World.yml" in result


def test_unzip_with_space_in_path(mocker):
    """
    Given
    - A zip file path with spaces
    - empty folder _dir
    When
    - run extract on that zip file
    Then
    - ensure the command passed to Popen is a list and contains the path correctly (not split)
    """
    import UnzipFile as unzip

    # Given
    zip_path = "/path/to/directory with spaces/test.zip"
    zipped_file_object = {"name": "test.zip", "path": zip_path}
    _dir = "/tmp/extract_dir"

    # Mock Popen
    mock_popen = mocker.patch.object(unzip, "Popen")
    mock_process = MagicMock()
    mock_process.communicate.return_value = (b"", b"")
    mock_popen.return_value = mock_process

    # When
    extract(zipped_file_object, _dir, zip_tool="7z")

    # Then
    args, _ = mock_popen.call_args
    cmd_list = args[0]

    assert isinstance(cmd_list, list)
    assert zip_path in cmd_list


class TestUploadFilesSafeHandling:
    """Tests for safe file handling in upload_files."""

    def test_symlinks_are_skipped(self, mocker):
        """
        Given
        - An extracted directory containing a symbolic link
        When
        - upload_files processes the directory
        Then
        - The symbolic link is skipped and not uploaded
        """
        dir_path = mkdtemp()
        try:
            # Create a real file and a symlink to it
            real_file = os.path.join(dir_path, "real_file.txt")
            link_file = os.path.join(dir_path, "link_file.txt")
            with open(real_file, "w") as f:
                f.write("real content")
            os.symlink(real_file, link_file)

            mock_debug = mocker.patch.object(demisto, "debug")
            mocker.patch.object(demisto, "results")

            upload_files([], [], dir_path)

            # The symlink should be skipped - check debug was called with skip message
            debug_messages = [str(c) for c in mock_debug.call_args_list]
            assert any("Skipping" in msg for msg in debug_messages)
        finally:
            shutil.rmtree(dir_path)

    def test_files_outside_extraction_dir_are_skipped(self, mocker):
        """
        Given
        - An extracted file whose real path resolves outside the extraction directory
        When
        - upload_files processes the directory
        Then
        - The file is skipped and not uploaded
        """
        dir_path = mkdtemp()
        outside_dir = mkdtemp()
        try:
            # Create a file outside the extraction directory
            outside_file = os.path.join(outside_dir, "outside.txt")
            with open(outside_file, "w") as f:
                f.write("outside content")
            # Create a symlink inside dir_path pointing to the outside file
            link_path = os.path.join(dir_path, "escape_link.txt")
            os.symlink(outside_file, link_path)

            mock_debug = mocker.patch.object(demisto, "debug")
            mocker.patch.object(demisto, "results")

            upload_files([], [], dir_path)

            # The out-of-tree symlink should be skipped
            debug_messages = [str(c) for c in mock_debug.call_args_list]
            assert any("Skipping" in msg for msg in debug_messages)
        finally:
            shutil.rmtree(dir_path)
            shutil.rmtree(outside_dir)

    def test_normal_files_within_dir_are_processed(self, mocker):
        """
        Given
        - A normal file within the extraction directory
        When
        - upload_files processes the directory
        Then
        - The file is read and results are returned
        """
        dir_path = mkdtemp()
        try:
            # Create a normal file inside the extraction directory
            normal_file = os.path.join(dir_path, "normal_file.txt")
            with open(normal_file, "wb") as f:
                f.write(b"file content")

            mock_results = mocker.patch.object(demisto, "results")

            upload_files([], [], dir_path)

            # demisto.results should have been called at least twice:
            # once for fileResult and once for the summary
            assert mock_results.call_count >= 1
            # Verify the file was included in the extracted files list
            all_calls_str = str(mock_results.call_args_list)
            assert "normal_file.txt" in all_calls_str
        finally:
            shutil.rmtree(dir_path)