xsoar-cli 1.3.0__tar.gz → 1.3.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-1.3.2/.github/workflows/main.yml +22 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/.github/workflows/pull-request-open.yml +18 -7
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/CONTRIBUTING.md +38 -28
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/PKG-INFO +29 -8
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/README.md +28 -7
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/_version.py +2 -2
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/pyproject.toml +6 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/cli.py +4 -0
- xsoar_cli-1.3.2/src/xsoar_cli/integration/commands.py +55 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/playbook/commands.py +3 -1
- xsoar_cli-1.3.2/src/xsoar_cli/rbac/commands.py +72 -0
- xsoar_cli-1.3.2/tests/test_connection_errors.py +129 -0
- xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_Layouts/Author_image.png +0 -0
- xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_Layouts/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/uv.lock +243 -83
- xsoar_cli-1.3.0/requirements.txt +0 -4
- xsoar_cli-1.3.0/requirements_dev.txt +0 -4
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/.github/workflows/release.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/.gitignore +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/CLAUDE.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/LICENSE.txt +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/__about__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/case/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/case/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/case/commands.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/config/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/config/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/config/commands.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/configuration.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/connection_errors.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/graph/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/graph/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/graph/commands.py +0 -0
- {xsoar_cli-1.3.0/src/xsoar_cli/manifest → xsoar_cli-1.3.2/src/xsoar_cli/integration}/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/log.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/manifest/README.md +0 -0
- {xsoar_cli-1.3.0/src/xsoar_cli/pack → xsoar_cli-1.3.2/src/xsoar_cli/manifest}/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/manifest/commands.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/pack/README.md +0 -0
- {xsoar_cli-1.3.0/src/xsoar_cli/playbook → xsoar_cli-1.3.2/src/xsoar_cli/pack}/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/pack/commands.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/playbook/README.md +0 -0
- /xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_CommonPlaybooks/.pack-ignore → /xsoar_cli-1.3.2/src/xsoar_cli/playbook/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/plugins/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/plugins/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/plugins/commands.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/plugins/manager.py +0 -0
- /xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_CommonPlaybooks/.secrets-ignore → /xsoar_cli-1.3.2/src/xsoar_cli/rbac/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/src/xsoar_cli/utilities.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/__init__.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/conftest.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_base.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_case.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_config.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Download/playbook-empty.yml +0 -0
- {xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_CommonScripts → xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_CommonPlaybooks}/.pack-ignore +0 -0
- {xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_CommonScripts → xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_CommonPlaybooks}/.secrets-ignore +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonPlaybooks/Author_image.png +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonPlaybooks/Playbooks/GenericPlaybook.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonPlaybooks/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonPlaybooks/pack_metadata.json +0 -0
- {xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_EDR → xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_CommonScripts}/.pack-ignore +0 -0
- {xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_EDR → xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_CommonScripts}/.secrets-ignore +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonScripts/Author_image.png +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonScripts/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonScripts/Scripts/GenericScript/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_CommonScripts/pack_metadata.json +0 -0
- {xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_Layouts → xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_EDR}/.pack-ignore +0 -0
- {xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_Layouts → xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_EDR}/.secrets-ignore +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Author_image.png +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Playbooks/EDR_InitialTriage.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_Triage/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/README.md +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/test_data/basescript-dummy.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_EDR/pack_metadata.json +0 -0
- /xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_Layouts/Author_image.png → /xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_Layouts/.pack-ignore +0 -0
- /xsoar_cli-1.3.0/tests/test_data/Packs/MyOrg_Layouts/README.md → /xsoar_cli-1.3.2/tests/test_data/Packs/MyOrg_Layouts/.secrets-ignore +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_Layouts/Layouts/layoutscontainer-GenericLayout.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/MyOrg_Layouts/pack_metadata.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/Packs/Not_applicable/Playbooks/Empty.yml +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/manifest_base.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/manifest_invalid.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/manifest_with_pack_not_on_server.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/server_base_response.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/server_base_response_missing_one_pack.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/server_base_response_with_updates.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_data/server_base_response_with_updates_and_one_extra.json +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_graph.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_manifest.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_pack.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_playbook.py +0 -0
- {xsoar_cli-1.3.0 → xsoar_cli-1.3.2}/tests/test_plugins.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
on:
|
|
2
|
+
push:
|
|
3
|
+
branches:
|
|
4
|
+
- main
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
coverage:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- name: Checkout code
|
|
11
|
+
uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Install uv and set the Python version
|
|
14
|
+
uses: astral-sh/setup-uv@v7
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.14"
|
|
17
|
+
|
|
18
|
+
- name: Sync uv
|
|
19
|
+
run: uv sync --all-groups
|
|
20
|
+
|
|
21
|
+
- name: Run unit tests with coverage
|
|
22
|
+
run: uv run pytest --cov=src/xsoar_cli --cov-report=term-missing
|
|
@@ -23,13 +23,24 @@ jobs:
|
|
|
23
23
|
python-version: ${{ matrix.python-version }}
|
|
24
24
|
|
|
25
25
|
- name: Sync uv
|
|
26
|
-
run: uv sync
|
|
27
|
-
|
|
28
|
-
- name: Install requirements
|
|
29
|
-
run: |
|
|
30
|
-
uv pip install -r requirements.txt
|
|
31
|
-
uv pip install -r requirements_dev.txt
|
|
32
|
-
uv pip install -e .
|
|
26
|
+
run: uv sync --all-groups
|
|
33
27
|
|
|
34
28
|
- name: Run unit tests
|
|
35
29
|
run: uv run pytest
|
|
30
|
+
|
|
31
|
+
coverage:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout code
|
|
35
|
+
uses: actions/checkout@v4
|
|
36
|
+
|
|
37
|
+
- name: Install uv and set the Python version
|
|
38
|
+
uses: astral-sh/setup-uv@v7
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.14"
|
|
41
|
+
|
|
42
|
+
- name: Sync uv
|
|
43
|
+
run: uv sync --all-groups
|
|
44
|
+
|
|
45
|
+
- name: Run unit tests with coverage
|
|
46
|
+
run: uv run pytest --cov=src/xsoar_cli --cov-report=term-missing
|
|
@@ -32,29 +32,52 @@ We welcome contributions of all kinds! Whether you're fixing bugs, adding featur
|
|
|
32
32
|
cd xsoar-cli
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
### 2.
|
|
35
|
+
### 2. Install uv
|
|
36
|
+
|
|
37
|
+
[uv](https://docs.astral.sh/uv/) is the recommended package manager for this project. Using legacy venv/pip workflows should also work, but uv is a great tool.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For other platforms and installation options, see the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/).
|
|
44
|
+
|
|
45
|
+
### 3. Set Up Development Environment
|
|
46
|
+
|
|
47
|
+
**Recommended: uv**
|
|
48
|
+
|
|
49
|
+
You may find that you will need to make modifications in [xsoar-client](https://github.com/tlium/xsoar-client) and/or [xsoar-dependency-graph](https://github.com/tlium/xsoar-dependency-graph) in order
|
|
50
|
+
to fix bugs or add new features to `xsoar-cli`. If you need to work on `xsoar-client` or `xsoar-dependency-graph`, install them in editable mode from your local Git clones:
|
|
51
|
+
|
|
36
52
|
```bash
|
|
37
|
-
|
|
53
|
+
uv pip install -e /path/to/your/xsoar-client
|
|
54
|
+
uv pip install -e /path/to/your/xsoar-dependency-graph
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Install dependencies and xsoar-cli in editable mode
|
|
59
|
+
uv sync
|
|
60
|
+
uv pip install -e .
|
|
61
|
+
```
|
|
62
|
+
**Alternative: pip/venv**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Create and activate a virtual environment
|
|
38
66
|
python -m venv venv
|
|
39
|
-
source venv/bin/activate
|
|
67
|
+
source venv/bin/activate
|
|
40
68
|
|
|
41
69
|
# Install development dependencies
|
|
42
70
|
pip install -r requirements_dev.txt
|
|
43
71
|
|
|
44
|
-
# Install
|
|
72
|
+
# Install xsoar-cli in editable mode
|
|
45
73
|
pip install -e .
|
|
46
74
|
```
|
|
47
75
|
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
# Run tests to ensure everything works
|
|
51
|
-
pytest
|
|
52
|
-
|
|
53
|
-
# Check code style
|
|
54
|
-
black --check src/ tests/
|
|
76
|
+
If you need to work on the supporting libraries, install them in editable mode as well:
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
xsoar-
|
|
78
|
+
```bash
|
|
79
|
+
pip install -e /path/to/your/xsoar-client
|
|
80
|
+
pip install -e /path/to/your/xsoar-dependency-graph
|
|
58
81
|
```
|
|
59
82
|
|
|
60
83
|
## Development Workflow
|
|
@@ -71,12 +94,6 @@ xsoar-cli --help
|
|
|
71
94
|
```bash
|
|
72
95
|
# Run tests
|
|
73
96
|
pytest
|
|
74
|
-
|
|
75
|
-
# Format code
|
|
76
|
-
black src/ tests/
|
|
77
|
-
|
|
78
|
-
# Check for any issues
|
|
79
|
-
python -m pytest --cov=src/xsoar_cli
|
|
80
97
|
```
|
|
81
98
|
|
|
82
99
|
4. **Commit with clear messages**:
|
|
@@ -91,8 +108,7 @@ xsoar-cli --help
|
|
|
91
108
|
```
|
|
92
109
|
|
|
93
110
|
### Code Style
|
|
94
|
-
- We use [
|
|
95
|
-
- Run `black src/ tests/` before committing
|
|
111
|
+
- We use [Ruff](https://docs.astral.sh/ruff/) for code formatting
|
|
96
112
|
- Follow existing code patterns and conventions
|
|
97
113
|
- Add type hints where appropriate
|
|
98
114
|
- Write docstrings for new functions and classes
|
|
@@ -127,12 +143,6 @@ Documentation improvements are especially welcome! You can contribute to documen
|
|
|
127
143
|
|
|
128
144
|
## Submitting Pull Requests
|
|
129
145
|
|
|
130
|
-
### Before Submitting
|
|
131
|
-
- [ ] Tests pass (`pytest`)
|
|
132
|
-
- [ ] Code is formatted (`black src/ tests/`)
|
|
133
|
-
- [ ] Documentation is updated if needed
|
|
134
|
-
- [ ] Commit messages are clear and descriptive
|
|
135
|
-
|
|
136
146
|
### Pull Request Guidelines
|
|
137
147
|
- **Describe your changes** clearly in the PR description
|
|
138
148
|
- **Reference any related issues** (e.g., "Fixes #123")
|
|
@@ -194,4 +204,4 @@ Be respectful and constructive in all interactions. We want this to be a welcomi
|
|
|
194
204
|
|
|
195
205
|
Contributors are recognized in release notes and we appreciate all contributions, no matter how small!
|
|
196
206
|
|
|
197
|
-
Thank you for contributing!
|
|
207
|
+
Thank you for contributing!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xsoar-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Project-URL: Documentation, https://github.com/tlium/xsoar-cli#readme
|
|
5
5
|
Project-URL: Issues, https://github.com/tlium/xsoar-cli/issues
|
|
6
6
|
Project-URL: Source, https://github.com/tlium/xsoar-cli
|
|
@@ -44,7 +44,7 @@ Pull Requests are very welcome and appreciated! See [CONTRIBUTING.md](CONTRIBUTI
|
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
# Install
|
|
47
|
-
|
|
47
|
+
uv tool install xsoar-cli
|
|
48
48
|
|
|
49
49
|
# Create configuration file
|
|
50
50
|
xsoar-cli config create
|
|
@@ -65,27 +65,47 @@ This CLI tool is made to be run from the root of a content repository. Some comm
|
|
|
65
65
|
|
|
66
66
|
### Core Requirements
|
|
67
67
|
- XSOAR servers version 6 or 8
|
|
68
|
-
- Python 3.
|
|
68
|
+
- Python 3.10+ (tested with Python 3.10-3.14, earlier versions may work but are untested)
|
|
69
|
+
- [uv](https://docs.astral.sh/uv/) for installation
|
|
69
70
|
|
|
70
|
-
### Additional Requirements
|
|
71
|
-
- **
|
|
72
|
-
- **Azure SDK for Python**
|
|
71
|
+
### Additional Requirements
|
|
72
|
+
- **demisto-sdk** - Required by several commands, most notably `xsoar-cli playbook download`. Should be installed separately.
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
## Installation
|
|
76
76
|
|
|
77
|
+
Installing xsoar-cli as a uv tool is recommended. This gives it an isolated environment with its own dependencies, avoiding version conflicts with other Python tools such as demisto-sdk.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv tool install xsoar-cli
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Alternatively, using pip:
|
|
84
|
+
|
|
77
85
|
```bash
|
|
78
86
|
pip install xsoar-cli
|
|
79
87
|
```
|
|
80
88
|
|
|
81
89
|
## Upgrading
|
|
82
90
|
|
|
91
|
+
```bash
|
|
92
|
+
uv tool upgrade xsoar-cli
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or with pip:
|
|
96
|
+
|
|
83
97
|
```bash
|
|
84
98
|
pip install --upgrade xsoar-cli
|
|
85
99
|
```
|
|
86
100
|
|
|
87
101
|
## Uninstalling
|
|
88
102
|
|
|
103
|
+
```bash
|
|
104
|
+
uv tool uninstall xsoar-cli
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or with pip:
|
|
108
|
+
|
|
89
109
|
```bash
|
|
90
110
|
pip uninstall xsoar-cli
|
|
91
111
|
```
|
|
@@ -106,6 +126,7 @@ After creating the config file, edit it with your XSOAR server details:
|
|
|
106
126
|
{
|
|
107
127
|
"default_environment": "xsoar6",
|
|
108
128
|
"default_new_case_type": "My case type",
|
|
129
|
+
"log_level": "INFO",
|
|
109
130
|
"custom_pack_authors": ["My_Org"],
|
|
110
131
|
"server_config": {
|
|
111
132
|
"xsoar6": {
|
|
@@ -149,6 +170,7 @@ After creating the config file, edit it with your XSOAR server details:
|
|
|
149
170
|
"default_environment": "dev",
|
|
150
171
|
"default_new_case_type": "My case type",
|
|
151
172
|
"custom_pack_authors": ["My_Org"],
|
|
173
|
+
"log_level": "DEBUG"
|
|
152
174
|
"server_config": {
|
|
153
175
|
"dev": {
|
|
154
176
|
"base_url": "https://your.dev.domain",
|
|
@@ -241,8 +263,7 @@ xsoar-cli supports a plugin system that allows you to extend the CLI with custom
|
|
|
241
263
|
- Use `xsoar-cli config validate` to verify your configuration
|
|
242
264
|
|
|
243
265
|
**Python compatibility issues**
|
|
244
|
-
- Ensure you're using Python 3.
|
|
245
|
-
- Consider using Python 3.12 for best compatibility
|
|
266
|
+
- Ensure you're using Python 3.10 or later
|
|
246
267
|
|
|
247
268
|
## Contributing
|
|
248
269
|
|
|
@@ -19,7 +19,7 @@ Pull Requests are very welcome and appreciated! See [CONTRIBUTING.md](CONTRIBUTI
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
# Install
|
|
22
|
-
|
|
22
|
+
uv tool install xsoar-cli
|
|
23
23
|
|
|
24
24
|
# Create configuration file
|
|
25
25
|
xsoar-cli config create
|
|
@@ -40,27 +40,47 @@ This CLI tool is made to be run from the root of a content repository. Some comm
|
|
|
40
40
|
|
|
41
41
|
### Core Requirements
|
|
42
42
|
- XSOAR servers version 6 or 8
|
|
43
|
-
- Python 3.
|
|
43
|
+
- Python 3.10+ (tested with Python 3.10-3.14, earlier versions may work but are untested)
|
|
44
|
+
- [uv](https://docs.astral.sh/uv/) for installation
|
|
44
45
|
|
|
45
|
-
### Additional Requirements
|
|
46
|
-
- **
|
|
47
|
-
- **Azure SDK for Python**
|
|
46
|
+
### Additional Requirements
|
|
47
|
+
- **demisto-sdk** - Required by several commands, most notably `xsoar-cli playbook download`. Should be installed separately.
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
52
|
+
Installing xsoar-cli as a uv tool is recommended. This gives it an isolated environment with its own dependencies, avoiding version conflicts with other Python tools such as demisto-sdk.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv tool install xsoar-cli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Alternatively, using pip:
|
|
59
|
+
|
|
52
60
|
```bash
|
|
53
61
|
pip install xsoar-cli
|
|
54
62
|
```
|
|
55
63
|
|
|
56
64
|
## Upgrading
|
|
57
65
|
|
|
66
|
+
```bash
|
|
67
|
+
uv tool upgrade xsoar-cli
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or with pip:
|
|
71
|
+
|
|
58
72
|
```bash
|
|
59
73
|
pip install --upgrade xsoar-cli
|
|
60
74
|
```
|
|
61
75
|
|
|
62
76
|
## Uninstalling
|
|
63
77
|
|
|
78
|
+
```bash
|
|
79
|
+
uv tool uninstall xsoar-cli
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or with pip:
|
|
83
|
+
|
|
64
84
|
```bash
|
|
65
85
|
pip uninstall xsoar-cli
|
|
66
86
|
```
|
|
@@ -81,6 +101,7 @@ After creating the config file, edit it with your XSOAR server details:
|
|
|
81
101
|
{
|
|
82
102
|
"default_environment": "xsoar6",
|
|
83
103
|
"default_new_case_type": "My case type",
|
|
104
|
+
"log_level": "INFO",
|
|
84
105
|
"custom_pack_authors": ["My_Org"],
|
|
85
106
|
"server_config": {
|
|
86
107
|
"xsoar6": {
|
|
@@ -124,6 +145,7 @@ After creating the config file, edit it with your XSOAR server details:
|
|
|
124
145
|
"default_environment": "dev",
|
|
125
146
|
"default_new_case_type": "My case type",
|
|
126
147
|
"custom_pack_authors": ["My_Org"],
|
|
148
|
+
"log_level": "DEBUG"
|
|
127
149
|
"server_config": {
|
|
128
150
|
"dev": {
|
|
129
151
|
"base_url": "https://your.dev.domain",
|
|
@@ -216,8 +238,7 @@ xsoar-cli supports a plugin system that allows you to extend the CLI with custom
|
|
|
216
238
|
- Use `xsoar-cli config validate` to verify your configuration
|
|
217
239
|
|
|
218
240
|
**Python compatibility issues**
|
|
219
|
-
- Ensure you're using Python 3.
|
|
220
|
-
- Consider using Python 3.12 for best compatibility
|
|
241
|
+
- Ensure you're using Python 3.10 or later
|
|
221
242
|
|
|
222
243
|
## Contributing
|
|
223
244
|
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.3.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 3,
|
|
31
|
+
__version__ = version = '1.3.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 3, 2)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -16,12 +16,14 @@ from .__about__ import __version__
|
|
|
16
16
|
from .case import commands as case_commands
|
|
17
17
|
from .config import commands as config_commands
|
|
18
18
|
from .graph import commands as graph_commands
|
|
19
|
+
from .integration import commands as integration_commands
|
|
19
20
|
from .log import LoggingSetup, setup_logging
|
|
20
21
|
from .manifest import commands as manifest_commands
|
|
21
22
|
from .pack import commands as pack_commands
|
|
22
23
|
from .playbook import commands as playbook_commands
|
|
23
24
|
from .plugins import commands as plugin_commands
|
|
24
25
|
from .plugins.manager import PluginManager
|
|
26
|
+
from .rbac import commands as rbac_commands
|
|
25
27
|
from .utilities import get_config_file_contents, get_config_file_path
|
|
26
28
|
|
|
27
29
|
|
|
@@ -64,10 +66,12 @@ def _register_commands() -> None:
|
|
|
64
66
|
cli.add_command(config_commands.config)
|
|
65
67
|
cli.add_command(case_commands.case)
|
|
66
68
|
cli.add_command(pack_commands.pack)
|
|
69
|
+
cli.add_command(integration_commands.integration)
|
|
67
70
|
cli.add_command(manifest_commands.manifest)
|
|
68
71
|
cli.add_command(playbook_commands.playbook)
|
|
69
72
|
cli.add_command(graph_commands.graph)
|
|
70
73
|
cli.add_command(plugin_commands.plugins)
|
|
74
|
+
cli.add_command(rbac_commands.rbac)
|
|
71
75
|
|
|
72
76
|
|
|
73
77
|
def _load_plugins() -> tuple[list[str], PluginManager]:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from xsoar_cli.utilities import get_xsoar_config, load_config, validate_xsoar_connectivity
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from xsoar_client.xsoar_client import Client
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def integration(ctx: click.Context) -> None:
|
|
18
|
+
"""(BETA) Save/load integration configuration for an integration instance."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
22
|
+
@click.command()
|
|
23
|
+
@click.argument("name", type=str)
|
|
24
|
+
@click.pass_context
|
|
25
|
+
@load_config
|
|
26
|
+
@validate_xsoar_connectivity()
|
|
27
|
+
def dumpconfig(ctx: click.Context, environment: str | None, name: str | None) -> None:
|
|
28
|
+
"""Dumps integration config to JSON file."""
|
|
29
|
+
config = get_xsoar_config(ctx)
|
|
30
|
+
xsoar_client: Client = config.get_client(environment)
|
|
31
|
+
logger.debug("Dumping integration config (environment: '%s')", environment or config.default_environment)
|
|
32
|
+
response = xsoar_client.get_integrations()
|
|
33
|
+
integrations = json.loads(response)
|
|
34
|
+
integration_data = next((i for i in integrations if i["name"] == name), None)
|
|
35
|
+
logger.debug("Fetching config for integration name '%s'(environment: '%s')", name, environment or config.default_environment)
|
|
36
|
+
if not integration_data:
|
|
37
|
+
click.echo(f"Cannot find integration instance '{name}'")
|
|
38
|
+
ctx.exit(1)
|
|
39
|
+
click.echo(json.dumps(integration_data, sort_keys=True, indent=4) + "\n")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
43
|
+
@click.command()
|
|
44
|
+
@click.argument("name", type=str)
|
|
45
|
+
@click.pass_context
|
|
46
|
+
@load_config
|
|
47
|
+
@validate_xsoar_connectivity()
|
|
48
|
+
def loadconfig(ctx: click.Context, environment: str | None, name: str, instance_name: str) -> None:
|
|
49
|
+
"""Loads integration config from JSON file."""
|
|
50
|
+
logger.debug("integration loadconfig command not implemented")
|
|
51
|
+
click.echo("Command not implemented")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
integration.add_command(dumpconfig)
|
|
55
|
+
integration.add_command(loadconfig)
|
|
@@ -72,9 +72,11 @@ def download(ctx: click.Context, environment: str | None, name: str) -> None:
|
|
|
72
72
|
[
|
|
73
73
|
"demisto-sdk",
|
|
74
74
|
"format",
|
|
75
|
-
"--assume-
|
|
75
|
+
"--assume-no",
|
|
76
76
|
"--no-validate",
|
|
77
77
|
"--no-graph",
|
|
78
|
+
"--from-version",
|
|
79
|
+
"6.10.0",
|
|
78
80
|
str(filepath),
|
|
79
81
|
],
|
|
80
82
|
check=False,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
import xsoar_cli
|
|
8
|
+
from xsoar_cli.utilities import get_xsoar_config, load_config, validate_xsoar_connectivity
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from xsoar_client.xsoar_client import Client
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def rbac(ctx: click.Context) -> None:
|
|
19
|
+
"""Dump roles, users and user groups"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.pass_context
|
|
25
|
+
@load_config
|
|
26
|
+
@validate_xsoar_connectivity()
|
|
27
|
+
def getroles(ctx: click.Context, environment: str | None) -> None:
|
|
28
|
+
"""Dump all roles in your environment."""
|
|
29
|
+
config = get_xsoar_config(ctx)
|
|
30
|
+
xsoar_client: Client = config.get_client(environment)
|
|
31
|
+
results = xsoar_client.get_roles()
|
|
32
|
+
roles = json.loads(results)
|
|
33
|
+
click.echo(json.dumps(roles, sort_keys=True, indent=4) + "\n")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
37
|
+
@click.command()
|
|
38
|
+
@click.pass_context
|
|
39
|
+
@load_config
|
|
40
|
+
@validate_xsoar_connectivity()
|
|
41
|
+
def getusers(ctx: click.Context, environment: str | None) -> None:
|
|
42
|
+
"""Dump all users in your environment."""
|
|
43
|
+
config = get_xsoar_config(ctx)
|
|
44
|
+
xsoar_client: Client = config.get_client(environment)
|
|
45
|
+
results = xsoar_client.get_users()
|
|
46
|
+
users = json.loads(results)
|
|
47
|
+
click.echo(json.dumps(users, sort_keys=True, indent=4) + "\n")
|
|
48
|
+
|
|
49
|
+
config = get_xsoar_config(ctx)
|
|
50
|
+
xsoar_client: Client = config.get_client(environment)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
54
|
+
@click.command()
|
|
55
|
+
@click.pass_context
|
|
56
|
+
@load_config
|
|
57
|
+
@validate_xsoar_connectivity()
|
|
58
|
+
def getusergroups(ctx: click.Context, environment: str | None) -> None:
|
|
59
|
+
"""Dump all roles in your environment."""
|
|
60
|
+
config = get_xsoar_config(ctx)
|
|
61
|
+
xsoar_client: Client = config.get_client(environment)
|
|
62
|
+
if xsoar_client.config.server_version < 8:
|
|
63
|
+
click.echo("Errot: Command not supported for XSOAR server versions less than 8")
|
|
64
|
+
ctx.exit(1)
|
|
65
|
+
results = xsoar_client.get_user_groups()
|
|
66
|
+
user_groups = json.loads(results)
|
|
67
|
+
click.echo(json.dumps(user_groups, sort_keys=True, indent=4) + "\n")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
rbac.add_command(getroles)
|
|
71
|
+
rbac.add_command(getusers)
|
|
72
|
+
rbac.add_command(getusergroups)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Tests for ConnectionErrorHandler.
|
|
2
|
+
|
|
3
|
+
Covers get_message and _extract_hostname using a single parametrized test method.
|
|
4
|
+
Exception objects that require raise/except chains are built by module-level helpers
|
|
5
|
+
and passed directly as parameter values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import socket
|
|
9
|
+
from unittest.mock import MagicMock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from urllib3.exceptions import NameResolutionError
|
|
13
|
+
|
|
14
|
+
from xsoar_cli.connection_errors import ConnectionErrorHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_name_resolution_error(hostname: str, reason: str) -> NameResolutionError:
|
|
18
|
+
"""Build a NameResolutionError for the given hostname and reason string.
|
|
19
|
+
|
|
20
|
+
Uses a MagicMock for the conn argument, with __str__ fixed to 'conn' so the
|
|
21
|
+
formatted message is predictable across test runs.
|
|
22
|
+
"""
|
|
23
|
+
conn = MagicMock()
|
|
24
|
+
conn.__str__ = lambda self: "conn"
|
|
25
|
+
return NameResolutionError(hostname, conn, socket.gaierror(reason))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _chained_name_resolution_error() -> NameResolutionError:
|
|
29
|
+
"""Return a NameResolutionError chained from an OSError root cause.
|
|
30
|
+
|
|
31
|
+
Represents the realistic urllib3 case where a NameResolutionError wraps a
|
|
32
|
+
lower-level socket error.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
try:
|
|
36
|
+
raise OSError("nodename nor servname provided, or not known")
|
|
37
|
+
except OSError as root:
|
|
38
|
+
raise _make_name_resolution_error("example.com", "nodename nor servname provided, or not known") from root
|
|
39
|
+
except NameResolutionError as exc:
|
|
40
|
+
return exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _chained_without_name_resolution_error() -> RuntimeError:
|
|
44
|
+
"""Return a RuntimeError chained from an OSError root cause, with no NameResolutionError in the chain.
|
|
45
|
+
|
|
46
|
+
Used to verify that get_message falls back to the root cause string when no
|
|
47
|
+
hostname can be extracted.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
try:
|
|
51
|
+
raise OSError("connection refused")
|
|
52
|
+
except OSError as root:
|
|
53
|
+
raise RuntimeError("operation failed") from root
|
|
54
|
+
except RuntimeError as exc:
|
|
55
|
+
return exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _name_resolution_error_no_args() -> NameResolutionError:
|
|
59
|
+
"""Return a NameResolutionError with an empty args tuple.
|
|
60
|
+
|
|
61
|
+
Bypasses the normal constructor to simulate a malformed exception object,
|
|
62
|
+
used to verify that _extract_hostname handles missing args gracefully.
|
|
63
|
+
"""
|
|
64
|
+
exc = NameResolutionError.__new__(NameResolutionError)
|
|
65
|
+
exc.args = ()
|
|
66
|
+
return exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _name_resolution_error_with_message(message: str) -> NameResolutionError:
|
|
70
|
+
"""Return a NameResolutionError with args set to the given message string.
|
|
71
|
+
|
|
72
|
+
Bypasses the normal constructor so arbitrary message content can be injected,
|
|
73
|
+
used to test _extract_hostname's message parsing edge cases.
|
|
74
|
+
"""
|
|
75
|
+
exc = NameResolutionError.__new__(NameResolutionError)
|
|
76
|
+
exc.args = (message,)
|
|
77
|
+
return exc
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
handler = ConnectionErrorHandler()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestConnectionErrorHandler:
|
|
84
|
+
"""Tests for ConnectionErrorHandler.
|
|
85
|
+
|
|
86
|
+
Both get_message and _extract_hostname are exercised through a single
|
|
87
|
+
parametrized method. The method column selects which handler method to call
|
|
88
|
+
via getattr, keeping all cases in one place.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize(
|
|
92
|
+
("method", "exception", "expected"),
|
|
93
|
+
[
|
|
94
|
+
# get_message: simple cases
|
|
95
|
+
("get_message", None, "Unknown error"),
|
|
96
|
+
("get_message", ValueError("something went wrong"), "something went wrong"),
|
|
97
|
+
# get_message: exception chain without NameResolutionError
|
|
98
|
+
("get_message", _chained_without_name_resolution_error(), "connection refused"),
|
|
99
|
+
# get_message: NameResolutionError with chained root cause
|
|
100
|
+
(
|
|
101
|
+
"get_message",
|
|
102
|
+
_chained_name_resolution_error(),
|
|
103
|
+
"Failed to resolve 'example.com' (nodename nor servname provided, or not known)",
|
|
104
|
+
),
|
|
105
|
+
# _extract_hostname: valid hostnames
|
|
106
|
+
(
|
|
107
|
+
"_extract_hostname",
|
|
108
|
+
_make_name_resolution_error("example.com", "nodename nor servname provided, or not known"),
|
|
109
|
+
"example.com",
|
|
110
|
+
),
|
|
111
|
+
("_extract_hostname", _make_name_resolution_error("my-host.internal", "Name or service not known"), "my-host.internal"),
|
|
112
|
+
# _extract_hostname: no args
|
|
113
|
+
("_extract_hostname", _name_resolution_error_no_args(), None),
|
|
114
|
+
# _extract_hostname: message without "Failed to resolve"
|
|
115
|
+
("_extract_hostname", _name_resolution_error_with_message("some unrelated error message"), None),
|
|
116
|
+
("_extract_hostname", _name_resolution_error_with_message("conn: Could not connect to host"), None),
|
|
117
|
+
# _extract_hostname: malformed quotes
|
|
118
|
+
("_extract_hostname", _name_resolution_error_with_message("conn: Failed to resolve 'example.com"), None),
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
def test_connection_error_handler(self, method: str, exception: BaseException | None, expected: str | None) -> None:
|
|
122
|
+
"""Invoke the named handler method with exception and assert the result matches expected.
|
|
123
|
+
|
|
124
|
+
method: name of the ConnectionErrorHandler method to call
|
|
125
|
+
exception: exception instance (or None) passed as the sole argument
|
|
126
|
+
expected: expected return value; None for _extract_hostname failure cases
|
|
127
|
+
"""
|
|
128
|
+
result = getattr(handler, method)(exception)
|
|
129
|
+
assert result == expected
|
|
File without changes
|
|
File without changes
|