xsoar-cli 1.0.9__py3-none-any.whl
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/__about__.py +4 -0
- xsoar_cli/__init__.py +1 -0
- xsoar_cli/case/README.md +57 -0
- xsoar_cli/case/__init__.py +0 -0
- xsoar_cli/case/commands.py +130 -0
- xsoar_cli/cli.py +41 -0
- xsoar_cli/config/README.md +64 -0
- xsoar_cli/config/__init__.py +0 -0
- xsoar_cli/config/commands.py +112 -0
- xsoar_cli/graph/README.md +17 -0
- xsoar_cli/graph/__init__.py +0 -0
- xsoar_cli/graph/commands.py +32 -0
- xsoar_cli/manifest/README.md +122 -0
- xsoar_cli/manifest/__init__.py +0 -0
- xsoar_cli/manifest/commands.py +271 -0
- xsoar_cli/pack/README.md +36 -0
- xsoar_cli/pack/__init__.py +0 -0
- xsoar_cli/pack/commands.py +59 -0
- xsoar_cli/playbook/README.md +43 -0
- xsoar_cli/playbook/__init__.py +0 -0
- xsoar_cli/playbook/commands.py +84 -0
- xsoar_cli/plugins/README.md +85 -0
- xsoar_cli/plugins/__init__.py +68 -0
- xsoar_cli/plugins/commands.py +273 -0
- xsoar_cli/plugins/manager.py +328 -0
- xsoar_cli/utilities.py +159 -0
- xsoar_cli-1.0.9.dist-info/METADATA +209 -0
- xsoar_cli-1.0.9.dist-info/RECORD +31 -0
- xsoar_cli-1.0.9.dist-info/WHEEL +4 -0
- xsoar_cli-1.0.9.dist-info/entry_points.txt +2 -0
- xsoar_cli-1.0.9.dist-info/licenses/LICENSE.txt +9 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from xsoar_cli.utilities import load_config
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from xsoar_client.xsoar_client import Client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_manifest(manifest: str): # noqa: ANN201
|
|
15
|
+
"""Calls json.load() on the manifest and returns a dict."""
|
|
16
|
+
filepath = Path(manifest)
|
|
17
|
+
try:
|
|
18
|
+
return json.load(filepath.open("r"))
|
|
19
|
+
except json.JSONDecodeError:
|
|
20
|
+
msg = f"Failed to decode JSON in {filepath}"
|
|
21
|
+
click.echo(msg)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
print(f"File not found: {filepath}")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_manifest(manifest: str, data: Any) -> None: # noqa: ANN401
|
|
29
|
+
"""Writes the xsoar_conf.json manifest using json.dumps()"""
|
|
30
|
+
manifest_path = Path(manifest)
|
|
31
|
+
with manifest_path.open("w") as f:
|
|
32
|
+
f.write(json.dumps(data, indent=4))
|
|
33
|
+
f.write("\n")
|
|
34
|
+
click.echo(f"Written updated manifest to '{manifest_path}'")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.group()
|
|
38
|
+
def manifest() -> None:
|
|
39
|
+
"""Various commands to interact/update/deploy content packs defined in the xsoar_config.json manifest."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
43
|
+
@click.argument("manifest_path", type=str)
|
|
44
|
+
@click.command()
|
|
45
|
+
@click.pass_context
|
|
46
|
+
@load_config
|
|
47
|
+
def generate(ctx: click.Context, environment: str | None, manifest_path: str) -> None:
|
|
48
|
+
"""Generate a new xsoar_config.json manifest from installed content packs.
|
|
49
|
+
|
|
50
|
+
This command assumes that you do not have any custom content packs uploaded to XSOAR.
|
|
51
|
+
All packs will be added as "marketplace_packs" in the manifest.
|
|
52
|
+
"""
|
|
53
|
+
if not environment:
|
|
54
|
+
environment = ctx.obj["default_environment"]
|
|
55
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
56
|
+
installed_packs = xsoar_client.get_installed_packs()
|
|
57
|
+
manifest_data = {
|
|
58
|
+
"marketplace_packs": [],
|
|
59
|
+
}
|
|
60
|
+
for item in installed_packs:
|
|
61
|
+
tmpobj = {
|
|
62
|
+
"id": item["id"],
|
|
63
|
+
"version": item["currentVersion"],
|
|
64
|
+
}
|
|
65
|
+
manifest_data["marketplace_packs"].append(tmpobj)
|
|
66
|
+
write_manifest(manifest_path, manifest_data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
70
|
+
@click.argument("manifest", type=str)
|
|
71
|
+
@click.command()
|
|
72
|
+
@click.pass_context
|
|
73
|
+
@load_config
|
|
74
|
+
def update(ctx: click.Context, environment: str | None, manifest: str) -> None:
|
|
75
|
+
"""Update manifest on disk with latest available content pack versions."""
|
|
76
|
+
if not environment:
|
|
77
|
+
environment = ctx.obj["default_environment"]
|
|
78
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
79
|
+
manifest_data = load_manifest(manifest)
|
|
80
|
+
click.echo("Fetching outdated packs from XSOAR server. This may take a minute...", nl=False)
|
|
81
|
+
results = xsoar_client.get_outdated_packs()
|
|
82
|
+
click.echo("done.")
|
|
83
|
+
if not results:
|
|
84
|
+
click.echo("No packs eligible for upgrade.")
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
|
|
87
|
+
item1 = "Pack ID"
|
|
88
|
+
item2 = "Installed version"
|
|
89
|
+
item3 = "Latest available version"
|
|
90
|
+
header = f"{item1:50}{item2:20}{item3:20}"
|
|
91
|
+
click.echo(header)
|
|
92
|
+
for pack in results:
|
|
93
|
+
click.echo(f"{pack['id']:50}{pack['currentVersion']:20}{pack['latest']:20}")
|
|
94
|
+
click.echo(f"Total number of outdated content packs: {len(results)}")
|
|
95
|
+
|
|
96
|
+
for pack in results:
|
|
97
|
+
key = "custom_packs" if pack["author"] in ctx.obj["custom_pack_authors"] else "marketplace_packs"
|
|
98
|
+
index = next((i for i, item in enumerate(manifest_data[key]) if item["id"] == pack["id"]), None)
|
|
99
|
+
if index is None:
|
|
100
|
+
msg = f"Pack {pack['id']} not found in manifest."
|
|
101
|
+
click.echo(msg)
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
comment = manifest_data[key][index].get("_comment", None)
|
|
104
|
+
if comment is not None:
|
|
105
|
+
print(f"WARNING: comment found in manifest for {pack['id']}: {comment}")
|
|
106
|
+
msg = f"Upgrade {pack['id']} from {pack['currentVersion']} to {pack['latest']}?"
|
|
107
|
+
should_upgrade = click.confirm(msg, default=True)
|
|
108
|
+
if should_upgrade:
|
|
109
|
+
manifest_data[key][index]["version"] = pack["latest"]
|
|
110
|
+
write_manifest(manifest, manifest_data)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
114
|
+
@click.option("--mode", type=click.Choice(["full", "diff"]), default="diff", help="Validate the full manifest, or only the definitions that diff with installed versions")
|
|
115
|
+
@click.argument("manifest", type=str)
|
|
116
|
+
@click.command()
|
|
117
|
+
@click.pass_context
|
|
118
|
+
@load_config
|
|
119
|
+
def validate(ctx: click.Context, environment: str | None, mode: str, manifest: str) -> None:
|
|
120
|
+
"""Validate manifest JSON and content pack availability by doing HTTP CONNECT to the appropriate artifacts repository.
|
|
121
|
+
Custom pack availability is implementation dependant."""
|
|
122
|
+
if not environment:
|
|
123
|
+
environment = ctx.obj["default_environment"]
|
|
124
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
125
|
+
|
|
126
|
+
manifest_data = load_manifest(manifest)
|
|
127
|
+
click.echo("Manifest is valid JSON")
|
|
128
|
+
keys = ["custom_packs", "marketplace_packs"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def found_in_local_filesystem() -> bool:
|
|
132
|
+
# If we are in a merge request and the merge request contains a new pack as well
|
|
133
|
+
# as a updated xsoar_config.json manifest, then the pack is not available in S3
|
|
134
|
+
# before the MR is merged. Check if we can find the appropriate pack version locally
|
|
135
|
+
# If so, we can ignore this error because the pack will become available after merge.
|
|
136
|
+
manifest_arg = Path(manifest)
|
|
137
|
+
manifest_path = manifest_arg.resolve()
|
|
138
|
+
repo_path = manifest_path.parent
|
|
139
|
+
pack_metadata_path = Path(f"{repo_path}/Packs/{pack['id']}/pack_metadata.json")
|
|
140
|
+
with Path.open(pack_metadata_path, encoding="utf-8") as f:
|
|
141
|
+
pack_metadata = json.load(f)
|
|
142
|
+
if pack_metadata["currentVersion"] == pack["version"]:
|
|
143
|
+
return True
|
|
144
|
+
# The relevant Pack locally does not have the requested version.
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
if mode == "full":
|
|
148
|
+
for key in keys:
|
|
149
|
+
custom = key == "custom_packs"
|
|
150
|
+
click.echo(f"Checking {key} availability ", nl=False)
|
|
151
|
+
for pack in manifest_data[key]:
|
|
152
|
+
available = xsoar_client.is_pack_available(pack_id=pack["id"], version=pack["version"], custom=custom)
|
|
153
|
+
# We check if a pack is found in local filesystem regardless of whether it's an upstream pack or not.
|
|
154
|
+
# This should cause any significantly negative performance penalties.
|
|
155
|
+
if not available and not found_in_local_filesystem():
|
|
156
|
+
click.echo(f"\nFailed to reach pack {pack['id']} version {pack['version']}")
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
click.echo(".", nl=False)
|
|
159
|
+
print()
|
|
160
|
+
click.echo("Manifest is valid JSON and all packs are reachable")
|
|
161
|
+
return
|
|
162
|
+
elif mode == "diff":
|
|
163
|
+
installed_packs = xsoar_client.get_installed_packs()
|
|
164
|
+
for key in keys:
|
|
165
|
+
found_diff = False
|
|
166
|
+
custom = key == "custom_packs"
|
|
167
|
+
click.echo(f"Checking {key} availability ", nl=False)
|
|
168
|
+
for pack in manifest_data[key]:
|
|
169
|
+
installed = next((item for item in installed_packs if item["id"] == pack["id"]), {})
|
|
170
|
+
if not installed or installed["currentVersion"] != pack["version"]:
|
|
171
|
+
available = xsoar_client.is_pack_available(pack_id=pack["id"], version=pack["version"], custom=custom)
|
|
172
|
+
# We check if a pack is found in local filesystem regardless of whether it's an upstream pack or not.
|
|
173
|
+
# This should cause any significantly negative performance penalties.
|
|
174
|
+
if not available and not found_in_local_filesystem():
|
|
175
|
+
click.echo(f"\nFailed to reach pack {pack['id']} version {pack['version']}")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
click.echo(".", nl=False)
|
|
178
|
+
found_diff = True
|
|
179
|
+
if not found_diff:
|
|
180
|
+
click.echo("- no diff from installed versions found in manifest.")
|
|
181
|
+
else:
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
click.echo("Manifest is valid JSON and all packs are reachable.")
|
|
185
|
+
return
|
|
186
|
+
else:
|
|
187
|
+
msg = "Invalid value for --mode detected. This should never happen"
|
|
188
|
+
raise RuntimeError(msg)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
192
|
+
@click.argument("manifest", type=str)
|
|
193
|
+
@click.command()
|
|
194
|
+
@click.pass_context
|
|
195
|
+
@load_config
|
|
196
|
+
def diff(ctx: click.Context, manifest: str, environment: str | None) -> None:
|
|
197
|
+
"""Prints out the differences (if any) between what is defined in the xsoar_config.json manifest and what is actually
|
|
198
|
+
installed on the XSOAR server."""
|
|
199
|
+
if not environment:
|
|
200
|
+
environment = ctx.obj["default_environment"]
|
|
201
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
202
|
+
manifest_data = load_manifest(manifest)
|
|
203
|
+
installed_packs = xsoar_client.get_installed_packs()
|
|
204
|
+
all_good = True
|
|
205
|
+
for key in manifest_data:
|
|
206
|
+
for pack in manifest_data[key]:
|
|
207
|
+
installed = next((item for item in installed_packs if item["id"] == pack["id"]), {})
|
|
208
|
+
if not installed:
|
|
209
|
+
click.echo(f"Pack {pack['id']} is not installed")
|
|
210
|
+
all_good = False
|
|
211
|
+
elif installed["currentVersion"] != pack["version"]:
|
|
212
|
+
msg = f"Manifest states {pack['id']} version {pack['version']} but version {installed['currentVersion']} is installed"
|
|
213
|
+
click.echo(msg)
|
|
214
|
+
all_good = False
|
|
215
|
+
if all_good:
|
|
216
|
+
click.echo("All packs up to date.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
220
|
+
@click.option("--verbose", is_flag=True, default=False)
|
|
221
|
+
@click.option("--yes", is_flag=True, default=False)
|
|
222
|
+
@click.command()
|
|
223
|
+
@click.argument("manifest", type=str)
|
|
224
|
+
@click.pass_context
|
|
225
|
+
@load_config
|
|
226
|
+
def deploy(ctx: click.Context, environment: str | None, manifest: str, verbose: bool, yes: bool) -> None: # noqa: FBT001
|
|
227
|
+
"""
|
|
228
|
+
Deploys content packs to the XSOAR server as defined in the xsoar_config.json manifest.
|
|
229
|
+
The PATH argument expects the full or relative path to xsoar_config.json
|
|
230
|
+
|
|
231
|
+
\b
|
|
232
|
+
Prompts for confirmation prior to pack installation.
|
|
233
|
+
"""
|
|
234
|
+
should_continue = True
|
|
235
|
+
if not yes:
|
|
236
|
+
should_continue = click.confirm(
|
|
237
|
+
f"WARNING: this operation will attempt to deploy all packs defined in the manifest to XSOAR {environment} environment. Continue?",
|
|
238
|
+
)
|
|
239
|
+
if not should_continue:
|
|
240
|
+
ctx.exit()
|
|
241
|
+
if not environment:
|
|
242
|
+
environment = ctx.obj["default_environment"]
|
|
243
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
244
|
+
manifest_data = load_manifest(manifest)
|
|
245
|
+
click.echo("Fetching installed packs...", err=True)
|
|
246
|
+
installed_packs = xsoar_client.get_installed_packs()
|
|
247
|
+
click.echo("done.")
|
|
248
|
+
none_installed = True
|
|
249
|
+
for key in manifest_data:
|
|
250
|
+
custom = key == "custom_packs"
|
|
251
|
+
for pack in manifest_data[key]:
|
|
252
|
+
installed = next((item for item in installed_packs if item["id"] == pack["id"]), {})
|
|
253
|
+
if not installed or installed["currentVersion"] != pack["version"]:
|
|
254
|
+
# Install pack
|
|
255
|
+
click.echo(f"Installing {pack['id']} version {pack['version']}...", nl=False)
|
|
256
|
+
xsoar_client.deploy_pack(pack_id=pack["id"], pack_version=pack["version"], custom=custom)
|
|
257
|
+
click.echo("OK.")
|
|
258
|
+
none_installed = False
|
|
259
|
+
elif verbose:
|
|
260
|
+
click.echo(f"Not installing {pack['id']} version {pack['version']}. Already installed.")
|
|
261
|
+
# Print message that install is skipped
|
|
262
|
+
|
|
263
|
+
if none_installed:
|
|
264
|
+
click.echo("No packs to install. All packs and versions in manifest is already installed on XSOAR server.")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
manifest.add_command(deploy)
|
|
268
|
+
manifest.add_command(diff)
|
|
269
|
+
manifest.add_command(update)
|
|
270
|
+
manifest.add_command(validate)
|
|
271
|
+
manifest.add_command(generate)
|
xsoar_cli/pack/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Pack
|
|
2
|
+
|
|
3
|
+
Content pack management commands for XSOAR.
|
|
4
|
+
|
|
5
|
+
## Delete
|
|
6
|
+
|
|
7
|
+
Delete a content pack from the XSOAR server. Verifies the pack is installed before attempting deletion.
|
|
8
|
+
|
|
9
|
+
**Syntax:** `xsoar-cli pack delete [OPTIONS] PACK_ID`
|
|
10
|
+
|
|
11
|
+
**Options:**
|
|
12
|
+
- `--environment TEXT` - Target environment (default: uses default environment from config)
|
|
13
|
+
|
|
14
|
+
**Arguments:**
|
|
15
|
+
- `PACK_ID` - The ID of the content pack to delete
|
|
16
|
+
|
|
17
|
+
**Examples:**
|
|
18
|
+
```
|
|
19
|
+
xsoar-cli pack delete MyCustomPack
|
|
20
|
+
xsoar-cli pack delete --environment prod CommonScripts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Get Outdated
|
|
24
|
+
|
|
25
|
+
Display a list of outdated content packs showing current and latest available versions in table format.
|
|
26
|
+
|
|
27
|
+
**Syntax:** `xsoar-cli pack get-outdated [OPTIONS]`
|
|
28
|
+
|
|
29
|
+
**Options:**
|
|
30
|
+
- `--environment TEXT` - Target environment (default: uses default environment from config)
|
|
31
|
+
|
|
32
|
+
**Examples:**
|
|
33
|
+
```
|
|
34
|
+
xsoar-cli pack get-outdated
|
|
35
|
+
xsoar-cli pack get-outdated --environment staging
|
|
36
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from xsoar_cli.utilities import load_config
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from xsoar_client.xsoar_client import Client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def pack(ctx: click.Context) -> None:
|
|
15
|
+
"""Various content pack related commands."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
19
|
+
@click.command()
|
|
20
|
+
@click.argument("pack_id", type=str)
|
|
21
|
+
@click.pass_context
|
|
22
|
+
@load_config
|
|
23
|
+
def delete(ctx: click.Context, environment: str | None, pack_id: str) -> None:
|
|
24
|
+
"""Deletes a content pack from the XSOAR server."""
|
|
25
|
+
if not environment:
|
|
26
|
+
environment = ctx.obj["default_environment"]
|
|
27
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
28
|
+
if not xsoar_client.is_installed(pack_id=pack_id):
|
|
29
|
+
click.echo(f"Pack ID {pack_id} is not installed. Cannot delete.")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
xsoar_client.delete(pack_id=pack_id)
|
|
32
|
+
click.echo(f"Deleted pack {pack_id} from XSOAR {environment}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.option("--environment", default=None, help="Default environment set in config file.")
|
|
36
|
+
@click.command()
|
|
37
|
+
@click.pass_context
|
|
38
|
+
@load_config
|
|
39
|
+
def get_outdated(ctx: click.Context, environment: str | None) -> None:
|
|
40
|
+
"""Prints out a list of outdated content packs."""
|
|
41
|
+
if not environment:
|
|
42
|
+
environment = ctx.obj["default_environment"]
|
|
43
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
44
|
+
click.echo("Fetching outdated packs. This may take a little while...", err=True)
|
|
45
|
+
outdated_packs = xsoar_client.get_outdated_packs()
|
|
46
|
+
if not outdated_packs:
|
|
47
|
+
click.echo("No outdated packs found")
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
id_header = "Pack ID"
|
|
50
|
+
installed_header = "Installed version"
|
|
51
|
+
latest_header = "Latest version"
|
|
52
|
+
click.echo(f"{id_header:<50}{installed_header:>17}{latest_header:>17}")
|
|
53
|
+
for pack in outdated_packs:
|
|
54
|
+
msg = f"{pack['id']:<50}{pack['currentVersion']:>17}{pack['latest']:>17}"
|
|
55
|
+
click.echo(msg)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
pack.add_command(delete)
|
|
59
|
+
pack.add_command(get_outdated)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Playbook
|
|
2
|
+
|
|
3
|
+
Playbook management commands for XSOAR development workflows.
|
|
4
|
+
|
|
5
|
+
## Download
|
|
6
|
+
|
|
7
|
+
Download a playbook from XSOAR, format it with demisto-sdk, and re-attach it to the server. Designed for content repository development workflows.
|
|
8
|
+
|
|
9
|
+
**Syntax:** `xsoar-cli playbook download [OPTIONS] NAME`
|
|
10
|
+
|
|
11
|
+
**Options:**
|
|
12
|
+
- `--environment TEXT` - Target environment (default: uses default environment from config)
|
|
13
|
+
|
|
14
|
+
**Arguments:**
|
|
15
|
+
- `NAME` - The name of the playbook to download
|
|
16
|
+
|
|
17
|
+
**Examples:**
|
|
18
|
+
```
|
|
19
|
+
xsoar-cli playbook download "My Awesome Playbook"
|
|
20
|
+
xsoar-cli playbook download --environment dev "Security Investigation"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Must be run from the root of a content repository with proper directory structure
|
|
26
|
+
- Target directory `Packs/<PackID>/Playbooks/` must exist
|
|
27
|
+
- `demisto-sdk` must be installed and available in PATH
|
|
28
|
+
|
|
29
|
+
## Behavior
|
|
30
|
+
|
|
31
|
+
1. Downloads the specified playbook from XSOAR
|
|
32
|
+
2. Detects the content pack ID from playbook metadata
|
|
33
|
+
3. Saves to `$(cwd)/Packs/<PackID>/Playbooks/<playbook_name>.yml`
|
|
34
|
+
4. Runs `demisto-sdk format --assume-yes --no-validate --no-graph` on the file
|
|
35
|
+
5. Re-attaches the formatted playbook to XSOAR
|
|
36
|
+
6. Replaces whitespace characters in filenames with underscores
|
|
37
|
+
|
|
38
|
+
## Limitations
|
|
39
|
+
|
|
40
|
+
- Only supports playbooks that are already part of a content pack
|
|
41
|
+
- Requires existing content repository directory structure
|
|
42
|
+
- Attempting to download non-existing playbooks results in server errors
|
|
43
|
+
- Does not support completely new playbooks (not yet implemented)
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from xsoar_cli.utilities import load_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from xsoar_client.xsoar_client import Client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def playbook(ctx: click.Context) -> None:
|
|
19
|
+
"""Download/attach/detach playbooks"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.option(
|
|
23
|
+
"--environment", default=None, help="Default environment set in config file."
|
|
24
|
+
)
|
|
25
|
+
@click.command()
|
|
26
|
+
@click.argument("name", type=str)
|
|
27
|
+
@click.pass_context
|
|
28
|
+
@load_config
|
|
29
|
+
def download(ctx: click.Context, environment: str | None, name: str) -> None:
|
|
30
|
+
"""Download and reattach playbook.
|
|
31
|
+
|
|
32
|
+
We try to detect output path to $(cwd)/Packs/<Pack ID>/Playbooks/<name>.yml
|
|
33
|
+
Whitespace in Pack ID and playbook filename will be replaced with underscores. After the playbook is downloaded,
|
|
34
|
+
then demisto-sdk format --assume-yes --no-validate --no-graph is done on the downloaded playbook before the item
|
|
35
|
+
is re-attached in XSOAR.
|
|
36
|
+
"""
|
|
37
|
+
if not environment:
|
|
38
|
+
environment = ctx.obj["default_environment"]
|
|
39
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]["xsoar_client"]
|
|
40
|
+
# Maybe we should search for the playbook before attempting download in
|
|
41
|
+
# case user specifies a cutsom playbook and not a system playbook
|
|
42
|
+
try:
|
|
43
|
+
click.echo("Downloading playbook...", nl=False)
|
|
44
|
+
playbook = xsoar_client.download_item(item_type="playbook", item_id=name)
|
|
45
|
+
click.echo("ok.")
|
|
46
|
+
except Exception as ex: # noqa: BLE001
|
|
47
|
+
click.echo(f"FAILED: {ex!s}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
playbook_bytes_data = StringIO(playbook.decode("utf-8"))
|
|
50
|
+
playbook_data = yaml.safe_load(playbook_bytes_data)
|
|
51
|
+
pack_id = playbook_data["contentitemexportablefields"]["contentitemfields"][
|
|
52
|
+
"packID"
|
|
53
|
+
]
|
|
54
|
+
cwd = pathlib.Path().cwd()
|
|
55
|
+
target_dir = pathlib.Path(cwd / "Packs" / pack_id / "Playbooks")
|
|
56
|
+
if not target_dir.is_dir():
|
|
57
|
+
msg = f"Cannot find target directory: {target_dir}\nMaybe you're not running xsoar-cli from the root of a content repository?"
|
|
58
|
+
click.echo(msg)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
filepath = pathlib.Path(
|
|
61
|
+
cwd / "Packs" / pack_id / "Playbooks" / f"{playbook_data['id']}.yml"
|
|
62
|
+
)
|
|
63
|
+
filepath = pathlib.Path(str(filepath).replace(" ", "_"))
|
|
64
|
+
with filepath.open("w") as f:
|
|
65
|
+
yaml.dump(playbook_data, f, default_flow_style=False)
|
|
66
|
+
click.echo(f"Written playbook to: {filepath}")
|
|
67
|
+
click.echo("Running demisto-sdk format on newly downloaded playbook")
|
|
68
|
+
subprocess.run(
|
|
69
|
+
[
|
|
70
|
+
"demisto-sdk",
|
|
71
|
+
"format",
|
|
72
|
+
"--assume-yes",
|
|
73
|
+
"--no-validate",
|
|
74
|
+
"--no-graph",
|
|
75
|
+
str(filepath),
|
|
76
|
+
],
|
|
77
|
+
check=False,
|
|
78
|
+
) # noqa: S603, S607
|
|
79
|
+
click.echo("Re-attaching playbook in XSOAR...", nl=False)
|
|
80
|
+
xsoar_client.attach_item(item_type="playbook", item_id=name)
|
|
81
|
+
click.echo("done.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
playbook.add_command(download)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# XSOAR CLI Plugin System
|
|
2
|
+
|
|
3
|
+
The XSOAR CLI plugin system allows you to extend the CLI with custom commands. Plugins are Python files placed in `~/.local/xsoar-cli/plugins/` that are automatically discovered and loaded.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. **Create the plugins directory**:
|
|
8
|
+
```bash
|
|
9
|
+
mkdir -p ~/.local/xsoar-cli/plugins
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. **Create a plugin file** (`~/.local/xsoar-cli/plugins/hello_plugin.py`):
|
|
13
|
+
```python
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
class HelloPlugin(XSOARPlugin):
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "hello"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def version(self) -> str:
|
|
23
|
+
return "1.0.0"
|
|
24
|
+
|
|
25
|
+
def get_command(self) -> click.Command:
|
|
26
|
+
@click.command(help="Say hello")
|
|
27
|
+
@click.option("--name", default="World", help="Name to greet")
|
|
28
|
+
def hello(name: str):
|
|
29
|
+
click.echo(f"Hello, {name}!")
|
|
30
|
+
return hello
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. **Use your plugin**:
|
|
34
|
+
```bash
|
|
35
|
+
xsoar-cli hello --name "Alice"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Plugin Structure
|
|
39
|
+
|
|
40
|
+
A plugin must:
|
|
41
|
+
- Be a Python class that inherits from `XSOARPlugin`
|
|
42
|
+
- Implement the `name`, `version`, and `get_command()` methods
|
|
43
|
+
- The `get_command()` method must return a Click command
|
|
44
|
+
|
|
45
|
+
### Required Methods
|
|
46
|
+
|
|
47
|
+
- `name` - Unique identifier for your plugin
|
|
48
|
+
- `version` - Version string
|
|
49
|
+
- `get_command()` - Returns the Click command to register
|
|
50
|
+
|
|
51
|
+
### Optional Methods
|
|
52
|
+
|
|
53
|
+
- `description` - Plugin description
|
|
54
|
+
- `initialize()` - Called when plugin loads
|
|
55
|
+
- `cleanup()` - Called when plugin unloads
|
|
56
|
+
|
|
57
|
+
## Plugin Management
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# List all plugins
|
|
61
|
+
xsoar-cli plugins list
|
|
62
|
+
|
|
63
|
+
# Show plugin details
|
|
64
|
+
xsoar-cli plugins info hello
|
|
65
|
+
|
|
66
|
+
# Validate plugins
|
|
67
|
+
xsoar-cli plugins validate
|
|
68
|
+
|
|
69
|
+
# Reload a plugin after changes
|
|
70
|
+
xsoar-cli plugins reload hello
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Command Conflicts
|
|
74
|
+
|
|
75
|
+
Plugin commands cannot use the same names as core CLI commands:
|
|
76
|
+
- `case`, `config`, `graph`, `manifest`, `pack`, `playbook`, `plugins`
|
|
77
|
+
|
|
78
|
+
If conflicts occur, use a different command name or create a command group.
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- The `XSOARPlugin` class is automatically available in plugin files
|
|
83
|
+
- Plugin files are discovered automatically from `~/.local/xsoar-cli/plugins/`
|
|
84
|
+
- Plugins are loaded when the CLI starts
|
|
85
|
+
- No special imports are needed for `XSOARPlugin`
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XSOAR CLI Plugin System
|
|
3
|
+
|
|
4
|
+
This module provides the infrastructure for creating and loading plugins
|
|
5
|
+
for the xsoar-cli application. Plugins can extend the CLI with custom
|
|
6
|
+
commands and functionality.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class XSOARPlugin(ABC):
|
|
16
|
+
"""
|
|
17
|
+
Abstract base class for XSOAR CLI plugins.
|
|
18
|
+
|
|
19
|
+
All plugins should inherit from this class and implement the required methods.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
"""Return the plugin name."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def version(self) -> str:
|
|
30
|
+
"""Return the plugin version."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def description(self) -> Optional[str]:
|
|
34
|
+
"""Return an optional description of the plugin."""
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_command(self) -> click.Command:
|
|
39
|
+
"""
|
|
40
|
+
Return the Click command or command group that this plugin provides.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
click.Command: The command to be registered with the CLI
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def initialize(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize the plugin. Called once when the plugin is loaded.
|
|
49
|
+
Override this method if your plugin needs initialization.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def cleanup(self) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Cleanup plugin resources. Called when the application shuts down.
|
|
55
|
+
Override this method if your plugin needs cleanup.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PluginError(Exception):
|
|
60
|
+
"""Exception raised when there's an error with plugin loading or execution."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PluginLoadError(PluginError):
|
|
64
|
+
"""Exception raised when a plugin fails to load."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PluginRegistrationError(PluginError):
|
|
68
|
+
"""Exception raised when a plugin fails to register."""
|