From 666de1417f8a5ab0b69590b8afd09a3d98b6039e Mon Sep 17 00:00:00 2001 From: Nasreddine Bencherchali Date: Tue, 11 Feb 2025 19:04:44 +0100 Subject: [PATCH 1/5] Update director.py --- contentctl/input/director.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/contentctl/input/director.py b/contentctl/input/director.py index 1b637101..a97848c0 100644 --- a/contentctl/input/director.py +++ b/contentctl/input/director.py @@ -258,15 +258,22 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: print("Done!") if len(validation_errors) > 0: - errors_string = "\n\n".join( - [ - f"File: {e_tuple[0]}\nError: {str(e_tuple[1])}" - for e_tuple in validation_errors - ] - ) + #errors_string = "\n\n".join( + # [ + # f"File: {e_tuple[0]}\nError: {str(e_tuple[1])}" + # for e_tuple in validation_errors + # ] + #) + + for entry in validation_errors: + print(f"File: {entry[0]}\n") + for error in entry[1].errors(): + error_type, msg = error["msg"].split(", ") + print(f"\t{error_type.upper()}: {msg}\n") + # print(f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED") # We quit after validation a single type/group of content because it can cause significant cascading errors in subsequent # types of content (since they may import or otherwise use it) - raise Exception( - f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED" - ) + #raise Exception( + # f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED" + #) From c7733c17562037488e084a4f5773efa1c33e0dd3 Mon Sep 17 00:00:00 2001 From: Michael Haag <5632822+MHaggis@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:43:36 -0700 Subject: [PATCH 2/5] first emoji pass --- contentctl/actions/validate.py | 58 +++++++------- contentctl/input/director.py | 137 +++++++++++++++++++++++---------- 2 files changed, 129 insertions(+), 66 deletions(-) diff --git a/contentctl/actions/validate.py b/contentctl/actions/validate.py index 2a16aa37..cecfbec1 100644 --- a/contentctl/actions/validate.py +++ b/contentctl/actions/validate.py @@ -1,41 +1,47 @@ import pathlib -from contentctl.input.director import Director, DirectorOutputDto -from contentctl.objects.config import validate from contentctl.enrichments.attack_enrichment import AttackEnrichment from contentctl.enrichments.cve_enrichment import CveEnrichment -from contentctl.objects.atomic import AtomicEnrichment -from contentctl.objects.lookup import FileBackedLookup +from contentctl.helper.splunk_app import SplunkApp from contentctl.helper.utils import Utils +from contentctl.input.director import (Director, DirectorOutputDto, + ValidationFailedError) +from contentctl.objects.atomic import AtomicEnrichment +from contentctl.objects.config import validate from contentctl.objects.data_source import DataSource -from contentctl.helper.splunk_app import SplunkApp +from contentctl.objects.lookup import FileBackedLookup class Validate: def execute(self, input_dto: validate) -> DirectorOutputDto: - director_output_dto = DirectorOutputDto( - AtomicEnrichment.getAtomicEnrichment(input_dto), - AttackEnrichment.getAttackEnrichment(input_dto), - CveEnrichment.getCveEnrichment(input_dto), - [], - [], - [], - [], - [], - [], - [], - [], - [], - [], - ) + try: + director_output_dto = DirectorOutputDto( + AtomicEnrichment.getAtomicEnrichment(input_dto), + AttackEnrichment.getAttackEnrichment(input_dto), + CveEnrichment.getCveEnrichment(input_dto), + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + ) - director = Director(director_output_dto) - director.execute(input_dto) - self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto) - if input_dto.data_source_TA_validation: - self.validate_latest_TA_information(director_output_dto.data_sources) + director = Director(director_output_dto) + director.execute(input_dto) + self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto) + if input_dto.data_source_TA_validation: + self.validate_latest_TA_information(director_output_dto.data_sources) - return director_output_dto + return director_output_dto + + except ValidationFailedError as e: + # Just re-raise without additional output since we already formatted everything + raise SystemExit(1) def ensure_no_orphaned_files_in_lookups( self, repo_path: pathlib.Path, director_output_dto: DirectorOutputDto diff --git a/contentctl/input/director.py b/contentctl/input/director.py index a97848c0..71e0e039 100644 --- a/contentctl/input/director.py +++ b/contentctl/input/director.py @@ -1,30 +1,30 @@ import os import sys -from pathlib import Path from dataclasses import dataclass, field -from pydantic import ValidationError +from pathlib import Path +from typing import Any, Dict 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 @@ -93,6 +93,27 @@ def addContentToDictMappings(self, content: SecurityContentObject): self.uuid_to_content_map[content.id] = content +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + END = '\033[0m' + MAGENTA = '\033[35m' + BRIGHT_MAGENTA = '\033[95m' + + +class ValidationFailedError(Exception): + """Custom exception for validation failures that already have formatted output""" + def __init__(self, message: str): + self.message = message + super().__init__(message) + + class Director: input_dto: validate output_dto: DirectorOutputDto @@ -113,9 +134,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))) @@ -255,25 +275,62 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: end="", flush=True, ) - print("Done!") if len(validation_errors) > 0: - #errors_string = "\n\n".join( - # [ - # f"File: {e_tuple[0]}\nError: {str(e_tuple[1])}" - # for e_tuple in validation_errors - # ] - #) - - for entry in validation_errors: - print(f"File: {entry[0]}\n") - for error in entry[1].errors(): - error_type, msg = error["msg"].split(", ") - print(f"\t{error_type.upper()}: {msg}\n") - - # print(f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED") - # We quit after validation a single type/group of content because it can cause significant cascading errors in subsequent - # types of content (since they may import or otherwise use it) - #raise Exception( - # f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED" - #) + print("\n") # Clean separation + print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}╔{'═' * 60}╗{Colors.END}") + print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}║{Colors.BLUE}{'🔍 Content Validation Summary':^60}{Colors.BRIGHT_MAGENTA}║{Colors.END}") + print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}╚{'═' * 60}╝{Colors.END}\n") + + print(f"{Colors.BOLD}{Colors.GREEN}✨ Validation Completed{Colors.END} – Issues detected in {Colors.RED}{Colors.BOLD}{len(validation_errors)}{Colors.END} files.\n") + + for index, entry in enumerate(validation_errors, 1): + file_path, error = entry + width = max(70, len(str(file_path)) + 15) + + # File header with numbered emoji + number_emoji = f"{index}️⃣" + print(f"{Colors.YELLOW}┏{'━' * width}┓{Colors.END}") + print(f"{Colors.YELLOW}┃{Colors.BOLD} {number_emoji} File: {Colors.CYAN}{file_path}{Colors.END}{' ' * (width - len(str(file_path)) - 12)}{Colors.YELLOW}┃{Colors.END}") + print(f"{Colors.YELLOW}┗{'━' * width}┛{Colors.END}") + + print(f" {Colors.RED}{Colors.BOLD}⚡ Validation Issues:{Colors.END}") + + if isinstance(error, ValidationError): + for err in error.errors(): + error_msg = err.get("msg", "") + if "https://errors.pydantic.dev" in error_msg: + continue + + # Clean error categorization + if "Field required" in error_msg: + print(f" {Colors.YELLOW}⚠️ Field Required: {err.get('loc', [''])[0]}{Colors.END}") + elif "Input should be" in error_msg: + print(f" {Colors.MAGENTA}🎯 Invalid Value for {err.get('loc', [''])[0]}{Colors.END}") + if "permitted values:" in error_msg: + options = error_msg.split("permitted values:")[-1].strip() + print(f" Valid options: {options}") + elif "Extra inputs" in error_msg: + print(f" {Colors.BLUE}❌ Unexpected Field: {err.get('loc', [''])[0]}{Colors.END}") + elif "Failed to find" in error_msg: + print(f" {Colors.RED}🔍 Missing Reference: {error_msg}{Colors.END}") + else: + print(f" {Colors.RED}❌ {error_msg}{Colors.END}") + else: + print(f" {Colors.RED}❌ {str(error)}{Colors.END}") + print("") + + # Clean footer with next steps + max_width = max(60, max(len(str(e[0])) + 15 for e in validation_errors)) + print(f"{Colors.BOLD}{Colors.CYAN}╔{'═' * max_width}╗{Colors.END}") + print(f"{Colors.BOLD}{Colors.CYAN}║{Colors.BLUE}{'🎯 Next Steps':^{max_width}}{Colors.CYAN}║{Colors.END}") + print(f"{Colors.BOLD}{Colors.CYAN}╚{'═' * max_width}╝{Colors.END}\n") + + print(f"{Colors.GREEN}🛠️ Fix the validation issues in the listed files{Colors.END}") + print(f"{Colors.YELLOW}📚 Check the documentation: {Colors.UNDERLINE}https://github.com/splunk/contentctl{Colors.END}") + print(f"{Colors.BLUE}💡 Use --verbose for detailed error information{Colors.END}\n") + + raise ValidationFailedError(f"Validation failed with {len(validation_errors)} error(s)") + + # Success case + print(f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]... {Colors.GREEN}✅ Done!{Colors.END}") From e589632db1c0960707debd32b81f836b592c38f8 Mon Sep 17 00:00:00 2001 From: Michael Haag <5632822+MHaggis@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:09:19 -0700 Subject: [PATCH 3/5] errors --- contentctl/actions/validate.py | 2 +- contentctl/input/director.py | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/contentctl/actions/validate.py b/contentctl/actions/validate.py index cecfbec1..fecde903 100644 --- a/contentctl/actions/validate.py +++ b/contentctl/actions/validate.py @@ -39,7 +39,7 @@ def execute(self, input_dto: validate) -> DirectorOutputDto: return director_output_dto - except ValidationFailedError as e: + except ValidationFailedError: # Just re-raise without additional output since we already formatted everything raise SystemExit(1) diff --git a/contentctl/input/director.py b/contentctl/input/director.py index 71e0e039..39120cff 100644 --- a/contentctl/input/director.py +++ b/contentctl/input/director.py @@ -106,6 +106,17 @@ class Colors: MAGENTA = '\033[35m' BRIGHT_MAGENTA = '\033[95m' + # Add fallback symbols for Windows + CHECK_MARK = '✓' if sys.platform != 'win32' else '*' + WARNING = '⚠️' if sys.platform != 'win32' else '!' + ERROR = '❌' if sys.platform != 'win32' else 'X' + ARROW = '🎯' if sys.platform != 'win32' else '>' + TOOLS = '🛠️' if sys.platform != 'win32' else '#' + DOCS = '📚' if sys.platform != 'win32' else '?' + BULB = '💡' if sys.platform != 'win32' else 'i' + SEARCH = '🔍' if sys.platform != 'win32' else '@' + ZAP = '⚡' if sys.platform != 'win32' else '!' + class ValidationFailedError(Exception): """Custom exception for validation failures that already have formatted output""" @@ -279,7 +290,7 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: if len(validation_errors) > 0: print("\n") # Clean separation print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}╔{'═' * 60}╗{Colors.END}") - print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}║{Colors.BLUE}{'🔍 Content Validation Summary':^60}{Colors.BRIGHT_MAGENTA}║{Colors.END}") + print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}║{Colors.BLUE}{f'{Colors.SEARCH} Content Validation Summary':^60}{Colors.BRIGHT_MAGENTA}║{Colors.END}") print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}╚{'═' * 60}╝{Colors.END}\n") print(f"{Colors.BOLD}{Colors.GREEN}✨ Validation Completed{Colors.END} – Issues detected in {Colors.RED}{Colors.BOLD}{len(validation_errors)}{Colors.END} files.\n") @@ -294,7 +305,7 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: print(f"{Colors.YELLOW}┃{Colors.BOLD} {number_emoji} File: {Colors.CYAN}{file_path}{Colors.END}{' ' * (width - len(str(file_path)) - 12)}{Colors.YELLOW}┃{Colors.END}") print(f"{Colors.YELLOW}┗{'━' * width}┛{Colors.END}") - print(f" {Colors.RED}{Colors.BOLD}⚡ Validation Issues:{Colors.END}") + print(f" {Colors.RED}{Colors.BOLD}{Colors.ZAP} Validation Issues:{Colors.END}") if isinstance(error, ValidationError): for err in error.errors(): @@ -304,9 +315,9 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: # Clean error categorization if "Field required" in error_msg: - print(f" {Colors.YELLOW}⚠️ Field Required: {err.get('loc', [''])[0]}{Colors.END}") + print(f" {Colors.YELLOW}{Colors.WARNING} Field Required: {err.get('loc', [''])[0]}{Colors.END}") elif "Input should be" in error_msg: - print(f" {Colors.MAGENTA}🎯 Invalid Value for {err.get('loc', [''])[0]}{Colors.END}") + print(f" {Colors.MAGENTA}{Colors.ARROW} Invalid Value for {err.get('loc', [''])[0]}{Colors.END}") if "permitted values:" in error_msg: options = error_msg.split("permitted values:")[-1].strip() print(f" Valid options: {options}") @@ -326,11 +337,11 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: print(f"{Colors.BOLD}{Colors.CYAN}║{Colors.BLUE}{'🎯 Next Steps':^{max_width}}{Colors.CYAN}║{Colors.END}") print(f"{Colors.BOLD}{Colors.CYAN}╚{'═' * max_width}╝{Colors.END}\n") - print(f"{Colors.GREEN}🛠️ Fix the validation issues in the listed files{Colors.END}") - print(f"{Colors.YELLOW}📚 Check the documentation: {Colors.UNDERLINE}https://github.com/splunk/contentctl{Colors.END}") - print(f"{Colors.BLUE}💡 Use --verbose for detailed error information{Colors.END}\n") + print(f"{Colors.GREEN}{Colors.TOOLS} Fix the validation issues in the listed files{Colors.END}") + print(f"{Colors.YELLOW}{Colors.DOCS} Check the documentation: {Colors.UNDERLINE}https://github.com/splunk/contentctl{Colors.END}") + print(f"{Colors.BLUE}{Colors.BULB} Use --verbose for detailed error information{Colors.END}\n") raise ValidationFailedError(f"Validation failed with {len(validation_errors)} error(s)") # Success case - print(f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]... {Colors.GREEN}✅ Done!{Colors.END}") + print(f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]... {Colors.GREEN}{Colors.CHECK_MARK} Done!{Colors.END}") From e9b7dfe43b073c89cb4fdc0c3c8605320fdac44f Mon Sep 17 00:00:00 2001 From: Michael Haag <5632822+MHaggis@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:23:39 -0700 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=92=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contentctl/input/director.py | 1 - 1 file changed, 1 deletion(-) diff --git a/contentctl/input/director.py b/contentctl/input/director.py index 39120cff..1eb926b1 100644 --- a/contentctl/input/director.py +++ b/contentctl/input/director.py @@ -2,7 +2,6 @@ import sys from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict from uuid import UUID from pydantic import ValidationError From 3388c69a9458ff56c8886120b119bae1ae9dfbcd Mon Sep 17 00:00:00 2001 From: Michael Haag <5632822+MHaggis@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:25:39 -0700 Subject: [PATCH 5/5] ruff --- contentctl/actions/validate.py | 9 +-- contentctl/input/director.py | 116 +++++++++++++++++++++------------ 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/contentctl/actions/validate.py b/contentctl/actions/validate.py index fecde903..620d8241 100644 --- a/contentctl/actions/validate.py +++ b/contentctl/actions/validate.py @@ -4,8 +4,7 @@ from contentctl.enrichments.cve_enrichment import CveEnrichment from contentctl.helper.splunk_app import SplunkApp from contentctl.helper.utils import Utils -from contentctl.input.director import (Director, DirectorOutputDto, - ValidationFailedError) +from contentctl.input.director import Director, DirectorOutputDto, ValidationFailedError from contentctl.objects.atomic import AtomicEnrichment from contentctl.objects.config import validate from contentctl.objects.data_source import DataSource @@ -33,12 +32,14 @@ def execute(self, input_dto: validate) -> DirectorOutputDto: director = Director(director_output_dto) director.execute(input_dto) - self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto) + self.ensure_no_orphaned_files_in_lookups( + input_dto.path, director_output_dto + ) if input_dto.data_source_TA_validation: self.validate_latest_TA_information(director_output_dto.data_sources) return director_output_dto - + except ValidationFailedError: # Just re-raise without additional output since we already formatted everything raise SystemExit(1) diff --git a/contentctl/input/director.py b/contentctl/input/director.py index 1eb926b1..ef362652 100644 --- a/contentctl/input/director.py +++ b/contentctl/input/director.py @@ -93,32 +93,33 @@ def addContentToDictMappings(self, content: SecurityContentObject): class Colors: - HEADER = '\033[95m' - BLUE = '\033[94m' - CYAN = '\033[96m' - GREEN = '\033[92m' - YELLOW = '\033[93m' - RED = '\033[91m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - END = '\033[0m' - MAGENTA = '\033[35m' - BRIGHT_MAGENTA = '\033[95m' + HEADER = "\033[95m" + BLUE = "\033[94m" + CYAN = "\033[96m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + END = "\033[0m" + MAGENTA = "\033[35m" + BRIGHT_MAGENTA = "\033[95m" # Add fallback symbols for Windows - CHECK_MARK = '✓' if sys.platform != 'win32' else '*' - WARNING = '⚠️' if sys.platform != 'win32' else '!' - ERROR = '❌' if sys.platform != 'win32' else 'X' - ARROW = '🎯' if sys.platform != 'win32' else '>' - TOOLS = '🛠️' if sys.platform != 'win32' else '#' - DOCS = '📚' if sys.platform != 'win32' else '?' - BULB = '💡' if sys.platform != 'win32' else 'i' - SEARCH = '🔍' if sys.platform != 'win32' else '@' - ZAP = '⚡' if sys.platform != 'win32' else '!' + CHECK_MARK = "✓" if sys.platform != "win32" else "*" + WARNING = "⚠️" if sys.platform != "win32" else "!" + ERROR = "❌" if sys.platform != "win32" else "X" + ARROW = "🎯" if sys.platform != "win32" else ">" + TOOLS = "🛠️" if sys.platform != "win32" else "#" + DOCS = "📚" if sys.platform != "win32" else "?" + BULB = "💡" if sys.platform != "win32" else "i" + SEARCH = "🔍" if sys.platform != "win32" else "@" + ZAP = "⚡" if sys.platform != "win32" else "!" class ValidationFailedError(Exception): - """Custom exception for validation failures that already have formatted output""" + """Custom exception for validation failures that already have formatted output.""" + def __init__(self, message: str): self.message = message super().__init__(message) @@ -144,8 +145,9 @@ 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))) @@ -289,41 +291,59 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: if len(validation_errors) > 0: print("\n") # Clean separation print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}╔{'═' * 60}╗{Colors.END}") - print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}║{Colors.BLUE}{f'{Colors.SEARCH} Content Validation Summary':^60}{Colors.BRIGHT_MAGENTA}║{Colors.END}") + print( + f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}║{Colors.BLUE}{f'{Colors.SEARCH} Content Validation Summary':^60}{Colors.BRIGHT_MAGENTA}║{Colors.END}" + ) print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}╚{'═' * 60}╝{Colors.END}\n") - print(f"{Colors.BOLD}{Colors.GREEN}✨ Validation Completed{Colors.END} – Issues detected in {Colors.RED}{Colors.BOLD}{len(validation_errors)}{Colors.END} files.\n") + print( + f"{Colors.BOLD}{Colors.GREEN}✨ Validation Completed{Colors.END} – Issues detected in {Colors.RED}{Colors.BOLD}{len(validation_errors)}{Colors.END} files.\n" + ) for index, entry in enumerate(validation_errors, 1): file_path, error = entry width = max(70, len(str(file_path)) + 15) - + # File header with numbered emoji number_emoji = f"{index}️⃣" print(f"{Colors.YELLOW}┏{'━' * width}┓{Colors.END}") - print(f"{Colors.YELLOW}┃{Colors.BOLD} {number_emoji} File: {Colors.CYAN}{file_path}{Colors.END}{' ' * (width - len(str(file_path)) - 12)}{Colors.YELLOW}┃{Colors.END}") + print( + f"{Colors.YELLOW}┃{Colors.BOLD} {number_emoji} File: {Colors.CYAN}{file_path}{Colors.END}{' ' * (width - len(str(file_path)) - 12)}{Colors.YELLOW}┃{Colors.END}" + ) print(f"{Colors.YELLOW}┗{'━' * width}┛{Colors.END}") - - print(f" {Colors.RED}{Colors.BOLD}{Colors.ZAP} Validation Issues:{Colors.END}") + + print( + f" {Colors.RED}{Colors.BOLD}{Colors.ZAP} Validation Issues:{Colors.END}" + ) if isinstance(error, ValidationError): for err in error.errors(): error_msg = err.get("msg", "") if "https://errors.pydantic.dev" in error_msg: continue - + # Clean error categorization if "Field required" in error_msg: - print(f" {Colors.YELLOW}{Colors.WARNING} Field Required: {err.get('loc', [''])[0]}{Colors.END}") + print( + f" {Colors.YELLOW}{Colors.WARNING} Field Required: {err.get('loc', [''])[0]}{Colors.END}" + ) elif "Input should be" in error_msg: - print(f" {Colors.MAGENTA}{Colors.ARROW} Invalid Value for {err.get('loc', [''])[0]}{Colors.END}") + print( + f" {Colors.MAGENTA}{Colors.ARROW} Invalid Value for {err.get('loc', [''])[0]}{Colors.END}" + ) if "permitted values:" in error_msg: - options = error_msg.split("permitted values:")[-1].strip() + options = error_msg.split("permitted values:")[ + -1 + ].strip() print(f" Valid options: {options}") elif "Extra inputs" in error_msg: - print(f" {Colors.BLUE}❌ Unexpected Field: {err.get('loc', [''])[0]}{Colors.END}") + print( + f" {Colors.BLUE}❌ Unexpected Field: {err.get('loc', [''])[0]}{Colors.END}" + ) elif "Failed to find" in error_msg: - print(f" {Colors.RED}🔍 Missing Reference: {error_msg}{Colors.END}") + print( + f" {Colors.RED}🔍 Missing Reference: {error_msg}{Colors.END}" + ) else: print(f" {Colors.RED}❌ {error_msg}{Colors.END}") else: @@ -333,14 +353,26 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None: # Clean footer with next steps max_width = max(60, max(len(str(e[0])) + 15 for e in validation_errors)) print(f"{Colors.BOLD}{Colors.CYAN}╔{'═' * max_width}╗{Colors.END}") - print(f"{Colors.BOLD}{Colors.CYAN}║{Colors.BLUE}{'🎯 Next Steps':^{max_width}}{Colors.CYAN}║{Colors.END}") + print( + f"{Colors.BOLD}{Colors.CYAN}║{Colors.BLUE}{'🎯 Next Steps':^{max_width}}{Colors.CYAN}║{Colors.END}" + ) print(f"{Colors.BOLD}{Colors.CYAN}╚{'═' * max_width}╝{Colors.END}\n") - print(f"{Colors.GREEN}{Colors.TOOLS} Fix the validation issues in the listed files{Colors.END}") - print(f"{Colors.YELLOW}{Colors.DOCS} Check the documentation: {Colors.UNDERLINE}https://github.com/splunk/contentctl{Colors.END}") - print(f"{Colors.BLUE}{Colors.BULB} Use --verbose for detailed error information{Colors.END}\n") - - raise ValidationFailedError(f"Validation failed with {len(validation_errors)} error(s)") + print( + f"{Colors.GREEN}{Colors.TOOLS} Fix the validation issues in the listed files{Colors.END}" + ) + print( + f"{Colors.YELLOW}{Colors.DOCS} Check the documentation: {Colors.UNDERLINE}https://github.com/splunk/contentctl{Colors.END}" + ) + print( + f"{Colors.BLUE}{Colors.BULB} Use --verbose for detailed error information{Colors.END}\n" + ) + + raise ValidationFailedError( + f"Validation failed with {len(validation_errors)} error(s)" + ) # Success case - print(f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]... {Colors.GREEN}{Colors.CHECK_MARK} Done!{Colors.END}") + print( + f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]... {Colors.GREEN}{Colors.CHECK_MARK} Done!{Colors.END}" + )