Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor XC validations #4150

Merged
merged 6 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/4150.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changes:
- description: Added the `validate-xsoar-config` pre-commit hook, replacing `XC` validations.
type: feature
pr_number: 4150
6 changes: 6 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@
entry: validate-content-path
language: python
pass_filenames: true

- id: validate-xsoar-config
name: validate-xsoar-config
entry: validate-xsoar-config
language: python
pass_filenames: true
74 changes: 74 additions & 0 deletions demisto_sdk/commands/content_graph/objects/xsoar_conf_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import typing
from abc import ABC

from pydantic import BaseModel, Extra, Field


class StrictBaseModel(BaseModel, ABC):
class Config:
extra = Extra.forbid


class BasePack(StrictBaseModel, ABC):
id_: str = Field(..., alias="id", description="ID of the pack to install")


class CustomPack(BasePack):
url: str = Field(..., description="URL of the pack to install")


class MarketplacePack(BasePack):
version: str = Field(..., description="version of the pack to install")


class List(StrictBaseModel):
name: str = Field(..., description="Name of the list to configure")
value: str = Field(..., description="Value of the list to configure")


class Job(BaseModel): # Not strict, unlike the others
type_: str = Field(..., alias="type", description="Type of incident to be created")
name: str = Field(..., description="Name of the job to configure")
playbook_id: str = Field(
...,
alias="playbookId",
description="ID of the playbook to be configured in the job",
)
scheduled: bool = Field(..., description="Whether to configure as a scheduled job")
recurrent: bool = Field(..., description="Whether to configure as a recurrent job")
cron_view: bool = Field(
...,
alias="cronView",
description="Whether to configure the recurrent time as a cron string",
)
cron: str = Field(
...,
description="Cron string to represent the recurrence of the job",
)
start_date: str = Field(
...,
alias="startDate",
description="ISO format start datetime string (YYYY-mm-ddTHH:MM:SS.fffZ)",
)
end_date: str = Field(
...,
alias="endingDate",
description="ISO format end datetime string (YYYY-mm-ddTHH:MM:SS.fffZ)",
)
should_trigger_new: bool = Field(
...,
alias="shouldTriggerNew",
description="Whether to trigger new job instance when a previous job instance is still active",
)
close_previous_run: bool = Field(
...,
alias="closePrevRun",
description="Whether to cancel the previous job run when one is still active",
)


class XSOAR_Configuration(StrictBaseModel):
custom_packs: typing.List[CustomPack]
marketplace_packs: typing.List[MarketplacePack]
lists: typing.List[List]
jobs: typing.List[Job]
50 changes: 50 additions & 0 deletions demisto_sdk/scripts/validate_xsoar_config_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from pathlib import Path

import typer
from pydantic import ValidationError

from demisto_sdk.commands.common.handlers.xsoar_handler import JSONDecodeError
from demisto_sdk.commands.common.logger import logger, logging_setup
from demisto_sdk.commands.common.tools import get_file
from demisto_sdk.commands.content_graph.objects.xsoar_conf_file import (
XSOAR_Configuration,
)

FILE_NAME = "xsoar_config.json"


class NotAJSONError(Exception):
...


def _validate(path: Path = Path(FILE_NAME)) -> None:
if path.suffix != ".json":
raise NotAJSONError
XSOAR_Configuration.validate(get_file(path))


def validate(path: Path) -> None:
try:
_validate(path)
logger.info(f"[green]{path} is valid [/green]")

except FileNotFoundError:
logger.error(f"File {path} does not exist")
raise typer.Exit(1)

except NotAJSONError:
logger.error(f"Path {path} is not to a JSON file")
raise typer.Exit(1)

except JSONDecodeError:
logger.error(f"Could not parse JSON from {path}")
raise typer.Exit(1)

except ValidationError as e:
logger.error(f"{path} is not a valid XSOAR configuration file: {e}")
raise typer.Exit(1)


if __name__ == "__main__":
logging_setup()
typer.run(validate)
174 changes: 174 additions & 0 deletions demisto_sdk/tests/validate_xsoar_config_file_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Union

import pytest
from pydantic import ValidationError

from demisto_sdk.commands.common.files.text_file import TextFile
from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json
from demisto_sdk.scripts.validate_xsoar_config_file import (
FILE_NAME,
NotAJSONError,
_validate,
)


def _create_and_validate(
file_body: Union[str, dict],
file_name: str = FILE_NAME,
):
"""
Generates a json file and calls validate on the created file.
"""
body_to_write = file_body if isinstance(file_body, str) else json.dumps(file_body)

with TemporaryDirectory() as dir:
path = Path(dir, file_name)
TextFile.write(body_to_write, path)
_validate(path)


def test_valid():
"""
Given a dictionary equivalent to a valid configuration
When calling validate
Then make sure the validation passes
"""
_create_and_validate(
{
"custom_packs": [{"id": "id1", "url": "url1"}],
"marketplace_packs": [{"id": "id1", "version": "*"}],
"lists": [{"name": "List #1", "value": "Value #1"}],
"jobs": [
{
"type": "Unclassified",
"name": "name1",
"playbookId": "playbook1",
"scheduled": True,
"recurrent": True,
"cronView": True,
"cron": "0 10,15 * * *",
"startDate": "2021-01-07T15:10:04.000Z",
"endingDate": "2021-01-07T15:10:04.000Z",
"endingType": "never",
"timezoneOffset": -120,
"timezone": "Asia/Jerusalem",
"shouldTriggerNew": True,
"closePrevRun": True,
}
],
}
)


def test_extra_root_key():
"""
Given a dictionary equivalent to json with unexpected root key
When calling validate
Then make sure a ValidationError is raised
"""
with pytest.raises(ValidationError) as e:
_create_and_validate(
{
"SURPRISE": [],
"custom_packs": [],
"marketplace_packs": [],
"lists": [],
"jobs": [],
}
)
assert len(e.value.errors()) == 1
assert dict((e.value.errors())[0]) == {
"loc": ("SURPRISE",),
"msg": "extra fields not permitted",
"type": "value_error.extra",
}


@pytest.mark.parametrize("key", ("custom_packs", "marketplace_packs", "lists", "jobs"))
def test_missing_root_key(key: str):
"""
Given a dictionary equivalent to json with a missing root key
When calling validate
Then make sure the correct ValidationError is raised
"""
config: dict = {
"custom_packs": [],
"marketplace_packs": [],
"lists": [],
"jobs": [],
}
config.pop(key)
with pytest.raises(ValidationError) as e:
_create_and_validate(config)
assert len(e.value.errors()) == 1
(e.value.errors())[0]["loc"] == (key,)
(e.value.errors())[0]["msg"] == ("missing key")


@pytest.mark.parametrize(
"custom_packs_id_key, marketplace_pack_id_key, list_name_key",
[
("bad", "id", "name"),
("id", "bad", "name"),
("id", "id", "bad"),
],
)
def test_invalid_file_bad_keys(
custom_packs_id_key: str,
marketplace_pack_id_key: str,
list_name_key: str,
):
"""
Given:
Invalid configuration file which has a bad key in one of the sections.
When:
Validating the file schema.
Then:
Validates verification returns that the file is invalid.
"""
with pytest.raises(ValidationError) as e:
_create_and_validate(
{
"custom_packs": [{custom_packs_id_key: "id1", "url": "url1"}],
"marketplace_packs": [{marketplace_pack_id_key: "id1", "version": "*"}],
"lists": [{list_name_key: "List #1", "value": "Value #1"}],
"jobs": [
{
"type": "Unclassified",
"name": "name1",
"playbookId": "playbook1",
"scheduled": True,
"recurrent": True,
"cronView": True,
"cron": "0 10,15 * * *",
"startDate": "2021-01-07T15:10:04.000Z",
"endingDate": "2021-01-07T15:10:04.000Z",
"endingType": "never",
"timezoneOffset": -120,
"timezone": "Asia/Jerusalem",
"shouldTriggerNew": True,
"closePrevRun": True,
}
],
}
)
assert len(e.value.errors()) == 2
assert {error["type"] for error in e.value.errors()} == {
"value_error.missing",
"value_error.extra",
}


@pytest.mark.parametrize(
"file_name", ("xsoar_config", "xsoar_config.yaml", "xsoar_config.yml")
)
def test_invalid_type(file_name: str):
"""
Given a file that isn't a json
When calling validate
Then make sure NotAJSONError is raised
"""
with pytest.raises(NotAJSONError):
_create_and_validate("not a json", file_name)