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/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(f"Configuration file '{filename}' not found. Run 'xenfra init' first.")
46
+ raise FileNotFoundError(
47
+ f"Configuration file '{filename}' not found. Run 'xenfra init' first."
48
+ )
29
49
 
30
- with open(filename, 'r') as f:
31
- return yaml.safe_load(f) or {}
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
- 'name': os.path.basename(os.getcwd()),
48
- 'framework': analysis.framework,
49
- 'port': analysis.port,
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 != 'none':
54
- config['database'] = {
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 != 'none':
61
- config['cache'] = {
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['workers'] = analysis.workers
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['env_vars'] = analysis.env_vars
91
+ config["env_vars"] = analysis.env_vars
73
92
 
74
93
  # Add instance size
75
- config['instance_size'] = analysis.instance_size
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['package_manager'] = analysis.package_manager
98
+ config["package_manager"] = analysis.package_manager
80
99
  if analysis.dependency_file:
81
- config['dependency_file'] = analysis.dependency_file
100
+ config["dependency_file"] = analysis.dependency_file
82
101
 
83
102
  # Write to file
84
- with open(filename, 'w') as f:
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('file')
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(('.yaml', '.yml')):
142
- with open(file_to_patch, 'r') as f:
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('operation')
147
- path = patch.get('path', '').strip('/')
148
- value = patch.get('value')
193
+ operation = patch.get("operation")
194
+ path = patch.get("path", "").strip("/")
195
+ value = patch.get("value")
149
196
 
150
- if operation == 'add':
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 == 'replace':
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, 'w') as f:
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('.txt'):
183
- operation = patch.get('operation')
184
- value = patch.get('value')
268
+ elif file_to_patch.endswith(".txt"):
269
+ operation = patch.get("operation")
270
+ value = patch.get("value")
185
271
 
186
- if operation == 'add':
272
+ if operation == "add":
187
273
  # Append to file
188
- with open(file_to_patch, 'a') as f:
274
+ with open(file_to_patch, "a") as f:
189
275
  f.write(f"\n{value}\n")
190
- elif operation == 'replace':
276
+ elif operation == "replace":
191
277
  # Replace entire file
192
- with open(file_to_patch, 'w') as f:
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['name'] = Prompt.ask("Project name", default=default_name)
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['framework'] = framework
306
+ config["framework"] = framework
223
307
 
224
308
  # Port
225
309
  port = IntPrompt.ask("Application port", default=8000)
226
- config['port'] = port
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['database'] = {
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
- "Cache type",
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['instance_size'] = instance_size
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['env_vars'] = env_vars
350
+ config["env_vars"] = env_vars
273
351
 
274
352
  # Write to file
275
- with open(filename, 'w') as f:
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.com" # TODO: Update with real production URL
18
+ PRODUCTION_API_URL = "https://api.xenfra.tech"
18
19
 
19
20
  # Allowed domains (whitelist) - Solution 2
20
21
  ALLOWED_DOMAINS = [
21
- "api.xenfra.com", # Production
22
- "api-staging.xenfra.com", # Staging
23
- "localhost", # Local development
24
- "127.0.0.1", # Local development (IP)
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.com": {
31
+ "api.xenfra.tech": {
31
32
  # SHA256 fingerprint of the expected certificate
32
33
  # Example: "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
33
- # To get: openssl s_client -connect api.xenfra.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
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 = os.getenv("XENFRA_ENABLE_CERT_PINNING", "false").lower() == "true"
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
- f"[dim]Whitelisted domains: {', '.join(ALLOWED_DOMAINS)}[/dim]"
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(f"[bold red]🔒 Security Validation Failed:[/bold red]")
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
- ("Domain Whitelist", "✅ Enforced" if security_config.enforce_whitelist else "⚠️ Warning Only"),
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
- ("Certificate Pinning", "✅ Enabled" if security_config.enable_cert_pinning else "❌ Disabled"),
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.com xenfra login
355
+ XENFRA_ENV=production XENFRA_API_URL=https://api.xenfra.tech xenfra login
350
356
  """