splunk-soar-sdk 3.5.0__tar.gz → 3.6.1__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.
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/workflows/integration_tests.yml +1 -2
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/PKG-INFO +3 -1
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/pyproject.toml +4 -2
- splunk_soar_sdk-3.6.1/release_notes.txt +21 -0
- splunk_soar_sdk-3.6.1/release_version.txt +1 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/actions_manager.py +1 -7
- splunk_soar_sdk-3.6.1/src/soar_sdk/apis/es/findings.py +27 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/manifests/processors.py +2 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/package/cli.py +33 -17
- splunk_soar_sdk-3.6.1/src/soar_sdk/decorators/on_es_poll.py +187 -0
- splunk_soar_sdk-3.6.1/src/soar_sdk/es_client.py +43 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/app.py +1 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/dependencies.py +68 -4
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/finding.py +3 -6
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/params.py +9 -2
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_convert_cli.py +4 -4
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_deserializers.py +3 -3
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_package_cli.py +53 -1
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/conftest.py +8 -1
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/app.json +75 -6
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/pyproject.toml +3 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/app.py +15 -18
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/uv.lock +166 -71
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/app.json +57 -6
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/pyproject.toml +3 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/uv.lock +101 -6
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/meta/test_dependencies.py +13 -1
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_app.py +21 -21
- splunk_soar_sdk-3.6.1/tests/test_es_client.py +46 -0
- splunk_soar_sdk-3.6.1/tests/test_es_on_poll.py +461 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/uv.lock +50 -1
- splunk_soar_sdk-3.5.0/release_notes.txt +0 -35
- splunk_soar_sdk-3.5.0/release_version.txt +0 -1
- splunk_soar_sdk-3.5.0/src/soar_sdk/decorators/on_es_poll.py +0 -215
- splunk_soar_sdk-3.5.0/test.png +0 -2
- splunk_soar_sdk-3.5.0/test.txt +0 -1
- splunk_soar_sdk-3.5.0/tests/test_es_on_poll.py +0 -586
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/pull_request_template.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/scripts/generate_test_summary.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/utils/github.js +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/utils/update_version.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/workflows/code_quality.yml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/workflows/commit_hygiene.yml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/workflows/generate_docs.yml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.github/workflows/semantic_release.yml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.gitignore +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.pre-commit-config.yaml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/.releaserc +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/LICENSE +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/README.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/commitlint.config.js +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/api_reference.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/app_structure/index.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/app_structure/pre-commit-config.yaml.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/app_structure/pyproject.toml.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/app_structure/src_app.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/changelog.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/cli_reference.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/conf.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/custom_views/index.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/custom_views/reusable_components.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/custom_views/templates.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/custom_views/view_handlers.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/getting_started/defining_asset.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/getting_started/first_action.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/getting_started/index.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/getting_started/init_app.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/getting_started/installation.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/getting_started/testing_and_building.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/docs/index.rst +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/README.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/install.sh +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/mcp_config.json +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/pyproject.toml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/pytest.ini +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/src/soar_test_assistant/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/src/soar_test_assistant/server.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/tests/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/tests/test_analyzer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/mcp_server/uv.lock +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/abstract.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/action_results.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/apis/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/apis/artifact.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/apis/container.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/apis/utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/apis/vault.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_cli_runner.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_client.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_templates/basic_app/.gitignore +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_templates/basic_app/.pre-commit-config.yaml +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_templates/basic_app/logo.svg +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_templates/basic_app/logo_dark.svg +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_templates/basic_app/src/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/app_templates/basic_app/uv.lock +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/asset.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/asset_state.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/async_utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/init/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/init/cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/manifests/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/manifests/cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/manifests/deserializers.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/manifests/serializers.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/package/utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/path_utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/test/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/test/cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/cli/utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/action_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/app_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/asset_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/templates/pyproject.toml.jinja +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/code_renderers/toml_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/colors.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/compat.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/crypto.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/action.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/make_request.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/on_poll.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/test_connectivity.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/view_handler.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/decorators/webhook.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/exceptions.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/extras/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/extras/email/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/extras/email/processor.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/extras/email/rfc5322.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/extras/email/utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/field_utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/input_spec.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/logging.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/actions.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/adapters.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/datatypes.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/meta/webhooks.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/artifact.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/attachment_input.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/container.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/vault_attachment.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/models/view.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/paths.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/py.typed +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/action_result.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/app.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/base_connector.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/connector_result.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/consts.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/encryption_helper.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/install_info.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/json_keys.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/ph_ipc.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom/vault.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom_common/app_interface/app_interface.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/shims/phantom_common/encryption/encryption_manager_factory.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/base/base_template.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/base/error.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/base/header.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/base/logo_header.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/components/pie_chart.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/widgets/widget_resize_snippet.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/templates/widgets/widget_template.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/types.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/component_registry.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/components/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/components/pie_chart.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/template_filters.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/template_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/views/view_parser.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/webhooks/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/webhooks/models.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/src/soar_sdk/webhooks/routing.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/datapath_parse.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/manifests/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/manifests/test_processors.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/manifests/test_python_version_resolution.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_assets/converted_app/actions.py.txt +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_init_cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_manifests_cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_serializers.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_test_cli.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/cli/test_utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/code_renderers/test_action_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/example_asset.json +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/logo.svg +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/logo_dark.svg +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/release_notes/v1.md +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/actions/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/actions/async_action.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/actions/generate_category.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/actions/reverse_string.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/src/ignoreme.txt +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app/templates/reverse_string.html +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/example_asset.json +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/logo.svg +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/logo_dark.svg +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/src/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/example_app_with_webhook/src/app.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/conftest.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/phantom_constants.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/phantom_instance.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/soar_client.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/test_example_app.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/integration/test_example_app_with_webhook.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/interfaces/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/interfaces/test_artifact_interface.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/interfaces/test_container_interface.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/interfaces/test_vault_interface.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/meta/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/meta/test_actions.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/meta/test_adapters.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/meta/test_datatypes.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/meta/test_webhooks.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/mocks/__init__.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/mocks/dynamic_mocks.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/mocks/importable_action.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/stubs.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_action_results.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_actions_manager.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_app_action.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_app_action_params.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_app_action_results.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_app_client.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_app_runner.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_asset.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_asset_state.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_assets/splunk-sdk-2.1.0.tar.gz +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_async_integration.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_async_utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_attachment_input.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_code_renderers.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_compat.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_container.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_custom_views.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_email_processor.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_encryption.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_field_utils.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_finding.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_input_spec.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_logging.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_make_request_action.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_on_poll.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_params.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_rfc5322.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_template_filters.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_template_renderer.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_test_connectivity.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/test_view_parser.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/webhooks/test_models.py +0 -0
- {splunk_soar_sdk-3.5.0 → splunk_soar_sdk-3.6.1}/tests/webhooks/test_routing.py +0 -0
|
@@ -121,8 +121,7 @@ jobs:
|
|
|
121
121
|
uv run soarapps package install \
|
|
122
122
|
/tmp/example_app_with_webhook.tgz \
|
|
123
123
|
"$phantom_ip" \
|
|
124
|
-
--username "${{ vars.PHANTOM_USERNAME }}"
|
|
125
|
-
--force
|
|
124
|
+
--username "${{ vars.PHANTOM_USERNAME }}"
|
|
126
125
|
|
|
127
126
|
- name: Run integration tests
|
|
128
127
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splunk-soar-sdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.1
|
|
4
4
|
Summary: The official framework for developing and testing Splunk SOAR Apps
|
|
5
5
|
Project-URL: Homepage, https://github.com/phantomcyber/splunk-soar-sdk
|
|
6
6
|
Project-URL: Documentation, https://github.com/phantomcyber/splunk-soar-sdk
|
|
@@ -22,6 +22,8 @@ Requires-Dist: bleach>=6.2.0
|
|
|
22
22
|
Requires-Dist: build>=1.3.0
|
|
23
23
|
Requires-Dist: click<8.2.0,>=8.0.0
|
|
24
24
|
Requires-Dist: distro>=1.8.0
|
|
25
|
+
Requires-Dist: hatchling>=1.28.0
|
|
26
|
+
Requires-Dist: httpx-retries>=0.4.5
|
|
25
27
|
Requires-Dist: httpx>=0.28.1
|
|
26
28
|
Requires-Dist: humanize>=4.12.2
|
|
27
29
|
Requires-Dist: jinja2>=3.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "splunk-soar-sdk"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.6.1"
|
|
4
4
|
description = "The official framework for developing and testing Splunk SOAR Apps"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13, <3.15"
|
|
@@ -44,6 +44,8 @@ dependencies = [
|
|
|
44
44
|
"packaging>=25.0",
|
|
45
45
|
"build>=1.3.0",
|
|
46
46
|
"setuptools>=80.9.0",
|
|
47
|
+
"httpx-retries>=0.4.5",
|
|
48
|
+
"hatchling>=1.28.0",
|
|
47
49
|
]
|
|
48
50
|
|
|
49
51
|
[project.urls]
|
|
@@ -243,4 +245,4 @@ match = "(?!test_).*\\.py"
|
|
|
243
245
|
match-dir = "(?!tests|__pycache__|build|dist).*"
|
|
244
246
|
|
|
245
247
|
[tool.codespell]
|
|
246
|
-
ignore-words-list = "MergeT"
|
|
248
|
+
ignore-words-list = "MergeT,asend"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## [3.6.1](https://github.com/phantomcyber/splunk-soar-sdk/compare/3.6.0...3.6.1) (2025-12-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* record paths in the manifest correctly when building wheels from source ([644889c](https://github.com/phantomcyber/splunk-soar-sdk/commit/644889c26bf2751df2d6166a30a47d0705b3344e))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [3.6.1](https://github.com/phantomcyber/splunk-soar-sdk/compare/3.6.0...3.6.1) (2025-12-15)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* record paths in the manifest correctly when building wheels from source ([644889c](https://github.com/phantomcyber/splunk-soar-sdk/commit/644889c26bf2751df2d6166a30a47d0705b3344e))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.6.1
|
|
@@ -24,6 +24,7 @@ class ActionsManager(BaseConnector):
|
|
|
24
24
|
|
|
25
25
|
self._actions: dict[str, Action] = {}
|
|
26
26
|
self.__app_dir: Path | None = None
|
|
27
|
+
self.supports_es_polling: bool = False
|
|
27
28
|
|
|
28
29
|
def get_action(self, identifier: str) -> Action | None:
|
|
29
30
|
"""Convenience method for getting an Action callable from its identifier.
|
|
@@ -123,13 +124,6 @@ class ActionsManager(BaseConnector):
|
|
|
123
124
|
# For non-broker just proceed as we did before
|
|
124
125
|
return super().get_app_dir()
|
|
125
126
|
|
|
126
|
-
def send_finding_to_es(self, finding: dict[str, Any]) -> str:
|
|
127
|
-
"""Send finding to ES.
|
|
128
|
-
|
|
129
|
-
Returns finding_id: ID for the finding in ES.
|
|
130
|
-
"""
|
|
131
|
-
return ""
|
|
132
|
-
|
|
133
127
|
@classmethod
|
|
134
128
|
def get_soar_base_url(cls) -> str:
|
|
135
129
|
"""Get the base URL of the Splunk SOAR instance this app is running on."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
|
|
4
|
+
from soar_sdk.models.finding import Finding
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CreateFindingResponse(Finding):
|
|
8
|
+
"""The return type from creating a Finding."""
|
|
9
|
+
|
|
10
|
+
time: str = Field(alias="_time")
|
|
11
|
+
finding_id: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Findings:
|
|
15
|
+
"""Client for ES Findings API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: httpx.Client) -> None:
|
|
18
|
+
self._client = client
|
|
19
|
+
|
|
20
|
+
def create(self, finding: Finding) -> CreateFindingResponse:
|
|
21
|
+
"""Create a new Finding."""
|
|
22
|
+
res = self._client.post(
|
|
23
|
+
"/services/public/v2/findings",
|
|
24
|
+
data=finding.model_dump(),
|
|
25
|
+
)
|
|
26
|
+
res.raise_for_status()
|
|
27
|
+
return CreateFindingResponse(**res.json())
|
|
@@ -27,35 +27,51 @@ package = typer.Typer()
|
|
|
27
27
|
console = Console() # For printing lots of pretty colors and stuff
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
async def collect_all_wheels(wheels:
|
|
31
|
-
"""Asynchronously collect all wheels from the given
|
|
32
|
-
|
|
30
|
+
async def collect_all_wheels(wheels: list[DependencyWheel]) -> list[tuple[str, bytes]]:
|
|
31
|
+
"""Asynchronously collect all wheels from the given list of DependencyWheel objects.
|
|
32
|
+
|
|
33
|
+
Downloads/builds each unique wheel once while updating every DependencyWheel instance
|
|
34
|
+
so the manifest records the final wheel filenames.
|
|
35
|
+
"""
|
|
36
|
+
dedupe_map: dict[int, list[DependencyWheel]] = {}
|
|
37
|
+
for wheel in wheels:
|
|
38
|
+
key = hash(wheel)
|
|
39
|
+
dedupe_map.setdefault(key, []).append(wheel)
|
|
40
|
+
|
|
33
41
|
progress = tqdm(
|
|
34
|
-
total=len(
|
|
42
|
+
total=len(dedupe_map),
|
|
35
43
|
desc="Downloading wheels",
|
|
36
44
|
unit="wheel",
|
|
37
45
|
colour="green",
|
|
38
46
|
ncols=80,
|
|
39
47
|
)
|
|
40
48
|
|
|
41
|
-
async def collect_from_wheel(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
async def collect_from_wheel(
|
|
50
|
+
cache_key: int, wheel: DependencyWheel
|
|
51
|
+
) -> tuple[int, list[tuple[str, bytes]]]:
|
|
52
|
+
result: list[tuple[str, bytes]] = []
|
|
45
53
|
async for path, data in wheel.collect_wheels(): # pragma: no cover
|
|
46
54
|
result.append((path, data))
|
|
47
|
-
# Update progress bar after each wheel is processed
|
|
48
55
|
progress.update(1)
|
|
49
|
-
return result
|
|
56
|
+
return cache_key, result
|
|
50
57
|
|
|
51
|
-
# Use asyncio.gather to truly run all wheel collections concurrently
|
|
52
58
|
with contextlib.closing(progress):
|
|
53
|
-
|
|
54
|
-
*(
|
|
59
|
+
gathered_results = await asyncio.gather(
|
|
60
|
+
*(
|
|
61
|
+
collect_from_wheel(key, wheel_group[0])
|
|
62
|
+
for key, wheel_group in dedupe_map.items()
|
|
63
|
+
)
|
|
55
64
|
)
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
cache = dict(gathered_results)
|
|
67
|
+
|
|
68
|
+
for key, wheel_group in dedupe_map.items():
|
|
69
|
+
for path, _ in cache[key]:
|
|
70
|
+
wheel_name = Path(path).name
|
|
71
|
+
for wheel in wheel_group:
|
|
72
|
+
wheel._record_built_wheel(wheel_name)
|
|
73
|
+
|
|
74
|
+
return list(chain.from_iterable(cache.values()))
|
|
59
75
|
|
|
60
76
|
|
|
61
77
|
@package.command()
|
|
@@ -124,13 +140,13 @@ def build(
|
|
|
124
140
|
|
|
125
141
|
with tarfile.open(output_file, "w:gz") as app_tarball:
|
|
126
142
|
# Collect all wheels from both Python versions
|
|
127
|
-
all_wheels =
|
|
143
|
+
all_wheels = (
|
|
128
144
|
app_meta.pip313_dependencies.wheel + app_meta.pip314_dependencies.wheel
|
|
129
145
|
)
|
|
130
146
|
|
|
131
147
|
# Run the async collection function within an event loop
|
|
132
148
|
console.print(
|
|
133
|
-
f"[yellow]Collecting [bold]{len(all_wheels)}[/bold] wheel{'' if len(all_wheels) == 1 else 's'} for package[/]"
|
|
149
|
+
f"[yellow]Collecting [bold]{len(all_wheels)}[/bold] wheel{'' if len(set(all_wheels)) == 1 else 's'} for package[/]"
|
|
134
150
|
)
|
|
135
151
|
wheel_data = asyncio.run(collect_all_wheels(all_wheels))
|
|
136
152
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import TYPE_CHECKING, Any, get_args
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from soar_sdk.abstract import SOARClient
|
|
10
|
+
from soar_sdk.action_results import ActionResult
|
|
11
|
+
from soar_sdk.es_client import ESClient
|
|
12
|
+
from soar_sdk.exceptions import ActionFailure
|
|
13
|
+
from soar_sdk.logging import getLogger
|
|
14
|
+
from soar_sdk.meta.actions import ActionMeta
|
|
15
|
+
from soar_sdk.models.container import Container
|
|
16
|
+
from soar_sdk.models.finding import Finding
|
|
17
|
+
from soar_sdk.params import OnESPollParams
|
|
18
|
+
from soar_sdk.types import Action, action_protocol
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from soar_sdk.app import App
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ESPollingYieldType = Finding
|
|
25
|
+
ESPollingSendType = int | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OnESPollDecorator:
|
|
29
|
+
"""Class-based decorator for tagging a function as the special 'on es poll' action."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, app: "App") -> None:
|
|
32
|
+
self.app = app
|
|
33
|
+
|
|
34
|
+
def __call__(self, function: Callable) -> Action:
|
|
35
|
+
"""Decorator for the 'on es poll' action. The decorated function must be a Generator or AsyncGenerator. Only one on_es_poll action is allowed per app.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
The generator should yield a `Finding`. Upon receiving an event from the generator, the SDK will submit the Finding to Splunk Enterprise Security and create a linked SOAR Container.
|
|
39
|
+
The generator should accept a "send type" of `int | None`. When a Finding is successfully delivered to ES and linked to a Container, the SDK will send the Container ID back into the generator. The Container is useful for storing large attachments included with the Finding.
|
|
40
|
+
If the Finding cannot be successfully delivered to ES, the SDK will stop polling and return a failed result for the action run.
|
|
41
|
+
"""
|
|
42
|
+
if self.app.actions_manager.get_action("on_es_poll"):
|
|
43
|
+
raise TypeError(
|
|
44
|
+
"The 'on_es_poll' decorator can only be used once per App instance."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
is_generator = inspect.isgeneratorfunction(function)
|
|
48
|
+
is_async_generator = inspect.isasyncgenfunction(function)
|
|
49
|
+
|
|
50
|
+
generator_type = inspect.signature(function).return_annotation
|
|
51
|
+
generator_type_args = get_args(generator_type)
|
|
52
|
+
|
|
53
|
+
if not (is_generator or is_async_generator) or len(generator_type_args) < 2:
|
|
54
|
+
raise TypeError(
|
|
55
|
+
"The on_es_poll function must be a Generator or AsyncGenerator (use 'yield')."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
yield_type = generator_type_args[0]
|
|
59
|
+
send_type = generator_type_args[1]
|
|
60
|
+
|
|
61
|
+
if yield_type != ESPollingYieldType:
|
|
62
|
+
raise TypeError(
|
|
63
|
+
f"@on_es_poll generator should have yield type {ESPollingYieldType}."
|
|
64
|
+
)
|
|
65
|
+
if send_type != ESPollingSendType:
|
|
66
|
+
raise TypeError(
|
|
67
|
+
f"@on_es_poll generator should have send type {ESPollingSendType}."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
action_identifier = "on_es_poll"
|
|
71
|
+
action_name = "on es poll"
|
|
72
|
+
|
|
73
|
+
validated_params_class = OnESPollParams
|
|
74
|
+
logger = getLogger()
|
|
75
|
+
|
|
76
|
+
@action_protocol
|
|
77
|
+
@wraps(function)
|
|
78
|
+
def inner(
|
|
79
|
+
params: OnESPollParams,
|
|
80
|
+
soar: SOARClient = self.app.soar_client,
|
|
81
|
+
*args: Any, # noqa: ANN401
|
|
82
|
+
**kwargs: Any, # noqa: ANN401
|
|
83
|
+
) -> bool:
|
|
84
|
+
try:
|
|
85
|
+
action_params = validated_params_class.model_validate(params)
|
|
86
|
+
except ValidationError as e:
|
|
87
|
+
logger.info(f"Parameter validation error: {e!s}")
|
|
88
|
+
return self.app._adapt_action_result(
|
|
89
|
+
ActionResult(status=False, message=f"Invalid parameters: {e!s}"),
|
|
90
|
+
self.app.actions_manager,
|
|
91
|
+
)
|
|
92
|
+
es = ESClient(params.es_base_url, params.es_session_key)
|
|
93
|
+
kwargs = self.app._build_magic_args(function, soar=soar, **kwargs)
|
|
94
|
+
generator = function(action_params, *args, **kwargs)
|
|
95
|
+
|
|
96
|
+
if is_async_generator:
|
|
97
|
+
|
|
98
|
+
def polling_step(
|
|
99
|
+
last_container_id: ESPollingSendType,
|
|
100
|
+
) -> ESPollingYieldType:
|
|
101
|
+
return asyncio.run(generator.asend(last_container_id))
|
|
102
|
+
else:
|
|
103
|
+
|
|
104
|
+
def polling_step(
|
|
105
|
+
last_container_id: ESPollingSendType,
|
|
106
|
+
) -> ESPollingYieldType:
|
|
107
|
+
return generator.send(last_container_id)
|
|
108
|
+
|
|
109
|
+
last_container_id = None
|
|
110
|
+
while True:
|
|
111
|
+
try:
|
|
112
|
+
item = polling_step(last_container_id)
|
|
113
|
+
except (StopIteration, StopAsyncIteration):
|
|
114
|
+
return self.app._adapt_action_result(
|
|
115
|
+
ActionResult(
|
|
116
|
+
status=True, message="Finding processing complete"
|
|
117
|
+
),
|
|
118
|
+
self.app.actions_manager,
|
|
119
|
+
)
|
|
120
|
+
except ActionFailure as e:
|
|
121
|
+
e.set_action_name(action_name)
|
|
122
|
+
return self.app._adapt_action_result(
|
|
123
|
+
ActionResult(status=False, message=str(e)),
|
|
124
|
+
self.app.actions_manager,
|
|
125
|
+
)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
self.app.actions_manager.add_exception(e)
|
|
128
|
+
logger.info(f"Error during finding processing: {e!s}")
|
|
129
|
+
return self.app._adapt_action_result(
|
|
130
|
+
ActionResult(status=False, message=str(e)),
|
|
131
|
+
self.app.actions_manager,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if type(item) is not ESPollingYieldType:
|
|
135
|
+
logger.info(
|
|
136
|
+
f"Warning: expected {ESPollingYieldType}, got {type(item)}, skipping"
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
finding = es.findings.create(item)
|
|
140
|
+
logger.info(f"Created finding {finding.finding_id}")
|
|
141
|
+
|
|
142
|
+
container = Container(
|
|
143
|
+
name=finding.rule_title,
|
|
144
|
+
description=finding.rule_description,
|
|
145
|
+
severity=finding.urgency or "medium",
|
|
146
|
+
status=finding.status,
|
|
147
|
+
owner_id=finding.owner,
|
|
148
|
+
sensitivity=finding.disposition,
|
|
149
|
+
tags=finding.source,
|
|
150
|
+
external_id=finding.finding_id,
|
|
151
|
+
data={
|
|
152
|
+
"security_domain": finding.security_domain,
|
|
153
|
+
"risk_score": finding.risk_score,
|
|
154
|
+
"risk_object": finding.risk_object,
|
|
155
|
+
"risk_object_type": finding.risk_object_type,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
ret_val, message, last_container_id = (
|
|
159
|
+
self.app.actions_manager.save_container(container.to_dict())
|
|
160
|
+
)
|
|
161
|
+
logger.info(f"Creating container for finding: {finding.rule_title}")
|
|
162
|
+
if not ret_val:
|
|
163
|
+
raise ActionFailure(f"Failed to create container: {message}")
|
|
164
|
+
|
|
165
|
+
inner.params_class = validated_params_class
|
|
166
|
+
|
|
167
|
+
class OnESPollActionMeta(ActionMeta):
|
|
168
|
+
def model_dump(self, *args: object, **kwargs: object) -> dict[str, Any]:
|
|
169
|
+
data = super().model_dump(*args, **kwargs)
|
|
170
|
+
data["output"] = []
|
|
171
|
+
return data
|
|
172
|
+
|
|
173
|
+
inner.meta = OnESPollActionMeta(
|
|
174
|
+
action=action_name,
|
|
175
|
+
identifier=action_identifier,
|
|
176
|
+
description=inspect.getdoc(function) or action_name,
|
|
177
|
+
verbose="Callback action for the on_es_poll ingest functionality",
|
|
178
|
+
type="ingest",
|
|
179
|
+
read_only=True,
|
|
180
|
+
parameters=validated_params_class,
|
|
181
|
+
versions="EQ(*)",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self.app.actions_manager.set_action(action_identifier, inner)
|
|
185
|
+
self.app.actions_manager.supports_es_polling = True
|
|
186
|
+
self.app._dev_skip_in_pytest(function, inner)
|
|
187
|
+
return inner
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from httpx_retries import Retry, RetryTransport
|
|
3
|
+
from httpx_retries.retry import HTTPMethod, HTTPStatus
|
|
4
|
+
|
|
5
|
+
from soar_sdk.apis.es.findings import Findings
|
|
6
|
+
|
|
7
|
+
RETRYABLE_METHODS = [
|
|
8
|
+
HTTPMethod.GET,
|
|
9
|
+
HTTPMethod.PUT,
|
|
10
|
+
HTTPMethod.POST,
|
|
11
|
+
HTTPMethod.PATCH,
|
|
12
|
+
HTTPMethod.DELETE,
|
|
13
|
+
]
|
|
14
|
+
RETRYABLE_STATUSES = [
|
|
15
|
+
HTTPStatus.TOO_MANY_REQUESTS,
|
|
16
|
+
HTTPStatus.BAD_GATEWAY,
|
|
17
|
+
HTTPStatus.SERVICE_UNAVAILABLE,
|
|
18
|
+
HTTPStatus.GATEWAY_TIMEOUT,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ESClient:
|
|
23
|
+
"""A client for accessing Splunk Enterprise Security APIs."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, base_url: str, session_key: str, verify: bool = True) -> None:
|
|
26
|
+
transport = RetryTransport(
|
|
27
|
+
transport=httpx.HTTPTransport(verify=verify),
|
|
28
|
+
retry=Retry(
|
|
29
|
+
allowed_methods=RETRYABLE_METHODS,
|
|
30
|
+
status_forcelist=RETRYABLE_STATUSES,
|
|
31
|
+
total=5,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
self._client = httpx.Client(
|
|
35
|
+
base_url=base_url,
|
|
36
|
+
transport=transport,
|
|
37
|
+
headers={"Authorization": f"Splunk {session_key}"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def findings(self) -> Findings:
|
|
42
|
+
"""The ES /public/v2/findings API."""
|
|
43
|
+
return Findings(self._client)
|
|
@@ -52,7 +52,8 @@ remove_when_soar_newer_than(
|
|
|
52
52
|
"If the Splunk SDK is available as a wheel now, remove it, and remove all of the code for building wheels from source.",
|
|
53
53
|
)
|
|
54
54
|
DEPENDENCIES_TO_BUILD = {
|
|
55
|
-
"splunk_sdk", # https://github.com/splunk/splunk-sdk-python/pull/656
|
|
55
|
+
"splunk_sdk", # https://github.com/splunk/splunk-sdk-python/pull/656,
|
|
56
|
+
"splunk_soar_sdk", # Useful to build from source when developing the SDK
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
|
|
@@ -189,6 +190,23 @@ class UvSourceDistribution(BaseModel):
|
|
|
189
190
|
return Path(wheel_path).name, f.read()
|
|
190
191
|
|
|
191
192
|
|
|
193
|
+
class UvSourceDirectory(BaseModel):
|
|
194
|
+
"""Represents a Python dependency to be built from a source directory on the local filesystem."""
|
|
195
|
+
|
|
196
|
+
directory: str
|
|
197
|
+
|
|
198
|
+
def build(self) -> tuple[str, bytes]:
|
|
199
|
+
"""Build a wheel from a local source directory."""
|
|
200
|
+
with TemporaryDirectory() as build_dir:
|
|
201
|
+
builder = build.ProjectBuilder(
|
|
202
|
+
self.directory,
|
|
203
|
+
runner=UvSourceDistribution._builder_runner,
|
|
204
|
+
)
|
|
205
|
+
wheel_path = builder.build("wheel", build_dir)
|
|
206
|
+
with open(wheel_path, "rb") as f:
|
|
207
|
+
return Path(wheel_path).name, f.read()
|
|
208
|
+
|
|
209
|
+
|
|
192
210
|
class DependencyWheel(BaseModel):
|
|
193
211
|
"""Represents a Python package dependency with all the information required to fetch its wheel(s) from the CDN."""
|
|
194
212
|
|
|
@@ -199,13 +217,43 @@ class DependencyWheel(BaseModel):
|
|
|
199
217
|
wheel: UvWheel | None = Field(exclude=True, default=None)
|
|
200
218
|
wheel_aarch64: UvWheel | None = Field(exclude=True, default=None)
|
|
201
219
|
sdist: UvSourceDistribution | None = Field(exclude=True, default=None)
|
|
220
|
+
source_dir: UvSourceDirectory | None = Field(exclude=True, default=None)
|
|
221
|
+
|
|
222
|
+
def _set_wheel_paths(self, wheel_name: str) -> str:
|
|
223
|
+
"""Assign the final wheel path (with any existing prefix) to both arches."""
|
|
224
|
+
base_path = Path(self.input_file or "wheels/shared")
|
|
225
|
+
# If there's already a filename component, replace it instead of nesting it
|
|
226
|
+
if base_path.suffix == ".whl":
|
|
227
|
+
base_path = base_path.parent
|
|
228
|
+
wheel_path = (base_path / wheel_name).as_posix()
|
|
229
|
+
self.input_file = wheel_path
|
|
230
|
+
self.input_file_aarch64 = wheel_path
|
|
231
|
+
return wheel_path
|
|
232
|
+
|
|
233
|
+
def set_placeholder_wheel_name(self, version: str) -> None:
|
|
234
|
+
"""Populate a clearly placeholder wheel path when we expect to build from source."""
|
|
235
|
+
# Use only a filename here; platform-specific prefixes are added later.
|
|
236
|
+
self.input_file = "<to_be_built>.whl"
|
|
237
|
+
self.input_file_aarch64 = "<to_be_built>.whl"
|
|
238
|
+
|
|
239
|
+
def _record_built_wheel(self, wheel_name: str) -> str:
|
|
240
|
+
"""Fill in missing wheel paths once a wheel has been built from source."""
|
|
241
|
+
return self._set_wheel_paths(wheel_name)
|
|
202
242
|
|
|
203
243
|
async def collect_wheels(self) -> AsyncGenerator[tuple[str, bytes]]:
|
|
204
244
|
"""Collect a list of wheel files to fetch for this dependency across all platforms."""
|
|
205
245
|
if self.wheel is None and self.sdist is not None:
|
|
206
246
|
logger.info(f"Building sdist for {self.input_file}")
|
|
207
247
|
wheel_name, wheel_bytes = await self.sdist.fetch_and_build()
|
|
208
|
-
|
|
248
|
+
wheel_path = self._record_built_wheel(wheel_name)
|
|
249
|
+
yield (wheel_path, wheel_bytes)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if self.wheel is None and self.source_dir is not None:
|
|
253
|
+
logger.info(f"Building local sources for {self.input_file}")
|
|
254
|
+
wheel_name, wheel_bytes = self.source_dir.build()
|
|
255
|
+
wheel_path = self._record_built_wheel(wheel_name)
|
|
256
|
+
yield (wheel_path, wheel_bytes)
|
|
209
257
|
return
|
|
210
258
|
|
|
211
259
|
if self.wheel is None:
|
|
@@ -247,6 +295,13 @@ class UvDependency(BaseModel):
|
|
|
247
295
|
name: str
|
|
248
296
|
|
|
249
297
|
|
|
298
|
+
class UvSource(BaseModel):
|
|
299
|
+
"""Represents the source of a Python package in the uv lock."""
|
|
300
|
+
|
|
301
|
+
registry: str | None = None
|
|
302
|
+
directory: str | None = None
|
|
303
|
+
|
|
304
|
+
|
|
250
305
|
class UvPackage(BaseModel):
|
|
251
306
|
"""Represents a Python package loaded from the uv lock."""
|
|
252
307
|
|
|
@@ -258,6 +313,7 @@ class UvPackage(BaseModel):
|
|
|
258
313
|
)
|
|
259
314
|
wheels: list[UvWheel] = []
|
|
260
315
|
sdist: UvSourceDistribution | None = None
|
|
316
|
+
source: UvSource
|
|
261
317
|
|
|
262
318
|
def _find_wheel(
|
|
263
319
|
self,
|
|
@@ -365,6 +421,14 @@ class UvPackage(BaseModel):
|
|
|
365
421
|
and UvLock.normalize_package_name(self.name) in DEPENDENCIES_TO_BUILD
|
|
366
422
|
):
|
|
367
423
|
wheel.sdist = self.sdist
|
|
424
|
+
wheel.set_placeholder_wheel_name(self.version)
|
|
425
|
+
|
|
426
|
+
if (
|
|
427
|
+
self.source.directory is not None
|
|
428
|
+
and UvLock.normalize_package_name(self.name) in DEPENDENCIES_TO_BUILD
|
|
429
|
+
):
|
|
430
|
+
wheel.source_dir = UvSourceDirectory(directory=self.source.directory)
|
|
431
|
+
wheel.set_placeholder_wheel_name(self.version)
|
|
368
432
|
|
|
369
433
|
try:
|
|
370
434
|
wheel_x86_64 = self._find_wheel(
|
|
@@ -373,7 +437,7 @@ class UvPackage(BaseModel):
|
|
|
373
437
|
wheel.input_file = f"{wheel_x86_64.basename}.whl"
|
|
374
438
|
wheel.wheel = wheel_x86_64
|
|
375
439
|
except FileNotFoundError as e:
|
|
376
|
-
if wheel.sdist is None:
|
|
440
|
+
if wheel.sdist is None and wheel.source_dir is None:
|
|
377
441
|
raise FileNotFoundError(
|
|
378
442
|
f"Could not find a suitable x86_64 wheel or source distribution for {self.name}"
|
|
379
443
|
) from e
|
|
@@ -390,7 +454,7 @@ class UvPackage(BaseModel):
|
|
|
390
454
|
wheel.input_file_aarch64 = f"{wheel_aarch64.basename}.whl"
|
|
391
455
|
wheel.wheel_aarch64 = wheel_aarch64
|
|
392
456
|
except FileNotFoundError:
|
|
393
|
-
if wheel.sdist is None:
|
|
457
|
+
if wheel.sdist is None and wheel.source_dir is None:
|
|
394
458
|
logger.warning(
|
|
395
459
|
f"Could not find a suitable aarch64 wheel for {self.name=}, {self.version=}, {abi_precedence=}, {python_precedence=} -- the built package might not work on ARM systems"
|
|
396
460
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class DrilldownSearch(BaseModel):
|
|
@@ -27,10 +27,7 @@ class Finding(BaseModel):
|
|
|
27
27
|
for investigation workflow.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
"""Pydantic config."""
|
|
32
|
-
|
|
33
|
-
extra = "forbid"
|
|
30
|
+
model_config = ConfigDict(extra="forbid")
|
|
34
31
|
|
|
35
32
|
rule_title: str
|
|
36
33
|
rule_description: str
|
|
@@ -52,4 +49,4 @@ class Finding(BaseModel):
|
|
|
52
49
|
|
|
53
50
|
def to_dict(self) -> dict[str, Any]:
|
|
54
51
|
"""Convert the finding to a dictionary."""
|
|
55
|
-
return self.
|
|
52
|
+
return self.model_dump(exclude_none=True)
|
|
@@ -207,17 +207,24 @@ class OnESPollParams(Params):
|
|
|
207
207
|
description="Start of time range, in epoch time (milliseconds).",
|
|
208
208
|
required=False,
|
|
209
209
|
)
|
|
210
|
-
|
|
211
210
|
end_time: int = Param(
|
|
212
211
|
description="End of time range, in epoch time (milliseconds).",
|
|
213
212
|
required=False,
|
|
214
213
|
)
|
|
215
|
-
|
|
216
214
|
container_count: int = Param(
|
|
217
215
|
description="Maximum number of container records to query for.",
|
|
218
216
|
required=False,
|
|
219
217
|
)
|
|
220
218
|
|
|
219
|
+
es_base_url: str = Param(
|
|
220
|
+
description="Base URL for the Splunk Enterprise Security API",
|
|
221
|
+
required=True,
|
|
222
|
+
)
|
|
223
|
+
es_session_key: str = Param(
|
|
224
|
+
description="Session token for the Splunk Enterprise Security API",
|
|
225
|
+
required=True,
|
|
226
|
+
)
|
|
227
|
+
|
|
221
228
|
|
|
222
229
|
class MakeRequestParams(Params):
|
|
223
230
|
"""Canonical parameters for the special make request action."""
|