Skip to content

Commit aceb70e

Browse files
authored
Merge pull request #370 from splunk/update-validate-output
Update validate output
2 parents d78668d + 3f5b031 commit aceb70e

File tree

2 files changed

+136
-23
lines changed

2 files changed

+136
-23
lines changed

contentctl/actions/validate.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from contentctl.enrichments.cve_enrichment import CveEnrichment
55
from contentctl.helper.splunk_app import SplunkApp
66
from contentctl.helper.utils import Utils
7-
from contentctl.input.director import Director, DirectorOutputDto
7+
from contentctl.input.director import Director, DirectorOutputDto, ValidationFailedError
88
from contentctl.objects.atomic import AtomicEnrichment
99
from contentctl.objects.config import validate
1010
from contentctl.objects.data_source import DataSource
@@ -13,19 +13,26 @@
1313

1414
class Validate:
1515
def execute(self, input_dto: validate) -> DirectorOutputDto:
16-
director_output_dto = DirectorOutputDto(
17-
AtomicEnrichment.getAtomicEnrichment(input_dto),
18-
AttackEnrichment.getAttackEnrichment(input_dto),
19-
CveEnrichment.getCveEnrichment(input_dto),
20-
)
16+
try:
17+
director_output_dto = DirectorOutputDto(
18+
AtomicEnrichment.getAtomicEnrichment(input_dto),
19+
AttackEnrichment.getAttackEnrichment(input_dto),
20+
CveEnrichment.getCveEnrichment(input_dto),
21+
)
22+
23+
director = Director(director_output_dto)
24+
director.execute(input_dto)
25+
self.ensure_no_orphaned_files_in_lookups(
26+
input_dto.path, director_output_dto
27+
)
28+
if input_dto.data_source_TA_validation:
29+
self.validate_latest_TA_information(director_output_dto.data_sources)
2130

22-
director = Director(director_output_dto)
23-
director.execute(input_dto)
24-
self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
25-
if input_dto.data_source_TA_validation:
26-
self.validate_latest_TA_information(director_output_dto.data_sources)
31+
return director_output_dto
2732

28-
return director_output_dto
33+
except ValidationFailedError:
34+
# Just re-raise without additional output since we already formatted everything
35+
raise SystemExit(1)
2936

3037
def ensure_no_orphaned_files_in_lookups(
3138
self, repo_path: pathlib.Path, director_output_dto: DirectorOutputDto

contentctl/input/director.py

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,39 @@ def addContentToDictMappings(self, content: SecurityContentObject):
109109
self.uuid_to_content_map[content.id] = content
110110

111111

112+
class Colors:
113+
HEADER = "\033[95m"
114+
BLUE = "\033[94m"
115+
CYAN = "\033[96m"
116+
GREEN = "\033[92m"
117+
YELLOW = "\033[93m"
118+
RED = "\033[91m"
119+
BOLD = "\033[1m"
120+
UNDERLINE = "\033[4m"
121+
END = "\033[0m"
122+
MAGENTA = "\033[35m"
123+
BRIGHT_MAGENTA = "\033[95m"
124+
125+
# Add fallback symbols for Windows
126+
CHECK_MARK = "✓" if sys.platform != "win32" else "*"
127+
WARNING = "⚠️" if sys.platform != "win32" else "!"
128+
ERROR = "❌" if sys.platform != "win32" else "X"
129+
ARROW = "🎯" if sys.platform != "win32" else ">"
130+
TOOLS = "🛠️" if sys.platform != "win32" else "#"
131+
DOCS = "📚" if sys.platform != "win32" else "?"
132+
BULB = "💡" if sys.platform != "win32" else "i"
133+
SEARCH = "🔍" if sys.platform != "win32" else "@"
134+
ZAP = "⚡" if sys.platform != "win32" else "!"
135+
136+
137+
class ValidationFailedError(Exception):
138+
"""Custom exception for validation failures that already have formatted output."""
139+
140+
def __init__(self, message: str):
141+
self.message = message
142+
super().__init__(message)
143+
144+
112145
class Director:
113146
input_dto: validate
114147
output_dto: DirectorOutputDto
@@ -268,18 +301,91 @@ def createSecurityContent(
268301
end="",
269302
flush=True,
270303
)
271-
print("Done!")
272304

273305
if len(validation_errors) > 0:
274-
errors_string = "\n\n".join(
275-
[
276-
f"File: {e_tuple[0]}\nError: {str(e_tuple[1])}"
277-
for e_tuple in validation_errors
278-
]
306+
print("\n") # Clean separation
307+
print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}{'═' * 60}{Colors.END}")
308+
print(
309+
f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}{Colors.BLUE}{f'{Colors.SEARCH} Content Validation Summary':^60}{Colors.BRIGHT_MAGENTA}{Colors.END}"
310+
)
311+
print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}{'═' * 60}{Colors.END}\n")
312+
313+
print(
314+
f"{Colors.BOLD}{Colors.GREEN}✨ Validation Completed{Colors.END} – Issues detected in {Colors.RED}{Colors.BOLD}{len(validation_errors)}{Colors.END} files.\n"
315+
)
316+
317+
for index, entry in enumerate(validation_errors, 1):
318+
file_path, error = entry
319+
width = max(70, len(str(file_path)) + 15)
320+
321+
# File header with numbered emoji
322+
number_emoji = f"{index}️⃣"
323+
print(f"{Colors.YELLOW}{'━' * width}{Colors.END}")
324+
print(
325+
f"{Colors.YELLOW}{Colors.BOLD} {number_emoji} File: {Colors.CYAN}{file_path}{Colors.END}{' ' * (width - len(str(file_path)) - 12)}{Colors.YELLOW}{Colors.END}"
326+
)
327+
print(f"{Colors.YELLOW}{'━' * width}{Colors.END}")
328+
329+
print(
330+
f" {Colors.RED}{Colors.BOLD}{Colors.ZAP} Validation Issues:{Colors.END}"
331+
)
332+
333+
if isinstance(error, ValidationError):
334+
for err in error.errors():
335+
error_msg = err.get("msg", "")
336+
if "https://errors.pydantic.dev" in error_msg:
337+
continue
338+
339+
# Clean error categorization
340+
if "Field required" in error_msg:
341+
print(
342+
f" {Colors.YELLOW}{Colors.WARNING} Field Required: {err.get('loc', [''])[0]}{Colors.END}"
343+
)
344+
elif "Input should be" in error_msg:
345+
print(
346+
f" {Colors.MAGENTA}{Colors.ARROW} Invalid Value for {err.get('loc', [''])[0]}{Colors.END}"
347+
)
348+
if err.get("ctx", {}).get("expected", None) is not None:
349+
print(
350+
f" Valid options: {err.get('ctx', {}).get('expected', None)}"
351+
)
352+
elif "Extra inputs" in error_msg:
353+
print(
354+
f" {Colors.BLUE}❌ Unexpected Field: {err.get('loc', [''])[0]}{Colors.END}"
355+
)
356+
elif "Failed to find" in error_msg:
357+
print(
358+
f" {Colors.RED}🔍 Missing Reference: {error_msg}{Colors.END}"
359+
)
360+
else:
361+
print(f" {Colors.RED}{error_msg}{Colors.END}")
362+
else:
363+
print(f" {Colors.RED}{str(error)}{Colors.END}")
364+
print("")
365+
366+
# Clean footer with next steps
367+
max_width = max(60, max(len(str(e[0])) + 15 for e in validation_errors))
368+
print(f"{Colors.BOLD}{Colors.CYAN}{'═' * max_width}{Colors.END}")
369+
print(
370+
f"{Colors.BOLD}{Colors.CYAN}{Colors.BLUE}{'🎯 Next Steps':^{max_width}}{Colors.CYAN}{Colors.END}"
371+
)
372+
print(f"{Colors.BOLD}{Colors.CYAN}{'═' * max_width}{Colors.END}\n")
373+
374+
print(
375+
f"{Colors.GREEN}{Colors.TOOLS} Fix the validation issues in the listed files{Colors.END}"
376+
)
377+
print(
378+
f"{Colors.YELLOW}{Colors.DOCS} Check the documentation: {Colors.UNDERLINE}https://github.com/splunk/contentctl{Colors.END}"
279379
)
280-
# print(f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED")
281-
# We quit after validation a single type/group of content because it can cause significant cascading errors in subsequent
282-
# types of content (since they may import or otherwise use it)
283-
raise Exception(
284-
f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED"
380+
print(
381+
f"{Colors.BLUE}{Colors.BULB} Use --verbose for detailed error information{Colors.END}\n"
285382
)
383+
384+
raise ValidationFailedError(
385+
f"Validation failed with {len(validation_errors)} error(s)"
386+
)
387+
388+
# Success case
389+
print(
390+
f"\r{f'{contentCartegoryName} Progress'.rjust(23)}: [{progress_percent:3.0f}%]... {Colors.GREEN}{Colors.CHECK_MARK} Done!{Colors.END}"
391+
)

0 commit comments

Comments
 (0)