SOCFWPackManager

Downloads a SOC Framework content pack from a ZIP URL and installs it on the tenant as system content. Used internally by the SOCFWPackManager script — end users invoke the script, not this integration directly.

Utilities · SOC Framework Pack Manager

Details

IDSOCFWPackManager
ProviderSOC Framework
CategoryUtilities
From Version8.0.0
Docker Imagedemisto/demisto-sdk:1.38.14.6091684
Supported ModulesAgentix XSIAM

README

SOC Framework Pack Manager — internal HTTP layer used by the SOCFWPackManager
script to install SOC Framework content packs as system content. End users do
not invoke this integration directly.

Architecture

This integration is paired with the SOCFWPackManager script in the same
pack. The script reads the SOC Framework pack catalog, sequences pack
installs, configures integration instances and jobs, and synchronizes the
value_tags lookup. Because Cortex XSIAM scripts can call
demisto.executeCommand, all orchestration lives there.

This integration stores the tenant URL, credentials, TLS verification setting,
and the pack catalog location. It exposes two commands: socfw-install-pack,
which downloads a pack ZIP and uploads it as system content, and
socfw-catalog-url-get, which returns the configured catalog location so the
script can read it. Cortex XSIAM integrations cannot call
demisto.executeCommand, so the integration deliberately performs only the
work that needs raw HTTP.

End users run !SOCFWPackManager action=apply pack_id=... from the XSIAM
Playground. The script invokes socfw-install-pack on this integration
internally.

Configure SOC Framework Pack Manager on Cortex XSIAM

  1. Navigate to Settings > Configurations > API Keys and create a
    Standard API key.
  2. Copy the Key, the Key ID, and click Copy URL to capture the
    tenant Server URL.
  3. Navigate to Settings > Configurations > Integrations.
  4. Search for SOC Framework Pack Manager.
  5. Click Add instance to create and configure a new integration instance.
Parameter Description Required
Server URL The tenant API URL or tenant URL. The integration adds the api- prefix when it is missing. True
API Key ID The numeric ID of the Standard API key, shown in the API Keys table. True
API Key The secret value of the Standard API key. True
Trust any certificate (not secure) Whether to disable TLS certificate validation. Off by default. False
Use system proxy settings Whether to route HTTP traffic through the system proxy. Off by default. False
Pack catalog URL The location of the SOC Framework pack_catalog.json. Override to point at a fork or branch. Leave empty to use the SOC Framework repository default. False
  1. Click Test to validate the URL and credentials, then Done.

Commands

You can execute these commands from the Cortex XSIAM CLI as part of an
automation or in a playbook. After you successfully execute a command, a
DBot message appears in the War Room with the command details.

socfw-install-pack


Downloads a SOC Framework pack ZIP from the supplied URL and installs it on
the tenant as system content. Called by the SOCFWPackManager script — do not
invoke directly.

Base Command

socfw-install-pack

Input

Argument Name Description Required
url URL of the pack ZIP to install (typically a GitHub release asset). Required
filename Asset filename, including the .zip extension. Derived from the URL when omitted. Optional

Context Output

Path Type Description
SOCFramework.PackInstall.filename String Installed pack filename.
SOCFramework.PackInstall.url String Source URL the pack was downloaded from.
SOCFramework.PackInstall.status String Install status (success on completion).
SOCFramework.PackInstall.response Unknown Raw response from the demisto-sdk upload step.

Command example

!socfw-install-pack url=https://github.com/Palo-Cortex/secops-framework/releases/download/soc-optimization-unified-v3.6.3/soc-optimization-unified-v3.6.3.zip

Context Example

{
    "SOCFramework": {
        "PackInstall": {
            "filename": "soc-optimization-unified-v3.6.3.zip",
            "url": "https://github.com/Palo-Cortex/secops-framework/releases/download/soc-optimization-unified-v3.6.3/soc-optimization-unified-v3.6.3.zip",
            "status": "success",
            "response": {
                "success": true,
                "message": "Uploaded /home/demisto/Packs/soc-optimization-unified-v3.6.3"
            }
        }
    }
}

Human Readable Output

Pack soc-optimization-unified-v3.6.3.zip installed successfully.

socfw-catalog-url-get


Returns the SOC Framework pack catalog URL configured on this instance. Called by the SOCFWPackManager script so the catalog location is set once on the instance instead of passed as an argument on every run.

Base Command

socfw-catalog-url-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
SOCFramework.PackManager.CatalogURL String The pack catalog URL configured on this instance, or the SOC Framework default when the field is empty.

Configuration parameters

  • url — Server URL (required)
  • credentials — API Key ID (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • catalog_url — Pack catalog URL

Commands (2)

  • socfw-catalog-url-get

    Returns the SOC Framework pack catalog URL configured on this instance. Called by the SOCFWPackManager script so the catalog location is set once on the instance instead of passed as an argument on every run.

  • socfw-install-pack

    Download and install a SOC Framework pack ZIP as system content. Called by the SOCFWPackManager script — do not invoke directly.

"""
SOCFWPackManager_test.py

Unit tests for the SOCFWPackManager integration.
Run:
    pytest Packs/SocFrameworkManager/Integrations/SOCFWPackManager/SOCFWPackManager_test.py -v
"""

import importlib
import json
import os
import shutil
import sys
import tempfile
import types
import zipfile

import pytest


MODULE_NAME = "SOCFWPackManager"


# ---------------------------------------------------------------------------
# CommonServerPython stub
# ---------------------------------------------------------------------------


def _make_common_server_python_stub(captured_results, captured_errors):
    """Build a minimal CommonServerPython module rich enough that the
    integration's ``from CommonServerPython import *`` resolves every name
    the integration references at import time.

    Attributes on this module are pulled into the integration namespace by
    the wildcard import — that's how ``BaseClient`` becomes available inside
    SOCFWPackManager.py during test load.
    """

    common = types.ModuleType("CommonServerPython")

    class _StubBaseClient:
        """Minimal BaseClient compatible with the integration's usage.

        ``_http_request`` is monkeypatched per test. ``stream`` is honored so
        callers can pass it without exploding.
        """

        def __init__(self, base_url, verify=True, proxy=False, headers=None, **kwargs):
            self._base_url = base_url
            self._verify = verify
            self._proxy = proxy
            self._headers = headers or {}

        def _http_request(self, method, url_suffix="", full_url=None, **kwargs):
            raise NotImplementedError("Test must replace _http_request on the client instance.")

    class _StubDemistoException(Exception):
        pass

    def _arg_to_boolean(value):
        if isinstance(value, bool):
            return value
        if value is None:
            return False
        if isinstance(value, str):
            return value.strip().lower() in ("true", "yes", "1", "y", "on")
        return bool(value)

    def _return_results(value):
        captured_results.append(value)

    def _return_error(message):
        captured_errors.append(message)
        raise RuntimeError(message)

    def _command_results(**kw):
        return kw

    common.BaseClient = _StubBaseClient
    common.DemistoException = _StubDemistoException
    common.argToBoolean = _arg_to_boolean
    common.return_results = _return_results
    common.return_error = _return_error
    common.CommandResults = _command_results

    return common


def load_integration(params=None, args=None):
    """(Re)load the integration module under a fresh stub harness."""
    dm = types.SimpleNamespace()
    dm._params = params or {}
    dm._args = args or {}
    dm._results = []
    dm._errors = []

    dm.params = lambda: dm._params
    dm.args = lambda: dm._args
    dm.command = lambda: dm._args.get("__command__", "socfw-install-pack")
    dm.debug = lambda x: None
    dm.error = lambda x: None

    sys.modules["demistomock"] = dm
    sys.modules["CommonServerPython"] = _make_common_server_python_stub(dm._results, dm._errors)

    if MODULE_NAME in sys.modules:
        del sys.modules[MODULE_NAME]

    module = importlib.import_module(MODULE_NAME)
    return module, dm


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


class _StreamingResponse:
    """Mock matching the ``iter_content``/``headers`` surface used by
    ``ContentClient.stream_download_zip``."""

    def __init__(self, content: bytes, status_code: int = 200, headers=None):
        self._content = content
        self.status_code = status_code
        self.headers = headers or {}

    def iter_content(self, chunk_size=1):
        idx = 0
        while idx < len(self._content):
            yield self._content[idx : idx + chunk_size]
            idx += chunk_size


def _make_client(mod, *, verify=True, proxy=False):
    """Build a ContentClient with neutral creds for tests."""
    return mod.ContentClient(
        base_url="https://tenant.xdr.us.paloaltonetworks.com",
        api_id="3",
        api_key="my-key",
        verify=verify,
        proxy=proxy,
    )


def _make_test_zip(tmp_dir, nested=True):
    zip_path = os.path.join(tmp_dir, "TestPack.zip")
    with zipfile.ZipFile(zip_path, "w") as zf:
        if nested:
            zf.writestr(
                "TestPack/pack_metadata.json",
                json.dumps({"name": "TestPack", "currentVersion": "1.0.0"}),
            )
            zf.writestr("TestPack/README.md", "# Test")
            zf.writestr("TestPack/CorrelationRules/rule.yml", "id: test-rule\n")
        else:
            zf.writestr(
                "pack_metadata.json",
                json.dumps({"name": "TestPack", "currentVersion": "1.0.0"}),
            )
    return zip_path


# ---------------------------------------------------------------------------
# ContentClient._set_sdk_env (covers the prior _set_sdk_env tests)
# ---------------------------------------------------------------------------


def test_set_sdk_env_uses_api_prefix():
    mod, _ = load_integration()
    client = mod.ContentClient(
        base_url="https://tenant.xdr.us.paloaltonetworks.com",
        api_id="3",
        api_key="my-key",
        verify=True,
        proxy=False,
    )
    client._set_sdk_env()
    assert os.environ["DEMISTO_BASE_URL"] == "https://api-tenant.xdr.us.paloaltonetworks.com"
    assert os.environ["DEMISTO_API_KEY"] == "my-key"
    assert os.environ["XSIAM_AUTH_ID"] == "3"


def test_set_sdk_env_preserves_existing_api_prefix():
    mod, _ = load_integration()
    client = mod.ContentClient(
        base_url="https://api-tenant.xdr.us.paloaltonetworks.com",
        api_id="5",
        api_key="k",
        verify=True,
        proxy=False,
    )
    client._set_sdk_env()
    assert os.environ["DEMISTO_BASE_URL"] == "https://api-tenant.xdr.us.paloaltonetworks.com"


# ---------------------------------------------------------------------------
# _prepare_pack_dir / _safe_extract_zip / _safe_flatten_one_level
# (covers the prior unzip_and_flatten tests)
# ---------------------------------------------------------------------------


def test_unzip_and_flatten_nested_zip():
    mod, _ = load_integration()
    tmp = tempfile.mkdtemp()
    orig = os.getcwd()
    try:
        os.chdir(tmp)
        pack_path = mod._prepare_pack_dir(_make_test_zip(tmp), "TestPack.zip")
        assert os.path.isdir(pack_path)
        assert os.path.exists(os.path.join(pack_path, "pack_metadata.json"))
        assert os.path.exists(os.path.join(pack_path, "CorrelationRules", "rule.yml"))
        assert not os.path.isdir(os.path.join(pack_path, "TestPack"))
    finally:
        os.chdir(orig)
        shutil.rmtree(tmp, ignore_errors=True)


def test_unzip_and_flatten_creates_landing_page_stub():
    mod, _ = load_integration()
    tmp = tempfile.mkdtemp()
    orig = os.getcwd()
    try:
        os.chdir(tmp)
        mod._prepare_pack_dir(_make_test_zip(tmp), "TestPack.zip")
        lp = os.path.join(tmp, "Tests", "Marketplace", "landingPage_sections.json")
        assert os.path.exists(lp)
        assert json.load(open(lp)) == {"sections": []}
    finally:
        os.chdir(orig)
        shutil.rmtree(tmp, ignore_errors=True)


def test_unzip_and_flatten_rejects_non_zip():
    mod, _ = load_integration()
    tmp = tempfile.mkdtemp()
    try:
        bad = os.path.join(tmp, "bad.zip")
        with open(bad, "w") as f:
            f.write("not a zip")
        with pytest.raises(Exception, match="not a valid zip"):
            mod._prepare_pack_dir(bad, "bad.zip")
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def test_unzip_and_flatten_rejects_missing_pack_metadata():
    mod, _ = load_integration()
    tmp = tempfile.mkdtemp()
    orig = os.getcwd()
    try:
        os.chdir(tmp)
        zip_path = os.path.join(tmp, "bad.zip")
        with zipfile.ZipFile(zip_path, "w") as zf:
            zf.writestr("some_file.yml", "content")
        with pytest.raises(Exception, match="missing pack_metadata.json"):
            mod._prepare_pack_dir(zip_path, "bad.zip")
    finally:
        os.chdir(orig)
        shutil.rmtree(tmp, ignore_errors=True)


# ---------------------------------------------------------------------------
# ContentClient.stream_download_zip (covers the prior _stream_download tests)
# ---------------------------------------------------------------------------


def test_stream_download_writes_full_payload(tmp_path):
    mod, _ = load_integration()
    payload = b"abc" * 100

    client = _make_client(mod)
    client._http_request = lambda **kw: _StreamingResponse(payload, headers={"Content-Length": str(len(payload))})

    dest = str(tmp_path / "out.zip")
    client.stream_download_zip("https://example.com/pack.zip", dest)
    assert open(dest, "rb").read() == payload


def test_stream_download_rejects_oversized_content_length(tmp_path):
    mod, _ = load_integration()
    too_big = mod.MAX_DOWNLOAD_BYTES + 1

    client = _make_client(mod)
    client._http_request = lambda **kw: _StreamingResponse(b"x", headers={"Content-Length": str(too_big)})

    dest = str(tmp_path / "out.zip")
    with pytest.raises(Exception, match="exceeds size limit"):
        client.stream_download_zip("https://example.com/pack.zip", dest)


def test_stream_download_rejects_oversized_streamed_body(tmp_path):
    """Server lies (or omits) Content-Length — guard during write must trip."""
    mod, _ = load_integration()
    mod.MAX_DOWNLOAD_BYTES = 1024
    mod.DOWNLOAD_CHUNK_BYTES = 256

    client = _make_client(mod)
    client._http_request = lambda **kw: _StreamingResponse(b"x" * 4096, headers={})

    dest = str(tmp_path / "out.zip")
    with pytest.raises(Exception, match="exceeds size limit during download"):
        client.stream_download_zip("https://example.com/pack.zip", dest)
    assert not os.path.exists(dest)


def test_stream_download_propagates_http_error(tmp_path):
    """BaseClient surfaces non-OK HTTP. We simulate that by raising."""
    mod, _ = load_integration()

    client = _make_client(mod)

    def fake_http_request(**kw):
        raise mod.DemistoException("Download failed HTTP 404")

    client._http_request = fake_http_request

    dest = str(tmp_path / "out.zip")
    with pytest.raises(Exception, match="Download failed HTTP 404"):
        client.stream_download_zip("https://example.com/missing.zip", dest)


def test_stream_download_threads_verify_flag(tmp_path):
    """verify is a ContentClient-level concern, plumbed into BaseClient.

    With verify=False the client records the flag at construction and any
    requests it issues use that setting. Our stub records what BaseClient
    was given so we can assert it.
    """
    mod, _ = load_integration()

    insecure_client = _make_client(mod, verify=False)
    assert insecure_client._verify is False

    secure_client = _make_client(mod, verify=True)
    assert secure_client._verify is True

    # And confirm stream=True is forwarded to the HTTP layer on download.
    seen = {}

    def capture(**kw):
        seen.update(kw)
        return _StreamingResponse(b"abc")

    secure_client._http_request = capture
    secure_client.stream_download_zip("https://example.com/pack.zip", str(tmp_path / "out.zip"))
    assert seen.get("stream") is True


# ---------------------------------------------------------------------------
# install_pack_command (covers the prior command_install_pack tests)
# ---------------------------------------------------------------------------


def test_command_install_pack_calls_download_and_sdk(tmp_path):
    mod, dm = load_integration()

    # Build a real valid zip
    zip_path = str(tmp_path / "Pack-v1.0.0.zip")
    with zipfile.ZipFile(zip_path, "w") as zf:
        zf.writestr(
            "Pack-v1.0.0/pack_metadata.json",
            json.dumps({"name": "Pack", "currentVersion": "1.0.0"}),
        )
    zip_bytes = open(zip_path, "rb").read()

    client = _make_client(mod, verify=True)
    client._http_request = lambda **kw: _StreamingResponse(zip_bytes, headers={"Content-Length": str(len(zip_bytes))})

    sdk_calls = []

    def fake_upload(pack_path):
        sdk_calls.append({"pack_path": pack_path, "verify": client._verify})
        return {"success": True}

    client.upload_pack_as_system_content = fake_upload

    orig = os.getcwd()
    try:
        os.chdir(str(tmp_path))
        result = mod.install_pack_command(
            client,
            args={
                "url": "https://github.com/example/releases/download/Pack-v1.0.0/Pack-v1.0.0.zip",
                "filename": "Pack-v1.0.0.zip",
            },
        )
    finally:
        os.chdir(orig)

    assert sdk_calls, "upload_pack_as_system_content should be called"
    # The pack directory name becomes the pack ID on the tenant, so the release
    # version must not survive into it.
    assert sdk_calls[0]["pack_path"].endswith(os.sep + "Pack")
    assert "Pack-v1.0.0" not in os.path.basename(sdk_calls[0]["pack_path"])
    assert sdk_calls[0]["verify"] is True
    # CommandResults stub returns kwargs; outputs include the filename.
    assert result.get("outputs", {}).get("filename") == "Pack-v1.0.0.zip"


def test_command_install_pack_threads_insecure_param(tmp_path):
    mod, _ = load_integration()

    zip_path = str(tmp_path / "Pack-v1.0.0.zip")
    with zipfile.ZipFile(zip_path, "w") as zf:
        zf.writestr(
            "Pack-v1.0.0/pack_metadata.json",
            json.dumps({"name": "Pack", "currentVersion": "1.0.0"}),
        )
    zip_bytes = open(zip_path, "rb").read()

    # Insecure instance — verify=False is what the integration's main()
    # would build from params={"insecure": True}.
    client = _make_client(mod, verify=False)
    client._http_request = lambda **kw: _StreamingResponse(zip_bytes, headers={"Content-Length": str(len(zip_bytes))})

    sdk_calls = []

    def fake_upload(pack_path):
        sdk_calls.append({"verify": client._verify})
        return {"success": True}

    client.upload_pack_as_system_content = fake_upload

    orig = os.getcwd()
    try:
        os.chdir(str(tmp_path))
        mod.install_pack_command(
            client,
            args={
                "url": "https://github.com/example/releases/download/Pack-v1.0.0/Pack-v1.0.0.zip",
            },
        )
    finally:
        os.chdir(orig)

    # verify=False propagates from ContentClient construction to the upload.
    assert client._verify is False
    assert sdk_calls[0]["verify"] is False


def test_command_install_pack_404_raises():
    mod, _ = load_integration()
    client = _make_client(mod)

    def fake_http_request(**kw):
        raise mod.DemistoException("Download failed HTTP 404")

    client._http_request = fake_http_request

    with pytest.raises(Exception, match="Download failed HTTP 404"):
        mod.install_pack_command(
            client,
            args={"url": "https://github.com/example/does-not-exist.zip"},
        )


def test_command_install_pack_derives_filename_from_url(monkeypatch):
    mod, _ = load_integration()
    client = _make_client(mod)
    client._http_request = lambda **kw: _StreamingResponse(b"fake")

    derived = []

    def capture(zip_path, filename):
        derived.append(filename)
        raise RuntimeError("stop_here")

    monkeypatch.setattr(mod, "_prepare_pack_dir", capture)

    with pytest.raises(RuntimeError, match="stop_here"):
        mod.install_pack_command(
            client,
            args={"url": "https://github.com/org/repo/releases/download/Pack-v2.0.0/Pack-v2.0.0.zip"},
        )

    assert derived == ["Pack-v2.0.0.zip"]


def test_command_install_pack_appends_zip_if_missing(monkeypatch):
    mod, _ = load_integration()
    client = _make_client(mod)
    client._http_request = lambda **kw: _StreamingResponse(b"fake")

    derived = []

    def capture(zip_path, filename):
        derived.append(filename)
        raise RuntimeError("stop")

    monkeypatch.setattr(mod, "_prepare_pack_dir", capture)

    with pytest.raises(RuntimeError, match="stop"):
        mod.install_pack_command(
            client,
            args={"url": "https://example.com/Pack-v1.0.0", "filename": "Pack-v1.0.0"},
        )

    assert derived
    assert derived[0].endswith(".zip")


def test_pack_dir_name_strips_release_version():
    """Regression: the pack directory name becomes the pack ID on the tenant,
    so leaving the version on installed every release as a separate pack --
    soc-optimization-unified-v3.11.2 alongside soc-optimization-unified --
    rather than upgrading in place.
    """
    integration, _ = load_integration()
    cases = {
        "soc-optimization-unified.zip": "soc-optimization-unified",
        "soc-optimization-unified-v3.11.2.zip": "soc-optimization-unified",
        "soc-optimization-unified-v3.11.1-pr1008.zip": "soc-optimization-unified",
        "soc-optimization-unified-v3.11.2": "soc-optimization-unified",
        "SocFrameworkAbnormalSecurity-v1.0.4.zip": "SocFrameworkAbnormalSecurity",
        "soc-vendor-thing.zip": "soc-vendor-thing",
    }
    for given, expected in cases.items():
        assert integration.pack_dir_name(given) == expected, given


def test_prepare_pack_dir_uses_version_stripped_directory(tmp_path, monkeypatch):
    integration, _ = load_integration()
    monkeypatch.chdir(tmp_path)

    zip_path = tmp_path / "soc-optimization-unified-v3.11.2.zip"
    with zipfile.ZipFile(zip_path, "w") as zf:
        zf.writestr("soc-optimization-unified/pack_metadata.json", '{"name": "SOC Framework Unified"}')

    pack_path = integration._prepare_pack_dir(str(zip_path), "soc-optimization-unified-v3.11.2.zip")

    assert os.path.basename(pack_path) == "soc-optimization-unified"
    assert not os.path.exists(tmp_path / "Packs" / "soc-optimization-unified-v3.11.2")


def test_get_catalog_url_command_returns_configured_value():
    """A script cannot read another integration instance's parameters, so the
    configured catalog location is surfaced through this command instead.
    """
    integration, _ = load_integration()
    res = integration.get_catalog_url_command({"catalog_url": "https://fork.example/pack_catalog.json"})
    assert res["outputs"] == {"CatalogURL": "https://fork.example/pack_catalog.json"}
    assert res["raw_response"] == {"catalog_url": "https://fork.example/pack_catalog.json"}


def test_get_catalog_url_command_defaults_when_unset():
    integration, _ = load_integration()
    for params in ({}, {"catalog_url": ""}, {"catalog_url": "   "}, {"catalog_url": None}):
        res = integration.get_catalog_url_command(params)
        assert res["outputs"]["CatalogURL"] == integration.DEFAULT_CATALOG_URL, params


def test_pack_dir_name_keeps_version_like_strings_mid_filename():
    """Only a trailing version suffix is a version.

    A version-like component in the middle of the name is part of the pack ID
    and must survive, and a legitimate trailing word must not be mistaken for a
    pre-release tag -- "soc-v3-tools" previously reduced to "soc", which would
    install the pack under the wrong ID.
    """
    integration, _ = load_integration()
    cases = {
        # Version-like component mid-name, real version at the end.
        "soc-v2-endpoint-v1.0.0.zip": "soc-v2-endpoint",
        "my-v10-agent-v2.5.zip": "my-v10-agent",
        # Version-like component mid-name, no trailing version.
        "soc-v3-tools.zip": "soc-v3-tools",
        "soc-v2-endpoint.zip": "soc-v2-endpoint",
        # Recognized pre-release tags are still stripped with the version.
        "soc-pack-v1.2.3-pr1008.zip": "soc-pack",
        "soc-pack-v1.2.3-rc2.zip": "soc-pack",
        "soc-pack-v1.2.3-beta.zip": "soc-pack",
        # An unrecognized trailing word is not a pre-release tag.
        "soc-pack-v1.2.3-tools.zip": "soc-pack-v1.2.3-tools",
        # Case-insensitive on the version marker.
        "soc-pack-V1.2.3.zip": "soc-pack",
    }
    for given, expected in cases.items():
        assert integration.pack_dir_name(given) == expected, given


def test_get_catalog_url_command_rejects_a_relative_url():
    """A scheme-less value would resolve against the tenant host at fetch time
    and surface as a confusing 404, so it is reported and ignored here.
    """
    integration, _ = load_integration()
    for bad in (
        "raw.githubusercontent.com/org/repo/main/pack_catalog.json",
        "/Packs/pack_catalog.json",
        "ftp://example.com/pack_catalog.json",
        "not a url",
        "https:///pack_catalog.json",
    ):
        res = integration.get_catalog_url_command({"catalog_url": bad})
        assert res["outputs"]["CatalogURL"] == integration.DEFAULT_CATALOG_URL, bad
        assert "ignored" in res["readable_output"], bad


def test_get_catalog_url_command_accepts_a_valid_override():
    integration, _ = load_integration()
    for good in (
        "https://fork.example/pack_catalog.json",
        "http://internal.example:8080/catalog/pack_catalog.json",
    ):
        res = integration.get_catalog_url_command({"catalog_url": good})
        assert res["outputs"]["CatalogURL"] == good, good
        assert "ignored" not in res["readable_output"], good