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.
- pingera_cli/__init__.py +11 -0
- pingera_cli/__main__.py +9 -0
- pingera_cli/commands/__init__.py +11 -0
- pingera_cli/commands/auth.py +214 -0
- pingera_cli/commands/base.py +251 -0
- pingera_cli/commands/checks.py +1011 -0
- pingera_cli/commands/on_demand_checks.py +646 -0
- pingera_cli/formatters/__init__.py +13 -0
- pingera_cli/formatters/base_formatter.py +45 -0
- pingera_cli/formatters/generic_formatter.py +51 -0
- pingera_cli/formatters/multistep_formatter.py +141 -0
- pingera_cli/formatters/registry.py +35 -0
- pingera_cli/formatters/ssl_formatter.py +104 -0
- pingera_cli/formatters/synthetic_formatter.py +350 -0
- pingera_cli/formatters/web_formatter.py +78 -0
- pingera_cli/main.py +214 -0
- pingera_cli/utils/__init__.py +8 -0
- pingera_cli/utils/config.py +187 -0
- pingera_cli/utils/console.py +87 -0
- pingera_cli-0.1.0.dist-info/METADATA +199 -0
- pingera_cli-0.1.0.dist-info/RECORD +25 -0
- pingera_cli-0.1.0.dist-info/WHEEL +5 -0
- pingera_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pingera_cli-0.1.0.dist-info/licenses/LICENSE +22 -0
- pingera_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|