diff --git a/contentctl/input/director.py b/contentctl/input/director.py index 1b637101..c7bca263 100644 --- a/contentctl/input/director.py +++ b/contentctl/input/director.py @@ -1,30 +1,29 @@ import os import sys -from pathlib import Path from dataclasses import dataclass, field -from pydantic import ValidationError +from pathlib import Path from uuid import UUID -from contentctl.input.yml_reader import YmlReader -from contentctl.objects.detection import Detection -from contentctl.objects.story import Story +from pydantic import ValidationError -from contentctl.objects.baseline import Baseline -from contentctl.objects.investigation import Investigation -from contentctl.objects.playbook import Playbook -from contentctl.objects.deployment import Deployment -from contentctl.objects.macro import Macro -from contentctl.objects.lookup import LookupAdapter, Lookup -from contentctl.objects.atomic import AtomicEnrichment -from contentctl.objects.security_content_object import SecurityContentObject -from contentctl.objects.data_source import DataSource -from contentctl.objects.dashboard import Dashboard from contentctl.enrichments.attack_enrichment import AttackEnrichment from contentctl.enrichments.cve_enrichment import CveEnrichment - +from contentctl.helper.utils import Utils +from contentctl.input.yml_reader import YmlReader +from contentctl.objects.atomic import AtomicEnrichment +from contentctl.objects.baseline import Baseline from contentctl.objects.config import validate +from contentctl.objects.dashboard import Dashboard +from contentctl.objects.data_source import DataSource +from contentctl.objects.deployment import Deployment +from contentctl.objects.detection import Detection from contentctl.objects.enums import SecurityContentType -from contentctl.helper.utils import Utils +from contentctl.objects.investigation import Investigation +from contentctl.objects.lookup import Lookup, LookupAdapter +from contentctl.objects.macro import Macro +from contentctl.objects.playbook import Playbook +from contentctl.objects.security_content_object import SecurityContentObject +from contentctl.objects.story import Story @dataclass @@ -113,9 +112,8 @@ def execute(self, input_dto: validate) -> None: self.createSecurityContent(SecurityContentType.detections) self.createSecurityContent(SecurityContentType.dashboards) - from contentctl.objects.abstract_security_content_objects.detection_abstract import ( - MISSING_SOURCES, - ) + from contentctl.objects.abstract_security_content_objects.detection_abstract import \ + MISSING_SOURCES if len(MISSING_SOURCES) > 0: missing_sources_string = "\n 🟡 ".join(sorted(list(MISSING_SOURCES))) diff --git a/contentctl/objects/abstract_security_content_objects/detection_abstract.py b/contentctl/objects/abstract_security_content_objects/detection_abstract.py index fc0505ce..7dc6732b 100644 --- a/contentctl/objects/abstract_security_content_objects/detection_abstract.py +++ b/contentctl/objects/abstract_security_content_objects/detection_abstract.py @@ -476,7 +476,7 @@ def serialize_model(self): "name": lookup.name, "description": lookup.description, "filename": lookup.filename.name, - "default_match": "true" if lookup.default_match else "false", + "default_match": lookup.default_match, "case_sensitive_match": "true" if lookup.case_sensitive_match else "false", diff --git a/contentctl/objects/lookup.py b/contentctl/objects/lookup.py index 029b2c9b..0cc8e845 100644 --- a/contentctl/objects/lookup.py +++ b/contentctl/objects/lookup.py @@ -6,9 +6,10 @@ import re from enum import StrEnum, auto from functools import cached_property -from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Self +from typing import TYPE_CHECKING, Annotated, Any, Literal, Self from pydantic import ( + BeforeValidator, Field, FilePath, NonNegativeInt, @@ -69,7 +70,19 @@ class Lookup_Type(StrEnum): # TODO (#220): Split Lookup into 2 classes class Lookup(SecurityContentObject, abc.ABC): - default_match: Optional[bool] = None + # We need to make sure that this is converted to a string because we widely + # use the string "False" in our lookup content. However, PyYAML reads this + # as a BOOL and this causes parsing to fail. As such, we will always + # convert this to a string if it is passed as a bool + default_match: Annotated[ + str, BeforeValidator(lambda dm: str(dm) if isinstance(dm, bool) else dm) + ] = Field( + default="", + description="This field is given a default value of ''" + "because it is the default value specified in the transforms.conf " + "docs. Giving it a type of str rather than str | None simplifies " + "the typing for the field.", + ) # Per the documentation for transforms.conf, EXACT should not be specified in this list, # so we include only WILDCARD and CIDR match_type: list[Annotated[str, Field(pattern=r"(^WILDCARD|CIDR)\(.+\)$")]] = Field( @@ -88,7 +101,7 @@ def serialize_model(self): # All fields custom to this model model = { - "default_match": "true" if self.default_match is True else "false", + "default_match": self.default_match, "match_type": self.match_type_to_conf_format, "min_matches": self.min_matches, "max_matches": self.max_matches, diff --git a/contentctl/output/templates/transforms.j2 b/contentctl/output/templates/transforms.j2 index f80f2569..8a485c90 100644 --- a/contentctl/output/templates/transforms.j2 +++ b/contentctl/output/templates/transforms.j2 @@ -7,8 +7,8 @@ filename = {{ lookup.app_filename.name }} collection = {{ lookup.collection }} external_type = kvstore {% endif %} -{% if lookup.default_match is defined and lookup.default_match != None %} -default_match = {{ lookup.default_match | lower }} +{% if lookup.default_match != '' %} +default_match = {{ lookup.default_match }} {% endif %} {% if lookup.case_sensitive_match is defined and lookup.case_sensitive_match != None %} case_sensitive_match = {{ lookup.case_sensitive_match | lower }}