pingera-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+
2
+ """
3
+ Base formatter class for check results
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Dict, Any
8
+
9
+
10
+ class BaseFormatter(ABC):
11
+ """Base class for all result formatters"""
12
+
13
+ def __init__(self, verbose: bool = False):
14
+ self.verbose = verbose
15
+
16
+ @abstractmethod
17
+ def can_format(self, metadata: Dict[str, Any]) -> bool:
18
+ """Check if this formatter can handle the given metadata"""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def format(self, metadata: Dict[str, Any]) -> str:
23
+ """Format the metadata into a rich string"""
24
+ pass
25
+
26
+ def _truncate_text(self, text: str, max_length: int = 100) -> str:
27
+ """Truncate text if it's too long"""
28
+ if len(text) > max_length:
29
+ return text[:max_length] + "..."
30
+ return text
31
+
32
+ def _format_size(self, size_bytes: int) -> str:
33
+ """Format size in bytes to human readable format"""
34
+ if size_bytes > 1024*1024:
35
+ return f"{size_bytes/(1024*1024):.1f}MB"
36
+ elif size_bytes > 1024:
37
+ return f"{size_bytes/1024:.1f}KB"
38
+ elif size_bytes > 0:
39
+ return f"{size_bytes}B"
40
+ else:
41
+ return "0B"
42
+
43
+ def _format_duration(self, duration_ms: float) -> str:
44
+ """Format duration in milliseconds"""
45
+ return f"{duration_ms:.0f}ms" if duration_ms > 0 else "0ms"
@@ -0,0 +1,51 @@
1
+
2
+ """
3
+ Generic check result formatter for unknown types
4
+ """
5
+
6
+ from typing import Dict, Any
7
+ from .base_formatter import BaseFormatter
8
+
9
+
10
+ class GenericFormatter(BaseFormatter):
11
+ """Formatter for generic/unknown check results"""
12
+
13
+ def can_format(self, metadata: Dict[str, Any]) -> bool:
14
+ """Generic formatter can handle any metadata"""
15
+ return True
16
+
17
+ def format(self, metadata: Dict[str, Any]) -> str:
18
+ """Format generic metadata"""
19
+ info = "\n[bold cyan]Check Metadata:[/bold cyan]"
20
+
21
+ # Display all metadata in a structured way
22
+ for key, value in metadata.items():
23
+ if isinstance(value, dict):
24
+ info += f"\n• {key.replace('_', ' ').title()}:"
25
+ info += self._format_dict_value(value)
26
+ elif isinstance(value, list):
27
+ info += f"\n• {key.replace('_', ' ').title()}: [white]{len(value)} items[/white]"
28
+ info += self._format_list_value(value)
29
+ else:
30
+ display_value = self._truncate_text(str(value))
31
+ info += f"\n• {key.replace('_', ' ').title()}: [white]{display_value}[/white]"
32
+
33
+ return info
34
+
35
+ def _format_dict_value(self, value: Dict[str, Any]) -> str:
36
+ """Format dictionary values"""
37
+ info = ""
38
+ for sub_key, sub_value in value.items():
39
+ if isinstance(sub_value, (str, int, float, bool)):
40
+ info += f"\n - {sub_key}: [white]{sub_value}[/white]"
41
+ return info
42
+
43
+ def _format_list_value(self, value: list) -> str:
44
+ """Format list values"""
45
+ info = ""
46
+ # Show first few items
47
+ for item in value[:3]:
48
+ if isinstance(item, str):
49
+ display_item = self._truncate_text(item, 50)
50
+ info += f"\n - [dim]{display_item}[/dim]"
51
+ return info
@@ -0,0 +1,141 @@
1
+
2
+ """
3
+ Multistep check result formatter
4
+ """
5
+
6
+ from typing import Dict, Any, List
7
+ from .base_formatter import BaseFormatter
8
+
9
+
10
+ class MultistepFormatter(BaseFormatter):
11
+ """Formatter for multistep check results"""
12
+
13
+ def can_format(self, metadata: Dict[str, Any]) -> bool:
14
+ """Check if this is multistep metadata"""
15
+ return 'test_results' in metadata and 'test_summary' in metadata
16
+
17
+ def format(self, metadata: Dict[str, Any]) -> str:
18
+ """Format multistep check metadata"""
19
+ info = "\n[bold cyan]Multistep Check Results:[/bold cyan]"
20
+
21
+ # Execution time
22
+ if 'execution_time' in metadata:
23
+ info += f"\n• Total Execution Time: [yellow]{metadata['execution_time']}ms[/yellow]"
24
+
25
+ # Test Summary
26
+ if 'test_summary' in metadata:
27
+ info += self._format_test_summary(metadata['test_summary'])
28
+
29
+ # Test Results
30
+ if 'test_results' in metadata and metadata['test_results']:
31
+ info += self._format_test_results(metadata['test_results'])
32
+
33
+ # Location info
34
+ if 'region' in metadata:
35
+ info += f"\n• Region: [white]{metadata['region']}[/white]"
36
+ if 'provider' in metadata:
37
+ info += f"\n• Provider: [white]{metadata['provider']}[/white]"
38
+
39
+ # Logs
40
+ if 'logs' in metadata and metadata['logs']:
41
+ info += self._format_logs(metadata['logs'])
42
+
43
+ return info
44
+
45
+ def _format_test_summary(self, summary: Dict[str, Any]) -> str:
46
+ """Format test summary"""
47
+ total = summary.get('total', 0)
48
+ passed = summary.get('passed', 0)
49
+ failed = summary.get('failed', 0)
50
+ duration = summary.get('duration', 0)
51
+
52
+ info = f"\n\n[bold cyan]Test Summary:[/bold cyan]"
53
+ info += f"\n• Tests: [green]{passed} passed[/green], [red]{failed} failed[/red], [white]{total} total[/white]"
54
+ info += f"\n• Duration: [yellow]{duration}ms[/yellow]"
55
+
56
+ return info
57
+
58
+ def _format_test_results(self, test_results: List[Dict[str, Any]]) -> str:
59
+ """Format test results"""
60
+ info = "\n\n[bold cyan]Test Results:[/bold cyan]"
61
+
62
+ for i, test in enumerate(test_results, 1):
63
+ status_color = "green" if test.get('status') == 'passed' else "red"
64
+ status_icon = "✅" if test.get('status') == 'passed' else "❌"
65
+ test_name = test.get('name', f'Test {i}')
66
+ duration = test.get('duration', 0)
67
+
68
+ info += f"\n• {test_name}: [{status_color}]{status_icon} {test.get('status', 'unknown')}[/{status_color}]"
69
+ info += f" ([yellow]{duration}ms[/yellow])"
70
+
71
+ # Soft assertion errors
72
+ if test.get('softAssertionErrors', 0) > 0:
73
+ info += f" [yellow]⚠️ {test['softAssertionErrors']} soft assertion errors[/yellow]"
74
+
75
+ # Error details
76
+ if test.get('error'):
77
+ error_text = self._truncate_text(test['error'], 80) if not self.verbose else test['error']
78
+ info += f"\n [red]Error: {error_text}[/red]"
79
+
80
+ # Steps
81
+ if 'steps' in test and test['steps']:
82
+ info += self._format_test_steps(test['steps'])
83
+
84
+ return info
85
+
86
+ def _format_test_steps(self, steps: List[Dict[str, Any]]) -> str:
87
+ """Format test steps"""
88
+ info = f"\n [bold dim]Steps:[/bold dim]"
89
+
90
+ for i, step in enumerate(steps, 1):
91
+ status_color = "green" if step.get('status') == 'passed' else "red"
92
+ status_icon = "✓" if step.get('status') == 'passed' else "✗"
93
+ step_name = step.get('name', f'Step {i}')
94
+ duration = step.get('duration', 0)
95
+
96
+ info += f"\n {i}. {step_name}: [{status_color}]{status_icon}[/{status_color}] ([dim]{duration}ms[/dim])"
97
+
98
+ # Step error
99
+ if step.get('error') and (self.verbose or step.get('status') == 'failed'):
100
+ error_text = self._truncate_text(step['error'], 60) if not self.verbose else step['error']
101
+ info += f"\n [red]Error: {error_text}[/red]"
102
+
103
+ return info
104
+
105
+ def _format_logs(self, logs: List[Dict[str, Any]]) -> str:
106
+ """Format execution logs"""
107
+ info = "\n\n[bold cyan]Execution Logs:[/bold cyan]"
108
+
109
+ if not self.verbose:
110
+ info += f"\n• [dim]Found {len(logs)} log entries (use --verbose to see all)[/dim]"
111
+ # Show only first few logs in non-verbose mode
112
+ for log in logs[:3]:
113
+ level = log.get('level', 'log')
114
+ message = self._truncate_text(log.get('message', ''), 60)
115
+ timestamp = log.get('timestamp', '')
116
+
117
+ level_color = self._get_log_level_color(level)
118
+ info += f"\n• [{level_color}]{level.upper()}[/{level_color}]: [dim]{message}[/dim]"
119
+ else:
120
+ # Show all logs in verbose mode
121
+ for i, log in enumerate(logs, 1):
122
+ level = log.get('level', 'log')
123
+ message = log.get('message', '')
124
+ timestamp = log.get('timestamp', '')
125
+
126
+ level_color = self._get_log_level_color(level)
127
+ info += f"\n• [{level_color}]{level.upper()}[/{level_color}] [{timestamp}]: {message}"
128
+
129
+ return info
130
+
131
+ def _get_log_level_color(self, level: str) -> str:
132
+ """Get color for log level"""
133
+ colors = {
134
+ 'error': 'red',
135
+ 'warn': 'yellow',
136
+ 'warning': 'yellow',
137
+ 'info': 'blue',
138
+ 'log': 'white',
139
+ 'debug': 'dim'
140
+ }
141
+ return colors.get(level.lower(), 'white')
@@ -0,0 +1,35 @@
1
+
2
+ """
3
+ Formatter registry for managing different check result formatters
4
+ """
5
+
6
+ from typing import Dict, Any, List
7
+ from .base_formatter import BaseFormatter
8
+ from .ssl_formatter import SSLFormatter
9
+ from .synthetic_formatter import SyntheticFormatter
10
+ from .multistep_formatter import MultistepFormatter
11
+ from .web_formatter import WebFormatter
12
+ from .generic_formatter import GenericFormatter
13
+
14
+
15
+ class FormatterRegistry:
16
+ """Registry for managing check result formatters"""
17
+
18
+ def __init__(self, verbose: bool = False):
19
+ self.verbose = verbose
20
+ self.formatters: List[BaseFormatter] = [
21
+ SSLFormatter(verbose),
22
+ SyntheticFormatter(verbose),
23
+ MultistepFormatter(verbose),
24
+ WebFormatter(verbose),
25
+ GenericFormatter(verbose) # Generic formatter should be last
26
+ ]
27
+
28
+ def format_metadata(self, metadata: Dict[str, Any]) -> str:
29
+ """Format metadata using the appropriate formatter"""
30
+ for formatter in self.formatters:
31
+ if formatter.can_format(metadata):
32
+ return formatter.format(metadata)
33
+
34
+ # This should never happen since GenericFormatter can handle anything
35
+ return "\n[bold cyan]Check Metadata:[/bold cyan]\n• [dim]No formatter available[/dim]"
@@ -0,0 +1,104 @@
1
+
2
+ """
3
+ SSL check result formatter
4
+ """
5
+
6
+ from typing import Dict, Any
7
+ from .base_formatter import BaseFormatter
8
+
9
+
10
+ class SSLFormatter(BaseFormatter):
11
+ """Formatter for SSL check results"""
12
+
13
+ def can_format(self, metadata: Dict[str, Any]) -> bool:
14
+ """Check if this is SSL metadata"""
15
+ return 'ssl_grade' in metadata or ('checks' in metadata and 'certificate_info' in metadata.get('checks', {}))
16
+
17
+ def format(self, metadata: Dict[str, Any]) -> str:
18
+ """Format SSL check metadata"""
19
+ info = "\n[bold cyan]SSL Check Results:[/bold cyan]"
20
+
21
+ # SSL Grade and Score
22
+ if 'ssl_grade' in metadata:
23
+ grade_color = "green" if metadata['ssl_grade'] in ['A+', 'A'] else "yellow" if metadata['ssl_grade'] in ['B', 'C'] else "red"
24
+ info += f"\n• SSL Grade: [{grade_color}]{metadata['ssl_grade']}[/{grade_color}]"
25
+
26
+ if 'ssl_score' in metadata:
27
+ score_color = "green" if metadata['ssl_score'] >= 80 else "yellow" if metadata['ssl_score'] >= 60 else "red"
28
+ info += f"\n• SSL Score: [{score_color}]{metadata['ssl_score']}/100[/{score_color}]"
29
+
30
+ # Certificate Information
31
+ if 'checks' in metadata and 'certificate_info' in metadata['checks']:
32
+ info += self._format_certificate_info(metadata['checks']['certificate_info'])
33
+
34
+ # Protocol Support
35
+ if 'checks' in metadata and 'protocol_support' in metadata['checks']:
36
+ info += self._format_protocol_support(metadata['checks']['protocol_support'])
37
+
38
+ # Vulnerabilities
39
+ if 'checks' in metadata and 'vulnerabilities' in metadata['checks']:
40
+ info += self._format_vulnerabilities(metadata['checks']['vulnerabilities'])
41
+
42
+ # Assessment Summary
43
+ if 'deduction_summary' in metadata:
44
+ info += "\n\n[bold cyan]Assessment Summary:[/bold cyan]"
45
+ for summary in metadata['deduction_summary']:
46
+ info += f"\n• [dim]{summary}[/dim]"
47
+
48
+ return info
49
+
50
+ def _format_certificate_info(self, cert: Dict[str, Any]) -> str:
51
+ """Format certificate information"""
52
+ info = "\n\n[bold cyan]Certificate Details:[/bold cyan]"
53
+
54
+ if 'subject' in cert:
55
+ info += f"\n• Subject: [white]{cert['subject']}[/white]"
56
+ if 'issuer' in cert:
57
+ info += f"\n• Issuer: [white]{cert['issuer']}[/white]"
58
+ if 'not_before' in cert and 'not_after' in cert:
59
+ info += f"\n• Valid From: [white]{cert['not_before']}[/white]"
60
+ info += f"\n• Valid Until: [white]{cert['not_after']}[/white]"
61
+ if 'key_size' in cert:
62
+ info += f"\n• Key Size: [white]{cert['key_size']} bits[/white]"
63
+ if 'signature_algorithm' in cert:
64
+ info += f"\n• Signature Algorithm: [white]{cert['signature_algorithm']}[/white]"
65
+
66
+ # Certificate validation checks
67
+ if 'cert_date_valid' in cert:
68
+ status = "✅ Valid" if cert['cert_date_valid'] else "❌ Invalid"
69
+ info += f"\n• Date Valid: {status}"
70
+ if 'hostname_mismatch' in cert:
71
+ status = "❌ Mismatch" if cert['hostname_mismatch'] else "✅ Match"
72
+ info += f"\n• Hostname: {status}"
73
+ if 'in_trust_store' in cert:
74
+ status = "✅ Trusted" if cert['in_trust_store'] else "❌ Not Trusted"
75
+ info += f"\n• Trust Store: {status}"
76
+
77
+ return info
78
+
79
+ def _format_protocol_support(self, protocols: Dict[str, Any]) -> str:
80
+ """Format protocol support information"""
81
+ info = "\n\n[bold cyan]Protocol Support:[/bold cyan]"
82
+
83
+ for protocol, details in protocols.items():
84
+ if details.get('supported'):
85
+ cipher_count = len(details.get('ciphers', []))
86
+ info += f"\n• {protocol.upper().replace('_', '.')}: [green]✅ Supported[/green] ({cipher_count} ciphers)"
87
+ else:
88
+ info += f"\n• {protocol.upper().replace('_', '.')}: [red]❌ Not Supported[/red]"
89
+
90
+ return info
91
+
92
+ def _format_vulnerabilities(self, vulns: Dict[str, Any]) -> str:
93
+ """Format vulnerability information"""
94
+ info = "\n\n[bold cyan]Security Vulnerabilities:[/bold cyan]"
95
+
96
+ for vuln_name, vuln_data in vulns.items():
97
+ if vuln_data.get('vulnerable'):
98
+ info += f"\n• {vuln_name.replace('_', ' ').title()}: [red]❌ Vulnerable[/red]"
99
+ if 'details' in vuln_data:
100
+ info += f"\n [dim]{vuln_data['details']}[/dim]"
101
+ else:
102
+ info += f"\n• {vuln_name.replace('_', ' ').title()}: [green]✅ Not Vulnerable[/green]"
103
+
104
+ return info