Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
104e71c
Handle stopped containers in testing
linuxdaemon Aug 14, 2023
bb910f8
Update new content generator with new formats
linuxdaemon Aug 14, 2023
74874e2
Allow absent tests for experimental detections
linuxdaemon Aug 9, 2023
59888f1
Update detection_abstract.py
pyth0n1c Nov 28, 2023
9efa110
Merge pull request #36 from linuxdaemon/allow-no-tests
pyth0n1c Nov 28, 2023
6d9ffef
Merge pull request #44 from linuxdaemon/generator-update
pyth0n1c Nov 28, 2023
a15e39c
Merge pull request #42 from linuxdaemon/docker-failure
pyth0n1c Nov 28, 2023
350c992
Clean up messy object definitions in prep to update jinja2 templates
pyth0n1c May 14, 2024
e6db597
Pass in entire app Object to jinja2 conf
pyth0n1c May 14, 2024
1fcfe09
Merge branch 'main' into add_ui_dispatch_app
pyth0n1c May 14, 2024
133af6f
Add experimental support for
pyth0n1c May 17, 2024
3aa669b
Change to always set
pyth0n1c May 17, 2024
e1662ce
Merge branch 'main' into add_ui_dispatch_app
josehelps Jun 15, 2024
c6aa99d
Add fields as requested
pyth0n1c Jun 20, 2024
b56b808
bump version in contentctl.yml
pyth0n1c Jul 2, 2024
51e863d
Merge branch 'main' into dashboard_support.
pyth0n1c Jul 2, 2024
10d3355
Some improvements to make sure
pyth0n1c Jul 2, 2024
d9f92bf
Merge in latest changes.
pyth0n1c Jul 3, 2024
52973c2
remove newline
pyth0n1c Jul 3, 2024
70d518c
Merge pull request #86 from splunk/customer_prs_1
pyth0n1c Jul 3, 2024
704e0b3
Merge branch 'main' into improve_ssa_fr
pyth0n1c Jul 11, 2024
1ac4623
Merge branch 'main' into release_v4.2.0
pyth0n1c Jul 12, 2024
24276c1
Merge branch 'main' into improve_ssa_fr
pyth0n1c Jul 12, 2024
34a873a
Merge branch 'release_v4.2.0' into improve_ssa_fr
pyth0n1c Jul 12, 2024
1021111
Fix error on missing roles if
pyth0n1c Jul 15, 2024
eaf87f2
improve output of risk severity field.
pyth0n1c Jul 15, 2024
4fa2878
Merge pull request #190 from splunk/fix_imported_roles
pyth0n1c Jul 15, 2024
2fdd58b
Merge pull request #169 from splunk/improve_ssa_fr
pyth0n1c Jul 15, 2024
9547940
Improve annotated strings that were defined
pyth0n1c Jul 16, 2024
2f9bcf6
Remove ui_dispatch_app from conf
pyth0n1c Jul 16, 2024
aeab52d
Merge pull request #145 from splunk/add_ui_dispatch_app
pyth0n1c Jul 16, 2024
0a01e06
Update setuptools requirement from >=69.5.1,<71.0.0 to >=69.5.1,<72.0.0
dependabot[bot] Jul 18, 2024
a30a902
handling the case where there are no tests
yaleman Jul 19, 2024
858a050
handling the case where there are no tests
yaleman Jul 19, 2024
5200a82
typing fixes
yaleman Jul 19, 2024
952d21e
Merge pull request #196 from splunk/dependabot/pip/setuptools-gte-69.…
pyth0n1c Jul 23, 2024
419434f
Merge branch 'main' into handle-no-answer
pyth0n1c Jul 23, 2024
c178918
Merge branch 'release_v4.2.0' into handle-no-answer
pyth0n1c Jul 23, 2024
00188f1
Merge branch 'main' into release_v4.2.0
pyth0n1c Jul 24, 2024
741c3b7
Merge pull request #189 from yaleman/handle-no-answer
pyth0n1c Jul 24, 2024
6994564
Update setuptools requirement from >=69.5.1,<71.0.0 to >=69.5.1,<72.0.0
dependabot[bot] Jul 24, 2024
92d53e7
Merge pull request #202 from splunk/dependabot/pip/setuptools-gte-69.…
pyth0n1c Jul 25, 2024
cc7505e
Merge branch 'main' into release_v4.2.0
pyth0n1c Jul 25, 2024
9a54259
Merge branch 'main' into no-tests-fix
pyth0n1c Jul 25, 2024
a9b09e8
Update setuptools requirement from >=69.5.1,<71.0.0 to >=69.5.1,<72.0.0
dependabot[bot] Jul 26, 2024
f3bd4a3
Merge pull request #205 from splunk/dependabot/pip/setuptools-gte-69.…
pyth0n1c Jul 26, 2024
41fa79d
Merge branch 'main' into no-tests-fix
pyth0n1c Jul 26, 2024
eb0813f
Remove extra validator that was a duplicate of functionality in anoth…
pyth0n1c Jul 26, 2024
1c248c6
Merge branch 'main' into no-tests-fix
pyth0n1c Jul 26, 2024
6e5b16a
Merge pull request #198 from yaleman/no-tests-fix
pyth0n1c Jul 26, 2024
681786f
Merge pull request #207 from splunk/no-tests-fix
pyth0n1c Jul 26, 2024
dc6bc6d
Merge branch 'release_v4.2.0' into add_throttling
pyth0n1c Jul 26, 2024
b8b4f5b
Update setuptools requirement from >=69.5.1,<71.0.0 to >=69.5.1,<73.0.0
dependabot[bot] Jul 29, 2024
76a2b14
Improving validations on name
pyth0n1c Jul 29, 2024
ae5536e
Merge branch 'release_v4.2.0' into fix_name_length
pyth0n1c Jul 29, 2024
7f79553
Name length improvements.
pyth0n1c Jul 30, 2024
14abf0a
Merge branch 'release_v4.2.0' into dependabot/pip/setuptools-gte-69.5…
pyth0n1c Jul 30, 2024
e1c7f24
Merge pull request #209 from splunk/dependabot/pip/setuptools-gte-69.…
pyth0n1c Jul 30, 2024
50b302d
Fix issue with name length at scale
pyth0n1c Jul 31, 2024
3a08a72
improvements to how name is gotten for
pyth0n1c Jul 31, 2024
7573307
change AlertSuppression to Throttling
pyth0n1c Aug 1, 2024
9867df6
Merge branch 'release_v4.2.0' into add_throttling
pyth0n1c Aug 1, 2024
af5a453
Merge pull request #192 from splunk/add_throttling
pyth0n1c Aug 1, 2024
bf7bc84
Merge branch 'release_v4.2.0' into dashboard_support
pyth0n1c Aug 1, 2024
d0e0a29
split json for dashboard
pyth0n1c Aug 2, 2024
34aa0b1
make some tweaks to how the label is generated.
pyth0n1c Aug 6, 2024
6e33bf2
Merge pull request #147 from splunk/dashboard_support
pyth0n1c Aug 8, 2024
d410bc4
More cleanup of SecurityContentObject_Abstract
pyth0n1c Aug 9, 2024
4726310
resolve merge conflicts with latest from main
pyth0n1c Aug 9, 2024
c69010b
same tweak to default fields in deployment
pyth0n1c Aug 19, 2024
e91721b
Merge branch 'release_v4.2.0' into fix_name_length
pyth0n1c Aug 20, 2024
ec3a0a3
Merge branch 'release_v4.2.0' into variable_severity
pyth0n1c Aug 20, 2024
5c0e28a
Make the fix for baselines and reponse
pyth0n1c Aug 21, 2024
5adc1b3
Make sure that analyticstories_detections and
pyth0n1c Aug 21, 2024
67d9cdd
Fix variable name which was incorrect.
pyth0n1c Aug 21, 2024
f8cbe3f
Merge branch 'main' into release_v4.2.0
pyth0n1c Aug 29, 2024
da18ef7
Merge branch 'release_v4.2.0' into fix_name_length
pyth0n1c Aug 29, 2024
f85ccbd
Merge pull request #213 from splunk/fix_name_length
pyth0n1c Aug 30, 2024
1ed3494
Merge branch 'release_v4.2.0' into variable_severity
pyth0n1c Aug 30, 2024
307d8f1
removed dupliocate risk functionality in
pyth0n1c Aug 30, 2024
48e31d3
Merge pull request #191 from splunk/variable_severity
pyth0n1c Aug 30, 2024
0466d16
Merge branch 'main' into release_v4.2.0
pyth0n1c Sep 18, 2024
02e64df
cleanup capitalization
pyth0n1c Sep 18, 2024
35ad3c5
Fix minor print typo
pyth0n1c Sep 18, 2024
8e27224
remove finding report, which is an ssa thing, from the repo
pyth0n1c Sep 19, 2024
3605586
Merge branch 'release_v4.2.0' of https://github.com/splunk/contentctl…
pyth0n1c Sep 19, 2024
d66a9f6
Updates in response to PR review
pyth0n1c Sep 19, 2024
59ea4f0
change default dashbaord theme to light
pyth0n1c Sep 20, 2024
a92a9ed
Updates to get the names
pyth0n1c Sep 25, 2024
b488c22
Update enum for RiskSeverity to
pyth0n1c Sep 25, 2024
a7b86ba
update risk level reference
pyth0n1c Sep 26, 2024
0b911b6
bump to release version 4.4.0
pyth0n1c Sep 26, 2024
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
1 change: 1 addition & 0 deletions contentctl/actions/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.investigations, SecurityContentType.investigations))
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.lookups, SecurityContentType.lookups))
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.macros, SecurityContentType.macros))
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.dashboards, SecurityContentType.dashboards))
updated_conf_files.update(conf_output.writeAppConf())

#Ensure that the conf file we just generated/update is syntactically valid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,17 +269,25 @@ def configure_imported_roles(
):
indexes.append(self.sync_obj.replay_index)
indexes_encoded = ";".join(indexes)

try:
# Set which roles should be configured. For Enterprise Security/Integration Testing,
# we must add some extra foles.
if self.global_config.enable_integration_testing:
roles = imported_roles + enterprise_security_roles
else:
roles = imported_roles

self.get_conn().roles.post(
self.infrastructure.splunk_app_username,
imported_roles=imported_roles + enterprise_security_roles,
imported_roles=roles,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixes the following warning message when testing with Enterprise Security:
image

Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

srchIndexesAllowed=indexes_encoded,
srchIndexesDefault=self.sync_obj.replay_index,
)
return
except Exception as e:
self.pbar.write(
f"Enterprise Security Roles do not exist:'{enterprise_security_roles}: {str(e)}"
f"The following role(s) do not exist:'{enterprise_security_roles}: {str(e)}"
)

self.get_conn().roles.post(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ def get_docker_client(self):
def check_for_teardown(self):

try:
self.get_docker_client().containers.get(self.get_name())
container: docker.models.containers.Container = self.get_docker_client().containers.get(self.get_name())
except Exception as e:
if self.sync_obj.terminate is not True:
self.pbar.write(
f"Error: could not get container [{self.get_name()}]: {str(e)}"
)
self.sync_obj.terminate = True
else:
if container.status != 'running':
self.sync_obj.terminate = True
self.container = None

if self.sync_obj.terminate:
self.finish()
Expand Down
12 changes: 10 additions & 2 deletions contentctl/actions/new_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ class NewContent:

def buildDetection(self)->dict[str,Any]:
questions = NewContentQuestions.get_questions_detection()
answers = questionary.prompt(questions)
answers: dict[str,str] = questionary.prompt(
questions,
kbi_msg="User did not answer all of the prompt questions. Exiting...")
if not answers:
raise ValueError("User didn't answer one or more questions!")
answers.update(answers)
answers['name'] = answers['detection_name']
del answers['detection_name']
Expand Down Expand Up @@ -70,7 +74,11 @@ def buildDetection(self)->dict[str,Any]:

def buildStory(self)->dict[str,Any]:
questions = NewContentQuestions.get_questions_story()
answers = questionary.prompt(questions)
answers = questionary.prompt(
questions,
kbi_msg="User did not answer all of the prompt questions. Exiting...")
if not answers:
raise ValueError("User didn't answer one or more questions!")
answers['name'] = answers['story_name']
del answers['story_name']
answers['id'] = str(uuid.uuid4())
Expand Down
3 changes: 2 additions & 1 deletion contentctl/actions/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


class Validate:
def execute(self, input_dto: validate) -> DirectorOutputDto:
def execute(self, input_dto: validate) -> DirectorOutputDto:
director_output_dto = DirectorOutputDto(
AtomicEnrichment.getAtomicEnrichment(input_dto),
AttackEnrichment.getAttackEnrichment(input_dto),
Expand All @@ -26,6 +26,7 @@ def execute(self, input_dto: validate) -> DirectorOutputDto:
[],
[],
[],
[]
)

director = Director(director_output_dto)
Expand Down
26 changes: 14 additions & 12 deletions contentctl/input/director.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import os
import sys
import pathlib
from typing import Union
from pathlib import Path
from dataclasses import dataclass, field
from pydantic import ValidationError
from uuid import UUID
from contentctl.input.yml_reader import YmlReader


from contentctl.objects.detection import Detection
from contentctl.objects.story import Story

from contentctl.objects.enums import SecurityContentProduct
from contentctl.objects.baseline import Baseline
from contentctl.objects.investigation import Investigation
from contentctl.objects.playbook import Playbook
Expand All @@ -21,20 +18,15 @@
from contentctl.objects.atomic import AtomicEnrichment
from contentctl.objects.security_content_object import SecurityContentObject
from contentctl.objects.data_source import DataSource
from contentctl.objects.event_source import EventSource

from contentctl.objects.dashboard import Dashboard
from contentctl.enrichments.attack_enrichment import AttackEnrichment
from contentctl.enrichments.cve_enrichment import CveEnrichment

from contentctl.objects.config import validate
from contentctl.objects.enums import SecurityContentType

from contentctl.objects.enums import DetectionStatus
from contentctl.helper.utils import Utils




@dataclass
class DirectorOutputDto:
# Atomic Tests are first because parsing them
Expand All @@ -50,6 +42,8 @@ class DirectorOutputDto:
macros: list[Macro]
lookups: list[Lookup]
deployments: list[Deployment]
dashboards: list[Dashboard]

data_sources: list[DataSource]
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
Expand Down Expand Up @@ -88,6 +82,9 @@ def addContentToDictMappings(self, content: SecurityContentObject):
self.stories.append(content)
elif isinstance(content, Detection):
self.detections.append(content)
elif isinstance(content, Dashboard):
self.dashboards.append(content)

elif isinstance(content, DataSource):
self.data_sources.append(content)
else:
Expand Down Expand Up @@ -115,7 +112,7 @@ def execute(self, input_dto: validate) -> None:
self.createSecurityContent(SecurityContentType.data_sources)
self.createSecurityContent(SecurityContentType.playbooks)
self.createSecurityContent(SecurityContentType.detections)

self.createSecurityContent(SecurityContentType.dashboards)

from contentctl.objects.abstract_security_content_objects.detection_abstract import MISSING_SOURCES
if len(MISSING_SOURCES) > 0:
Expand All @@ -137,6 +134,7 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
SecurityContentType.playbooks,
SecurityContentType.detections,
SecurityContentType.data_sources,
SecurityContentType.dashboards
]:
files = Utils.get_all_yml_files_from_directory(
os.path.join(self.input_dto.path, str(contentType.name))
Expand All @@ -147,7 +145,7 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
else:
raise (Exception(f"Cannot createSecurityContent for unknown product {contentType}."))

validation_errors = []
validation_errors:list[tuple[Path,ValueError]] = []

already_ran = False
progress_percent = 0
Expand Down Expand Up @@ -189,6 +187,10 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
elif contentType == SecurityContentType.detections:
detection = Detection.model_validate(modelDict, context={"output_dto":self.output_dto, "app":self.input_dto.app})
self.output_dto.addContentToDictMappings(detection)

elif contentType == SecurityContentType.dashboards:
dashboard = Dashboard.model_validate(modelDict,context={"output_dto":self.output_dto})
self.output_dto.addContentToDictMappings(dashboard)

elif contentType == SecurityContentType.data_sources:
data_source = DataSource.model_validate(
Expand Down
7 changes: 5 additions & 2 deletions contentctl/input/new_content_questions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import Any


class NewContentQuestions:

@classmethod
def get_questions_detection(self) -> list:
def get_questions_detection(cls) -> list[dict[str,Any]]:
questions = [
{
"type": "text",
Expand Down Expand Up @@ -116,7 +119,7 @@ def get_questions_detection(self) -> list:
return questions

@classmethod
def get_questions_story(self) -> list:
def get_questions_story(cls)-> list[dict[str,Any]]:
questions = [
{
"type": "text",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
if TYPE_CHECKING:
from contentctl.input.director import DirectorOutputDto
from contentctl.objects.baseline import Baseline

from contentctl.objects.config import CustomApp

from contentctl.objects.security_content_object import SecurityContentObject
from contentctl.objects.enums import AnalyticsType
from contentctl.objects.enums import DataModel
Expand All @@ -36,10 +37,16 @@
from contentctl.objects.data_source import DataSource
from contentctl.objects.base_test_result import TestResultStatus

# from contentctl.objects.playbook import Playbook
from contentctl.objects.enums import ProvidingTechnology
from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
import datetime
from contentctl.objects.constants import (
ES_MAX_STANZA_LENGTH,
ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE,
CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE
)

MISSING_SOURCES: set[str] = set()

# Those AnalyticsTypes that we do not test via contentctl
Expand All @@ -51,8 +58,8 @@
# TODO (#266): disable the use_enum_values configuration
class Detection_Abstract(SecurityContentObject):
model_config = ConfigDict(use_enum_values=True)

# contentType: SecurityContentType = SecurityContentType.detections
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
#contentType: SecurityContentType = SecurityContentType.detections
type: AnalyticsType = Field(...)
status: DetectionStatus = Field(...)
data_source: list[str] = []
Expand All @@ -70,10 +77,30 @@ class Detection_Abstract(SecurityContentObject):
# https://github.com/pydantic/pydantic/issues/9101#issuecomment-2019032541
tests: List[Annotated[Union[UnitTest, IntegrationTest, ManualTest], Field(union_mode='left_to_right')]] = []
# A list of groups of tests, relying on the same data
test_groups: Union[list[TestGroup], None] = Field(None, validate_default=True)
test_groups: list[TestGroup] = []

data_source_objects: list[DataSource] = []

def get_conf_stanza_name(self, app:CustomApp)->str:
stanza_name = CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
self.check_conf_stanza_max_length(stanza_name)
return stanza_name


def get_action_dot_correlationsearch_dot_label(self, app:CustomApp, max_stanza_length:int=ES_MAX_STANZA_LENGTH)->str:
stanza_name = self.get_conf_stanza_name(app)
stanza_name_after_saving_in_es = ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(
security_domain_value = self.tags.security_domain.value,
search_name = stanza_name
)


if len(stanza_name_after_saving_in_es) > max_stanza_length:
raise ValueError(f"label may only be {max_stanza_length} characters to allow updating in-product, "
f"but stanza was actually {len(stanza_name_after_saving_in_es)} characters: '{stanza_name_after_saving_in_es}' ")

return stanza_name

@field_validator("search", mode="before")
@classmethod
def validate_presence_of_filter_macro(cls, value:str, info:ValidationInfo)->str:
Expand Down Expand Up @@ -518,7 +545,7 @@ def model_post_init(self, __context: Any) -> None:
self.data_source_objects = matched_data_sources

for story in self.tags.analytic_story:
story.detections.append(self)
story.detections.append(self)

self.cve_enrichment_func(__context)

Expand Down Expand Up @@ -653,6 +680,27 @@ def addTags_nist(self):
else:
self.tags.nist = [NistCategory.DE_AE]
return self


@model_validator(mode="after")
def ensureThrottlingFieldsExist(self):
'''
For throttling to work properly, the fields to throttle on MUST
exist in the search itself. If not, then we cannot apply the throttling
'''
if self.tags.throttling is None:
# No throttling configured for this detection
return self

missing_fields:list[str] = [field for field in self.tags.throttling.fields if field not in self.search]
if len(missing_fields) > 0:
raise ValueError(f"The following throttle fields were missing from the search: {missing_fields}")

else:
# All throttling fields present in search
return self



@model_validator(mode="after")
def ensureProperObservablesExist(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from contentctl.objects.deployment import Deployment
from contentctl.objects.security_content_object import SecurityContentObject
from contentctl.input.director import DirectorOutputDto
from contentctl.objects.config import CustomApp

from contentctl.objects.enums import AnalyticsType
from contentctl.objects.constants import CONTENTCTL_MAX_STANZA_LENGTH
import abc
import uuid
import datetime
Expand All @@ -31,14 +33,14 @@

# TODO (#266): disable the use_enum_values configuration
class SecurityContentObject_Abstract(BaseModel, abc.ABC):
model_config = ConfigDict(use_enum_values=True, validate_default=True)

name: str = Field(...)
author: str = Field("Content Author", max_length=255)
date: datetime.date = Field(datetime.date.today())
version: NonNegativeInt = 1
id: uuid.UUID = Field(default_factory=uuid.uuid4) # we set a default here until all content has a uuid
description: str = Field("Enter Description Here", max_length=10000)
model_config = ConfigDict(use_enum_values=True,validate_default=True)
name: str = Field(...,max_length=99)
author: str = Field(...,max_length=255)
date: datetime.date = Field(...)
version: NonNegativeInt = Field(...)
id: uuid.UUID = Field(...) #we set a default here until all content has a uuid
description: str = Field(...,max_length=10000)
file_path: Optional[FilePath] = None
references: Optional[List[HttpUrl]] = None

Expand All @@ -56,7 +58,13 @@ def serialize_model(self):
"description": self.description,
"references": [str(url) for url in self.references or []]
}



def check_conf_stanza_max_length(self, stanza_name:str, max_stanza_length:int=CONTENTCTL_MAX_STANZA_LENGTH) -> None:
if len(stanza_name) > max_stanza_length:
raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")

@staticmethod
def objectListToNameList(objects: list[SecurityContentObject]) -> list[str]:
return [object.getName() for object in objects]
Expand Down
Loading
Loading