xsoar-cli 0.0.3__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.
Potentially problematic release.
This version of xsoar-cli might be problematic. Click here for more details.
- xsoar_cli/__about__.py +4 -0
- xsoar_cli/__init__.py +1 -0
- xsoar_cli/case/README.md +31 -0
- xsoar_cli/case/__init__.py +0 -0
- xsoar_cli/case/commands.py +86 -0
- xsoar_cli/cli.py +41 -0
- xsoar_cli/config/README.md +12 -0
- xsoar_cli/config/__init__.py +0 -0
- xsoar_cli/config/commands.py +99 -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 +23 -0
- xsoar_cli/manifest/__init__.py +0 -0
- xsoar_cli/manifest/commands.py +200 -0
- xsoar_cli/pack/README.md +7 -0
- xsoar_cli/pack/__init__.py +0 -0
- xsoar_cli/pack/commands.py +55 -0
- xsoar_cli/playbook/README.md +19 -0
- xsoar_cli/playbook/__init__.py +0 -0
- xsoar_cli/playbook/commands.py +67 -0
- xsoar_cli/plugins/README.md +433 -0
- xsoar_cli/plugins/__init__.py +68 -0
- xsoar_cli/plugins/commands.py +296 -0
- xsoar_cli/plugins/manager.py +399 -0
- xsoar_cli/utilities.py +94 -0
- xsoar_cli-0.0.3.dist-info/METADATA +128 -0
- xsoar_cli-0.0.3.dist-info/RECORD +31 -0
- xsoar_cli-0.0.3.dist-info/WHEEL +4 -0
- xsoar_cli-0.0.3.dist-info/entry_points.txt +2 -0
- xsoar_cli-0.0.3.dist-info/licenses/LICENSE.txt +9 -0
xsoar_cli/__about__.py
ADDED
xsoar_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
xsoar_cli/case/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Case
|
|
2
|
+
|
|
3
|
+
**Various case/incident related commands**
|
|
4
|
+
|
|
5
|
+
## Clone
|
|
6
|
+
It may be useful to clone a case from the production environment to the dev environment. This will of course require a configuration file
|
|
7
|
+
with API keys for both environments. The syntax is `xsoar-cli case clone --dest TEXT --source TEXT CASENUMBER` where default environments for `dest` and `source` are "dev" and "prod", respectively.
|
|
8
|
+
|
|
9
|
+
#### Example invocation
|
|
10
|
+
```
|
|
11
|
+
xsoar-cli case clone --dest prod --source dev 312412 # explicitly using the default options
|
|
12
|
+
xsoar-cli case clone 312412 # shorthand for the same invocation as above
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Create
|
|
17
|
+
It may be useful to create a case in the dev environment. The syntax is `xsoar-cli case create [OPTIONS] [NAME] [DETAILS]` where NAME and
|
|
18
|
+
DETAILS are optional. If NAME and DETAILS are not filled in, then default values are used.
|
|
19
|
+
#### Example invocation
|
|
20
|
+
```
|
|
21
|
+
xsoar-cli case create 312412
|
|
22
|
+
xsoar-cli case create 312412 "This is the name/title of the case" "This is some descriptive text on what the case is about."
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Get
|
|
27
|
+
It may be useful to quickly fetch some case data. The syntax is `xsoar-cli case clone [OPTIONS] CASENUMBER` and the command outputs raw JSON indented with 4 whitespaces to make the resulting output at least somewhat readable.
|
|
28
|
+
#### Example invocation
|
|
29
|
+
```
|
|
30
|
+
xsoar-cli case get 312412
|
|
31
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from xsoar_cli.utilities import load_config, validate_environments
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from xsoar_client.xsoar_client import Client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group(help="Add new or modify existing XSOAR case")
|
|
13
|
+
def case() -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.argument("casenumber", type=int)
|
|
18
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
19
|
+
@click.command(help="Get basic information about a single case in XSOAR")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
@load_config
|
|
22
|
+
def get(ctx: click.Context, casenumber: int, environment: str) -> None:
|
|
23
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
24
|
+
response = xsoar_client.get_case(casenumber)
|
|
25
|
+
if response["total"] == 0 and not response["data"]:
|
|
26
|
+
click.echo(f"Cannot find case ID {casenumber}")
|
|
27
|
+
ctx.exit(1)
|
|
28
|
+
click.echo(json.dumps(response, indent=4))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.argument("casenumber", type=int)
|
|
32
|
+
@click.option("--source", default="prod", show_default=True, help="Source environment")
|
|
33
|
+
@click.option("--dest", default="dev", show_default=True, help="Destination environment")
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.pass_context
|
|
36
|
+
@load_config
|
|
37
|
+
def clone(ctx: click.Context, casenumber: int, source: str, dest: str) -> None:
|
|
38
|
+
"""Clones a case from source to destination environment."""
|
|
39
|
+
valid_envs = validate_environments(source, dest, ctx=ctx)
|
|
40
|
+
if not valid_envs:
|
|
41
|
+
click.echo(f"Error: cannot find environments {source} and/or {dest} in config")
|
|
42
|
+
ctx.exit(1)
|
|
43
|
+
xsoar_source_client: Client = ctx.obj["server_envs"][source]
|
|
44
|
+
results = xsoar_source_client.get_case(casenumber)
|
|
45
|
+
data = results["data"][0]
|
|
46
|
+
# Dbot mirror info is irrelevant. This will be added again if applicable by XSOAR after ticket creation in dev.
|
|
47
|
+
data.pop("dbotMirrorId")
|
|
48
|
+
data.pop("dbotMirrorInstance")
|
|
49
|
+
data.pop("dbotMirrorDirection")
|
|
50
|
+
data.pop("dbotDirtyFields")
|
|
51
|
+
data.pop("dbotCurrentDirtyFields")
|
|
52
|
+
data.pop("dbotMirrorTags")
|
|
53
|
+
data.pop("dbotMirrorLastSync")
|
|
54
|
+
data.pop("id")
|
|
55
|
+
data.pop("created")
|
|
56
|
+
data.pop("modified")
|
|
57
|
+
# Ensure that playbooks run immediately when the case is created
|
|
58
|
+
data["createInvestigation"] = True
|
|
59
|
+
|
|
60
|
+
xsoar_dest_client: Client = ctx.obj["server_envs"][dest]
|
|
61
|
+
case_data = xsoar_dest_client.create_case(data=data)
|
|
62
|
+
click.echo(json.dumps(case_data, indent=4))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
66
|
+
@click.argument("details", type=str, default="Placeholder case details")
|
|
67
|
+
@click.argument("name", type=str, default="Test case created from xsoar-cli")
|
|
68
|
+
@click.command(help="Creates a single new case in XSOAR")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
@load_config
|
|
71
|
+
def create(ctx: click.Context, environment: str, name: str, details: str) -> None:
|
|
72
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
73
|
+
data = {
|
|
74
|
+
"createInvestigation": True,
|
|
75
|
+
"name": name,
|
|
76
|
+
"type": "MyCaseType", # grab this from config maybe?
|
|
77
|
+
"details": details,
|
|
78
|
+
}
|
|
79
|
+
case_data = xsoar_client.create_case(data=data)
|
|
80
|
+
case_id = case_data["id"]
|
|
81
|
+
click.echo(f"Created XSOAR case {case_id}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
case.add_command(get)
|
|
85
|
+
case.add_command(clone)
|
|
86
|
+
case.add_command(create)
|
xsoar_cli/cli.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .__about__ import __version__
|
|
6
|
+
from .case import commands as case_commands
|
|
7
|
+
from .config import commands as config_commands
|
|
8
|
+
from .graph import commands as graph_commands
|
|
9
|
+
from .manifest import commands as manifest_commands
|
|
10
|
+
from .pack import commands as pack_commands
|
|
11
|
+
from .playbook import commands as playbook_commands
|
|
12
|
+
from .plugins import commands as plugin_commands
|
|
13
|
+
from .plugins.manager import PluginManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.pass_context
|
|
18
|
+
@click.version_option(__version__)
|
|
19
|
+
@click.option("--debug", is_flag=True, help="Enable debug logging")
|
|
20
|
+
def cli(ctx: click.Context, debug: bool) -> None:
|
|
21
|
+
"""XSOAR CLI - Command line interface for XSOAR operations."""
|
|
22
|
+
if debug:
|
|
23
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
cli.add_command(config_commands.config)
|
|
27
|
+
cli.add_command(case_commands.case)
|
|
28
|
+
cli.add_command(pack_commands.pack)
|
|
29
|
+
cli.add_command(manifest_commands.manifest)
|
|
30
|
+
cli.add_command(playbook_commands.playbook)
|
|
31
|
+
cli.add_command(graph_commands.graph)
|
|
32
|
+
cli.add_command(plugin_commands.plugins)
|
|
33
|
+
|
|
34
|
+
# Load and register plugins after all core commands are added
|
|
35
|
+
plugin_manager = PluginManager()
|
|
36
|
+
try:
|
|
37
|
+
plugin_manager.load_all_plugins(ignore_errors=True)
|
|
38
|
+
plugin_manager.register_plugin_commands(cli)
|
|
39
|
+
except Exception:
|
|
40
|
+
# Silently ignore plugin loading errors to not break the CLI
|
|
41
|
+
pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Config
|
|
2
|
+
At the moment, only a single config command is implemented
|
|
3
|
+
|
|
4
|
+
## Create
|
|
5
|
+
Creates a new configuration file in `~/.config/xsoar-cli/config.json` based on a template. If the file already exists, then the user is prompted to overwrite
|
|
6
|
+
the existing file.
|
|
7
|
+
|
|
8
|
+
## Get
|
|
9
|
+
Reads the current configuration file and prints it out as JSON indented with 4 whitespaces. API keys are masked from output.
|
|
10
|
+
|
|
11
|
+
## Validate
|
|
12
|
+
Ensures that the configuration file is valid parsable JSON and that each server environment defined is reachable.
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from xsoar_client.xsoar_client import Client
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
|
|
11
|
+
from xsoar_cli.utilities import get_config_file_contents, get_config_file_path, get_config_file_template_contents, load_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(help="Create/validate etc")
|
|
15
|
+
def config() -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command()
|
|
20
|
+
@click.option("--unmask", "masked", is_flag=True, default=False)
|
|
21
|
+
@click.pass_context
|
|
22
|
+
@load_config
|
|
23
|
+
def show(ctx: click.Context, masked: bool) -> None:
|
|
24
|
+
"""Prints out current config. API keys are masked."""
|
|
25
|
+
config_file = get_config_file_path()
|
|
26
|
+
config = get_config_file_contents(config_file)
|
|
27
|
+
if not masked:
|
|
28
|
+
for key in config["server_config"]:
|
|
29
|
+
config["server_config"][key]["api_token"] = "MASKED" # noqa: S105
|
|
30
|
+
print(json.dumps(config, indent=4))
|
|
31
|
+
ctx.exit()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.option("--only-test-environment", default=None, show_default=True, help="Environment as defined in config file")
|
|
36
|
+
@click.pass_context
|
|
37
|
+
@load_config
|
|
38
|
+
def validate(ctx: click.Context, only_test_environment: str) -> None:
|
|
39
|
+
"""Validates that the configuration file is JSON and tests connectivity for each XSOAR Client environment defined."""
|
|
40
|
+
return_code = 0
|
|
41
|
+
for server_env in ctx.obj["server_envs"]:
|
|
42
|
+
if only_test_environment and server_env != only_test_environment:
|
|
43
|
+
# Ignore environment if --only-test-environment option is given and environment does not match
|
|
44
|
+
# what the user specified in option
|
|
45
|
+
continue
|
|
46
|
+
click.echo(f'Testing "{server_env}" environment...', nl=False)
|
|
47
|
+
xsoar_client: Client = ctx.obj["server_envs"][server_env]
|
|
48
|
+
try:
|
|
49
|
+
xsoar_client.test_connectivity()
|
|
50
|
+
except ConnectionError as ex:
|
|
51
|
+
raise RuntimeError from ex
|
|
52
|
+
click.echo("FAILED")
|
|
53
|
+
return_code = 1
|
|
54
|
+
continue
|
|
55
|
+
click.echo("OK")
|
|
56
|
+
ctx.exit(return_code)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@click.command()
|
|
60
|
+
def create() -> None:
|
|
61
|
+
"""Create a new configuration file based on a template."""
|
|
62
|
+
# if confdir does not exist, create it
|
|
63
|
+
# if conf file does not exist, create it
|
|
64
|
+
config_file = get_config_file_path()
|
|
65
|
+
if config_file.is_file():
|
|
66
|
+
click.confirm(f"WARNING: {config_file} already exists. Overwrite?", abort=True)
|
|
67
|
+
config_file.parent.mkdir(exist_ok=True, parents=True)
|
|
68
|
+
config_data = get_config_file_template_contents()
|
|
69
|
+
config_file.write_text(json.dumps(config_data, indent=4))
|
|
70
|
+
click.echo(f"Wrote template configuration file {config_file}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
74
|
+
@click.option("--key_id", type=int, help="If set then server config for server_version will be set to 8.")
|
|
75
|
+
@click.argument("apitoken", type=str)
|
|
76
|
+
@click.command()
|
|
77
|
+
@click.pass_context
|
|
78
|
+
@load_config
|
|
79
|
+
def set_credentials(ctx: click.Context, environment: str, apitoken: str, key_id: int) -> None: # noqa: ARG001
|
|
80
|
+
"""Set individual credentials for an environment in the config file."""
|
|
81
|
+
config_file = get_config_file_path()
|
|
82
|
+
config_data = json.loads(config_file.read_text())
|
|
83
|
+
config_data["server_config"][environment]["api_token"] = apitoken
|
|
84
|
+
# If we are given an API key ID, then we know it's supposed to be used in XSOAR 8.
|
|
85
|
+
# Set or remove the xsiam_auth_id accordingly.
|
|
86
|
+
if key_id:
|
|
87
|
+
config_data["server_config"][environment]["xsiam_auth_id"] = key_id
|
|
88
|
+
config_data["server_config"][environment]["server_version"] = 8
|
|
89
|
+
else:
|
|
90
|
+
config_data["server_config"][environment]["server_version"] = 6
|
|
91
|
+
with contextlib.suppress(KeyError):
|
|
92
|
+
config_data["server_config"][environment].pop("xsiam_auth_id")
|
|
93
|
+
config_file.write_text(json.dumps(config_data, indent=4))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
config.add_command(create)
|
|
97
|
+
config.add_command(validate)
|
|
98
|
+
config.add_command(show)
|
|
99
|
+
config.add_command(set_credentials)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Graph
|
|
2
|
+
BETA command. Under active development.
|
|
3
|
+
|
|
4
|
+
## Generate
|
|
5
|
+
Creates a graph representation of the entire content repository and plots connected components. The syntax is `xsoar-cli graph generate [OPTIONS] [PACKS]...`
|
|
6
|
+
where the only required option is the path to the content repository. An optional PACKS argument can be specified with the paths of one or more content packs
|
|
7
|
+
you want to be plotted.
|
|
8
|
+
|
|
9
|
+
### Example invocations
|
|
10
|
+
```
|
|
11
|
+
# Plot the connected components from the graph of the entire content repository
|
|
12
|
+
xsoar-cli graph generate -rp ./content_repo
|
|
13
|
+
# Plot only the components found in MyFirstPack
|
|
14
|
+
xsoar-cli graph generate -rp ./content_repo ./content_repo/Packs/MyFirstPack
|
|
15
|
+
# Plot only connected components from MyFirstPack and MySecondPack
|
|
16
|
+
xsoar-cli graph generate -rp ./content_repo ./content_repo/Packs/MyFirstPack ./content_repo/Packs/MySecondPack
|
|
17
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from xsoar_dependency_graph.xsoar_dependency_graph import ContentGraph
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group(help="(BETA) Create dependency graphs from one or more content packs")
|
|
8
|
+
def graph() -> None:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.option("-rp", "--repo-path", required=True, type=click.Path(exists=True), help="Path to content repository")
|
|
13
|
+
@click.argument("packs", nargs=-1, required=False, type=click.Path(exists=True))
|
|
14
|
+
@click.command()
|
|
15
|
+
def generate(packs: tuple[Path], repo_path: Path) -> None:
|
|
16
|
+
"""BETA
|
|
17
|
+
|
|
18
|
+
Generates a XSOAR dependency graph for one or more content packs. If no packs are defined in the [PACKS] argument,
|
|
19
|
+
a dependency graph is created for all content packs in the content repository.
|
|
20
|
+
|
|
21
|
+
Usage examples:
|
|
22
|
+
|
|
23
|
+
xsoar-cli graph generate -rp . Packs/Pack_one Packs/Pack_two
|
|
24
|
+
|
|
25
|
+
xsoar-cli graph generate -rp ."""
|
|
26
|
+
cg: ContentGraph = ContentGraph(repo_path=Path(repo_path))
|
|
27
|
+
packs_list = [Path(item) for item in packs]
|
|
28
|
+
cg.create_content_graph(pack_paths=packs_list)
|
|
29
|
+
cg.plot_connected_components()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
graph.add_command(generate)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Manifest
|
|
2
|
+
|
|
3
|
+
## Validate
|
|
4
|
+
Verifies that the xsoar_config.json manifest is valid JSON. Does a HTTP HEAD calls to every upstream content pack defined. Checks
|
|
5
|
+
custom content pack availability in AWS S3 artefact repository.
|
|
6
|
+
```
|
|
7
|
+
xsoar-cli manifest validate /my/full/path/to/xsoar_config.json
|
|
8
|
+
xsoar-cli manifest validate relative/path/to/xsoar_config.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Update
|
|
12
|
+
Updates and prompts the user to write the xsoar_config.json manifest with the latest available content packs. Both upstream and custom content packs are checked.
|
|
13
|
+
```
|
|
14
|
+
xsoar-cli manifest update /my/full/path/to/xsoar_config.json
|
|
15
|
+
xsoar-cli manifest update relative/path/to/xsoar_config.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Diff
|
|
19
|
+
Queries the XSOAR server for installed packages and compares them to what is defined in the manifest. Prints out the diff.
|
|
20
|
+
```
|
|
21
|
+
xsoar-cli manifest diff /my/full/path/to/xsoar_config.json
|
|
22
|
+
xsoar-cli manifest diff relative/path/to/xsoar_config.json
|
|
23
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,200 @@
|
|
|
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="dev", show_default=True, help="Environment as defined in config file")
|
|
43
|
+
@click.argument("manifest", type=str)
|
|
44
|
+
@click.command()
|
|
45
|
+
@click.pass_context
|
|
46
|
+
@load_config
|
|
47
|
+
def update(ctx: click.Context, environment: str, manifest: str) -> None:
|
|
48
|
+
"""Update manifest on disk with latest available content pack versions."""
|
|
49
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
50
|
+
manifest_data = load_manifest(manifest)
|
|
51
|
+
click.echo("Fetching outdated packs from XSOAR server. This may take a minute...", nl=False)
|
|
52
|
+
results = xsoar_client.get_outdated_packs()
|
|
53
|
+
click.echo("done.")
|
|
54
|
+
if not results:
|
|
55
|
+
click.echo("No packs eligible for upgrade.")
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
item1 = "Pack ID"
|
|
59
|
+
item2 = "Installed version"
|
|
60
|
+
item3 = "Latest available version"
|
|
61
|
+
header = f"{item1:50}{item2:20}{item3:20}"
|
|
62
|
+
click.echo(header)
|
|
63
|
+
for pack in results:
|
|
64
|
+
click.echo(f"{pack['id']:50}{pack['currentVersion']:20}{pack['latest']:20}")
|
|
65
|
+
click.echo(f"Total number of outdated content packs: {len(results)}")
|
|
66
|
+
|
|
67
|
+
for pack in results:
|
|
68
|
+
key = "custom_packs" if pack["author"] in ctx.obj["custom_pack_authors"] else "marketplace_packs"
|
|
69
|
+
index = next((i for i, item in enumerate(manifest_data[key]) if item["id"] == pack["id"]), None)
|
|
70
|
+
if index is None:
|
|
71
|
+
msg = f"Pack {pack['id']} not found in manifest."
|
|
72
|
+
click.echo(msg)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
comment = manifest_data[key][index].get("_comment", None)
|
|
75
|
+
if comment is not None:
|
|
76
|
+
print(f"WARNING: comment found in manifest for {pack['id']}: {comment}")
|
|
77
|
+
msg = f"Upgrade {pack['id']} from {pack['currentVersion']} to {pack['latest']}?"
|
|
78
|
+
should_upgrade = click.confirm(msg, default=True)
|
|
79
|
+
if should_upgrade:
|
|
80
|
+
manifest_data[key][index]["version"] = pack["latest"]
|
|
81
|
+
write_manifest(manifest, manifest_data)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
85
|
+
@click.argument("manifest", type=str)
|
|
86
|
+
@click.command()
|
|
87
|
+
@click.pass_context
|
|
88
|
+
@load_config
|
|
89
|
+
def validate(ctx: click.Context, environment: str, manifest: str) -> None:
|
|
90
|
+
"""Validate manifest JSON and all pack availability. Validates upstream pack availability by doing HTTP CONNECT.
|
|
91
|
+
Custom pack availability is implementation dependant."""
|
|
92
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
93
|
+
manifest_data = load_manifest(manifest)
|
|
94
|
+
click.echo("Manifest is valid JSON")
|
|
95
|
+
keys = ["custom_packs", "marketplace_packs"]
|
|
96
|
+
for key in keys:
|
|
97
|
+
custom = key == "custom_packs"
|
|
98
|
+
click.echo(f"Checking {key} availability ", nl=False)
|
|
99
|
+
for pack in manifest_data[key]:
|
|
100
|
+
if not xsoar_client.is_pack_available(pack_id=pack["id"], version=pack["version"], custom=custom):
|
|
101
|
+
# If we are in a merge request and the merge request contains a new pack as well
|
|
102
|
+
# as a updated xsoar_config.json manifest, then the pack is not available in S3
|
|
103
|
+
# before the MR is merged. Check if we can find the appropriate pack version locally
|
|
104
|
+
# If so, we can ignore this error because the pack will become available after merge.
|
|
105
|
+
manifest_arg = Path(manifest)
|
|
106
|
+
manifest_path = manifest_arg.resolve()
|
|
107
|
+
repo_path = manifest_path.parent
|
|
108
|
+
pack_metadata_path = Path(f"{repo_path}/Packs/{pack['id']}/pack_metadata.json")
|
|
109
|
+
with Path.open(pack_metadata_path, encoding="utf-8") as f:
|
|
110
|
+
pack_metadata = json.load(f)
|
|
111
|
+
|
|
112
|
+
if pack_metadata["currentVersion"] == pack["version"]:
|
|
113
|
+
continue
|
|
114
|
+
# The object key does not exist in AWS S3, and the relevant Pack locally does not have
|
|
115
|
+
# the requested version.
|
|
116
|
+
click.echo(f"\nFailed to reach pack {pack['id']} version {pack['version']}")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
click.echo(".", nl=False)
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
click.echo("Manifest is valid JSON and all packs are reachable.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
125
|
+
@click.argument("manifest", type=str)
|
|
126
|
+
@click.command()
|
|
127
|
+
@click.pass_context
|
|
128
|
+
@load_config
|
|
129
|
+
def diff(ctx: click.Context, manifest: str, environment: str) -> None:
|
|
130
|
+
"""Prints out the differences (if any) between what is defined in the xsoar_config.json manifest and what is actually
|
|
131
|
+
installed on the XSOAR server."""
|
|
132
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
133
|
+
manifest_data = load_manifest(manifest)
|
|
134
|
+
installed_packs = xsoar_client.get_installed_packs()
|
|
135
|
+
all_good = True
|
|
136
|
+
for key in manifest_data:
|
|
137
|
+
for pack in manifest_data[key]:
|
|
138
|
+
installed = next((item for item in installed_packs if item["id"] == pack["id"]), {})
|
|
139
|
+
if not installed:
|
|
140
|
+
click.echo(f"Pack {pack['id']} is not installed")
|
|
141
|
+
all_good = False
|
|
142
|
+
elif installed["currentVersion"] != pack["version"]:
|
|
143
|
+
msg = f"Manifest states {pack['id']} version {pack['version']} but version {installed['currentVersion']} is installed"
|
|
144
|
+
click.echo(msg)
|
|
145
|
+
all_good = False
|
|
146
|
+
if all_good:
|
|
147
|
+
click.echo("All packs up to date.")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
151
|
+
@click.option("--verbose", is_flag=True, default=False)
|
|
152
|
+
@click.option("--yes", is_flag=True, default=False)
|
|
153
|
+
@click.command()
|
|
154
|
+
@click.argument("manifest", type=str)
|
|
155
|
+
@click.pass_context
|
|
156
|
+
@load_config
|
|
157
|
+
def deploy(ctx: click.Context, environment: str, manifest: str, verbose: bool, yes: bool) -> None: # noqa: FBT001
|
|
158
|
+
"""
|
|
159
|
+
Deploys content packs to the XSOAR server as defined in the xsoar_config.json manifest.
|
|
160
|
+
The PATH argument expects the full or relative path to xsoar_config.json
|
|
161
|
+
|
|
162
|
+
\b
|
|
163
|
+
Prompts for confirmation prior to pack installation.
|
|
164
|
+
"""
|
|
165
|
+
should_continue = True
|
|
166
|
+
if not yes:
|
|
167
|
+
should_continue = click.confirm(
|
|
168
|
+
f"WARNING: this operation will attempt to deploy all packs defined in the manifest to XSOAR {environment} environment. Continue?",
|
|
169
|
+
)
|
|
170
|
+
if not should_continue:
|
|
171
|
+
ctx.exit()
|
|
172
|
+
|
|
173
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
174
|
+
manifest_data = load_manifest(manifest)
|
|
175
|
+
click.echo("Fetching installed packs...", err=True)
|
|
176
|
+
installed_packs = xsoar_client.get_installed_packs()
|
|
177
|
+
click.echo("done.")
|
|
178
|
+
none_installed = True
|
|
179
|
+
for key in manifest_data:
|
|
180
|
+
custom = key == "custom_packs"
|
|
181
|
+
for pack in manifest_data[key]:
|
|
182
|
+
installed = next((item for item in installed_packs if item["id"] == pack["id"]), {})
|
|
183
|
+
if not installed or installed["currentVersion"] != pack["version"]:
|
|
184
|
+
# Install pack
|
|
185
|
+
click.echo(f"Installing {pack['id']} version {pack['version']}...", nl=False)
|
|
186
|
+
xsoar_client.deploy_pack(pack_id=pack["id"], pack_version=pack["version"], custom=custom)
|
|
187
|
+
click.echo("OK.")
|
|
188
|
+
none_installed = False
|
|
189
|
+
elif verbose:
|
|
190
|
+
click.echo(f"Not installing {pack['id']} version {pack['version']}. Already installed.")
|
|
191
|
+
# Print message that install is skipped
|
|
192
|
+
|
|
193
|
+
if none_installed:
|
|
194
|
+
click.echo("No packs to install. XSOAR server is up to date with manifest.")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
manifest.add_command(deploy)
|
|
198
|
+
manifest.add_command(diff)
|
|
199
|
+
manifest.add_command(update)
|
|
200
|
+
manifest.add_command(validate)
|
xsoar_cli/pack/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
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="dev", show_default=True, help="Environment as defined 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, pack_id: str) -> None:
|
|
24
|
+
"""Deletes a content pack from the XSOAR server."""
|
|
25
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
26
|
+
if not xsoar_client.is_installed(pack_id=pack_id):
|
|
27
|
+
click.echo(f"Pack ID {pack_id} is not installed. Cannot delete.")
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
xsoar_client.delete(pack_id=pack_id)
|
|
30
|
+
click.echo(f"Deleted pack {pack_id} from XSOAR {environment}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.option("--environment", default="dev", show_default=True, help="Environment as defined in config file")
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.pass_context
|
|
36
|
+
@load_config
|
|
37
|
+
def get_outdated(ctx: click.Context, environment: str) -> None:
|
|
38
|
+
"""Prints out a list of outdated content packs."""
|
|
39
|
+
xsoar_client: Client = ctx.obj["server_envs"][environment]
|
|
40
|
+
click.echo("Fetching outdated packs. This may take a little while...", err=True)
|
|
41
|
+
outdated_packs = xsoar_client.get_outdated_packs()
|
|
42
|
+
if not outdated_packs:
|
|
43
|
+
click.echo("No outdated packs found")
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
id_header = "Pack ID"
|
|
46
|
+
installed_header = "Installed version"
|
|
47
|
+
latest_header = "Latest version"
|
|
48
|
+
click.echo(f"{id_header:<50}{installed_header:>17}{latest_header:>17}")
|
|
49
|
+
for pack in outdated_packs:
|
|
50
|
+
msg = f"{pack['id']:<50}{pack['currentVersion']:>17}{pack['latest']:>17}"
|
|
51
|
+
click.echo(msg)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
pack.add_command(delete)
|
|
55
|
+
pack.add_command(get_outdated)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Playbook
|
|
2
|
+
|
|
3
|
+
## Download
|
|
4
|
+
**This command requires a content repository-like directory structure in the current working directory**
|
|
5
|
+
It attempts to provide a smoother development experience when modifying playbooks by doing the following steps:
|
|
6
|
+
1. download the playbook. Attempts to detect the proper content pack and outputs the file to $(cwd)/Packs/PackID/Playbooks/<playbook name>.yml
|
|
7
|
+
2. run demisto-sdk format --assume-yes --no-validate --no-graph <playbook_name.yml>
|
|
8
|
+
3. re-attach the downloaded playbook in XSOAR
|
|
9
|
+
Whitespace characters in Pack ID and Playbook name will be converted to underscores "_".
|
|
10
|
+
|
|
11
|
+
### Current limitations
|
|
12
|
+
- This command only supports downloading playbooks which are already part of a content pack. Proper implementation on completely new
|
|
13
|
+
playbooks is yet to be implemented.
|
|
14
|
+
- Attempting to download a non-existing playbook will result in a 500 Internal Server Error
|
|
15
|
+
|
|
16
|
+
#### Example invocation:
|
|
17
|
+
```
|
|
18
|
+
xsoar-cli playbook download "my awesome playbook.yml"
|
|
19
|
+
```
|
|
File without changes
|