xsoar-cli 2.4.0__tar.gz → 2.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/CHANGELOG.md +8 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/PKG-INFO +2 -2
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/_version.py +2 -2
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/case/README.md +47 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/case/commands.py +106 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/manifest/commands.py +21 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/azure.py +7 -2
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/base.py +8 -2
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/s3.py +7 -2
- xsoar_cli-2.4.2/src/xsoar_cli/xsoar_client/cases.py +78 -0
- xsoar_cli-2.4.2/src/xsoar_cli/xsoar_client/constants.py +8 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/execution.py +2 -5
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/conftest.py +13 -3
- xsoar_cli-2.4.2/tests/cli/test_case.py +123 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_manifest.py +64 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_artifact_azure.py +2 -3
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_artifact_s3.py +12 -3
- xsoar_cli-2.4.2/tests/unit/test_case_commands.py +39 -0
- xsoar_cli-2.4.2/tests/unit/test_cases.py +353 -0
- xsoar_cli-2.4.0/src/xsoar_cli/xsoar_client/cases.py +0 -25
- xsoar_cli-2.4.0/src/xsoar_cli/xsoar_client/constants.py +0 -4
- xsoar_cli-2.4.0/tests/cli/test_case.py +0 -54
- xsoar_cli-2.4.0/tests/unit/test_cases.py +0 -175
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/main.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/pull-request-open.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/release.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.github/workflows/update-changelog.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/.gitignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/CLAUDE.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/CONTRIBUTING.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/LICENSE.txt +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/pyproject.toml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/cli.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/case/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/completions/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/completions/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/completions/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/config/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/config/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/config/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/content/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/content/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/content/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/execute/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/execute/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/execute/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/graph/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/graph/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/graph/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/integration/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/integration/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/integration/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/manifest/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/manifest/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/pack/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/pack/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/pack/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/plugins/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/plugins/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/plugins/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/rbac/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/rbac/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/commands/rbac/commands.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/configuration.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/error_handling/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/error_handling/connection.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/error_handling/http.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/log.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/plugins/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/plugins/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/plugins/manager.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/config_file.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/content.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/download_content_handlers.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/manifest.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/output.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/validators.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/utilities/version_check.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/artifact_providers/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/client.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/content.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/integrations.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/packs.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/src/xsoar_cli/xsoar_client/rbac.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_base.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_completions.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_config.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_content.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_execute.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_graph.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_pack.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/cli/test_plugins.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/conftest.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/cases/create.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/cases/get.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/content/automation_search.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/content/playbook_search.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/content/user_commands.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/integrations/instances.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/manifest_base.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/manifest_invalid.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/manifest_with_pack_not_on_server.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response_missing_one_pack.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response_with_updates.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/manifest/server_base_response_with_updates_and_one_extra.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/packs/installed.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/packs/installed_expired.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/rbac/roles.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/rbac/user_groups.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/fixtures/rbac/users.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Download/playbook-empty.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/.pack-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/.secrets-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/Author_image.png +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/Playbooks/GenericPlaybook.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonPlaybooks/pack_metadata.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/.pack-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/.secrets-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Author_image.png +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/Scripts/GenericScript/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_CommonScripts/pack_metadata.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/.pack-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/.secrets-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Author_image.png +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Playbooks/EDR_InitialTriage.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/EDR_Triage/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/Scripts/LegacyItem/test_data/basescript-dummy.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_EDR/pack_metadata.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/.pack-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/.secrets-ignore +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/Author_image.png +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/Layouts/layoutscontainer-GenericLayout.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/README.md +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/MyOrg_Layouts/pack_metadata.json +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/mock_content/Packs/Not_applicable/Playbooks/Empty.yml +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/__init__.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/conftest.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_artifact_base.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_client.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_config_file.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_content_domain.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_content_filters.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_content_handlers.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_error_handling.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_execution.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_integrations.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_main.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_manifest_utils.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_output.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_packs.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_plugin_manager.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_rbac.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_validators.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/tests/unit/test_version_check.py +0 -0
- {xsoar_cli-2.4.0 → xsoar_cli-2.4.2}/uv.lock +0 -0
|
@@ -6,12 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.4.1] - 2026-06-19
|
|
10
|
+
|
|
11
|
+
## [2.4.0] - 2026-06-17
|
|
12
|
+
|
|
9
13
|
### Added
|
|
10
14
|
|
|
11
15
|
- `content get-detached --type all` queries both scripts and playbooks and displays results for each type.
|
|
12
16
|
- `content list --output-format` option (choices: `table`, `json`, `plain`, default: `table`). `table` is human-readable aligned columns, `json` is machine-readable, and `plain` is tab-separated values suitable for piping to tools like `grep`.
|
|
13
17
|
- `content list --search TERM` option. Filters the listed items with a case-insensitive substring match against each item's id, name, and description. Content types and integration brands with no matching items are omitted, and a `No content matching '<term>' found.` message is shown when nothing matches.
|
|
14
18
|
- `content describe --type [script|playbook|command] NAME` command. Shows a single content item in detail: description, arguments, and inputs/outputs. Commands also show the integration brand and its configured instances (name and state). Supports `--output-format [table|json]` (default: `table`). See the [content README](src/xsoar_cli/commands/content/README.md) for details.
|
|
19
|
+
- `case get-context CASENUMBER` command. Retrieves the investigation context tree for a case as JSON, matching `demisto.context()` at runtime. The incident record is not merged in. See the [case README](src/xsoar_cli/commands/case/README.md) for details.
|
|
20
|
+
- `case get-entry ENTRY_ID` command. Retrieves a single War Room entry by its full ID (e.g. `112@153483`) as JSON. The case ID is derived from the entry ID. Exits non-zero with a clear message when the entry ID is malformed or the entry is not found. See the [case README](src/xsoar_cli/commands/case/README.md) for details.
|
|
21
|
+
- `case get-entries CASENUMBER` command. Retrieves all War Room entries for a case as a JSON array, or an empty array when the case has no entries. See the [case README](src/xsoar_cli/commands/case/README.md) for details.
|
|
15
22
|
|
|
16
23
|
### Changed
|
|
17
24
|
|
|
@@ -26,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
26
33
|
|
|
27
34
|
### Fixed
|
|
28
35
|
|
|
36
|
+
- `manifest update` no longer crashes with `ValueError: max() iterable argument is empty` when a custom pack is installed on the XSOAR server but has no artifacts in the configured artifact repository. The command now skips such packs (they are already reported in the pre-existing "installed but not found in artifacts repo" warning) and continues updating the remaining packs. As part of the fix, `S3ArtifactProvider.get_latest_version()` and `AzureArtifactProvider.get_latest_version()` now return `None` instead of raising when the pack has no versions in the repo.
|
|
29
37
|
- `content get-detached` now returns only the content items that are actually detached. The previous query returned all system items, so the command reported every system script (or playbook) as detached instead of just the detached ones. Results are now filtered on each item's `detached` field.
|
|
30
38
|
|
|
31
39
|
## [2.3.0] - 2026-06-04
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: xsoar-cli
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: A command-line interface for managing Palo Alto Networks XSOAR (Cortex XSOAR)
|
|
5
5
|
Project-URL: Documentation, https://github.com/tlium/xsoar-cli#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/tlium/xsoar-cli/issues
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 4,
|
|
21
|
+
__version__ = version = '2.4.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -17,6 +17,53 @@ xsoar-cli case get 312412
|
|
|
17
17
|
xsoar-cli case get --environment prod 312412
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## Get Context
|
|
21
|
+
|
|
22
|
+
Retrieve the investigation context tree for a single case. Output is the raw context tree as JSON, matching `demisto.context()` at runtime. The incident record is not merged in.
|
|
23
|
+
|
|
24
|
+
**Syntax:** `xsoar-cli case get-context [OPTIONS] CASENUMBER`
|
|
25
|
+
|
|
26
|
+
**Options:**
|
|
27
|
+
- `--environment TEXT` - Target environment (default: uses default environment from config)
|
|
28
|
+
|
|
29
|
+
**Examples:**
|
|
30
|
+
```
|
|
31
|
+
xsoar-cli case get-context 153483
|
|
32
|
+
xsoar-cli case get-context --environment prod 153483
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Get Entry
|
|
36
|
+
|
|
37
|
+
Retrieve a single War Room entry by its full ID. Output is the raw entry as JSON. The command exits non-zero with a clear message when no entry with that ID exists in the case.
|
|
38
|
+
|
|
39
|
+
`ENTRY_ID` is the full entry ID as shown in the GUI, in the form `<n>@<case-id>` (e.g. `112@153483`). The case ID is taken from the entry ID, so it does not need to be supplied separately.
|
|
40
|
+
|
|
41
|
+
**Syntax:** `xsoar-cli case get-entry [OPTIONS] ENTRY_ID`
|
|
42
|
+
|
|
43
|
+
**Options:**
|
|
44
|
+
- `--environment TEXT` - Target environment (default: uses default environment from config)
|
|
45
|
+
|
|
46
|
+
**Examples:**
|
|
47
|
+
```
|
|
48
|
+
xsoar-cli case get-entry 112@153483
|
|
49
|
+
xsoar-cli case get-entry --environment prod 112@153483
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Get Entries
|
|
53
|
+
|
|
54
|
+
Retrieve all War Room entries for a single case. Output is the full list of entries as a JSON array, or an empty array when the case has no entries.
|
|
55
|
+
|
|
56
|
+
**Syntax:** `xsoar-cli case get-entries [OPTIONS] CASENUMBER`
|
|
57
|
+
|
|
58
|
+
**Options:**
|
|
59
|
+
- `--environment TEXT` - Target environment (default: uses default environment from config)
|
|
60
|
+
|
|
61
|
+
**Examples:**
|
|
62
|
+
```
|
|
63
|
+
xsoar-cli case get-entries 153483
|
|
64
|
+
xsoar-cli case get-entries --environment prod 153483
|
|
65
|
+
```
|
|
66
|
+
|
|
20
67
|
## Clone
|
|
21
68
|
|
|
22
69
|
Clone a case from one environment to another. Useful for copying production cases to development environment for testing.
|
|
@@ -25,6 +25,19 @@ def parse_string_to_dict(input_string: str | None, delimiter: str) -> dict:
|
|
|
25
25
|
return {key.strip(): value.strip() for key, value in valid_pairs}
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def parse_entry_id(entry_id: str) -> int:
|
|
29
|
+
"""Extract the numeric case ID from a full entry ID of the form '<n>@<case-id>'.
|
|
30
|
+
|
|
31
|
+
Raises ValueError if the entry ID is not in that form.
|
|
32
|
+
"""
|
|
33
|
+
parts = entry_id.split("@")
|
|
34
|
+
expected_parts = 2
|
|
35
|
+
if len(parts) != expected_parts or not parts[0].isdigit() or not parts[1].isdigit():
|
|
36
|
+
msg = f"invalid entry id '{entry_id}', expected the form <n>@<case-id> (e.g. 112@153483)"
|
|
37
|
+
raise ValueError(msg)
|
|
38
|
+
return int(parts[1])
|
|
39
|
+
|
|
40
|
+
|
|
28
41
|
@click.group()
|
|
29
42
|
def case() -> None:
|
|
30
43
|
"""Create, retrieve, and clone cases"""
|
|
@@ -210,6 +223,99 @@ def create( # noqa: PLR0913
|
|
|
210
223
|
click.echo(f"Created XSOAR case {case_id}")
|
|
211
224
|
|
|
212
225
|
|
|
226
|
+
@click.command(name="get-context")
|
|
227
|
+
@click.argument("casenumber", type=int)
|
|
228
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
229
|
+
@click.pass_context
|
|
230
|
+
@load_config
|
|
231
|
+
@validate_xsoar_connectivity
|
|
232
|
+
def get_context(ctx: click.Context, casenumber: int, environment: str | None) -> None:
|
|
233
|
+
"""Retrieve the investigation context tree for a single case.
|
|
234
|
+
|
|
235
|
+
CASENUMBER is the numeric case ID to look up. Output is the raw context
|
|
236
|
+
tree as JSON, matching demisto.context() at runtime.
|
|
237
|
+
|
|
238
|
+
Usage examples:
|
|
239
|
+
|
|
240
|
+
xsoar-cli case get-context 153483
|
|
241
|
+
"""
|
|
242
|
+
config = get_xsoar_config(ctx)
|
|
243
|
+
xsoar_client: Client = config.get_client(environment)
|
|
244
|
+
try:
|
|
245
|
+
response = xsoar_client.cases.get_context(casenumber)
|
|
246
|
+
except HTTPError as ex:
|
|
247
|
+
handler = HTTPErrorHandler()
|
|
248
|
+
click.echo(f"Error: {handler.get_message(ex, context='case')}")
|
|
249
|
+
ctx.exit(1)
|
|
250
|
+
click.echo(json.dumps(response, indent=4))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@click.command(name="get-entry")
|
|
254
|
+
@click.argument("entry_id", type=str)
|
|
255
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
256
|
+
@click.pass_context
|
|
257
|
+
@load_config
|
|
258
|
+
@validate_xsoar_connectivity
|
|
259
|
+
def get_entry(ctx: click.Context, entry_id: str, environment: str | None) -> None:
|
|
260
|
+
"""Retrieve a single War Room entry by its full ID.
|
|
261
|
+
|
|
262
|
+
ENTRY_ID is the full entry ID as shown in the GUI, in the form
|
|
263
|
+
<n>@<case-id> (e.g. 112@153483). The case ID is taken from the entry ID, so
|
|
264
|
+
it does not need to be supplied separately. Output is the raw entry as JSON.
|
|
265
|
+
|
|
266
|
+
Usage examples:
|
|
267
|
+
|
|
268
|
+
xsoar-cli case get-entry 112@153483
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
casenumber = parse_entry_id(entry_id)
|
|
272
|
+
except ValueError as ex:
|
|
273
|
+
click.echo(f"Error: {ex}")
|
|
274
|
+
ctx.exit(1)
|
|
275
|
+
config = get_xsoar_config(ctx)
|
|
276
|
+
xsoar_client: Client = config.get_client(environment)
|
|
277
|
+
try:
|
|
278
|
+
response = xsoar_client.cases.get_entry(casenumber, entry_id)
|
|
279
|
+
except HTTPError as ex:
|
|
280
|
+
handler = HTTPErrorHandler()
|
|
281
|
+
click.echo(f"Error: {handler.get_message(ex, context='case')}")
|
|
282
|
+
ctx.exit(1)
|
|
283
|
+
except ValueError as ex:
|
|
284
|
+
click.echo(f"Error: {ex}")
|
|
285
|
+
ctx.exit(1)
|
|
286
|
+
click.echo(json.dumps(response, indent=4))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@click.command(name="get-entries")
|
|
290
|
+
@click.argument("casenumber", type=int)
|
|
291
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
292
|
+
@click.pass_context
|
|
293
|
+
@load_config
|
|
294
|
+
@validate_xsoar_connectivity
|
|
295
|
+
def get_entries(ctx: click.Context, casenumber: int, environment: str | None) -> None:
|
|
296
|
+
"""Retrieve all War Room entries for a single case.
|
|
297
|
+
|
|
298
|
+
CASENUMBER is the numeric case ID. Output is the full list of entries as a
|
|
299
|
+
JSON array, or an empty array when the case has no entries.
|
|
300
|
+
|
|
301
|
+
Usage examples:
|
|
302
|
+
|
|
303
|
+
xsoar-cli case get-entries 153483
|
|
304
|
+
"""
|
|
305
|
+
config = get_xsoar_config(ctx)
|
|
306
|
+
xsoar_client: Client = config.get_client(environment)
|
|
307
|
+
try:
|
|
308
|
+
response = xsoar_client.cases.get_entries(casenumber)
|
|
309
|
+
except HTTPError as ex:
|
|
310
|
+
handler = HTTPErrorHandler()
|
|
311
|
+
click.echo(f"Error: {handler.get_message(ex, context='case')}")
|
|
312
|
+
ctx.exit(1)
|
|
313
|
+
click.echo(json.dumps(response, indent=4))
|
|
314
|
+
|
|
315
|
+
|
|
213
316
|
case.add_command(get)
|
|
214
317
|
case.add_command(clone)
|
|
215
318
|
case.add_command(create)
|
|
319
|
+
case.add_command(get_context)
|
|
320
|
+
case.add_command(get_entry)
|
|
321
|
+
case.add_command(get_entries)
|
|
@@ -190,12 +190,33 @@ def update(ctx: click.Context, environment: str | None, manifest: str) -> None:
|
|
|
190
190
|
click.echo(f" - {pack_id}", err=True)
|
|
191
191
|
|
|
192
192
|
changes_made = False
|
|
193
|
+
skipped_custom_packs = set(result.skipped)
|
|
193
194
|
for key in MANIFEST_KEYS:
|
|
194
195
|
custom = key == "custom_packs"
|
|
195
196
|
pack_type = "Custom" if custom else "Marketplace"
|
|
196
197
|
for index, manifest_pack in enumerate(manifest_data[key]):
|
|
197
198
|
if custom:
|
|
199
|
+
if manifest_pack["id"] in skipped_custom_packs:
|
|
200
|
+
# Pack is installed on the server but has no artifacts in the repo.
|
|
201
|
+
# Already surfaced to the user via the warning above.
|
|
202
|
+
logger.debug(
|
|
203
|
+
"%s pack '%s' skipped: not found in artifacts repo",
|
|
204
|
+
pack_type,
|
|
205
|
+
manifest_pack["id"],
|
|
206
|
+
)
|
|
207
|
+
continue
|
|
198
208
|
latest = xsoar_client.artifact_provider.get_latest_version(manifest_pack["id"]) # ty: ignore[unresolved-attribute]
|
|
209
|
+
if latest is None:
|
|
210
|
+
# Defensive guard: the artifact provider reports no versions for this
|
|
211
|
+
# pack. This overlaps with the skipped_custom_packs check above, but
|
|
212
|
+
# keeps update() safe if a pack is missing from the repo without
|
|
213
|
+
# having been flagged by get_outdated().
|
|
214
|
+
logger.debug(
|
|
215
|
+
"%s pack '%s' skipped: no versions returned by artifact provider",
|
|
216
|
+
pack_type,
|
|
217
|
+
manifest_pack["id"],
|
|
218
|
+
)
|
|
219
|
+
continue
|
|
199
220
|
else:
|
|
200
221
|
pack = next((item for item in outdated_installed_packs if item["id"] == manifest_pack["id"]), None)
|
|
201
222
|
if not pack:
|
|
@@ -58,9 +58,14 @@ class AzureArtifactProvider(BaseArtifactProvider):
|
|
|
58
58
|
download_stream = self.container_client.download_blob(blob=key_name)
|
|
59
59
|
return download_stream.readall()
|
|
60
60
|
|
|
61
|
-
def get_latest_version(self, pack_id: str) -> str:
|
|
62
|
-
"""Fetch the latest version of a Pack
|
|
61
|
+
def get_latest_version(self, pack_id: str) -> str | None:
|
|
62
|
+
"""Fetch the latest version of a Pack.
|
|
63
|
+
|
|
64
|
+
Returns None if the pack has no artifacts in the container.
|
|
65
|
+
"""
|
|
63
66
|
prefix = f"content/packs/{pack_id}/"
|
|
64
67
|
iter_names = self.container_client.list_blob_names(name_starts_with=prefix)
|
|
65
68
|
version_list = [x.split("/")[3] for x in list(iter_names)]
|
|
69
|
+
if not version_list:
|
|
70
|
+
return None
|
|
66
71
|
return str(max(version_list, key=version.parse))
|
|
@@ -22,8 +22,14 @@ class BaseArtifactProvider(ABC):
|
|
|
22
22
|
...
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
25
|
-
def get_latest_version(self, pack_id: str) -> str:
|
|
26
|
-
"""Get the latest version of a pack.
|
|
25
|
+
def get_latest_version(self, pack_id: str) -> str | None:
|
|
26
|
+
"""Get the latest version of a pack.
|
|
27
|
+
|
|
28
|
+
Returns the highest available version string, or None if the pack has
|
|
29
|
+
no artifacts in the storage backend. Callers must handle the None case
|
|
30
|
+
(typically a pack installed on the server but missing from the artifact
|
|
31
|
+
repository).
|
|
32
|
+
"""
|
|
27
33
|
...
|
|
28
34
|
|
|
29
35
|
def get_pack_path(self, pack_id: str, pack_version: str) -> str:
|
|
@@ -49,8 +49,11 @@ class S3ArtifactProvider(BaseArtifactProvider):
|
|
|
49
49
|
response = obj.get()
|
|
50
50
|
return response["Body"].read()
|
|
51
51
|
|
|
52
|
-
def get_latest_version(self, pack_id: str) -> str:
|
|
53
|
-
"""Fetch the latest version of a Pack.
|
|
52
|
+
def get_latest_version(self, pack_id: str) -> str | None:
|
|
53
|
+
"""Fetch the latest version of a Pack.
|
|
54
|
+
|
|
55
|
+
Returns None if the pack has no artifacts in the S3 bucket.
|
|
56
|
+
"""
|
|
54
57
|
client = self.session.client("s3", verify=self.verify_ssl)
|
|
55
58
|
result = client.list_objects_v2(
|
|
56
59
|
Bucket=self.bucket_name,
|
|
@@ -58,4 +61,6 @@ class S3ArtifactProvider(BaseArtifactProvider):
|
|
|
58
61
|
Delimiter="/",
|
|
59
62
|
)
|
|
60
63
|
version_list = [x["Prefix"].split("/")[3] for x in result.get("CommonPrefixes", [])]
|
|
64
|
+
if not version_list:
|
|
65
|
+
return None
|
|
61
66
|
return str(max(version_list, key=version.parse))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .constants import INVESTIGATION_PAGE_SIZE
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .client import Client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Cases:
|
|
12
|
+
def __init__(self, client: Client) -> None:
|
|
13
|
+
self.client = client
|
|
14
|
+
|
|
15
|
+
def get(self, case_id: int) -> dict:
|
|
16
|
+
"""Fetches a case by ID."""
|
|
17
|
+
endpoint = f"/incident/load/{case_id}"
|
|
18
|
+
response = self.client.make_request(endpoint=endpoint, method="GET")
|
|
19
|
+
response.raise_for_status()
|
|
20
|
+
return response.json()
|
|
21
|
+
|
|
22
|
+
def create(self, data: dict) -> dict:
|
|
23
|
+
"""Creates a new case."""
|
|
24
|
+
endpoint = self.client.resolve_endpoint(v6="/incident", v8="/xsoar/public/v1/incident")
|
|
25
|
+
response = self.client.make_request(endpoint=endpoint, json=data, method="POST")
|
|
26
|
+
response.raise_for_status()
|
|
27
|
+
return response.json()
|
|
28
|
+
|
|
29
|
+
def get_context(self, case_id: int) -> dict:
|
|
30
|
+
"""Fetches the investigation context tree for a case, verbatim.
|
|
31
|
+
|
|
32
|
+
Returns the raw POST /investigation/<id>/context response, which matches
|
|
33
|
+
demisto.context() at runtime. The incident record is not merged in.
|
|
34
|
+
|
|
35
|
+
The body query '${.}' is a DT expression meaning "the entire context
|
|
36
|
+
root", so the full context tree is returned.
|
|
37
|
+
"""
|
|
38
|
+
endpoint = f"/investigation/{case_id}/context"
|
|
39
|
+
response = self.client.make_request(endpoint=endpoint, method="POST", json={"query": "${.}"})
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
return response.json()
|
|
42
|
+
|
|
43
|
+
def get_entry(self, case_id: int, entry_id: str) -> dict:
|
|
44
|
+
"""Fetches a single War Room entry by its full ID (e.g. '112@153483').
|
|
45
|
+
|
|
46
|
+
XSOAR has no per-entry endpoint, so the full War Room history is fetched
|
|
47
|
+
via POST /investigation/<id> and the entry with the matching id is
|
|
48
|
+
returned verbatim. Raises ValueError if no entry with that id exists.
|
|
49
|
+
"""
|
|
50
|
+
for entry in self._fetch_entries(case_id):
|
|
51
|
+
if entry.get("id") == entry_id:
|
|
52
|
+
return entry
|
|
53
|
+
msg = f"Entry '{entry_id}' not found in case {case_id}"
|
|
54
|
+
raise ValueError(msg)
|
|
55
|
+
|
|
56
|
+
def get_entries(self, case_id: int) -> list[dict]:
|
|
57
|
+
"""Fetches all War Room entries for a case, verbatim.
|
|
58
|
+
|
|
59
|
+
Returns the full entries list from POST /investigation/<id>. An empty
|
|
60
|
+
list is returned when the case has no entries.
|
|
61
|
+
"""
|
|
62
|
+
return self._fetch_entries(case_id)
|
|
63
|
+
|
|
64
|
+
def _fetch_entries(self, case_id: int) -> list[dict]:
|
|
65
|
+
"""Fetches the full War Room entry history for a case.
|
|
66
|
+
|
|
67
|
+
XSOAR has no per-entry endpoint, so the whole history is retrieved with
|
|
68
|
+
a single POST /investigation/<id> call and the entries list is returned
|
|
69
|
+
verbatim.
|
|
70
|
+
"""
|
|
71
|
+
data, _, _ = self.client.demisto_py_instance.generic_request(
|
|
72
|
+
path=f"/investigation/{case_id}",
|
|
73
|
+
method="POST",
|
|
74
|
+
body={"pageSize": INVESTIGATION_PAGE_SIZE},
|
|
75
|
+
content_type="application/json",
|
|
76
|
+
response_type=object,
|
|
77
|
+
)
|
|
78
|
+
return data.get("entries") or []
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared constants for the xsoar_client package."""
|
|
2
|
+
|
|
3
|
+
XSOAR_OLD_VERSION = 6
|
|
4
|
+
HTTP_CALL_TIMEOUT = 30
|
|
5
|
+
|
|
6
|
+
# Page size requested when fetching investigation entries via POST /investigation/<id>.
|
|
7
|
+
# Set high so that all War Room entries are returned in a single response.
|
|
8
|
+
INVESTIGATION_PAGE_SIZE = 1000
|
|
@@ -4,6 +4,8 @@ import logging
|
|
|
4
4
|
import time
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
+
from .constants import INVESTIGATION_PAGE_SIZE
|
|
8
|
+
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
10
|
from .client import Client
|
|
9
11
|
|
|
@@ -23,11 +25,6 @@ DEFAULT_SYNC_TIMEOUT = 30
|
|
|
23
25
|
# result entries.
|
|
24
26
|
SYNC_POLL_INTERVAL = 2
|
|
25
27
|
|
|
26
|
-
# Page size requested when fetching investigation entries during polling. The
|
|
27
|
-
# playground accumulates history over time, so this is set high to ensure a
|
|
28
|
-
# freshly created result entry is included in the response.
|
|
29
|
-
INVESTIGATION_PAGE_SIZE = 1000
|
|
30
|
-
|
|
31
28
|
|
|
32
29
|
def _format_arg_value(value: str) -> str:
|
|
33
30
|
"""Format a single argument value for a War Room command string.
|
|
@@ -272,9 +272,10 @@ def mock_execute_env(mock_config_file) -> Iterator[types.SimpleNamespace]: # no
|
|
|
272
272
|
def mock_case_env(mock_config_file, make_case_response, make_case_create_response) -> Iterator[types.SimpleNamespace]: # noqa: ANN001
|
|
273
273
|
"""Mock environment for ``case`` commands.
|
|
274
274
|
|
|
275
|
-
Patches ``Cases.get
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
Patches ``Cases.get``, ``Cases.create``, ``Cases.get_context``,
|
|
276
|
+
``Cases.get_entry``, and ``Cases.get_entries`` with default success
|
|
277
|
+
responses. The mocks are exposed on the yielded namespace so tests can
|
|
278
|
+
override return values or side effects as needed.
|
|
278
279
|
"""
|
|
279
280
|
import types as _types
|
|
280
281
|
|
|
@@ -282,13 +283,22 @@ def mock_case_env(mock_config_file, make_case_response, make_case_create_respons
|
|
|
282
283
|
patch("xsoar_cli.xsoar_client.client.Client.test_connectivity", return_value=True) as mock_conn,
|
|
283
284
|
patch("xsoar_cli.xsoar_client.cases.Cases.get") as mock_get,
|
|
284
285
|
patch("xsoar_cli.xsoar_client.cases.Cases.create") as mock_create,
|
|
286
|
+
patch("xsoar_cli.xsoar_client.cases.Cases.get_context") as mock_get_context,
|
|
287
|
+
patch("xsoar_cli.xsoar_client.cases.Cases.get_entry") as mock_get_entry,
|
|
288
|
+
patch("xsoar_cli.xsoar_client.cases.Cases.get_entries") as mock_get_entries,
|
|
285
289
|
):
|
|
286
290
|
mock_get.return_value = make_case_response()
|
|
287
291
|
mock_create.return_value = make_case_create_response()
|
|
292
|
+
mock_get_context.return_value = {"playbook_name": "My Playbook", "File": []}
|
|
293
|
+
mock_get_entry.return_value = {"id": "112@153483", "contents": "entry data"}
|
|
294
|
+
mock_get_entries.return_value = [{"id": "112@153483", "contents": "entry data"}]
|
|
288
295
|
ns = _types.SimpleNamespace(
|
|
289
296
|
config=mock_config_file,
|
|
290
297
|
connectivity=mock_conn,
|
|
291
298
|
get=mock_get,
|
|
292
299
|
create=mock_create,
|
|
300
|
+
get_context=mock_get_context,
|
|
301
|
+
get_entry=mock_get_entry,
|
|
302
|
+
get_entries=mock_get_entries,
|
|
293
303
|
)
|
|
294
304
|
yield ns
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""CLI integration tests for the ``case`` command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from tests.cli.conftest import InvokeHelper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCaseGroup:
|
|
12
|
+
"""The bare ``case`` command and its help output."""
|
|
13
|
+
|
|
14
|
+
def test_shows_help(self, invoke: InvokeHelper) -> None:
|
|
15
|
+
result = invoke(["case"])
|
|
16
|
+
assert result.exit_code == 0
|
|
17
|
+
|
|
18
|
+
def test_help_flag(self, invoke: InvokeHelper) -> None:
|
|
19
|
+
result = invoke(["case", "--help"])
|
|
20
|
+
assert result.exit_code == 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestCaseGet:
|
|
24
|
+
"""Tests for ``case get``."""
|
|
25
|
+
|
|
26
|
+
def test_get_case_success(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
27
|
+
result = invoke(["case", "get", "152230"])
|
|
28
|
+
assert result.exit_code == 0
|
|
29
|
+
|
|
30
|
+
def test_get_case_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
|
|
31
|
+
mock_case_env.get.side_effect = make_http_error(400, text="Bad Request")
|
|
32
|
+
result = invoke(["case", "get", "152230"])
|
|
33
|
+
assert result.exit_code == 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestCaseCreate:
|
|
37
|
+
"""Tests for ``case create``."""
|
|
38
|
+
|
|
39
|
+
def test_create_case_success(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
40
|
+
result = invoke(["case", "create"])
|
|
41
|
+
assert result.exit_code == 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestCaseClone:
|
|
45
|
+
"""Tests for ``case clone``."""
|
|
46
|
+
|
|
47
|
+
def test_clone_missing_options(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
48
|
+
result = invoke(["case", "clone", "152230"])
|
|
49
|
+
assert result.exit_code == 2
|
|
50
|
+
|
|
51
|
+
def test_clone_with_bogus_environments(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
52
|
+
result = invoke(["case", "clone", "--dest", "bogus", "--source", "bogus", "152230"])
|
|
53
|
+
assert result.exit_code == 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestCaseGetContext:
|
|
57
|
+
"""Tests for ``case get-context``."""
|
|
58
|
+
|
|
59
|
+
def test_get_context_success(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
60
|
+
result = invoke(["case", "get-context", "153483"])
|
|
61
|
+
assert result.exit_code == 0
|
|
62
|
+
mock_case_env.get_context.assert_called_once_with(153483)
|
|
63
|
+
|
|
64
|
+
def test_get_context_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
|
|
65
|
+
mock_case_env.get_context.side_effect = make_http_error(404, text="Not Found")
|
|
66
|
+
result = invoke(["case", "get-context", "153483"])
|
|
67
|
+
assert result.exit_code == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestCaseGetEntry:
|
|
71
|
+
"""Tests for ``case get-entry``."""
|
|
72
|
+
|
|
73
|
+
def test_get_entry_success(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
74
|
+
result = invoke(["case", "get-entry", "112@153483"])
|
|
75
|
+
assert result.exit_code == 0
|
|
76
|
+
mock_case_env.get_entry.assert_called_once_with(153483, "112@153483")
|
|
77
|
+
|
|
78
|
+
def test_get_entry_not_found(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
79
|
+
mock_case_env.get_entry.side_effect = ValueError("Entry '999@153483' not found in case 153483")
|
|
80
|
+
result = invoke(["case", "get-entry", "999@153483"])
|
|
81
|
+
assert result.exit_code == 1
|
|
82
|
+
assert "not found" in result.output
|
|
83
|
+
|
|
84
|
+
def test_get_entry_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
|
|
85
|
+
mock_case_env.get_entry.side_effect = make_http_error(400, text="Bad Request")
|
|
86
|
+
result = invoke(["case", "get-entry", "112@153483"])
|
|
87
|
+
assert result.exit_code == 1
|
|
88
|
+
|
|
89
|
+
def test_get_entry_missing_entry_id_argument(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
90
|
+
result = invoke(["case", "get-entry"])
|
|
91
|
+
assert result.exit_code == 2
|
|
92
|
+
|
|
93
|
+
def test_get_entry_malformed_entry_id(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
94
|
+
result = invoke(["case", "get-entry", "112"])
|
|
95
|
+
assert result.exit_code == 1
|
|
96
|
+
assert "invalid entry id" in result.output
|
|
97
|
+
mock_case_env.get_entry.assert_not_called()
|
|
98
|
+
|
|
99
|
+
def test_get_entry_non_numeric_entry_number(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
100
|
+
result = invoke(["case", "get-entry", "12INVALID21@1234"])
|
|
101
|
+
assert result.exit_code == 1
|
|
102
|
+
assert "invalid entry id" in result.output
|
|
103
|
+
mock_case_env.get_entry.assert_not_called()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestCaseGetEntries:
|
|
107
|
+
"""Tests for ``case get-entries``."""
|
|
108
|
+
|
|
109
|
+
def test_get_entries_success(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
110
|
+
result = invoke(["case", "get-entries", "153483"])
|
|
111
|
+
assert result.exit_code == 0
|
|
112
|
+
mock_case_env.get_entries.assert_called_once_with(153483)
|
|
113
|
+
|
|
114
|
+
def test_get_entries_empty(self, invoke: InvokeHelper, mock_case_env) -> None:
|
|
115
|
+
mock_case_env.get_entries.return_value = []
|
|
116
|
+
result = invoke(["case", "get-entries", "153483"])
|
|
117
|
+
assert result.exit_code == 0
|
|
118
|
+
assert result.output.strip() == "[]"
|
|
119
|
+
|
|
120
|
+
def test_get_entries_http_error(self, invoke: InvokeHelper, mock_case_env, make_http_error) -> None:
|
|
121
|
+
mock_case_env.get_entries.side_effect = make_http_error(404, text="Not Found")
|
|
122
|
+
result = invoke(["case", "get-entries", "153483"])
|
|
123
|
+
assert result.exit_code == 1
|
|
@@ -115,6 +115,70 @@ class TestManifestValidate:
|
|
|
115
115
|
assert mock_xsoar_env.client.packs.is_available.call_count == 2
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
class TestManifestUpdate:
|
|
119
|
+
"""Tests for ``manifest update``."""
|
|
120
|
+
|
|
121
|
+
def test_skips_custom_pack_missing_from_artifacts(self, invoke: InvokeHelper, mock_xsoar_env, tmp_path: Path) -> None:
|
|
122
|
+
"""Regression test: a custom pack installed on the server but missing from
|
|
123
|
+
the artifacts repo is surfaced in the warning and then skipped, without
|
|
124
|
+
crashing on an empty version list."""
|
|
125
|
+
from xsoar_cli.xsoar_client.packs import OutdatedResult
|
|
126
|
+
|
|
127
|
+
manifest = tmp_path / "xsoar_config.json"
|
|
128
|
+
manifest.write_text(
|
|
129
|
+
json.dumps(
|
|
130
|
+
{
|
|
131
|
+
"custom_packs": [{"id": "OrphanPack", "version": "1.0.0"}],
|
|
132
|
+
"marketplace_packs": [],
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
mock_xsoar_env.client.packs.get_outdated.return_value = OutdatedResult(
|
|
137
|
+
outdated=[],
|
|
138
|
+
skipped=["OrphanPack"],
|
|
139
|
+
)
|
|
140
|
+
# If get_latest_version is reached for the orphan pack, this would
|
|
141
|
+
# historically raise ValueError. The command must not call it.
|
|
142
|
+
mock_xsoar_env.client.artifact_provider.get_latest_version.return_value = None
|
|
143
|
+
|
|
144
|
+
result = invoke(["manifest", "update", str(manifest)])
|
|
145
|
+
|
|
146
|
+
assert result.exit_code == 0, result.output
|
|
147
|
+
assert "OrphanPack" in result.output
|
|
148
|
+
assert "installed but not found in artifacts repo" in result.output
|
|
149
|
+
mock_xsoar_env.client.artifact_provider.get_latest_version.assert_not_called()
|
|
150
|
+
# Manifest must remain unchanged.
|
|
151
|
+
assert json.loads(manifest.read_text()) == {
|
|
152
|
+
"custom_packs": [{"id": "OrphanPack", "version": "1.0.0"}],
|
|
153
|
+
"marketplace_packs": [],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def test_skips_when_provider_returns_none(self, invoke: InvokeHelper, mock_xsoar_env, tmp_path: Path) -> None:
|
|
157
|
+
"""Defensive guard: even if a pack is not in the skipped list, a None from
|
|
158
|
+
the artifact provider is handled cleanly instead of raising."""
|
|
159
|
+
from xsoar_cli.xsoar_client.packs import OutdatedResult
|
|
160
|
+
|
|
161
|
+
manifest = tmp_path / "xsoar_config.json"
|
|
162
|
+
manifest.write_text(
|
|
163
|
+
json.dumps(
|
|
164
|
+
{
|
|
165
|
+
"custom_packs": [{"id": "MyOrg_EDR", "version": "1.0.0"}],
|
|
166
|
+
"marketplace_packs": [],
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
mock_xsoar_env.client.packs.get_outdated.return_value = OutdatedResult(
|
|
171
|
+
outdated=[],
|
|
172
|
+
skipped=[],
|
|
173
|
+
)
|
|
174
|
+
mock_xsoar_env.client.artifact_provider.get_latest_version.return_value = None
|
|
175
|
+
|
|
176
|
+
result = invoke(["manifest", "update", str(manifest)])
|
|
177
|
+
|
|
178
|
+
assert result.exit_code == 0, result.output
|
|
179
|
+
mock_xsoar_env.client.artifact_provider.get_latest_version.assert_called_once_with("MyOrg_EDR")
|
|
180
|
+
|
|
181
|
+
|
|
118
182
|
class TestManifestValidateOnlyChanged:
|
|
119
183
|
"""Tests for ``manifest validate --only-changed``."""
|
|
120
184
|
|