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

Add section and sectionOrder validations. #4739

Merged
merged 19 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/4739.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changes:
- description: This PR adds to the ST-110 validation which will now validate the sectionOrder and individual sections.
talzich marked this conversation as resolved.
Show resolved Hide resolved
type: feature
pr_number: 4739
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ class StructureError(BaseStrictModel):
def __str__(self):
field_name = ",".join(more_itertools.always_iterable(self.field_name))
if self.error_type == "assertion_error":
error_message = f"The field {field_name} is not required, but should not be None if it exists"
error_message = (
self.error_message
or f"An assertion error occurred for field {field_name}"
)
elif self.error_type == "value_error.extra":
error_message = f"The field {field_name} is extra and {self.error_message}"
elif self.error_type == "value_error.missing":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def prevent_none(cls, value, field):
"breaking_changes_notes", # release-notes-config
}:
# The assertion is caught by pydantic and converted to a pydantic.ValidationError
assert value is not None, f"{value} may not be None"
assert (
value is not None
), f"The field {field.name} is not required, but should not be None if it exists"
return value


Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Any, List, Optional

from pydantic import Field
from pydantic import Field, conlist, validator

from demisto_sdk.commands.common.constants import (
TYPE_PYTHON2,
TYPE_PYTHON3,
MarketplaceVersions,
)
from demisto_sdk.commands.common.StrEnum import StrEnum
from demisto_sdk.commands.content_graph.strict_objects.base_strict_model import (
Argument,
BaseIntegrationScript,
Expand Down Expand Up @@ -40,7 +41,7 @@

class _Configuration(BaseStrictModel):
display: Optional[str] = None
section: Optional[str] = None
section: Optional[str]
talzich marked this conversation as resolved.
Show resolved Hide resolved
advanced: Optional[str] = None
default_value: Optional[Any] = Field(None, alias="defaultvalue")
name: str
Expand Down Expand Up @@ -137,13 +138,21 @@ class _CommonFieldsIntegration(BaseStrictModel):
)


class SectionOrderValues(StrEnum):
talzich marked this conversation as resolved.
Show resolved Hide resolved
CONNECT = "Connect"
COLLECT = "Collect"
OPTIMIZE = "Optimize"


class _StrictIntegration(BaseStrictModel):
common_fields: CommonFieldsIntegration = Field(..., alias="commonfields") # type:ignore[valid-type]
display: str
beta: Optional[bool] = None
category: str
section_order_pascal_case: Optional[List[str]] = Field(None, alias="sectionOrder")
section_order_lower_case: Optional[List[str]] = Field(None, alias="sectionorder")
section_order: Optional[conlist(SectionOrderValues, min_items=1, max_items=3)] = ( # type:ignore[valid-type]
Field(alias="sectionorder")
)
configurations: List[Configuration] = Field(..., alias="configuration") # type:ignore[valid-type]
image: Optional[str] = None
description: str
default_mapper_in: Optional[str] = Field(None, alias="defaultmapperin")
Expand All @@ -152,7 +161,6 @@ class _StrictIntegration(BaseStrictModel):
detailed_description: Optional[str] = Field(None, alias="detaileddescription")
auto_config_instance: Optional[bool] = Field(None, alias="autoconfiginstance")
support_level_header: MarketplaceVersions = Field(None, alias="supportlevelheader")
configuration: List[Configuration] # type:ignore[valid-type]
script: Script # type:ignore[valid-type]
hidden: Optional[bool] = None
videos: Optional[List[str]] = None
Expand All @@ -161,6 +169,41 @@ class _StrictIntegration(BaseStrictModel):
script_not_visible: Optional[bool] = Field(None, alias="scriptNotVisible")
hybrid: Optional[bool] = None

def __init__(self, **data):
"""
Initializes the _StrictIntegration object.
Using this custom init function to support two aliases for the section_order field.
"""
if "sectionOrder" in data and "sectionorder" not in data:
data["sectionorder"] = data.pop("sectionOrder")
elif "sectionOrder" in data and "sectionorder" in data:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we have sectionOrder and sectionorder in the same yml? Wouldn't we want to fail this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought we agreed on combining the two (Union) in our initial discussion with Shunim

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an example of such a case?

data["sectionorder"] = list(
set(data["section_order"]) | set(data["section_order_camel_case"])
)
super().__init__(**data)

@validator("configurations")
def validate_sections(cls, configurations, values):
"""
Validates each configuration object has a valid section clause.
A valid section clause is a section which is included in the list of the integration's section_order.
Even if the section is an allowed value (currently Collect, Connect or Optimize),it could be invalid if the
specific value is not present in section_order.
"""
section_order_field = values.get("section_order")
if not section_order_field:
return configurations
integration_sections = [
section_name.value for section_name in section_order_field
]
for config in configurations:
if not config.section:
return configurations
assert (
config.section in integration_sections
), f"section {config.section} of {config.display} is not present in section_order {integration_sections}"
return configurations


StrictIntegration = create_model(
model_name="StrictIntegration",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ fromversion: 5.0.0
category: Forensics & Malware Analysis
description: Analyze files using the malwr sandbox
detaileddescription: This integration uses the Marlwr sandbox API to submit, analyze and detonate files.
sectionorder:
- Connect
configuration:
- display: URL
name: server
defaultvalue: https://malwr.com
type: 0
required: true
section: Connect
- display: Credentials
name: credentials
defaultvalue: ""
type: 9
required: true
section: Connect
script:
type: python
subtype: python3
Expand Down
1 change: 1 addition & 0 deletions demisto_sdk/commands/validate/sdk_validation_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ select = [
"MR107",
"MR108",
"ST110",
"ST111",
"RN103",
"RN105",
"RN106",
Expand Down
164 changes: 164 additions & 0 deletions demisto_sdk/commands/validate/tests/ST_validators_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
from demisto_sdk.commands.validate.validators.ST_validators.ST110_is_valid_scheme import (
SchemaValidator,
)
from demisto_sdk.commands.validate.validators.ST_validators.ST111_no_exclusions_schema import (
StrictSchemaValidator,
)
from TestSuite.pack import Pack


Expand Down Expand Up @@ -240,3 +243,164 @@ def test_pack_parser_errors_check(pack: Pack):
"value is not a valid enumeration member; permitted: 'xsoar', 'partner', 'community', 'developer'",
} == error_messages
assert {"type_error.bool", "type_error.enum"} == error_types


def test_invalid_section_order(pack: Pack):
"""
Given:
- an integration which contains invalid section order
When:
- executing the IntegrationParser
Then:
- the integration is invalid and the correct error message is returned
"""
integration = pack.create_integration(yml=load_yaml("integration.yml"))
integration.yml.update({"sectionorder": ["Connect", "Run"]})

integration_parser = IntegrationParser(
Path(integration.path), list(MarketplaceVersions)
)

results = SchemaValidator().obtain_invalid_content_items([integration_parser])
assert len(results) == 1
talzich marked this conversation as resolved.
Show resolved Hide resolved
assert results[0].message == ("Structure error (type_error.enum) in field sectionorder,1 of integration_0.yml: "
"value is not a valid enumeration member; permitted: "
"'Connect', 'Collect', 'Optimize'")


def test_missing_section_order(pack: Pack):
"""
Given:
- an integration with a missing section order
When:
- executing the IntegrationParser
Then:
- the validation does not fail as it is only addressed in ST111
"""
integration = pack.create_integration(yml=load_yaml("integration.yml"))
integration.yml.delete_key("sectionorder")

integration_parser = IntegrationParser(
Path(integration.path), list(MarketplaceVersions)
)

results = SchemaValidator().obtain_invalid_content_items([integration_parser])
assert len(results) == 0


def test_invalid_section(pack: Pack):
"""
Given:
- an integration which contains invalid section clause in one of its configuration objects
When:
- executing the IntegrationParser
Then:
- the integration is invalid and the correct error message is returned
"""
integration = pack.create_integration(yml=load_yaml("integration.yml"))
curr_config = integration.yml.read_dict()["configuration"]
curr_config[0]["section"] = "Run"
integration.yml.update({"configuration": curr_config})

integration_parser = IntegrationParser(
Path(integration.path), list(MarketplaceVersions)
)

results = SchemaValidator().obtain_invalid_content_items([integration_parser])
assert len(results) == 1
talzich marked this conversation as resolved.
Show resolved Hide resolved
assert results[0].message == ("Structure error (assertion_error) in field configuration of integration_0.yml: "
"section Run of URL is not present in section_order ['Connect']")


def test_missing_section(pack: Pack):
"""
Given:
- an integration with a missing section clause in one of its configuration objects
When:
- executing the IntegrationParser
Then:
- the validation does not fail as it is only addressed in ST111
"""
integration = pack.create_integration(yml=load_yaml("integration.yml"))
curr_config = integration.yml.read_dict()["configuration"]
curr_config[0].pop("section")
integration.yml.update({"configuration": curr_config})

integration_parser = IntegrationParser(
Path(integration.path), list(MarketplaceVersions)
)

results = SchemaValidator().obtain_invalid_content_items([integration_parser])
assert len(results) == 0


class TestST111:
def test_invalid_section_order(self):
"""
Given:
- an integration which contains invalid section order
When:
- executing the IntegrationParser
Then:
- the validation does not fail as it is only addressed in ST110
"""
integration = create_integration_object(
paths=["sectionorder"], values=[["Connect", "Run"]]
)
results = StrictSchemaValidator().obtain_invalid_content_items([integration])

assert len(results) == 0

def test_missing_section_order(self):
"""
Given:
- an integration with a missing section order
When:
- executing the IntegrationParser
Then:
- the integration is invalid and the correct error message is returned
"""
integration = create_integration_object()
integration.data.pop("sectionorder")
results = StrictSchemaValidator().obtain_invalid_content_items([integration])

assert len(results) == 1
assert results[0].message == "Missing section order"

def test_invalid_section(self):
"""
Given:
- an integration which contains invalid section clause in one of its configuration objects
When:
- executing the IntegrationParser
Then:
- the validation does not fail as it is only addressed in ST110
"""
integration = create_integration_object()
curr_config = integration.data["configuration"]
curr_config[0]["section"] = "Run"
integration.data["configuration"] = curr_config

results = StrictSchemaValidator().obtain_invalid_content_items([integration])
assert len(results) == 0

def test_missing_section(self, pack: Pack):
"""
Given:
- an integration with a missing section clause in one of its configuration objects
When:
- executing the IntegrationParser
Then:
- the integration is invalid and the correct error message is returned
"""
integration = create_integration_object()
curr_config = integration.data["configuration"]
curr_config[0].pop("section")
integration.data["configuration"] = curr_config

results = StrictSchemaValidator().obtain_invalid_content_items([integration])
assert len(results) == 1
assert (
results[0].message
== f'Missing section for configuration {curr_config[0].get("name")}'
)
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@

class SchemaValidator(BaseValidator[ContentTypes]):
error_code = "ST110"
description = "Validate that the scheme's structure is valid."
description = (
"Validate that the scheme's structure is valid, while excluding certain fields."
)
rationale = "Maintain valid structure for content items."

def obtain_invalid_content_items(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Iterable

from demisto_sdk.commands.content_graph.objects.integration import Integration
from demisto_sdk.commands.validate.validators.base_validator import (
BaseValidator,
ValidationResult,
)

ContentTypes = Integration

ALLOWED_SECTIONS = [
"Connect",
"Collect",
"Optimize",
]


class StrictSchemaValidator(BaseValidator[ContentTypes]):
error_code = "ST111"
description = "Validate that the scheme's structure is valid, no fields excluded."
rationale = "Maintain valid structure for content items."

def obtain_invalid_content_items(
self,
content_items: Iterable[ContentTypes],
) -> list[ValidationResult]:
invalid_content_items = []
for content_item in content_items:
error_message = self.get_error_message(content_item)
if error_message:
talzich marked this conversation as resolved.
Show resolved Hide resolved
invalid_content_items.append(
ValidationResult(
validator=self,
message=error_message,
content_object=content_item,
)
)
return invalid_content_items

def get_error_message(self, content_item: ContentTypes) -> str:
talzich marked this conversation as resolved.
Show resolved Hide resolved
section_order = content_item.data.get("sectionorder") or content_item.data.get(
"sectionOrder"
)
if not section_order:
return "Missing section order"
configurations = content_item.data.get("configuration")
for configuration in configurations: # type:ignore[union-attr]
section = configuration.get("section")
if not section:
return f'Missing section for configuration {configuration.get("name")}'
return ""
Loading