xenfra 0.2.3__py3-none-any.whl → 0.2.5__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.
- xenfra/commands/auth.py +181 -78
- xenfra/commands/deployments.py +42 -1
- xenfra/commands/intelligence.py +59 -28
- xenfra/commands/projects.py +54 -13
- xenfra/commands/security_cmd.py +16 -18
- xenfra/main.py +1 -0
- xenfra/utils/auth.py +99 -21
- xenfra/utils/codebase.py +53 -15
- xenfra/utils/config.py +144 -66
- xenfra/utils/security.py +27 -21
- xenfra/utils/validation.py +229 -0
- {xenfra-0.2.3.dist-info → xenfra-0.2.5.dist-info}/METADATA +26 -25
- xenfra-0.2.5.dist-info/RECORD +18 -0
- xenfra-0.2.5.dist-info/entry_points.txt +3 -0
- xenfra-0.2.3.dist-info/RECORD +0 -17
- xenfra-0.2.3.dist-info/entry_points.txt +0 -3
- {xenfra-0.2.3.dist-info → xenfra-0.2.5.dist-info}/WHEEL +0 -0
xenfra/utils/config.py
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Configuration file generation utilities.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import yaml
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
10
13
|
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
11
14
|
from xenfra_sdk import CodebaseAnalysisResponse
|
|
12
15
|
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
def read_xenfra_yaml(filename: str = "xenfra.yaml") -> dict:
|
|
15
20
|
"""
|
|
16
21
|
Read and parse xenfra.yaml configuration file.
|
|
17
22
|
|
|
23
|
+
Args:
|
|
24
|
+
filename: Path to the config file (default: xenfra.yaml)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dictionary containing the configuration
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
FileNotFoundError: If the config file doesn't exist
|
|
31
|
+
yaml.YAMLError: If the YAML is malformed
|
|
32
|
+
"""
|
|
33
|
+
"""
|
|
34
|
+
Read and parse xenfra.yaml configuration file.
|
|
35
|
+
|
|
18
36
|
Args:
|
|
19
37
|
filename: Path to the config file (default: xenfra.yaml)
|
|
20
38
|
|
|
@@ -25,10 +43,17 @@ def read_xenfra_yaml(filename: str = "xenfra.yaml") -> dict:
|
|
|
25
43
|
FileNotFoundError: If the config file doesn't exist
|
|
26
44
|
"""
|
|
27
45
|
if not Path(filename).exists():
|
|
28
|
-
raise FileNotFoundError(
|
|
46
|
+
raise FileNotFoundError(
|
|
47
|
+
f"Configuration file '{filename}' not found. Run 'xenfra init' first."
|
|
48
|
+
)
|
|
29
49
|
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
try:
|
|
51
|
+
with open(filename, "r") as f:
|
|
52
|
+
return yaml.safe_load(f) or {}
|
|
53
|
+
except yaml.YAMLError as e:
|
|
54
|
+
raise ValueError(f"Invalid YAML in {filename}: {e}")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
raise IOError(f"Failed to read {filename}: {e}")
|
|
32
57
|
|
|
33
58
|
|
|
34
59
|
def generate_xenfra_yaml(analysis: CodebaseAnalysisResponse, filename: str = "xenfra.yaml") -> str:
|
|
@@ -44,44 +69,38 @@ def generate_xenfra_yaml(analysis: CodebaseAnalysisResponse, filename: str = "xe
|
|
|
44
69
|
"""
|
|
45
70
|
# Build configuration dictionary
|
|
46
71
|
config = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
"name": os.path.basename(os.getcwd()),
|
|
73
|
+
"framework": analysis.framework,
|
|
74
|
+
"port": analysis.port,
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
# Add database configuration if detected
|
|
53
|
-
if analysis.database and analysis.database !=
|
|
54
|
-
config[
|
|
55
|
-
'type': analysis.database,
|
|
56
|
-
'env_var': 'DATABASE_URL'
|
|
57
|
-
}
|
|
78
|
+
if analysis.database and analysis.database != "none":
|
|
79
|
+
config["database"] = {"type": analysis.database, "env_var": "DATABASE_URL"}
|
|
58
80
|
|
|
59
81
|
# Add cache configuration if detected
|
|
60
|
-
if analysis.cache and analysis.cache !=
|
|
61
|
-
config[
|
|
62
|
-
'type': analysis.cache,
|
|
63
|
-
'env_var': f"{analysis.cache.upper()}_URL"
|
|
64
|
-
}
|
|
82
|
+
if analysis.cache and analysis.cache != "none":
|
|
83
|
+
config["cache"] = {"type": analysis.cache, "env_var": f"{analysis.cache.upper()}_URL"}
|
|
65
84
|
|
|
66
85
|
# Add worker configuration if detected
|
|
67
86
|
if analysis.workers and len(analysis.workers) > 0:
|
|
68
|
-
config[
|
|
87
|
+
config["workers"] = analysis.workers
|
|
69
88
|
|
|
70
89
|
# Add environment variables
|
|
71
90
|
if analysis.env_vars and len(analysis.env_vars) > 0:
|
|
72
|
-
config[
|
|
91
|
+
config["env_vars"] = analysis.env_vars
|
|
73
92
|
|
|
74
93
|
# Add instance size
|
|
75
|
-
config[
|
|
94
|
+
config["instance_size"] = analysis.instance_size
|
|
76
95
|
|
|
77
96
|
# Add package manager info (for intelligent diagnosis)
|
|
78
97
|
if analysis.package_manager:
|
|
79
|
-
config[
|
|
98
|
+
config["package_manager"] = analysis.package_manager
|
|
80
99
|
if analysis.dependency_file:
|
|
81
|
-
config[
|
|
100
|
+
config["dependency_file"] = analysis.dependency_file
|
|
82
101
|
|
|
83
102
|
# Write to file
|
|
84
|
-
with open(filename,
|
|
103
|
+
with open(filename, "w") as f:
|
|
85
104
|
yaml.dump(config, f, sort_keys=False, default_flow_style=False)
|
|
86
105
|
|
|
87
106
|
return filename
|
|
@@ -116,6 +135,34 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
116
135
|
"""
|
|
117
136
|
Apply a JSON patch to a configuration file with automatic backup.
|
|
118
137
|
|
|
138
|
+
Args:
|
|
139
|
+
patch: Patch object with file, operation, path, value
|
|
140
|
+
target_file: Optional override for the file to patch
|
|
141
|
+
create_backup_file: Whether to create a backup before patching (default: True)
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Path to the backup file if created, None otherwise
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If patch structure is invalid
|
|
148
|
+
FileNotFoundError: If target file doesn't exist
|
|
149
|
+
NotImplementedError: If file type is not supported
|
|
150
|
+
"""
|
|
151
|
+
# Validate patch structure
|
|
152
|
+
if not isinstance(patch, dict):
|
|
153
|
+
raise ValueError("Patch must be a dictionary")
|
|
154
|
+
|
|
155
|
+
required_fields = ["file", "operation"]
|
|
156
|
+
for field in required_fields:
|
|
157
|
+
if field not in patch:
|
|
158
|
+
raise ValueError(f"Patch missing required field: {field}")
|
|
159
|
+
|
|
160
|
+
operation = patch.get("operation")
|
|
161
|
+
if operation not in ["add", "replace", "remove"]:
|
|
162
|
+
raise ValueError(f"Invalid patch operation: {operation}. Must be 'add', 'replace', or 'remove'")
|
|
163
|
+
"""
|
|
164
|
+
Apply a JSON patch to a configuration file with automatic backup.
|
|
165
|
+
|
|
119
166
|
Args:
|
|
120
167
|
patch: Patch object with file, operation, path, value
|
|
121
168
|
target_file: Optional override for the file to patch
|
|
@@ -124,7 +171,7 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
124
171
|
Returns:
|
|
125
172
|
Path to the backup file if created, None otherwise
|
|
126
173
|
"""
|
|
127
|
-
file_to_patch = target_file or patch.get(
|
|
174
|
+
file_to_patch = target_file or patch.get("file")
|
|
128
175
|
|
|
129
176
|
if not file_to_patch:
|
|
130
177
|
raise ValueError("No target file specified in patch")
|
|
@@ -138,19 +185,19 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
138
185
|
backup_path = create_backup(file_to_patch)
|
|
139
186
|
|
|
140
187
|
# For YAML files
|
|
141
|
-
if file_to_patch.endswith((
|
|
142
|
-
with open(file_to_patch,
|
|
188
|
+
if file_to_patch.endswith((".yaml", ".yml")):
|
|
189
|
+
with open(file_to_patch, "r") as f:
|
|
143
190
|
config_data = yaml.safe_load(f) or {}
|
|
144
191
|
|
|
145
192
|
# Apply patch based on operation
|
|
146
|
-
operation = patch.get(
|
|
147
|
-
path = patch.get(
|
|
148
|
-
value = patch.get(
|
|
193
|
+
operation = patch.get("operation")
|
|
194
|
+
path = patch.get("path", "").strip("/")
|
|
195
|
+
value = patch.get("value")
|
|
149
196
|
|
|
150
|
-
if operation ==
|
|
197
|
+
if operation == "add":
|
|
151
198
|
# For simple paths, add to root
|
|
152
199
|
if path:
|
|
153
|
-
path_parts = path.split(
|
|
200
|
+
path_parts = path.split("/")
|
|
154
201
|
current = config_data
|
|
155
202
|
for part in path_parts[:-1]:
|
|
156
203
|
if part not in current:
|
|
@@ -164,9 +211,9 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
164
211
|
else:
|
|
165
212
|
config_data = value
|
|
166
213
|
|
|
167
|
-
elif operation ==
|
|
214
|
+
elif operation == "replace":
|
|
168
215
|
if path:
|
|
169
|
-
path_parts = path.split(
|
|
216
|
+
path_parts = path.split("/")
|
|
170
217
|
current = config_data
|
|
171
218
|
for part in path_parts[:-1]:
|
|
172
219
|
current = current[part]
|
|
@@ -175,21 +222,60 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
175
222
|
config_data = value
|
|
176
223
|
|
|
177
224
|
# Write back
|
|
178
|
-
with open(file_to_patch,
|
|
225
|
+
with open(file_to_patch, "w") as f:
|
|
179
226
|
yaml.dump(config_data, f, sort_keys=False, default_flow_style=False)
|
|
180
227
|
|
|
228
|
+
# For JSON files
|
|
229
|
+
elif file_to_patch.endswith(".json"):
|
|
230
|
+
import json
|
|
231
|
+
with open(file_to_patch, "r") as f:
|
|
232
|
+
config_data = json.load(f)
|
|
233
|
+
|
|
234
|
+
operation = patch.get("operation")
|
|
235
|
+
path = patch.get("path", "").strip("/")
|
|
236
|
+
value = patch.get("value")
|
|
237
|
+
|
|
238
|
+
if operation == "add":
|
|
239
|
+
if path:
|
|
240
|
+
path_parts = path.split("/")
|
|
241
|
+
current = config_data
|
|
242
|
+
for part in path_parts[:-1]:
|
|
243
|
+
if part not in current:
|
|
244
|
+
current[part] = {}
|
|
245
|
+
current = current[part]
|
|
246
|
+
current[path_parts[-1]] = value
|
|
247
|
+
else:
|
|
248
|
+
if isinstance(value, dict):
|
|
249
|
+
config_data.update(value)
|
|
250
|
+
else:
|
|
251
|
+
config_data = value
|
|
252
|
+
|
|
253
|
+
elif operation == "replace":
|
|
254
|
+
if path:
|
|
255
|
+
path_parts = path.split("/")
|
|
256
|
+
current = config_data
|
|
257
|
+
for part in path_parts[:-1]:
|
|
258
|
+
current = current[part]
|
|
259
|
+
current[path_parts[-1]] = value
|
|
260
|
+
else:
|
|
261
|
+
config_data = value
|
|
262
|
+
|
|
263
|
+
# Write back
|
|
264
|
+
with open(file_to_patch, "w") as f:
|
|
265
|
+
json.dump(config_data, f, indent=2)
|
|
266
|
+
|
|
181
267
|
# For text files (like requirements.txt)
|
|
182
|
-
elif file_to_patch.endswith(
|
|
183
|
-
operation = patch.get(
|
|
184
|
-
value = patch.get(
|
|
268
|
+
elif file_to_patch.endswith(".txt"):
|
|
269
|
+
operation = patch.get("operation")
|
|
270
|
+
value = patch.get("value")
|
|
185
271
|
|
|
186
|
-
if operation ==
|
|
272
|
+
if operation == "add":
|
|
187
273
|
# Append to file
|
|
188
|
-
with open(file_to_patch,
|
|
274
|
+
with open(file_to_patch, "a") as f:
|
|
189
275
|
f.write(f"\n{value}\n")
|
|
190
|
-
elif operation ==
|
|
276
|
+
elif operation == "replace":
|
|
191
277
|
# Replace entire file
|
|
192
|
-
with open(file_to_patch,
|
|
278
|
+
with open(file_to_patch, "w") as f:
|
|
193
279
|
f.write(str(value))
|
|
194
280
|
else:
|
|
195
281
|
raise NotImplementedError(f"Patching not supported for file type: {file_to_patch}")
|
|
@@ -211,19 +297,23 @@ def manual_prompt_for_config(filename: str = "xenfra.yaml") -> str:
|
|
|
211
297
|
|
|
212
298
|
# Project name (default to directory name)
|
|
213
299
|
default_name = os.path.basename(os.getcwd())
|
|
214
|
-
config[
|
|
300
|
+
config["name"] = Prompt.ask("Project name", default=default_name)
|
|
215
301
|
|
|
216
302
|
# Framework
|
|
217
303
|
framework = Prompt.ask(
|
|
218
|
-
"Framework",
|
|
219
|
-
choices=["fastapi", "flask", "django", "other"],
|
|
220
|
-
default="fastapi"
|
|
304
|
+
"Framework", choices=["fastapi", "flask", "django", "other"], default="fastapi"
|
|
221
305
|
)
|
|
222
|
-
config[
|
|
306
|
+
config["framework"] = framework
|
|
223
307
|
|
|
224
308
|
# Port
|
|
225
309
|
port = IntPrompt.ask("Application port", default=8000)
|
|
226
|
-
|
|
310
|
+
# Validate port
|
|
311
|
+
from .validation import validate_port
|
|
312
|
+
is_valid, error_msg = validate_port(port)
|
|
313
|
+
if not is_valid:
|
|
314
|
+
console.print(f"[bold red]Invalid port: {error_msg}[/bold red]")
|
|
315
|
+
raise click.Abort()
|
|
316
|
+
config["port"] = port
|
|
227
317
|
|
|
228
318
|
# Database
|
|
229
319
|
use_database = Confirm.ask("Does your app use a database?", default=False)
|
|
@@ -231,33 +321,21 @@ def manual_prompt_for_config(filename: str = "xenfra.yaml") -> str:
|
|
|
231
321
|
db_type = Prompt.ask(
|
|
232
322
|
"Database type",
|
|
233
323
|
choices=["postgresql", "mysql", "sqlite", "mongodb"],
|
|
234
|
-
default="postgresql"
|
|
324
|
+
default="postgresql",
|
|
235
325
|
)
|
|
236
|
-
config[
|
|
237
|
-
'type': db_type,
|
|
238
|
-
'env_var': 'DATABASE_URL'
|
|
239
|
-
}
|
|
326
|
+
config["database"] = {"type": db_type, "env_var": "DATABASE_URL"}
|
|
240
327
|
|
|
241
328
|
# Cache
|
|
242
329
|
use_cache = Confirm.ask("Does your app use caching?", default=False)
|
|
243
330
|
if use_cache:
|
|
244
|
-
cache_type = Prompt.ask(
|
|
245
|
-
|
|
246
|
-
choices=["redis", "memcached"],
|
|
247
|
-
default="redis"
|
|
248
|
-
)
|
|
249
|
-
config['cache'] = {
|
|
250
|
-
'type': cache_type,
|
|
251
|
-
'env_var': f"{cache_type.upper()}_URL"
|
|
252
|
-
}
|
|
331
|
+
cache_type = Prompt.ask("Cache type", choices=["redis", "memcached"], default="redis")
|
|
332
|
+
config["cache"] = {"type": cache_type, "env_var": f"{cache_type.upper()}_URL"}
|
|
253
333
|
|
|
254
334
|
# Instance size
|
|
255
335
|
instance_size = Prompt.ask(
|
|
256
|
-
"Instance size",
|
|
257
|
-
choices=["basic", "standard", "premium"],
|
|
258
|
-
default="basic"
|
|
336
|
+
"Instance size", choices=["basic", "standard", "premium"], default="basic"
|
|
259
337
|
)
|
|
260
|
-
config[
|
|
338
|
+
config["instance_size"] = instance_size
|
|
261
339
|
|
|
262
340
|
# Environment variables
|
|
263
341
|
add_env = Confirm.ask("Add environment variables?", default=False)
|
|
@@ -269,10 +347,10 @@ def manual_prompt_for_config(filename: str = "xenfra.yaml") -> str:
|
|
|
269
347
|
break
|
|
270
348
|
env_vars.append(env_var)
|
|
271
349
|
if env_vars:
|
|
272
|
-
config[
|
|
350
|
+
config["env_vars"] = env_vars
|
|
273
351
|
|
|
274
352
|
# Write to file
|
|
275
|
-
with open(filename,
|
|
353
|
+
with open(filename, "w") as f:
|
|
276
354
|
yaml.dump(config, f, sort_keys=False, default_flow_style=False)
|
|
277
355
|
|
|
278
356
|
return filename
|
xenfra/utils/security.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Security utilities for Xenfra CLI.
|
|
3
3
|
Implements comprehensive URL validation, domain whitelisting, HTTPS enforcement, and certificate pinning.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
import os
|
|
6
7
|
import ssl
|
|
7
8
|
from urllib.parse import urlparse
|
|
@@ -14,23 +15,25 @@ from rich.console import Console
|
|
|
14
15
|
console = Console()
|
|
15
16
|
|
|
16
17
|
# Production API URL
|
|
17
|
-
PRODUCTION_API_URL = "https://api.xenfra.
|
|
18
|
+
PRODUCTION_API_URL = "https://api.xenfra.tech"
|
|
18
19
|
|
|
19
20
|
# Allowed domains (whitelist) - Solution 2
|
|
20
21
|
ALLOWED_DOMAINS = [
|
|
21
|
-
"api.xenfra.
|
|
22
|
-
"api-staging.xenfra.
|
|
23
|
-
"localhost",
|
|
24
|
-
"127.0.0.1",
|
|
22
|
+
"api.xenfra.tech", # Production
|
|
23
|
+
"api-staging.xenfra.tech", # Staging
|
|
24
|
+
"localhost", # Local development
|
|
25
|
+
"127.0.0.1", # Local development (IP)
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
# Certificate fingerprints for pinning - Solution 4
|
|
28
29
|
# These should be updated when certificates are rotated
|
|
29
30
|
PINNED_CERTIFICATES = {
|
|
30
|
-
"api.xenfra.
|
|
31
|
+
"api.xenfra.tech": {
|
|
31
32
|
# SHA256 fingerprint of the expected certificate
|
|
32
33
|
# Example: "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
|
33
|
-
# To get
|
|
34
|
+
# To get fingerprint, run:
|
|
35
|
+
# openssl s_client -connect api.xenfra.tech:443 | openssl x509 -pubkey -noout \
|
|
36
|
+
# | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
|
|
34
37
|
"fingerprints": [], # Add actual fingerprints when you have production cert
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -47,7 +50,9 @@ class SecurityConfig:
|
|
|
47
50
|
# Security settings (can be overridden by environment variables)
|
|
48
51
|
self.enforce_https = os.getenv("XENFRA_ENFORCE_HTTPS", "false").lower() == "true"
|
|
49
52
|
self.enforce_whitelist = os.getenv("XENFRA_ENFORCE_WHITELIST", "false").lower() == "true"
|
|
50
|
-
self.enable_cert_pinning =
|
|
53
|
+
self.enable_cert_pinning = (
|
|
54
|
+
os.getenv("XENFRA_ENABLE_CERT_PINNING", "false").lower() == "true"
|
|
55
|
+
)
|
|
51
56
|
self.warn_on_http = os.getenv("XENFRA_WARN_ON_HTTP", "true").lower() == "true"
|
|
52
57
|
|
|
53
58
|
# Auto-enable strict security in production
|
|
@@ -88,8 +93,7 @@ def validate_url_format(url: str) -> dict:
|
|
|
88
93
|
# Check scheme
|
|
89
94
|
if parsed.scheme not in ["http", "https"]:
|
|
90
95
|
raise ValueError(
|
|
91
|
-
f"Invalid URL scheme '{parsed.scheme}'. "
|
|
92
|
-
f"Only 'http' and 'https' are allowed."
|
|
96
|
+
f"Invalid URL scheme '{parsed.scheme}'. " f"Only 'http' and 'https' are allowed."
|
|
93
97
|
)
|
|
94
98
|
|
|
95
99
|
# Check hostname exists
|
|
@@ -108,7 +112,7 @@ def validate_url_format(url: str) -> dict:
|
|
|
108
112
|
"scheme": parsed.scheme,
|
|
109
113
|
"hostname": parsed.hostname,
|
|
110
114
|
"port": parsed.port,
|
|
111
|
-
"url": url
|
|
115
|
+
"url": url,
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
except Exception as e:
|
|
@@ -145,12 +149,8 @@ def check_domain_whitelist(hostname: str) -> bool:
|
|
|
145
149
|
console.print(
|
|
146
150
|
f"[yellow]⚠️ Warning: Domain '{hostname}' is not in the official whitelist.[/yellow]"
|
|
147
151
|
)
|
|
148
|
-
console.print(
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
console.print(
|
|
152
|
-
f"[yellow]Are you sure you want to connect to this API?[/yellow]"
|
|
153
|
-
)
|
|
152
|
+
console.print(f"[dim]Whitelisted domains: {', '.join(ALLOWED_DOMAINS)}[/dim]")
|
|
153
|
+
console.print("[yellow]Are you sure you want to connect to this API?[/yellow]")
|
|
154
154
|
|
|
155
155
|
if not click.confirm("Continue?", default=False):
|
|
156
156
|
raise click.Abort()
|
|
@@ -285,7 +285,7 @@ def validate_and_get_api_url(url: str = None) -> str:
|
|
|
285
285
|
return url
|
|
286
286
|
|
|
287
287
|
except ValueError as e:
|
|
288
|
-
console.print(
|
|
288
|
+
console.print("[bold red]🔒 Security Validation Failed:[/bold red]")
|
|
289
289
|
console.print(f"[red]{e}[/red]")
|
|
290
290
|
raise click.Abort()
|
|
291
291
|
|
|
@@ -297,9 +297,15 @@ def display_security_info():
|
|
|
297
297
|
table_data = [
|
|
298
298
|
("Environment", security_config.environment),
|
|
299
299
|
("HTTPS Enforcement", "✅ Enabled" if security_config.enforce_https else "⚠️ Disabled"),
|
|
300
|
-
(
|
|
300
|
+
(
|
|
301
|
+
"Domain Whitelist",
|
|
302
|
+
"✅ Enforced" if security_config.enforce_whitelist else "⚠️ Warning Only",
|
|
303
|
+
),
|
|
301
304
|
("HTTP Warning", "✅ Enabled" if security_config.warn_on_http else "❌ Disabled"),
|
|
302
|
-
(
|
|
305
|
+
(
|
|
306
|
+
"Certificate Pinning",
|
|
307
|
+
"✅ Enabled" if security_config.enable_cert_pinning else "❌ Disabled",
|
|
308
|
+
),
|
|
303
309
|
]
|
|
304
310
|
|
|
305
311
|
for key, value in table_data:
|
|
@@ -346,5 +352,5 @@ XENFRA_API_URL=http://localhost:8000 xenfra login
|
|
|
346
352
|
XENFRA_API_URL=https://xenfra.mycompany.com XENFRA_ENFORCE_WHITELIST=false xenfra login
|
|
347
353
|
|
|
348
354
|
# Production (strict):
|
|
349
|
-
XENFRA_ENV=production XENFRA_API_URL=https://api.xenfra.
|
|
355
|
+
XENFRA_ENV=production XENFRA_API_URL=https://api.xenfra.tech xenfra login
|
|
350
356
|
"""
|