iitian commited on
Commit
1b47063
·
1 Parent(s): f1a1961

feat: add interactive Cloud Security Dashboard UI

Browse files

- Added premium dark-mode dashboard with glassmorphism theme
- Sidebar task selector (Easy/Medium/Hard)
- Infrastructure overview with animated resource cards
- Vulnerability highlighting for public S3 & open RDP ports
- Terminal-style execution log with manual action input
- Smart command parser (list, describe, logs, submit)
- Updated FastAPI to serve static files and added /state endpoint

server/app.py CHANGED
@@ -1,15 +1,29 @@
 
 
 
1
  from openenv_core.env_server import create_fastapi_app
2
  from .models import CloudAction, CloudObservation
3
  from .environment import CloudAuditEnv
 
4
 
5
  # Initialize the environment
6
  env = CloudAuditEnv()
7
 
8
- # Create the FastAPI app
9
- # Note: create_fastapi_app expects the environment instance,
10
- # and the Action/Observation models for typing.
11
  app = create_fastapi_app(env, CloudAction, CloudObservation)
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  if __name__ == "__main__":
14
  import uvicorn
15
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ from fastapi import FastAPI
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse
4
  from openenv_core.env_server import create_fastapi_app
5
  from .models import CloudAction, CloudObservation
6
  from .environment import CloudAuditEnv
7
+ import os
8
 
9
  # Initialize the environment
10
  env = CloudAuditEnv()
11
 
12
+ # Create the FastAPI app using openenv-core
 
 
13
  app = create_fastapi_app(env, CloudAction, CloudObservation)
14
 
15
+ # Add custom routes for the UI
16
+ static_dir = os.path.join(os.path.dirname(__file__), "static")
17
+ app.mount("/ui", StaticFiles(directory=static_dir), name="static")
18
+
19
+ @app.get("/")
20
+ async def read_index():
21
+ return FileResponse(os.path.join(static_dir, "index.html"))
22
+
23
+ @app.get("/state")
24
+ async def get_state():
25
+ return env.state()
26
+
27
  if __name__ == "__main__":
28
  import uvicorn
29
  uvicorn.run(app, host="0.0.0.0", port=8000)
server/static/app.js ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ let currentTaskId = 'easy';
3
+ let currentReward = 0.0;
4
+
5
+ const taskBtns = document.querySelectorAll('.task-btn');
6
+ const resetBtn = document.getElementById('reset-btn');
7
+ const clearLogBtn = document.getElementById('clear-log');
8
+ const resourceDisplay = document.getElementById('resource-display');
9
+ const actionLog = document.getElementById('action-log');
10
+ const manualInput = document.getElementById('manual-input');
11
+ const runBtn = document.getElementById('run-action');
12
+ const currentTaskDisplay = document.getElementById('current-task-display');
13
+ const currentRewardDisplay = document.getElementById('current-reward');
14
+ const envStatus = document.getElementById('env-status');
15
+ const resourceCount = document.getElementById('resource-count');
16
+
17
+ // --- Core API Functions ---
18
+
19
+ async function resetEnv(taskId) {
20
+ log(`Resetting environment for task: ${taskId}...`, 'system');
21
+ envStatus.textContent = 'Resetting...';
22
+ envStatus.className = 'status-badge';
23
+ currentReward = 0.0;
24
+ currentRewardDisplay.textContent = '0.00';
25
+ try {
26
+ const response = await fetch('/reset', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ task_id: taskId })
30
+ });
31
+ const data = await response.json();
32
+ const obs = data.observation || data;
33
+ updateUIFromObs(obs, false, data);
34
+ log(`Environment ready. ${obs.info || ''}`, 'system');
35
+ showToast(`✅ Task "${taskId}" loaded`);
36
+ } catch (err) {
37
+ log(`Error resetting: ${err.message}`, 'error');
38
+ envStatus.textContent = 'Error';
39
+ }
40
+ }
41
+
42
+ async function stepEnv(actionObj) {
43
+ log(`▶ Executing: ${formatAction(actionObj)}`, 'action');
44
+ envStatus.textContent = 'Processing...';
45
+ try {
46
+ const response = await fetch('/step', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ action: actionObj })
50
+ });
51
+ const data = await response.json();
52
+ const obs = data.observation || data;
53
+ const reward = data.reward !== undefined ? data.reward : (obs.reward || 0);
54
+ const done = data.done !== undefined ? data.done : (obs.done || false);
55
+ updateUIFromObs(obs, true, data);
56
+
57
+ if (obs.status) {
58
+ log(`Response: ${obs.status}`, 'system');
59
+ }
60
+ if (obs.info) {
61
+ log(`ℹ️ ${obs.info}`, 'system');
62
+ }
63
+ if (reward > 0) {
64
+ log(`🏆 Reward: +${reward.toFixed(2)}`, 'reward');
65
+ }
66
+ if (done) {
67
+ log(`🎯 Task Completed!`, 'reward');
68
+ showToast('🎯 Task Completed Successfully!');
69
+ }
70
+ } catch (err) {
71
+ log(`❌ Execution failed: ${err.message}`, 'error');
72
+ envStatus.textContent = 'Error';
73
+ }
74
+ }
75
+
76
+ // --- UI Update ---
77
+
78
+ function updateUIFromObs(obs, isStep, data) {
79
+ const reward = data ? (data.reward !== undefined ? data.reward : 0) : 0;
80
+ const done = data ? (data.done !== undefined ? data.done : false) : false;
81
+
82
+ // Update reward
83
+ if (isStep && reward !== undefined) {
84
+ currentReward += reward;
85
+ currentRewardDisplay.textContent = currentReward.toFixed(2);
86
+ if (currentReward > 0) {
87
+ currentRewardDisplay.classList.add('reward-positive');
88
+ }
89
+ }
90
+
91
+ // Update status badge
92
+ if (done) {
93
+ envStatus.textContent = '✓ Completed';
94
+ envStatus.className = 'status-badge completed';
95
+ } else {
96
+ envStatus.textContent = 'Active';
97
+ envStatus.className = 'status-badge active';
98
+ }
99
+
100
+ // Update Resources
101
+ const resources = obs.resources || [];
102
+ if (resources.length > 0) {
103
+ resourceCount.textContent = `${resources.length} Resources`;
104
+ resourceDisplay.innerHTML = resources.map(r => createResourceCard(r)).join('');
105
+ // Animate cards in
106
+ document.querySelectorAll('.resource-card').forEach((card, i) => {
107
+ card.style.animationDelay = `${i * 0.08}s`;
108
+ card.classList.add('fade-in');
109
+ });
110
+ }
111
+
112
+ // Update Details (for describe actions)
113
+ if (obs.details) {
114
+ const detail = obs.details;
115
+ resourceCount.textContent = '1 Resource (Detail)';
116
+ resourceDisplay.innerHTML = createDetailCard(detail);
117
+ }
118
+
119
+ // Update Logs (for log actions)
120
+ if (obs.logs && obs.logs.length > 0) {
121
+ obs.logs.forEach(entry => {
122
+ const color = entry.action === 'DeleteStorage' ? 'error' : 'system';
123
+ log(` [${entry.timestamp}] ${entry.user} → ${entry.action} (IP: ${entry.ip})`, color);
124
+ });
125
+ }
126
+
127
+ actionLog.scrollTop = actionLog.scrollHeight;
128
+ }
129
+
130
+ function createResourceCard(r) {
131
+ const isVulnerable = r.public === true || hasOpenPorts(r);
132
+ const type = detectResourceType(r);
133
+ const id = r.id || 'unknown';
134
+
135
+ let tagsHtml = '';
136
+ if (r.tags) {
137
+ tagsHtml = Object.entries(r.tags).map(([k, v]) =>
138
+ `<span class="tag ${k === 'env' && v === 'prod' ? 'env-prod' : ''}">${k}: ${v}</span>`
139
+ ).join('');
140
+ }
141
+ if (r.public) {
142
+ tagsHtml += '<span class="tag status-public">⚠ PUBLIC</span>';
143
+ }
144
+ if (r.state) {
145
+ tagsHtml += `<span class="tag">${r.state}</span>`;
146
+ }
147
+ if (r.region) {
148
+ tagsHtml += `<span class="tag">${r.region}</span>`;
149
+ }
150
+
151
+ // Security groups info for EC2
152
+ let sgHtml = '';
153
+ if (r.security_groups) {
154
+ const ports = r.security_groups.flatMap(sg => sg.rules.map(rule => rule.port));
155
+ const dangerPorts = [3389, 445, 23]; // RDP, SMB, Telnet
156
+ const openDangerPorts = ports.filter(p => dangerPorts.includes(p));
157
+ if (openDangerPorts.length > 0) {
158
+ sgHtml = `<div class="sg-warning">🔓 Dangerous ports open: ${openDangerPorts.join(', ')}</div>`;
159
+ } else {
160
+ sgHtml = `<div class="sg-info">🔒 Ports: ${ports.join(', ')}</div>`;
161
+ }
162
+ }
163
+
164
+ return `
165
+ <div class="resource-card ${isVulnerable ? 'vulnerable' : 'secure'}">
166
+ <div class="card-status-dot ${isVulnerable ? 'dot-danger' : 'dot-safe'}"></div>
167
+ <span class="resource-type">${type}</span>
168
+ <span class="resource-id">${id}</span>
169
+ ${sgHtml}
170
+ <div class="resource-tags">${tagsHtml}</div>
171
+ </div>
172
+ `;
173
+ }
174
+
175
+ function createDetailCard(detail) {
176
+ return `
177
+ <div class="resource-card detail-card">
178
+ <span class="resource-type">${detectResourceType(detail)}</span>
179
+ <span class="resource-id">${detail.id}</span>
180
+ <pre class="detail-json">${JSON.stringify(detail, null, 2)}</pre>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ function detectResourceType(r) {
186
+ if (r.id && r.id.startsWith('i-')) return 'EC2 Instance';
187
+ if (r.id && r.id.includes('prod-')) return 'S3 Bucket';
188
+ if (r.id && r.id.includes('dev-')) return 'S3 Bucket';
189
+ if (r.type) return r.type.toUpperCase();
190
+ return 'Resource';
191
+ }
192
+
193
+ function hasOpenPorts(r) {
194
+ if (!r.security_groups) return false;
195
+ return r.security_groups.some(sg =>
196
+ sg.rules.some(rule => rule.port === 3389 && rule.cidr === '0.0.0.0/0')
197
+ );
198
+ }
199
+
200
+ function formatAction(obj) {
201
+ let str = obj.action;
202
+ if (obj.resource_type) str += ` ${obj.resource_type}`;
203
+ if (obj.resource_id) str += ` → ${obj.resource_id}`;
204
+ if (obj.answer) str += ` (answer: ${obj.answer})`;
205
+ return str;
206
+ }
207
+
208
+ // --- Logging ---
209
+
210
+ function log(msg, type = 'system') {
211
+ const div = document.createElement('div');
212
+ div.className = `log-entry ${type}`;
213
+ const time = new Date().toLocaleTimeString([], {
214
+ hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
215
+ });
216
+ div.textContent = `[${time}] ${msg}`;
217
+ actionLog.appendChild(div);
218
+ actionLog.scrollTop = actionLog.scrollHeight;
219
+ }
220
+
221
+ function showToast(msg) {
222
+ const toast = document.getElementById('toast');
223
+ toast.textContent = msg;
224
+ toast.classList.remove('hidden');
225
+ toast.classList.add('show');
226
+ setTimeout(() => {
227
+ toast.classList.remove('show');
228
+ toast.classList.add('hidden');
229
+ }, 3000);
230
+ }
231
+
232
+ // --- Manual Action Parser ---
233
+ // Supports: list s3, list ec2, describe <id>, logs <name>, submit <answer>
234
+ function parseManualAction(input) {
235
+ const parts = input.trim().split(/\s+/);
236
+ const cmd = parts[0]?.toLowerCase();
237
+
238
+ switch (cmd) {
239
+ case 'list':
240
+ return { action: 'list', resource_type: parts[1] || 's3' };
241
+ case 'describe':
242
+ return { action: 'describe', resource_id: parts.slice(1).join(' ') };
243
+ case 'logs':
244
+ return { action: 'logs', resource_id: parts.slice(1).join(' ') || 'auth-logs' };
245
+ case 'submit':
246
+ return { action: 'submit', answer: parts.slice(1).join(' ') };
247
+ case 'modify':
248
+ // For modify, just pass raw — advanced users can use JSON
249
+ return { action: 'modify', resource_id: parts[1], patch: {} };
250
+ default:
251
+ return { action: cmd, resource_type: parts[1] || '' };
252
+ }
253
+ }
254
+
255
+ // --- Event Listeners ---
256
+
257
+ taskBtns.forEach(btn => {
258
+ btn.addEventListener('click', () => {
259
+ taskBtns.forEach(b => b.classList.remove('active'));
260
+ btn.classList.add('active');
261
+ currentTaskId = btn.dataset.task;
262
+ currentTaskDisplay.textContent = btn.querySelector('.task-name').textContent;
263
+ resourceDisplay.innerHTML = '<div class="empty-state">No resources discovered. Run "list" to see resources.</div>';
264
+ resourceCount.textContent = '0 Resources';
265
+ resetEnv(currentTaskId);
266
+ });
267
+ });
268
+
269
+ resetBtn.addEventListener('click', () => {
270
+ resourceDisplay.innerHTML = '<div class="empty-state">No resources discovered. Run "list" to see resources.</div>';
271
+ resourceCount.textContent = '0 Resources';
272
+ resetEnv(currentTaskId);
273
+ });
274
+
275
+ clearLogBtn.addEventListener('click', () => {
276
+ actionLog.innerHTML = '';
277
+ log('Log cleared.', 'system');
278
+ });
279
+
280
+ runBtn.addEventListener('click', () => {
281
+ const val = manualInput.value.trim();
282
+ if (!val) return;
283
+ const actionObj = parseManualAction(val);
284
+ stepEnv(actionObj);
285
+ manualInput.value = '';
286
+ });
287
+
288
+ manualInput.addEventListener('keydown', (e) => {
289
+ if (e.key === 'Enter') {
290
+ e.preventDefault();
291
+ runBtn.click();
292
+ }
293
+ });
294
+
295
+ // --- Initial Load ---
296
+ currentTaskDisplay.textContent = 'S3 Public Audit';
297
+ resetEnv('easy');
298
+ });
server/static/index.css ADDED
@@ -0,0 +1,650 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-dark: #0a0e14;
3
+ --bg-card: #141b25;
4
+ --bg-surface: #1e293b;
5
+ --primary: #00f5ff;
6
+ --primary-glow: rgba(0, 245, 255, 0.3);
7
+ --secondary: #94a3b8;
8
+ --accent: #7c3aed;
9
+ --success: #10b981;
10
+ --warning: #f59e0b;
11
+ --danger: #ef4444;
12
+ --text-main: #f8fafc;
13
+ --text-muted: #94a3b8;
14
+ --neon-shadow: 0 0 15px rgba(0, 245, 255, 0.35);
15
+ --danger-shadow: 0 0 15px rgba(239, 68, 68, 0.3);
16
+ --glass-bg: rgba(20, 27, 37, 0.7);
17
+ --border: rgba(255, 255, 255, 0.08);
18
+ --radius: 14px;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Inter', sans-serif;
26
+ }
27
+
28
+ body {
29
+ background-color: var(--bg-dark);
30
+ color: var(--text-main);
31
+ overflow: hidden;
32
+ min-height: 100vh;
33
+ }
34
+
35
+ h1, h2, h3 {
36
+ font-family: 'Outfit', sans-serif;
37
+ }
38
+
39
+ /* ===== Layout ===== */
40
+ .app-container {
41
+ display: grid;
42
+ grid-template-columns: 280px 1fr;
43
+ height: 100vh;
44
+ }
45
+
46
+ /* ===== Sidebar ===== */
47
+ .sidebar {
48
+ background: var(--bg-card);
49
+ border-right: 1px solid var(--border);
50
+ display: flex;
51
+ flex-direction: column;
52
+ padding: 2rem 1.25rem;
53
+ position: relative;
54
+ overflow: hidden;
55
+ }
56
+
57
+ .sidebar::before {
58
+ content: '';
59
+ position: absolute;
60
+ top: -50%;
61
+ left: -50%;
62
+ width: 200%;
63
+ height: 200%;
64
+ background: radial-gradient(ellipse at 30% 20%, rgba(0, 245, 255, 0.03) 0%, transparent 50%);
65
+ pointer-events: none;
66
+ }
67
+
68
+ .brand {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 0.75rem;
72
+ margin-bottom: 2.5rem;
73
+ position: relative;
74
+ }
75
+
76
+ .logo {
77
+ font-size: 1.6rem;
78
+ filter: drop-shadow(0 0 6px rgba(0, 245, 255, 0.5));
79
+ }
80
+
81
+ .brand h1 {
82
+ font-size: 1.3rem;
83
+ letter-spacing: -0.5px;
84
+ color: var(--text-main);
85
+ }
86
+
87
+ .brand span {
88
+ color: var(--primary);
89
+ font-weight: 300;
90
+ }
91
+
92
+ .nav-label {
93
+ font-size: 0.7rem;
94
+ text-transform: uppercase;
95
+ letter-spacing: 1.5px;
96
+ color: var(--text-muted);
97
+ margin-bottom: 1rem;
98
+ font-weight: 600;
99
+ }
100
+
101
+ .task-btn {
102
+ width: 100%;
103
+ background: transparent;
104
+ border: 1px solid var(--border);
105
+ padding: 1rem 1.15rem;
106
+ border-radius: var(--radius);
107
+ margin-bottom: 0.65rem;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: flex-start;
111
+ cursor: pointer;
112
+ transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
113
+ position: relative;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .task-btn::after {
118
+ content: '';
119
+ position: absolute;
120
+ inset: 0;
121
+ background: linear-gradient(135deg, transparent 60%, rgba(0, 245, 255, 0.05));
122
+ opacity: 0;
123
+ transition: opacity 0.3s;
124
+ }
125
+
126
+ .task-btn:hover {
127
+ border-color: var(--primary);
128
+ transform: translateY(-2px);
129
+ }
130
+
131
+ .task-btn:hover::after {
132
+ opacity: 1;
133
+ }
134
+
135
+ .task-btn.active {
136
+ background: linear-gradient(135deg, var(--primary), #00c8d4);
137
+ border-color: transparent;
138
+ box-shadow: var(--neon-shadow);
139
+ transform: translateY(-1px);
140
+ }
141
+
142
+ .difficulty {
143
+ font-size: 0.65rem;
144
+ font-weight: 800;
145
+ text-transform: uppercase;
146
+ letter-spacing: 0.5px;
147
+ margin-bottom: 0.3rem;
148
+ }
149
+
150
+ .difficulty.easy { color: var(--success); }
151
+ .difficulty.medium { color: var(--warning); }
152
+ .difficulty.hard { color: var(--danger); }
153
+
154
+ .task-btn.active .difficulty,
155
+ .task-btn.active .task-name {
156
+ color: #0a0e14;
157
+ }
158
+
159
+ .task-name {
160
+ font-weight: 600;
161
+ font-size: 0.92rem;
162
+ color: var(--text-main);
163
+ }
164
+
165
+ .sidebar-footer {
166
+ margin-top: auto;
167
+ padding-top: 1.5rem;
168
+ }
169
+
170
+ .primary-btn {
171
+ width: 100%;
172
+ padding: 0.9rem;
173
+ border-radius: var(--radius);
174
+ border: none;
175
+ background: linear-gradient(135deg, var(--primary), #00c8d4);
176
+ color: #0a0e14;
177
+ font-weight: 700;
178
+ font-size: 0.9rem;
179
+ cursor: pointer;
180
+ font-family: 'Outfit', sans-serif;
181
+ letter-spacing: 0.3px;
182
+ transition: all 0.2s;
183
+ }
184
+
185
+ .primary-btn:hover {
186
+ box-shadow: var(--neon-shadow);
187
+ transform: translateY(-1px);
188
+ }
189
+
190
+ .primary-btn:active {
191
+ transform: translateY(0);
192
+ }
193
+
194
+ /* ===== Main Content ===== */
195
+ .main-content {
196
+ background: var(--bg-dark);
197
+ display: flex;
198
+ flex-direction: column;
199
+ padding: 1.75rem 2rem;
200
+ overflow-y: auto;
201
+ position: relative;
202
+ }
203
+
204
+ .main-content::before {
205
+ content: '';
206
+ position: absolute;
207
+ top: 0;
208
+ right: 0;
209
+ width: 400px;
210
+ height: 400px;
211
+ background: radial-gradient(circle, rgba(124, 58, 237, 0.04) 0%, transparent 70%);
212
+ pointer-events: none;
213
+ }
214
+
215
+ /* ===== Top Header Stats ===== */
216
+ .top-header {
217
+ margin-bottom: 1.5rem;
218
+ position: relative;
219
+ }
220
+
221
+ .status-summary {
222
+ display: flex;
223
+ gap: 3rem;
224
+ background: var(--bg-card);
225
+ padding: 1.25rem 2rem;
226
+ border-radius: var(--radius);
227
+ border: 1px solid var(--border);
228
+ backdrop-filter: blur(12px);
229
+ }
230
+
231
+ .stat-item {
232
+ display: flex;
233
+ flex-direction: column;
234
+ gap: 0.3rem;
235
+ }
236
+
237
+ .stat-item label {
238
+ font-size: 0.7rem;
239
+ color: var(--text-muted);
240
+ text-transform: uppercase;
241
+ letter-spacing: 1px;
242
+ font-weight: 600;
243
+ }
244
+
245
+ .stat-item span {
246
+ font-size: 1.15rem;
247
+ font-weight: 700;
248
+ font-family: 'JetBrains Mono', monospace;
249
+ }
250
+
251
+ .status-badge {
252
+ color: var(--text-muted);
253
+ transition: color 0.3s;
254
+ }
255
+
256
+ .status-badge.active {
257
+ color: var(--primary);
258
+ }
259
+
260
+ .status-badge.completed {
261
+ color: var(--success);
262
+ }
263
+
264
+ .reward-positive {
265
+ color: var(--warning) !important;
266
+ }
267
+
268
+ /* ===== Dashboard Grid ===== */
269
+ .dashboard-grid {
270
+ display: grid;
271
+ grid-template-columns: 1fr 380px;
272
+ gap: 1.25rem;
273
+ flex: 1;
274
+ min-height: 0;
275
+ }
276
+
277
+ .panel {
278
+ background: var(--bg-card);
279
+ border-radius: 18px;
280
+ border: 1px solid var(--border);
281
+ display: flex;
282
+ flex-direction: column;
283
+ overflow: hidden;
284
+ }
285
+
286
+ .panel-header {
287
+ background: var(--bg-surface);
288
+ padding: 1rem 1.5rem;
289
+ border-bottom: 1px solid var(--border);
290
+ display: flex;
291
+ justify-content: space-between;
292
+ align-items: center;
293
+ flex-shrink: 0;
294
+ }
295
+
296
+ .panel-header h2 {
297
+ font-size: 1rem;
298
+ color: var(--text-main);
299
+ font-weight: 600;
300
+ }
301
+
302
+ .resource-count {
303
+ font-size: 0.75rem;
304
+ color: var(--text-muted);
305
+ background: rgba(255, 255, 255, 0.06);
306
+ padding: 0.2rem 0.65rem;
307
+ border-radius: 999px;
308
+ font-family: 'JetBrains Mono', monospace;
309
+ }
310
+
311
+ .icon-btn {
312
+ background: none;
313
+ border: none;
314
+ cursor: pointer;
315
+ font-size: 1rem;
316
+ opacity: 0.5;
317
+ transition: opacity 0.2s;
318
+ }
319
+
320
+ .icon-btn:hover {
321
+ opacity: 1;
322
+ }
323
+
324
+ /* ===== Resource Grid ===== */
325
+ .resource-grid {
326
+ padding: 1.25rem;
327
+ display: grid;
328
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
329
+ gap: 1rem;
330
+ overflow-y: auto;
331
+ flex: 1;
332
+ }
333
+
334
+ .resource-card {
335
+ background: var(--bg-surface);
336
+ border: 1px solid var(--border);
337
+ border-radius: var(--radius);
338
+ padding: 1.25rem;
339
+ transition: all 0.25s ease;
340
+ position: relative;
341
+ overflow: hidden;
342
+ }
343
+
344
+ .resource-card::before {
345
+ content: '';
346
+ position: absolute;
347
+ top: 0;
348
+ left: 0;
349
+ right: 0;
350
+ height: 3px;
351
+ background: var(--success);
352
+ opacity: 0.6;
353
+ }
354
+
355
+ .resource-card:hover {
356
+ transform: translateY(-3px);
357
+ border-color: rgba(255, 255, 255, 0.15);
358
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
359
+ }
360
+
361
+ .resource-card.vulnerable {
362
+ border-color: rgba(239, 68, 68, 0.4);
363
+ background: rgba(239, 68, 68, 0.04);
364
+ }
365
+
366
+ .resource-card.vulnerable::before {
367
+ background: var(--danger);
368
+ opacity: 1;
369
+ animation: pulse-bar 2s ease-in-out infinite;
370
+ }
371
+
372
+ .resource-card.secure::before {
373
+ background: var(--success);
374
+ }
375
+
376
+ @keyframes pulse-bar {
377
+ 0%, 100% { opacity: 0.6; }
378
+ 50% { opacity: 1; }
379
+ }
380
+
381
+ .card-status-dot {
382
+ position: absolute;
383
+ top: 1rem;
384
+ right: 1rem;
385
+ width: 8px;
386
+ height: 8px;
387
+ border-radius: 50%;
388
+ }
389
+
390
+ .dot-danger {
391
+ background: var(--danger);
392
+ box-shadow: 0 0 8px var(--danger);
393
+ animation: blink 1.5s ease-in-out infinite;
394
+ }
395
+
396
+ .dot-safe {
397
+ background: var(--success);
398
+ box-shadow: 0 0 6px var(--success);
399
+ }
400
+
401
+ @keyframes blink {
402
+ 0%, 100% { opacity: 1; }
403
+ 50% { opacity: 0.3; }
404
+ }
405
+
406
+ .resource-type {
407
+ font-size: 0.6rem;
408
+ font-weight: 800;
409
+ color: var(--text-muted);
410
+ text-transform: uppercase;
411
+ letter-spacing: 1px;
412
+ margin-bottom: 0.4rem;
413
+ display: block;
414
+ }
415
+
416
+ .resource-id {
417
+ font-family: 'JetBrains Mono', monospace;
418
+ font-size: 0.9rem;
419
+ color: var(--text-main);
420
+ margin-bottom: 0.75rem;
421
+ display: block;
422
+ }
423
+
424
+ .sg-warning {
425
+ font-size: 0.78rem;
426
+ color: var(--danger);
427
+ margin-bottom: 0.75rem;
428
+ padding: 0.4rem 0.6rem;
429
+ background: rgba(239, 68, 68, 0.08);
430
+ border-radius: 6px;
431
+ border-left: 2px solid var(--danger);
432
+ }
433
+
434
+ .sg-info {
435
+ font-size: 0.78rem;
436
+ color: var(--success);
437
+ margin-bottom: 0.75rem;
438
+ padding: 0.4rem 0.6rem;
439
+ background: rgba(16, 185, 129, 0.08);
440
+ border-radius: 6px;
441
+ border-left: 2px solid var(--success);
442
+ }
443
+
444
+ .resource-tags {
445
+ display: flex;
446
+ flex-wrap: wrap;
447
+ gap: 0.4rem;
448
+ }
449
+
450
+ .tag {
451
+ font-size: 0.68rem;
452
+ background: rgba(255, 255, 255, 0.05);
453
+ padding: 0.15rem 0.55rem;
454
+ border-radius: 4px;
455
+ color: var(--text-muted);
456
+ font-family: 'JetBrains Mono', monospace;
457
+ }
458
+
459
+ .tag.env-prod {
460
+ border: 1px solid rgba(16, 185, 129, 0.4);
461
+ color: var(--success);
462
+ }
463
+
464
+ .tag.status-public {
465
+ border: 1px solid rgba(239, 68, 68, 0.5);
466
+ color: var(--danger);
467
+ font-weight: 600;
468
+ }
469
+
470
+ .detail-card {
471
+ grid-column: 1 / -1;
472
+ }
473
+
474
+ .detail-json {
475
+ font-family: 'JetBrains Mono', monospace;
476
+ font-size: 0.78rem;
477
+ color: var(--primary);
478
+ background: rgba(0, 0, 0, 0.3);
479
+ padding: 1rem;
480
+ border-radius: 8px;
481
+ overflow-x: auto;
482
+ margin-top: 0.5rem;
483
+ line-height: 1.5;
484
+ white-space: pre-wrap;
485
+ }
486
+
487
+ /* ===== Card Animations ===== */
488
+ .fade-in {
489
+ animation: fadeSlideIn 0.4s ease forwards;
490
+ opacity: 0;
491
+ }
492
+
493
+ @keyframes fadeSlideIn {
494
+ from {
495
+ opacity: 0;
496
+ transform: translateY(12px);
497
+ }
498
+ to {
499
+ opacity: 1;
500
+ transform: translateY(0);
501
+ }
502
+ }
503
+
504
+ /* ===== Terminal Log ===== */
505
+ .log-panel {
506
+ display: flex;
507
+ flex-direction: column;
508
+ }
509
+
510
+ .terminal-log {
511
+ flex: 1;
512
+ background: #000000;
513
+ padding: 1rem;
514
+ font-family: 'JetBrains Mono', monospace;
515
+ font-size: 0.8rem;
516
+ overflow-y: auto;
517
+ color: #4ade80;
518
+ line-height: 1.7;
519
+ min-height: 200px;
520
+ }
521
+
522
+ .log-entry {
523
+ margin-bottom: 0.35rem;
524
+ word-break: break-word;
525
+ }
526
+
527
+ .log-entry.system { color: #94a3b8; }
528
+ .log-entry.error { color: #f87171; }
529
+ .log-entry.action { color: #38bdf8; }
530
+ .log-entry.reward { color: #fbbf24; font-weight: bold; }
531
+
532
+ .manual-action {
533
+ display: flex;
534
+ padding: 0.75rem;
535
+ background: var(--bg-surface);
536
+ gap: 0.5rem;
537
+ border-top: 1px solid var(--border);
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .manual-action input {
542
+ flex: 1;
543
+ background: var(--bg-dark);
544
+ border: 1px solid var(--border);
545
+ border-radius: 8px;
546
+ padding: 0.7rem 0.85rem;
547
+ color: var(--text-main);
548
+ outline: none;
549
+ font-family: 'JetBrains Mono', monospace;
550
+ font-size: 0.82rem;
551
+ transition: border-color 0.2s;
552
+ }
553
+
554
+ .manual-action input::placeholder {
555
+ color: rgba(148, 163, 184, 0.5);
556
+ }
557
+
558
+ .manual-action input:focus {
559
+ border-color: var(--primary);
560
+ box-shadow: 0 0 0 2px rgba(0, 245, 255, 0.1);
561
+ }
562
+
563
+ .manual-action button {
564
+ background: var(--primary);
565
+ border: none;
566
+ padding: 0 1.25rem;
567
+ border-radius: 8px;
568
+ color: #0a0e14;
569
+ font-weight: 700;
570
+ font-size: 0.82rem;
571
+ cursor: pointer;
572
+ transition: all 0.2s;
573
+ font-family: 'Inter', sans-serif;
574
+ }
575
+
576
+ .manual-action button:hover {
577
+ box-shadow: var(--neon-shadow);
578
+ }
579
+
580
+ /* ===== Empty State ===== */
581
+ .empty-state {
582
+ grid-column: 1 / -1;
583
+ text-align: center;
584
+ color: var(--text-muted);
585
+ padding: 3.5rem 2rem;
586
+ border: 2px dashed rgba(255, 255, 255, 0.06);
587
+ border-radius: var(--radius);
588
+ font-size: 0.9rem;
589
+ }
590
+
591
+ /* ===== Toast ===== */
592
+ .toast {
593
+ position: fixed;
594
+ bottom: 2rem;
595
+ right: 2rem;
596
+ background: var(--bg-card);
597
+ border: 1px solid var(--primary);
598
+ padding: 0.9rem 1.75rem;
599
+ border-radius: 12px;
600
+ box-shadow: var(--neon-shadow);
601
+ z-index: 1000;
602
+ font-weight: 600;
603
+ font-size: 0.9rem;
604
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
605
+ transform: translateY(0);
606
+ opacity: 1;
607
+ }
608
+
609
+ .toast.hidden {
610
+ display: none;
611
+ opacity: 0;
612
+ transform: translateY(20px);
613
+ }
614
+
615
+ .toast.show {
616
+ display: block;
617
+ opacity: 1;
618
+ transform: translateY(0);
619
+ }
620
+
621
+ /* ===== Scrollbar ===== */
622
+ ::-webkit-scrollbar {
623
+ width: 6px;
624
+ }
625
+
626
+ ::-webkit-scrollbar-track {
627
+ background: transparent;
628
+ }
629
+
630
+ ::-webkit-scrollbar-thumb {
631
+ background: rgba(255, 255, 255, 0.1);
632
+ border-radius: 3px;
633
+ }
634
+
635
+ ::-webkit-scrollbar-thumb:hover {
636
+ background: rgba(255, 255, 255, 0.2);
637
+ }
638
+
639
+ /* ===== Responsive ===== */
640
+ @media (max-width: 1024px) {
641
+ .app-container {
642
+ grid-template-columns: 1fr;
643
+ }
644
+ .sidebar {
645
+ display: none;
646
+ }
647
+ .dashboard-grid {
648
+ grid-template-columns: 1fr;
649
+ }
650
+ }
server/static/index.html ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cloud Security Auditor Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Outfit:wght@500;700&family=JetBrains+Mono&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/ui/index.css">
11
+ </head>
12
+ <body>
13
+ <div class="app-container">
14
+ <!-- Sidebar -->
15
+ <aside class="sidebar">
16
+ <div class="brand">
17
+ <div class="logo">🛡️</div>
18
+ <h1>Auditor<span>OpenEnv</span></h1>
19
+ </div>
20
+
21
+ <nav class="nav-menu">
22
+ <div class="nav-label">Select Task</div>
23
+ <button class="task-btn active" data-task="easy">
24
+ <span class="difficulty easy">Easy</span>
25
+ <span class="task-name">S3 Public Audit</span>
26
+ </button>
27
+ <button class="task-btn" data-task="medium">
28
+ <span class="difficulty medium">Medium</span>
29
+ <span class="task-name">EC2 Security Patch</span>
30
+ </button>
31
+ <button class="task-btn" data-task="hard">
32
+ <span class="difficulty hard">Hard</span>
33
+ <span class="task-name">IAM Log Forensic</span>
34
+ </button>
35
+ </nav>
36
+
37
+ <div class="sidebar-footer">
38
+ <button id="reset-btn" class="primary-btn">Reset Environment</button>
39
+ </div>
40
+ </aside>
41
+
42
+ <!-- Main Content -->
43
+ <main class="main-content">
44
+ <header class="top-header">
45
+ <div class="status-summary">
46
+ <div class="stat-item">
47
+ <label>Current Task</label>
48
+ <span id="current-task-display">None</span>
49
+ </div>
50
+ <div class="stat-item">
51
+ <label>Reward</label>
52
+ <span id="current-reward">0.0</span>
53
+ </div>
54
+ <div class="stat-item">
55
+ <label>Status</label>
56
+ <span id="env-status" class="status-badge">Idle</span>
57
+ </div>
58
+ </div>
59
+ </header>
60
+
61
+ <section class="dashboard-grid">
62
+ <!-- Resources Panel -->
63
+ <div class="panel resources-panel">
64
+ <div class="panel-header">
65
+ <h2>Infrastructure Overview</h2>
66
+ <span class="resource-count" id="resource-count">0 Resources</span>
67
+ </div>
68
+ <div class="resource-grid" id="resource-display">
69
+ <!-- Resource cards injected here -->
70
+ <div class="empty-state">No resources discovered. Reset or step to view.</div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Action Log -->
75
+ <div class="panel log-panel">
76
+ <div class="panel-header">
77
+ <h2>Execution Log</h2>
78
+ <button id="clear-log" class="icon-btn">🗑️</button>
79
+ </div>
80
+ <div class="terminal-log" id="action-log">
81
+ <div class="log-entry system">System initialized. Awaiting task selection...</div>
82
+ </div>
83
+ <div class="manual-action">
84
+ <input type="text" id="manual-input" placeholder="Enter manual action (e.g. list s3)...">
85
+ <button id="run-action">Execute</button>
86
+ </div>
87
+ </div>
88
+ </section>
89
+ </main>
90
+ </div>
91
+
92
+ <!-- Modals / Toast -->
93
+ <div id="toast" class="toast hidden"></div>
94
+
95
+ <script src="/ui/app.js"></script>
96
+ </body>
97
+ </html>