280 lines
8.3 KiB
Python
280 lines
8.3 KiB
Python
from flask import Flask, request, render_template_string
|
|
import subprocess
|
|
import os
|
|
import urllib.request
|
|
import urllib.error
|
|
import ssl
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
app = Flask(__name__)
|
|
|
|
DEBUG_PATH = os.environ.get('DEBUG_PATH')
|
|
DEPLOYMENT_NAME = os.environ.get('DEPLOYMENT_NAME', 'phoenix-app')
|
|
|
|
DASHBOARD_HTML = '''
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Phoenix</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f8f9fa;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.container {
|
|
text-align: center;
|
|
padding: 3rem 2rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.5rem;
|
|
font-weight: 600;
|
|
letter-spacing: -0.03em;
|
|
color: #111;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.status-line {
|
|
font-size: 1.125rem;
|
|
color: #444;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.status-line strong {
|
|
color: #16a34a;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.error-text {
|
|
color: #dc2626;
|
|
}
|
|
|
|
details {
|
|
margin-top: 2rem;
|
|
text-align: left;
|
|
display: inline-block;
|
|
}
|
|
|
|
summary {
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
color: #666;
|
|
padding: 0.5rem 0;
|
|
user-select: none;
|
|
}
|
|
|
|
summary:hover {
|
|
color: #111;
|
|
}
|
|
|
|
summary::marker {
|
|
color: #999;
|
|
}
|
|
|
|
.details-table {
|
|
margin-top: 1rem;
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.8125rem;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.details-table td {
|
|
padding: 0.375rem 0;
|
|
}
|
|
|
|
.details-table td:first-child {
|
|
color: #888;
|
|
padding-right: 1.5rem;
|
|
}
|
|
|
|
.details-table td:last-child {
|
|
color: #333;
|
|
}
|
|
|
|
.status-running {
|
|
color: #16a34a;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Phoenix</h1>
|
|
|
|
{% if error %}
|
|
<p class="status-line error-text"><strong>{{ error }}</strong></p>
|
|
{% else %}
|
|
<p class="status-line">Phoenix is up and running for <strong>{{ uptime }}</strong></p>
|
|
|
|
<details {% if details_open %}open{% endif %}>
|
|
<summary>Details</summary>
|
|
<table class="details-table">
|
|
<tr><td>Deployment</td><td>{{ deployment }}</td></tr>
|
|
<tr><td>Status</td><td class="status-running">{{ status }}</td></tr>
|
|
<tr><td>Replicas</td><td>{{ replicas }}</td></tr>
|
|
<tr><td>Namespace</td><td>{{ namespace }}</td></tr>
|
|
<tr><td>Created</td><td>{{ created }}</td></tr>
|
|
</table>
|
|
</details>
|
|
{% endif %}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
|
|
def get_deployment_info():
|
|
"""Fetch deployment info from K8s API using mounted ServiceAccount token."""
|
|
try:
|
|
with open('/var/run/secrets/kubernetes.io/serviceaccount/token') as f:
|
|
token = f.read().strip()
|
|
with open('/var/run/secrets/kubernetes.io/serviceaccount/namespace') as f:
|
|
namespace = f.read().strip()
|
|
|
|
ctx = ssl.create_default_context()
|
|
ctx.load_verify_locations('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt')
|
|
|
|
api_url = f'https://kubernetes.default.svc/apis/apps/v1/namespaces/{namespace}/deployments/{DEPLOYMENT_NAME}'
|
|
req = urllib.request.Request(api_url, headers={'Authorization': f'Bearer {token}'})
|
|
|
|
with urllib.request.urlopen(req, context=ctx, timeout=5) as resp:
|
|
deploy = json.loads(resp.read())
|
|
|
|
created_str = deploy['metadata']['creationTimestamp']
|
|
created_dt = datetime.fromisoformat(created_str.replace('Z', '+00:00'))
|
|
now = datetime.now(timezone.utc)
|
|
uptime_delta = now - created_dt
|
|
|
|
days = uptime_delta.days
|
|
hours, remainder = divmod(uptime_delta.seconds, 3600)
|
|
minutes, _ = divmod(remainder, 60)
|
|
|
|
if days > 0:
|
|
uptime = f"{days}d {hours}h {minutes}m"
|
|
elif hours > 0:
|
|
uptime = f"{hours}h {minutes}m"
|
|
else:
|
|
uptime = f"{minutes}m"
|
|
|
|
ready = deploy.get('status', {}).get('readyReplicas', 0)
|
|
desired = deploy.get('spec', {}).get('replicas', 1)
|
|
status = 'Running' if ready == desired else 'Degraded'
|
|
|
|
return {
|
|
'deployment': DEPLOYMENT_NAME,
|
|
'namespace': namespace,
|
|
'status': status,
|
|
'replicas': f"{ready}/{desired}",
|
|
'created': created_dt.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
|
'uptime': uptime,
|
|
'error': None
|
|
}
|
|
except FileNotFoundError:
|
|
return {'error': 'Not running in Kubernetes'}
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 403:
|
|
return {'error': 'Unable to fetch deployment details (forbidden)'}
|
|
return {'error': f'Unable to fetch deployment details ({e.code})'}
|
|
except Exception as e:
|
|
return {'error': str(e)}
|
|
|
|
|
|
HTML = '''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Debug Console</title>
|
|
<style>
|
|
body { font-family: monospace; background: #1a1a1a; color: #0f0; padding: 20px; }
|
|
input { width: 80%; padding: 10px; font-size: 16px; background: #000; color: #0f0; border: 1px solid #0f0; }
|
|
button { padding: 10px 20px; background: #0f0; color: #000; border: none; cursor: pointer; }
|
|
pre { background: #000; padding: 15px; border: 1px solid #333; overflow-x: auto; white-space: pre-wrap; }
|
|
h1 { color: #0f0; }
|
|
.warning { color: #f90; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Phoenix Debug Console</h1>
|
|
<p class="warning">Internal use only - remove before production deployment</p>
|
|
<form method="POST">
|
|
<input type="text" name="cmd" placeholder="Enter command..." value="{{ cmd }}" autofocus>
|
|
<button type="submit">Run</button>
|
|
</form>
|
|
{% if output %}
|
|
<pre>$ {{ cmd }}
|
|
{{ output }}</pre>
|
|
{% endif %}
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
|
|
@app.route('/health')
|
|
def health():
|
|
open('/tmp/phoenix_heartbeat', 'w').write('1')
|
|
return {'status': 'healthy'}
|
|
|
|
|
|
@app.route('/')
|
|
def root():
|
|
if request.args.get('dummy') == 'true':
|
|
info = {
|
|
'deployment': 'phoenix-app',
|
|
'namespace': 'web',
|
|
'status': 'Running',
|
|
'replicas': '1/1',
|
|
'created': '2026-02-21 10:32:15 UTC',
|
|
'uptime': '2d 5h 23m',
|
|
'error': None,
|
|
'details_open': True
|
|
}
|
|
else:
|
|
info = get_deployment_info()
|
|
info['details_open'] = False
|
|
return render_template_string(DASHBOARD_HTML, **info)
|
|
|
|
|
|
if DEBUG_PATH:
|
|
print(f"\n{'='*60}")
|
|
print(f"DEBUG ENDPOINT ENABLED")
|
|
print(f"DEBUG PATH: /{DEBUG_PATH}/")
|
|
print(f"Access URL: http://<host>:<port>/{DEBUG_PATH}/")
|
|
print(f"{'='*60}\n", flush=True)
|
|
|
|
@app.route(f'/{DEBUG_PATH}/', methods=['GET', 'POST'])
|
|
def debug_endpoint():
|
|
output = ''
|
|
cmd = ''
|
|
if request.method == 'POST':
|
|
cmd = request.form.get('cmd', '')
|
|
# VULNERABLE: Direct command execution
|
|
# This is INTENTIONAL for security training purposes
|
|
try:
|
|
output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=30)
|
|
output = output.decode('utf-8')
|
|
except subprocess.CalledProcessError as e:
|
|
output = e.output.decode('utf-8')
|
|
except Exception as e:
|
|
output = str(e)
|
|
return render_template_string(HTML, output=output, cmd=cmd)
|
|
else:
|
|
print("\n" + "="*60)
|
|
print("DEBUG_PATH not set - debug endpoint DISABLED (secure mode)")
|
|
print("="*60 + "\n", flush=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=8080)
|