xenfra 0.2.2__py3-none-any.whl → 0.2.4__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,235 @@
1
+ """
2
+ Security configuration commands for Xenfra CLI.
3
+ """
4
+ import os
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from ..utils.security import (
12
+ ALLOWED_DOMAINS,
13
+ display_security_info,
14
+ security_config,
15
+ validate_and_get_api_url,
16
+ )
17
+
18
+ console = Console()
19
+
20
+
21
+ @click.group(hidden=True)
22
+ def security():
23
+ """Security and configuration management (for debugging/advanced users)."""
24
+ pass
25
+
26
+
27
+ @security.command()
28
+ def check():
29
+ """Display current security configuration."""
30
+ # Get current API URL
31
+ try:
32
+ api_url = validate_and_get_api_url()
33
+ except click.Abort:
34
+ api_url = os.getenv("XENFRA_API_URL", "Not set")
35
+
36
+ # Display configuration
37
+ console.print("\n[bold cyan]🔒 Xenfra CLI Security Configuration[/bold cyan]\n")
38
+
39
+ # API URL
40
+ console.print(f"[bold]API URL:[/bold] {api_url}")
41
+
42
+ # Environment
43
+ env_color = "green" if security_config.is_production() else "yellow"
44
+ console.print(f"[bold]Environment:[/bold] [{env_color}]{security_config.environment}[/{env_color}]")
45
+
46
+ console.print()
47
+
48
+ # Security features table
49
+ table = Table(title="Security Features", show_header=True)
50
+ table.add_column("Feature", style="cyan")
51
+ table.add_column("Status", style="white")
52
+ table.add_column("Description", style="dim")
53
+
54
+ features = [
55
+ (
56
+ "HTTPS Enforcement",
57
+ "✅ Enabled" if security_config.enforce_https else "⚠️ Disabled",
58
+ "Blocks HTTP connections (except localhost)"
59
+ ),
60
+ (
61
+ "Domain Whitelist",
62
+ "✅ Enforced" if security_config.enforce_whitelist else "⚠️ Warning Only",
63
+ "Restricts connections to approved domains"
64
+ ),
65
+ (
66
+ "HTTP Warning",
67
+ "✅ Enabled" if security_config.warn_on_http else "❌ Disabled",
68
+ "Warns when using insecure HTTP"
69
+ ),
70
+ (
71
+ "Certificate Pinning",
72
+ "✅ Enabled" if security_config.enable_cert_pinning else "❌ Disabled",
73
+ "Validates SSL certificate fingerprints"
74
+ ),
75
+ ]
76
+
77
+ for feature, status, description in features:
78
+ table.add_row(feature, status, description)
79
+
80
+ console.print(table)
81
+ console.print()
82
+
83
+ # Whitelisted domains
84
+ console.print("[bold]Whitelisted Domains:[/bold]")
85
+ for domain in ALLOWED_DOMAINS:
86
+ console.print(f" • {domain}")
87
+
88
+ console.print()
89
+
90
+ # Environment variables
91
+ console.print("[bold]Configuration via Environment Variables:[/bold]")
92
+ env_vars = [
93
+ ("XENFRA_ENV", os.getenv("XENFRA_ENV", "development")),
94
+ ("XENFRA_API_URL", os.getenv("XENFRA_API_URL", "http://localhost:8000")),
95
+ ("XENFRA_ENFORCE_HTTPS", os.getenv("XENFRA_ENFORCE_HTTPS", "auto")),
96
+ ("XENFRA_ENFORCE_WHITELIST", os.getenv("XENFRA_ENFORCE_WHITELIST", "auto")),
97
+ ("XENFRA_ENABLE_CERT_PINNING", os.getenv("XENFRA_ENABLE_CERT_PINNING", "auto")),
98
+ ("XENFRA_WARN_ON_HTTP", os.getenv("XENFRA_WARN_ON_HTTP", "true")),
99
+ ]
100
+
101
+ for var, value in env_vars:
102
+ console.print(f" {var}=[cyan]{value}[/cyan]")
103
+
104
+ console.print()
105
+
106
+ # Security recommendations
107
+ if not security_config.is_production():
108
+ console.print(
109
+ Panel(
110
+ "[yellow]⚠️ Development Mode Active[/yellow]\n\n"
111
+ "For production use:\n"
112
+ "1. Set XENFRA_ENV=production\n"
113
+ "2. Use HTTPS API URL\n"
114
+ "3. All security features will auto-enable",
115
+ title="Recommendation",
116
+ border_style="yellow"
117
+ )
118
+ )
119
+ else:
120
+ console.print(
121
+ Panel(
122
+ "[green]✅ Production Security Enabled[/green]\n\n"
123
+ "All security features are active.\n"
124
+ "Your credentials and data are protected.",
125
+ title="Status",
126
+ border_style="green"
127
+ )
128
+ )
129
+
130
+
131
+ @security.command()
132
+ @click.argument('url')
133
+ def validate(url):
134
+ """Validate an API URL against security policies."""
135
+ console.print(f"\n[cyan]Validating URL:[/cyan] {url}\n")
136
+
137
+ try:
138
+ validated_url = validate_and_get_api_url(url)
139
+ console.print(f"[bold green]✅ URL is valid and passed all security checks![/bold green]")
140
+ console.print(f"[dim]Validated URL: {validated_url}[/dim]")
141
+ except click.Abort:
142
+ console.print(f"[bold red]❌ URL failed security validation[/bold red]")
143
+ except Exception as e:
144
+ console.print(f"[bold red]❌ Validation error: {e}[/bold red]")
145
+
146
+
147
+ @security.command()
148
+ def docs():
149
+ """Show security documentation."""
150
+ docs_text = """
151
+ [bold cyan]Xenfra CLI Security Guide[/bold cyan]
152
+
153
+ [bold]Environment Detection:[/bold]
154
+ The CLI automatically adjusts security based on the environment:
155
+
156
+ • [green]production[/green]: All security features enforced
157
+ • [yellow]staging[/yellow]: HTTPS required, whitelist warnings
158
+ • [blue]development[/blue]: Permissive (localhost allowed)
159
+
160
+ [bold]Security Features:[/bold]
161
+
162
+ 1. [cyan]URL Validation[/cyan]
163
+ - Prevents malicious URL patterns
164
+ - Blocks URLs with embedded credentials
165
+ - Validates scheme (http/https only)
166
+
167
+ 2. [cyan]Domain Whitelist[/cyan]
168
+ - Restricts connections to approved domains
169
+ - Prevents credential theft via fake APIs
170
+ - Can be disabled for self-hosted instances
171
+
172
+ 3. [cyan]HTTPS Enforcement[/cyan]
173
+ - Requires encrypted connections in production
174
+ - Warns on insecure HTTP (non-localhost)
175
+ - Protects credentials and data in transit
176
+
177
+ 4. [cyan]Certificate Pinning[/cyan]
178
+ - Validates SSL certificate fingerprints
179
+ - Prevents man-in-the-middle attacks
180
+ - Optional (enabled in production by default)
181
+
182
+ [bold]Configuration Examples:[/bold]
183
+
184
+ [yellow]Development (default):[/yellow]
185
+ $ xenfra login
186
+ # Uses http://localhost:8000
187
+
188
+ [yellow]Self-hosted instance:[/yellow]
189
+ $ export XENFRA_API_URL=https://xenfra.mycompany.com
190
+ $ export XENFRA_ENFORCE_WHITELIST=false
191
+ $ xenfra login
192
+
193
+ [yellow]Production (strict):[/yellow]
194
+ $ export XENFRA_ENV=production
195
+ $ xenfra login
196
+ # All security features enabled
197
+
198
+ [bold]Environment Variables:[/bold]
199
+
200
+ XENFRA_ENV
201
+ Values: production | staging | development
202
+ Default: development
203
+
204
+ XENFRA_API_URL
205
+ Default: http://localhost:8000 (dev), https://api.xenfra.com (prod)
206
+
207
+ XENFRA_ENFORCE_HTTPS
208
+ Values: true | false
209
+ Default: false (dev), true (prod)
210
+
211
+ XENFRA_ENFORCE_WHITELIST
212
+ Values: true | false
213
+ Default: false (dev), true (prod)
214
+
215
+ XENFRA_ENABLE_CERT_PINNING
216
+ Values: true | false
217
+ Default: false (dev), true (prod)
218
+
219
+ XENFRA_WARN_ON_HTTP
220
+ Values: true | false
221
+ Default: true
222
+
223
+ [bold]Security Best Practices:[/bold]
224
+
225
+ 1. Always use HTTPS in production
226
+ 2. Never disable security features without understanding risks
227
+ 3. Keep whitelisted domains list updated
228
+ 4. Rotate credentials if you suspect compromise
229
+ 5. Use environment-specific configurations
230
+ 6. Enable all features for production deployments
231
+
232
+ [dim]For more information: https://docs.xenfra.com/security[/dim]
233
+ """
234
+
235
+ console.print(Panel(docs_text, border_style="cyan", padding=(1, 2)))
xenfra/main.py ADDED
@@ -0,0 +1,70 @@
1
+ """
2
+ Xenfra CLI - Main entry point.
3
+
4
+ A modern, AI-powered CLI for deploying Python apps to DigitalOcean.
5
+ """
6
+ import os
7
+
8
+ import click
9
+ from rich.console import Console
10
+
11
+ from .commands.auth import auth
12
+ from .commands.deployments import deploy, logs, status
13
+ from .commands.intelligence import analyze, diagnose, init
14
+ from .commands.projects import projects
15
+ from .commands.security_cmd import security
16
+
17
+ console = Console()
18
+
19
+
20
+ @click.group()
21
+ @click.version_option(version="0.2.3")
22
+ def cli():
23
+ """
24
+ Xenfra CLI: Deploy Python apps to DigitalOcean with zero configuration.
25
+
26
+ Quick Start:
27
+ xenfra auth login # Authenticate with Xenfra
28
+ xenfra init # Initialize your project (AI-powered)
29
+ xenfra deploy # Deploy to DigitalOcean
30
+
31
+ Commands:
32
+ auth Authentication (login, logout, whoami)
33
+ projects Manage projects (list, show, delete)
34
+ init Smart project initialization (AI-powered)
35
+ diagnose Diagnose deployment failures (AI-powered)
36
+ analyze Analyze codebase without creating config
37
+
38
+ For help on a specific command:
39
+ xenfra <command> --help
40
+ """
41
+ # Configure keyring backend
42
+ os.environ["KEYRING_BACKEND"] = "keyrings.alt.file.PlaintextKeyring"
43
+
44
+ # Security works silently in the background
45
+ # Only shows warnings if there's an actual security issue
46
+
47
+
48
+ # Register command groups
49
+ cli.add_command(auth)
50
+ cli.add_command(projects)
51
+ cli.add_command(security)
52
+
53
+ # Register intelligence commands at root level
54
+ cli.add_command(init)
55
+ cli.add_command(diagnose)
56
+ cli.add_command(analyze)
57
+
58
+ # Register deployment commands at root level
59
+ cli.add_command(deploy)
60
+ cli.add_command(status)
61
+ cli.add_command(logs)
62
+
63
+
64
+ def main():
65
+ """Main entry point."""
66
+ cli()
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
@@ -0,0 +1,3 @@
1
+ """
2
+ Utility functions for Xenfra CLI.
3
+ """
xenfra/utils/auth.py ADDED
@@ -0,0 +1,148 @@
1
+ """
2
+ Authentication utilities for Xenfra CLI.
3
+ Handles OAuth2 PKCE flow and token management.
4
+ """
5
+ import os
6
+ from http.server import BaseHTTPRequestHandler, HTTPServer
7
+ from urllib.parse import parse_qs, urlparse
8
+
9
+ import httpx
10
+ import keyring
11
+ from rich.console import Console
12
+
13
+ from .security import validate_and_get_api_url
14
+
15
+ console = Console()
16
+
17
+ # Get validated API URL (includes all security checks)
18
+ API_BASE_URL = validate_and_get_api_url()
19
+ SERVICE_ID = "xenfra"
20
+
21
+ # CLI OAuth2 Configuration
22
+ CLI_CLIENT_ID = "xenfra-cli"
23
+ CLI_REDIRECT_PATH = "/auth/callback"
24
+ CLI_LOCAL_SERVER_START_PORT = 8001
25
+ CLI_LOCAL_SERVER_END_PORT = 8005
26
+
27
+ # Global storage for OAuth callback data
28
+ oauth_data = {"code": None, "state": None, "error": None}
29
+
30
+
31
+ class AuthCallbackHandler(BaseHTTPRequestHandler):
32
+ """HTTP handler for OAuth redirect callback."""
33
+
34
+ def do_GET(self):
35
+ global oauth_data
36
+ self.send_response(200)
37
+ self.send_header("Content-type", "text/html")
38
+ self.end_headers()
39
+
40
+ query_params = parse_qs(urlparse(self.path).query)
41
+
42
+ if "code" in query_params:
43
+ oauth_data["code"] = query_params["code"][0]
44
+ oauth_data["state"] = query_params["state"][0] if "state" in query_params else None
45
+ self.wfile.write(
46
+ b"<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>"
47
+ )
48
+ elif "error" in query_params:
49
+ oauth_data["error"] = query_params["error"][0]
50
+ self.wfile.write(
51
+ f"<html><body><h1>Authentication failed!</h1><p>Error: {oauth_data['error']}</p></body></html>".encode()
52
+ )
53
+ else:
54
+ self.wfile.write(
55
+ b"<html><body><h1>Authentication callback received.</h1><p>Waiting for code...</p></body></html>"
56
+ )
57
+
58
+ # Shut down the server after processing
59
+ self.server.shutdown() # type: ignore
60
+
61
+
62
+ def run_local_oauth_server(port: int, redirect_path: str):
63
+ """Start a local HTTP server to capture the OAuth redirect."""
64
+ server_address = ("127.0.0.1", port)
65
+ httpd = HTTPServer(server_address, AuthCallbackHandler)
66
+ httpd.timeout = 30 # seconds
67
+ console.print(
68
+ f"[dim]Listening for OAuth redirect on http://localhost:{port}{redirect_path}...[/dim]"
69
+ )
70
+
71
+ # Store the server instance in the handler for shutdown
72
+ AuthCallbackHandler.server = httpd # type: ignore
73
+
74
+ # Handle a single request (blocking call)
75
+ httpd.handle_request()
76
+ console.print("[dim]Local OAuth server shut down.[/dim]")
77
+
78
+
79
+ def get_auth_token() -> str | None:
80
+ """
81
+ Retrieve a valid access token, refreshing it if necessary.
82
+
83
+ Returns:
84
+ Valid access token or None if not authenticated
85
+ """
86
+ access_token = keyring.get_password(SERVICE_ID, "access_token")
87
+ refresh_token = keyring.get_password(SERVICE_ID, "refresh_token")
88
+
89
+ if not access_token:
90
+ return None
91
+
92
+ # Check if access token is expired
93
+ try:
94
+ from jose import JWTError, jwt
95
+
96
+ # Decode without verifying signature to check expiration
97
+ claims = jwt.decode(access_token, options={"verify_signature": False, "verify_exp": True})
98
+ except JWTError:
99
+ claims = None
100
+ except Exception as e:
101
+ console.print(f"[dim]Error decoding access token: {e}[/dim]")
102
+ claims = None
103
+
104
+ # Refresh token if expired
105
+ if not claims and refresh_token:
106
+ console.print("[dim]Access token expired. Attempting to refresh...[/dim]")
107
+ try:
108
+ with httpx.Client() as client:
109
+ response = client.post(
110
+ f"{API_BASE_URL}/auth/refresh",
111
+ data={"refresh_token": refresh_token, "client_id": CLI_CLIENT_ID},
112
+ )
113
+ response.raise_for_status()
114
+ token_data = response.json()
115
+ new_access_token = token_data.get("access_token")
116
+ new_refresh_token = token_data.get("refresh_token")
117
+
118
+ if new_access_token:
119
+ keyring.set_password(SERVICE_ID, "access_token", new_access_token)
120
+ if new_refresh_token:
121
+ keyring.set_password(SERVICE_ID, "refresh_token", new_refresh_token)
122
+ console.print("[bold green]Token refreshed successfully.[/bold green]")
123
+ return new_access_token
124
+ else:
125
+ console.print("[bold red]Failed to get new access token.[/bold red]")
126
+ return None
127
+ except httpx.HTTPStatusError as exc:
128
+ if exc.response.status_code == 400:
129
+ console.print("[bold red]Refresh token expired. Please log in again.[/bold red]")
130
+ else:
131
+ console.print(f"[bold red]Token refresh failed: {exc.response.status_code}[/bold red]")
132
+ keyring.delete_password(SERVICE_ID, "access_token")
133
+ keyring.delete_password(SERVICE_ID, "refresh_token")
134
+ return None
135
+ except httpx.RequestError as exc:
136
+ console.print(f"[bold red]Token refresh failed: {exc}[/bold red]")
137
+ return None
138
+
139
+ return access_token
140
+
141
+
142
+ def clear_tokens():
143
+ """Clear stored access and refresh tokens."""
144
+ try:
145
+ keyring.delete_password(SERVICE_ID, "access_token")
146
+ keyring.delete_password(SERVICE_ID, "refresh_token")
147
+ except keyring.errors.PasswordDeleteError:
148
+ pass # Tokens already cleared
@@ -0,0 +1,86 @@
1
+ """
2
+ Codebase scanning utilities for AI-powered project initialization.
3
+ """
4
+ import os
5
+ from pathlib import Path
6
+
7
+
8
+ def scan_codebase(max_files: int = 10, max_size: int = 50000) -> dict[str, str]:
9
+ """
10
+ Scan current directory for important code files.
11
+
12
+ Args:
13
+ max_files: Maximum number of files to include
14
+ max_size: Maximum file size in bytes (default 50KB)
15
+
16
+ Returns:
17
+ Dictionary of filename -> content for AI analysis
18
+ """
19
+ code_snippets = {}
20
+
21
+ # Priority files to scan (in order)
22
+ important_files = [
23
+ # Python entry points
24
+ 'main.py', 'app.py', 'wsgi.py', 'asgi.py', 'manage.py',
25
+ # Configuration files
26
+ 'requirements.txt', 'pyproject.toml', 'Pipfile', 'setup.py',
27
+ # Django/Flask specific
28
+ 'settings.py', 'config.py',
29
+ # Docker
30
+ 'Dockerfile', 'docker-compose.yml',
31
+ # Xenfra config
32
+ 'xenfra.yaml', 'xenfra.yml',
33
+ ]
34
+
35
+ # Scan for important files in current directory
36
+ for filename in important_files:
37
+ if len(code_snippets) >= max_files:
38
+ break
39
+
40
+ if os.path.exists(filename) and os.path.isfile(filename):
41
+ try:
42
+ file_size = os.path.getsize(filename)
43
+ if file_size > max_size:
44
+ continue
45
+
46
+ with open(filename, 'r', encoding='utf-8') as f:
47
+ content = f.read(max_size)
48
+ code_snippets[filename] = content
49
+ except (IOError, UnicodeDecodeError):
50
+ # Skip files that can't be read
51
+ continue
52
+
53
+ # If we haven't found enough files, look for Python files in common locations
54
+ if len(code_snippets) < 3:
55
+ search_patterns = [
56
+ 'src/**/*.py',
57
+ 'app/**/*.py',
58
+ '*.py',
59
+ ]
60
+
61
+ for pattern in search_patterns:
62
+ if len(code_snippets) >= max_files:
63
+ break
64
+
65
+ for filepath in Path('.').glob(pattern):
66
+ if len(code_snippets) >= max_files:
67
+ break
68
+
69
+ if filepath.is_file() and filepath.name not in code_snippets:
70
+ try:
71
+ file_size = filepath.stat().st_size
72
+ if file_size > max_size:
73
+ continue
74
+
75
+ with open(filepath, 'r', encoding='utf-8') as f:
76
+ content = f.read(max_size)
77
+ code_snippets[str(filepath)] = content
78
+ except (IOError, UnicodeDecodeError):
79
+ continue
80
+
81
+ return code_snippets
82
+
83
+
84
+ def has_xenfra_config() -> bool:
85
+ """Check if xenfra.yaml already exists."""
86
+ return os.path.exists('xenfra.yaml') or os.path.exists('xenfra.yml')