bughunter230 commited on
Commit
5d60d2c
·
verified ·
1 Parent(s): b004d78

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +1028 -19
index.html CHANGED
@@ -1,19 +1,1028 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>Evil Twin Attack Detector (ML) — Browser Demo</title>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <meta name="description"
9
+ content="Evil Twin attack detection demo using a lightweight ML model in the browser (TensorFlow.js). No backend required." />
10
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
11
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.20.0/dist/tf.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
13
+ <style>
14
+ :root {
15
+ --bg: #0b1020;
16
+ --bg-2: #0f1630;
17
+ --card: #121a35;
18
+ --card-2: #0f1a3a;
19
+ --text: #dfe7ff;
20
+ --muted: #8ea0c8;
21
+ --primary: #5b8cff;
22
+ --primary-2: #7ea2ff;
23
+ --accent: #8cffd9;
24
+ --danger: #ff6b6b;
25
+ --warn: #f7b267;
26
+ --good: #57e39a;
27
+ --shadow: 0 8px 30px rgba(0, 0, 0, .35);
28
+ --radius: 14px;
29
+ --radius-sm: 10px;
30
+ --radius-xs: 8px;
31
+ --grid-gap: 16px;
32
+ --ring: 0 0 0 2px rgba(91, 140, 255, .2), 0 0 0 6px rgba(91, 140, 255, .15);
33
+ }
34
+
35
+ * {
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ html,
40
+ body {
41
+ height: 100%;
42
+ }
43
+
44
+ body {
45
+ margin: 0;
46
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
47
+ color: var(--text);
48
+ background:
49
+ radial-gradient(1200px 600px at 20% -10%, #1c2447 0%, rgba(28, 36, 71, 0) 60%),
50
+ radial-gradient(900px 800px at 100% 0%, #12204a 0%, rgba(18, 32, 74, 0) 60%),
51
+ linear-gradient(180deg, var(--bg) 0%, #060912 100%);
52
+ background-attachment: fixed;
53
+ }
54
+
55
+ header {
56
+ position: sticky;
57
+ top: 0;
58
+ z-index: 5;
59
+ background: linear-gradient(180deg, rgba(10, 15, 30, 0.95), rgba(10, 15, 30, 0.6));
60
+ backdrop-filter: blur(10px);
61
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
62
+ }
63
+
64
+ .nav {
65
+ max-width: 1200px;
66
+ margin: 0 auto;
67
+ padding: 14px 20px;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ gap: 12px;
72
+ }
73
+
74
+ .brand {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 12px;
78
+ font-weight: 700;
79
+ letter-spacing: .3px;
80
+ }
81
+
82
+ .brand .logo {
83
+ width: 36px;
84
+ height: 36px;
85
+ border-radius: 10px;
86
+ background: conic-gradient(from 180deg at 50% 50%, #6ea8ff, #8cffd9, #6ea8ff);
87
+ box-shadow: inset 0 0 20px rgba(255, 255, 255, .25), 0 8px 25px rgba(140, 255, 217, .15);
88
+ position: relative;
89
+ overflow: hidden;
90
+ }
91
+
92
+ .brand .logo::after {
93
+ content: '';
94
+ position: absolute;
95
+ inset: 2px;
96
+ background: radial-gradient(120px 120px at 30% 20%, rgba(255, 255, 255, .45), rgba(255, 255, 255, 0));
97
+ border-radius: 8px;
98
+ }
99
+
100
+ .brand .title {
101
+ display: flex;
102
+ flex-direction: column;
103
+ line-height: 1.15;
104
+ }
105
+
106
+ .brand .title b {
107
+ font-size: 16px;
108
+ }
109
+
110
+ .brand .title small {
111
+ color: var(--muted);
112
+ font-weight: 500;
113
+ }
114
+
115
+ .hdr-actions {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 12px;
119
+ flex-wrap: wrap;
120
+ }
121
+
122
+ .link {
123
+ color: var(--primary);
124
+ text-decoration: none;
125
+ padding: 8px 10px;
126
+ border-radius: 8px;
127
+ background: rgba(91, 140, 255, .08);
128
+ border: 1px solid rgba(91, 140, 255, .25);
129
+ }
130
+
131
+ .link:hover {
132
+ background: rgba(91, 140, 255, .15);
133
+ }
134
+
135
+ .btn {
136
+ background: linear-gradient(180deg, var(--primary) 0%, var(--primary-2) 100%);
137
+ color: #071022;
138
+ border: none;
139
+ padding: 10px 14px;
140
+ border-radius: 10px;
141
+ font-weight: 700;
142
+ cursor: pointer;
143
+ box-shadow: 0 8px 20px rgba(91, 140, 255, .35);
144
+ }
145
+
146
+ .btn.secondary {
147
+ background: linear-gradient(180deg, #1a244a, #121a3a);
148
+ color: var(--text);
149
+ border: 1px solid rgba(255, 255, 255, .08);
150
+ box-shadow: none;
151
+ }
152
+
153
+ .btn.warn {
154
+ background: linear-gradient(180deg, #ffb86c, #ff9a52);
155
+ color: #2c1200;
156
+ }
157
+
158
+ .btn:disabled {
159
+ opacity: .6;
160
+ cursor: not-allowed;
161
+ }
162
+
163
+ main {
164
+ max-width: 1200px;
165
+ margin: 18px auto 60px;
166
+ padding: 0 20px;
167
+ display: grid;
168
+ grid-template-columns: 1.1fr 1.6fr;
169
+ gap: 22px;
170
+ }
171
+
172
+ @media (max-width: 1000px) {
173
+ main {
174
+ grid-template-columns: 1fr;
175
+ }
176
+ }
177
+
178
+ .card {
179
+ background: linear-gradient(180deg, rgba(20, 30, 60, .9), rgba(16, 22, 45, .85));
180
+ border: 1px solid rgba(255, 255, 255, .06);
181
+ border-radius: var(--radius);
182
+ box-shadow: var(--shadow);
183
+ overflow: clip;
184
+ }
185
+
186
+ .card h2 {
187
+ margin: 0;
188
+ padding: 16px 18px;
189
+ font-size: 16px;
190
+ border-bottom: 1px solid rgba(255, 255, 255, .06);
191
+ background: linear-gradient(180deg, rgba(255, 255, 255, .04), rgba(255, 255, 255, 0));
192
+ }
193
+
194
+ .card .body {
195
+ padding: 16px;
196
+ }
197
+
198
+ .grid {
199
+ display: grid;
200
+ gap: var(--grid-gap);
201
+ grid-template-columns: repeat(12, 1fr);
202
+ }
203
+
204
+ .col-12 {
205
+ grid-column: span 12;
206
+ }
207
+
208
+ .col-6 {
209
+ grid-column: span 6;
210
+ }
211
+
212
+ .col-4 {
213
+ grid-column: span 4;
214
+ }
215
+
216
+ .col-8 {
217
+ grid-column: span 8;
218
+ }
219
+
220
+ @media (max-width: 800px) {
221
+
222
+ .col-6,
223
+ .col-4,
224
+ .col-8 {
225
+ grid-column: span 12;
226
+ }
227
+ }
228
+
229
+ .field {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 6px;
233
+ }
234
+
235
+ .field label {
236
+ font-size: 12px;
237
+ color: var(--muted);
238
+ text-transform: uppercase;
239
+ letter-spacing: .7px;
240
+ }
241
+
242
+ input,
243
+ select {
244
+ appearance: none;
245
+ outline: none;
246
+ background: #0d1530;
247
+ border: 1px solid rgba(255, 255, 255, .08);
248
+ color: var(--text);
249
+ padding: 10px 12px;
250
+ border-radius: var(--radius-sm);
251
+ transition: border .2s ease, box-shadow .2s ease, background .2s ease;
252
+ }
253
+
254
+ input:focus,
255
+ select:focus {
256
+ border-color: rgba(91, 140, 255, .7);
257
+ box-shadow: var(--ring);
258
+ background: #0f1a3f;
259
+ }
260
+
261
+ .actions {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 10px;
265
+ flex-wrap: wrap;
266
+ }
267
+
268
+ .muted {
269
+ color: var(--muted);
270
+ font-size: 13px;
271
+ }
272
+
273
+ .badge {
274
+ display: inline-flex;
275
+ align-items: center;
276
+ gap: 6px;
277
+ padding: 6px 10px;
278
+ border-radius: 999px;
279
+ font-size: 12px;
280
+ font-weight: 700;
281
+ border: 1px solid rgba(255, 255, 255, .1);
282
+ background: rgba(255, 255, 255, .04);
283
+ }
284
+
285
+ .badge.good {
286
+ background: rgba(87, 227, 154, .12);
287
+ color: var(--good);
288
+ border-color: rgba(87, 227, 154, .35);
289
+ }
290
+
291
+ .badge.warn {
292
+ background: rgba(247, 178, 103, .12);
293
+ color: var(--warn);
294
+ border-color: rgba(247, 178, 103, .35);
295
+ }
296
+
297
+ .badge.danger {
298
+ background: rgba(255, 107, 107, .14);
299
+ color: var(--danger);
300
+ border-color: rgba(255, 107, 107, .35);
301
+ }
302
+
303
+ .sep {
304
+ height: 1px;
305
+ background: rgba(255, 255, 255, .06);
306
+ margin: 12px 0;
307
+ }
308
+
309
+ .kpi {
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: space-between;
313
+ padding: 10px 12px;
314
+ border: 1px solid rgba(255, 255, 255, .08);
315
+ border-radius: var(--radius-sm);
316
+ background: rgba(255, 255, 255, .03);
317
+ }
318
+
319
+ .kpi .label {
320
+ color: var(--muted);
321
+ font-size: 12px;
322
+ }
323
+
324
+ .kpi .value {
325
+ font-weight: 800;
326
+ font-size: 18px;
327
+ }
328
+
329
+ .progress {
330
+ height: 10px;
331
+ width: 100%;
332
+ background: rgba(255, 255, 255, .08);
333
+ border-radius: 999px;
334
+ overflow: hidden;
335
+ border: 1px solid rgba(255, 255, 255, .06);
336
+ }
337
+
338
+ .progress>div {
339
+ height: 100%;
340
+ background: linear-gradient(90deg, #8cffd9, #5b8cff);
341
+ width: 0%;
342
+ transition: width .2s ease;
343
+ }
344
+
345
+ .list {
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 10px;
349
+ max-height: 300px;
350
+ overflow: auto;
351
+ }
352
+
353
+ .list .row {
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ gap: 10px;
358
+ padding: 10px 12px;
359
+ border-radius: 10px;
360
+ border: 1px solid rgba(255, 255, 255, .06);
361
+ background: rgba(255, 255, 255, .03);
362
+ }
363
+
364
+ .mini {
365
+ font-size: 12px;
366
+ color: var(--muted);
367
+ }
368
+
369
+ .score {
370
+ font-weight: 800;
371
+ font-variant-numeric: tabular-nums;
372
+ }
373
+
374
+ .pill {
375
+ padding: 4px 8px;
376
+ border-radius: 999px;
377
+ font-size: 12px;
378
+ font-weight: 800;
379
+ border: 1px solid rgba(255, 255, 255, .08);
380
+ }
381
+
382
+ .pill.good {
383
+ background: rgba(87, 227, 154, .12);
384
+ color: var(--good);
385
+ border-color: rgba(87, 227, 154, .25);
386
+ }
387
+
388
+ .pill.bad {
389
+ background: rgba(255, 107, 107, .12);
390
+ color: var(--danger);
391
+ border-color: rgba(255, 107, 107, .25);
392
+ }
393
+
394
+ .chart-wrap {
395
+ height: 220px;
396
+ border-radius: var(--radius-sm);
397
+ background: #0e1430;
398
+ border: 1px solid rgba(255, 255, 255, .06);
399
+ padding: 10px;
400
+ }
401
+
402
+ .note {
403
+ font-size: 12px;
404
+ color: var(--muted);
405
+ padding: 10px 12px;
406
+ border-radius: var(--radius-sm);
407
+ background: rgba(255, 255, 255, .03);
408
+ border: 1px solid rgba(255, 255, 255, .06);
409
+ }
410
+
411
+ .toast {
412
+ position: fixed;
413
+ right: 20px;
414
+ bottom: 20px;
415
+ z-index: 50;
416
+ padding: 12px 14px;
417
+ background: #121b3a;
418
+ border: 1px solid rgba(255, 255, 255, .1);
419
+ border-left: 4px solid var(--primary);
420
+ border-radius: 10px;
421
+ color: var(--text);
422
+ box-shadow: var(--shadow);
423
+ display: none;
424
+ max-width: 420px;
425
+ }
426
+
427
+ .toast.show {
428
+ display: block;
429
+ animation: slideIn .25s ease;
430
+ }
431
+
432
+ @keyframes slideIn {
433
+ from {
434
+ transform: translateY(10px);
435
+ opacity: 0;
436
+ }
437
+
438
+ to {
439
+ transform: translateY(0);
440
+ opacity: 1;
441
+ }
442
+ }
443
+
444
+ .grid-2 {
445
+ display: grid;
446
+ grid-template-columns: 1fr 1fr;
447
+ gap: 12px;
448
+ }
449
+
450
+ @media (max-width: 700px) {
451
+ .grid-2 {
452
+ grid-template-columns: 1fr;
453
+ }
454
+ }
455
+
456
+ .small {
457
+ font-size: 12px;
458
+ }
459
+ </style>
460
+ </head>
461
+
462
+ <body>
463
+ <header>
464
+ <div class="nav">
465
+ <div class="brand">
466
+ <div class="logo" aria-hidden="true"></div>
467
+ <div class="title">
468
+ <b>Evil Twin Attack Detector</b>
469
+ <small>Client-side ML demo (TensorFlow.js)</small>
470
+ </div>
471
+ </div>
472
+ <div class="hdr-actions">
473
+ <a class="link" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noreferrer">Built
474
+ with anycoder</a>
475
+ <button class="btn secondary" id="btnRetrain">Retrain</button>
476
+ </div>
477
+ </div>
478
+ </header>
479
+
480
+ <main>
481
+ <section class="card">
482
+ <h2>1) Add Wi‑Fi sample & predict</h2>
483
+ <div class="body grid">
484
+ <div class="col-12">
485
+ <div class="grid-2">
486
+ <div class="field">
487
+ <label for="ssid">SSID</label>
488
+ <input id="ssid" placeholder="e.g., CoffeeShop_WiFi" value="CoffeeShop_WiFi" />
489
+ </div>
490
+ <div class="field">
491
+ <label for="bssid">BSSID (MAC)</label>
492
+ <input id="bssid" placeholder="02:00:00:AA:BB:CC" value="02:00:00:AA:BB:CC" />
493
+ </div>
494
+ <div class="field">
495
+ <label for="rssi">Signal (RSSI dBm)</label>
496
+ <input id="rssi" type="number" step="1" value="-55" />
497
+ </div>
498
+ <div class="field">
499
+ <label for="channel">Channel</label>
500
+ <input id="channel" type="number" step="1" value="6" />
501
+ </div>
502
+ <div class="field">
503
+ <label for="frequency">Frequency (MHz)</label>
504
+ <input id="frequency" type="number" step="1" value="2437" />
505
+ </div>
506
+ <div class="field">
507
+ <label for="encryption">Encryption</label>
508
+ <select id="encryption">
509
+ <option value="WPA2-PSK">WPA2-PSK</option>
510
+ <option value="WPA3-PSK">WPA3-PSK</option>
511
+ <option value="WEP">WEP</option>
512
+ <option value="Open">Open</option>
513
+ </select>
514
+ </div>
515
+ <div class="field">
516
+ <label for="hidden">Hidden</label>
517
+ <select id="hidden">
518
+ <option value="false">No</option>
519
+ <option value="true">Yes</option>
520
+ </select>
521
+ </div>
522
+ <div class="field">
523
+ <label for="enc-mismatch">Encryption mismatch</label>
524
+ <select id="enc-mismatch">
525
+ <option value="false">No</option>
526
+ <option value="true">Yes</option>
527
+ </select>
528
+ </div>
529
+ <div class="field">
530
+ <label for="expected-bssids">Expected BSSIDs (comma)</label>
531
+ <input id="expected-bssids" placeholder="02:00:00:AA:BB:CC, 06:00:00:11:22:33" value="02:00:00:AA:BB:CC, 06:00:00:11:22:33" />
532
+ </div>
533
+ <div class="field">
534
+ <label for="known-ch">Known channels (comma)</label>
535
+ <input id="known-ch" placeholder="6, 11, 1" value="6, 11, 1" />
536
+ </div>
537
+ </div>
538
+ </div>
539
+ <div class="col-12">
540
+ <div class="actions">
541
+ <button class="btn" id="btnPredict">Predict</button>
542
+ <button class="btn secondary" id="btnRandom">Randomize sample</button>
543
+ <span class="muted">Feature vector is automatically derived from these inputs.</span>
544
+ </div>
545
+ </div>
546
+
547
+ <div class="col-12 sep"></div>
548
+
549
+ <div class="col-12">
550
+ <div id="predCard" class="kpi" style="display:none">
551
+ <div style="display:flex; flex-direction:column; gap:6px;">
552
+ <div class="label">Prediction</div>
553
+ <div id="predLabel" class="value">—</div>
554
+ <div class="mini" id="predExplain">—</div>
555
+ </div>
556
+ <div style="display:flex; flex-direction:column; gap:6px; align-items:flex-end;">
557
+ <div class="label">Evil Twin probability</div>
558
+ <div class="score" id="predProb">—</div>
559
+ <div class="progress" style="width:220px;">
560
+ <div id="predBar" style="width:0%"></div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+ </div>
565
+
566
+ <div class="col-12">
567
+ <div class="note">
568
+ Tip: Toggle “Encryption mismatch” to simulate when an AP claims a different security type than expected. The
569
+ model uses signal strength, channel, and known channels/BSSIDs to inform its inference.
570
+ </div>
571
+ </div>
572
+ </div>
573
+ </section>
574
+
575
+ <section class="card">
576
+ <h2>2) Real-time threat monitor</h2>
577
+ <div class="body">
578
+ <div class="grid">
579
+ <div class="col-12">
580
+ <div class="actions">
581
+ <button class="btn" id="btnMonitor">Start monitoring</button>
582
+ <button class="btn warn" id="btnClearDetections">Clear detections</button>
583
+ <span class="muted">Simulated environment: random APs appear; malicious ones are generated by a hidden adversary policy.</span>
584
+ </div>
585
+ </div>
586
+ <div class="col-6">
587
+ <div class="kpi">
588
+ <div>
589
+ <div class="label">Total scanned</div>
590
+ <div class="value" id="kpiTotal">0</div>
591
+ </div>
592
+ <div>
593
+ <div class="label">Detections (evil twin)</div>
594
+ <div class="value" id="kpiDetections" style="color:var(--danger)">0</div>
595
+ </div>
596
+ </div>
597
+ </div>
598
+ <div class="col-6">
599
+ <div class="kpi">
600
+ <div>
601
+ <div class="label">Detection rate</div>
602
+ <div class="value" id="kpiRate">0%</div>
603
+ </div>
604
+ <div>
605
+ <div class="label">Last risk</div>
606
+ <div class="value" id="kpiLast">—</div>
607
+ </div>
608
+ </div>
609
+ </div>
610
+ <div class="col-12 chart-wrap">
611
+ <canvas id="monitorChart"></canvas>
612
+ </div>
613
+ <div class="col-12 list" id="detList"></div>
614
+ <div class="col-12">
615
+ <div class="note">
616
+ Detection logic uses the trained model score with a calibrated threshold of 0.50. In production, tune the
617
+ threshold with a validation set to balance false positives/negatives.
618
+ </div>
619
+ </div>
620
+ </div>
621
+ </div>
622
+ </section>
623
+
624
+ <section class="card">
625
+ <h2>3) Train the model (on synthetic data)</h2>
626
+ <div class="body grid">
627
+ <div class="col-12 actions">
628
+ <button class="btn" id="btnTrain">Generate data & Train</button>
629
+ <span class="muted">The dataset is generated in-browser and based on plausible Evil Twin behaviors.</span>
630
+ </div>
631
+ <div class="col-12 kpi" id="trainKPI" style="display:none">
632
+ <div style="display:flex; gap:16px; flex-wrap:wrap;">
633
+ <div>
634
+ <div class="label">Training samples</div>
635
+ <div class="value" id="kpiTrainN">0</div>
636
+ </div>
637
+ <div>
638
+ <div class="label">Validation samples</div>
639
+ <div class="value" id="kpiValN">0</div>
640
+ </div>
641
+ <div>
642
+ <div class="label">Epochs</div>
643
+ <div class="value" id="kpiEpochs">0/10</div>
644
+ </div>
645
+ <div>
646
+ <div class="label">Final loss</div>
647
+ <div class="value" id="kpiLoss">—</div>
648
+ </div>
649
+ <div>
650
+ <div class="label">Val accuracy</div>
651
+ <div class="value" id="kpiAcc">—</div>
652
+ </div>
653
+ </div>
654
+ </div>
655
+ <div class="col-12">
656
+ <div class="chart-wrap" style="height: 260px;">
657
+ <canvas id="lossChart"></canvas>
658
+ </div>
659
+ </div>
660
+ <div class="col-12">
661
+ <div class="chart-wrap" style="height: 260px;">
662
+ <canvas id="cmChart"></canvas>
663
+ </div>
664
+ </div>
665
+ <div class="col-12">
666
+ <div class="note">
667
+ This demo runs fully in your browser using TensorFlow.js. No network scans are performed; it simulates Wi‑Fi
668
+ features for training and prediction. For a production deployment, feed real measurements (e.g., BSSID,
669
+ signal, channel) from your platform’s Wi‑Fi stack, and monitor over time for anomalies.
670
+ </div>
671
+ </div>
672
+ </div>
673
+ </section>
674
+ </main>
675
+
676
+ <div class="toast" id="toast"></div>
677
+
678
+ <script>
679
+ // --- Utilities ---
680
+ const $ = (sel) => document.querySelector(sel);
681
+ const fmtPct = (x) => (x*100).toFixed(1) + '%';
682
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
683
+ const randn = (() => {
684
+ // Box-Muller
685
+ let spare = null;
686
+ return () => {
687
+ if (spare !== null) { const n = spare; spare = null; return n; }
688
+ let u = 0, v = 0, s = 0;
689
+ do { u = Math.random()*2-1; v = Math.random()*2-1; s = u*u+v*v; } while (s === 0 || s >= 1);
690
+ const mul = Math.sqrt(-2.0 * Math.log(s) / s);
691
+ spare = v * mul;
692
+ return u * mul;
693
+ };
694
+ })();
695
+ const logistic = (x) => 1 / (1 + Math.exp(-x));
696
+ const showToast = (msg, color) => {
697
+ const t = $('#toast');
698
+ t.textContent = msg;
699
+ t.style.borderLeftColor = color || 'var(--primary)';
700
+ t.classList.add('show');
701
+ setTimeout(() => t.classList.remove('show'), 2500);
702
+ };
703
+
704
+ // --- Model (simple logistic regression with 3 features) ---
705
+ // Features: [signalNorm, chDeltaNorm, mismatchScore]
706
+ const normalizeSignal = (rssi) => (clamp((rssi + 100) / 50, 0, 1));
707
+ const normalizeChDelta = (delta) => (clamp(Math.abs(delta) / 5, 0, 1));
708
+ function mismatchScore({encryption, encMismatch, hidden}){
709
+ let s = 0;
710
+ if (encMismatch) s += 0.35;
711
+ if (hidden) s += 0.15;
712
+ if (encryption === 'WEP') s += 0.20;
713
+ if (encryption === 'Open') s += 0.20;
714
+ if (encryption === 'WPA3-PSK') s -= 0.05; // slightly less suspicious in isolation
715
+ return clamp(s, 0, 1);
716
+ }
717
+ function extractFeatures({rssi, channel, knownChannels, encryption, encMismatch, hidden}){
718
+ // nearest known channel
719
+ let nearest = Infinity;
720
+ for (const kc of knownChannels) nearest = Math.min(nearest, Math.abs(kc - channel));
721
+ const chDelta = isFinite(nearest) ? nearest : 10;
722
+ const signalNorm = normalizeSignal(rssi);
723
+ const chDeltaNorm = normalizeChDelta(chDelta);
724
+ const mismatch = mismatchScore({encryption, encMismatch, hidden});
725
+ return [signalNorm, chDeltaNorm, mismatch];
726
+ }
727
+ // Hidden adversary policy for monitor
728
+ function adversaryIsEvilTwinLabel(features){
729
+ const [sNorm, chDeltaNorm, mismatch] = features;
730
+ const y = -1.4 + 2.0*mismatch + 1.3*(1 - sNorm) + 1.0*chDeltaNorm;
731
+ const p = 1/(1+Math.exp(-y));
732
+ return Math.random() < p;
733
+ }
734
+
735
+ // Weights learned by gradient descent on synthetic data
736
+ let W = tf.tensor1d([0,0,0]); // [w1, w2, w3]
737
+ let b = tf.scalar(0);
738
+ let trained = false;
739
+ let training = false;
740
+
741
+ function predictProba(featuresArr){
742
+ if (!trained) return 0.0;
743
+ const t = tf.tensor2d([featuresArr]);
744
+ const z = tf.sum(tf.mul(W, t.reshape([3])), 0).add(b);
745
+ return logistic(z.dataSync()[0]);
746
+ }
747
+
748
+ function predictLabel(featuresArr){
749
+ const p = predictProba(featuresArr);
750
+ return p >= 0.5 ? 1 : 0;
751
+ }
752
+
753
+ // --- Data generation (synthetic, but plausible) ---
754
+ function genSamples(n, knownChannels, advPolicy){
755
+ const X = [];
756
+ const y = [];
757
+ for (let i=0; i<n; i++){
758
+ const isEvil = Math.random() < 0.45; // 45% malicious in training
759
+ let rssi, channel, encryption, encMismatch=false, hidden=false;
760
+
761
+ if (isEvil){
762
+ rssi = clamp(-30 + randn()*6, -100, -25);
763
+ channel = Math.random() < 0.55 ? (knownChannels[0] ?? 6) : (1 + Math.floor(Math.random()*14));
764
+ encryption = Math.random() < 0.5 ? 'WEP' : (Math.random() < 0.5 ? 'Open' : 'WPA2-PSK');
765
+ encMismatch = Math.random() < 0.55;
766
+ hidden = Math.random() < 0.35;
767
+ } else {
768
+ rssi = clamp(-65 + randn()*8, -100, -25);
769
+ channel = (Math.random() < 0.75) ? (knownChannels[Math.floor(Math.random()*knownChannels.length)] ?? 6) : (1 + Math.floor(Math.random()*14));
770
+ encryption = Math.random() < 0.65 ? 'WPA2-PSK' : (Math.random() < 0.5 ? 'WPA3-PSK' : 'Open');
771
+ encMismatch = Math.random() < 0.08;
772
+ hidden = Math.random() < 0.12;
773
+ }
774
+ const features = extractFeatures({rssi, channel, knownChannels, encryption, encMismatch, hidden});
775
+ const label = advPolicy ? (adversaryIsEvilTwinLabel(features) ? 1 : 0) : isEvil ? 1 : 0;
776
+ X.push(features);
777
+ y.push(label);
778
+ }
779
+ return {X, y};
780
+ }
781
+
782
+ // --- Training loop ---
783
+ async function trainModel({epochs=10, lr=0.35, trainN=6000, valN=2000, knownChannels=[1,6,11]}){
784
+ if (trained || training) return;
785
+ training = true;
786
+ showToast('Generating synthetic dataset...', 'var(--primary)');
787
+ $('#trainKPI').style.display = 'flex';
788
+
789
+ const train = genSamples(trainN, knownChannels, true);
790
+ const val = genSamples(valN, knownChannels, true);
791
+
792
+ $('#kpiTrainN').textContent = trainN.toLocaleString();
793
+ $('#kpiValN').textContent = valN.toLocaleString();
794
+ $('#kpiEpochs').textContent = `0/${epochs}`;
795
+ $('#kpiLoss').textContent = '—';
796
+ $('#kpiAcc').textContent = '—';
797
+
798
+ const Xtr = tf.tensor2d(train.X);
799
+ const ytr = tf.tensor2d(train.y.map(v => [v]), [trainN, 1]);
800
+ const Xval = tf.tensor2d(val.X);
801
+ const yval = tf.tensor2d(val.y.map(v => [v]), [valN, 1]);
802
+
803
+ // Initialize weights with small noise
804
+ W.dispose(); b.dispose();
805
+ W = tf.tensor1d([ (Math.random()-0.5)*0.01, (Math.random()-0.5)*0.01, (Math.random()-0.5)*0.01 ]);
806
+ b = tf.scalar((Math.random()-0.5)*0.01);
807
+
808
+ const lossSeries = [];
809
+ const accSeries = [];
810
+
811
+ for (let e=1; e<=epochs; e++){
812
+ // Forward + backward
813
+ const lossT = tf.tidy(() => {
814
+ const z = Xtr.matMul(W.reshape([3,1])).add(b);
815
+ const preds = z.sigmoid();
816
+ const loss = tf.losses.logLoss(ytr, preds).mean();
817
+ return loss;
818
+ });
819
+ const lossVal = (await lossT.data())[0];
820
+ lossT.dispose();
821
+
822
+ // Accuracy
823
+ const acc = await accuracyOn(Xval, yval);
824
+ lossSeries.push(lossVal);
825
+ accSeries.push(acc);
826
+
827
+ $('#kpiEpochs').textContent = `${e}/${epochs}`;
828
+ $('#kpiLoss').textContent = lossVal.toFixed(3);
829
+ $('#kpiAcc').textContent = fmtPct(acc);
830
+
831
+ // Update weights
832
+ const opt = tf.train.sgd(lr);
833
+ await opt.minimize(() => {
834
+ const z = Xtr.matMul(W.reshape([3,1])).add(b);
835
+ const preds = z.sigmoid();
836
+ const loss = tf.losses.logLoss(ytr, preds).mean();
837
+ return loss;
838
+ });
839
+
840
+ // Update charts
841
+ updateLossChart(lossSeries, accSeries);
842
+ await tf.nextFrame();
843
+ }
844
+
845
+ // Final validation metrics + confusion matrix
846
+ const predsVal = tf.tidy(() => {
847
+ const z = Xval.matMul(W.reshape([3,1])).add(b);
848
+ return z.sigmoid();
849
+ });
850
+ const yhat = (await predsVal.data()).map(v => v >= 0.5 ? 1 : 0);
851
+ predsVal.dispose();
852
+
853
+ const cm = confusionMatrix(val.y, yhat, 2);
854
+ renderConfusionMatrix(cm);
855
+
856
+ Xtr.dispose(); ytr.dispose(); Xval.dispose(); yval.dispose();
857
+
858
+ trained = true;
859
+ training = false;
860
+ showToast('Model trained. Try predicting samples or start the monitor.', 'var(--good)');
861
+ }
862
+
863
+ async function accuracyOn(X, y){
864
+ const preds = tf.tidy(() => {
865
+ const z = X.matMul(W.reshape([3,1])).add(b);
866
+ return z.sigmoid();
867
+ });
868
+ const arr = await preds.data();
869
+ preds.dispose();
870
+ const n = y.shape[0];
871
+ let correct = 0;
872
+ for (let i=0; i<n; i++){
873
+ const yi = y.get(i,0);
874
+ const pi = arr[i] >= 0.5 ? 1 : 0;
875
+ if (yi === pi) correct++;
876
+ }
877
+ return correct / n;
878
+ }
879
+
880
+ function confusionMatrix(yTrue, yPred, classes=2){
881
+ const cm = Array.from({length: classes}, () => Array(classes).fill(0));
882
+ for (let i=0; i<yTrue.length; i++){
883
+ cm[yTrue[i]][yPred[i]] += 1;
884
+ }
885
+ return cm;
886
+ }
887
+
888
+ // --- Charts ---
889
+ let lossChart, cmChart, monitorChart;
890
+ function initCharts(){
891
+ const lossCtx = $('#lossChart').getContext('2d');
892
+ lossChart = new Chart(lossCtx, {
893
+ type: 'line',
894
+ data: {
895
+ labels: [],
896
+ datasets: [
897
+ { label: 'Loss', data: [], borderColor: '#ff9a52', backgroundColor: 'rgba(255,154,82,.2)', tension: .2, yAxisID: 'y' },
898
+ { label: 'Val Acc', data: [], borderColor: '#8cffd9', backgroundColor: 'rgba(140,255,217,.2)', tension: .2, yAxisID: 'y1' }
899
+ ]
900
+ },
901
+ options: {
902
+ responsive: true,
903
+ maintainAspectRatio: false,
904
+ scales: {
905
+ y: { type: 'linear', position: 'left', min: 0, grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } },
906
+ y1: { type: 'linear', position: 'right', min: 0, max: 1, grid:{ drawOnChartArea:false }, ticks:{ color:'#9ab' } },
907
+ x: { grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } }
908
+ },
909
+ plugins: {
910
+ legend: { labels: { color:'#dfe7ff' } }
911
+ }
912
+ }
913
+ });
914
+
915
+ const cmCtx = $('#cmChart').getContext('2d');
916
+ cmChart = new Chart(cmCtx, {
917
+ type: 'bar',
918
+ data: {
919
+ labels: ['True Neg', 'False Pos', 'False Neg', 'True Pos'],
920
+ datasets: [{ label:'Validation set', data: [0,0,0,0], backgroundColor: ['#57e39a','#f7b267','#f7b267','#57e39a'] }]
921
+ },
922
+ options: {
923
+ responsive: true,
924
+ maintainAspectRatio: false,
925
+ scales: {
926
+ y: { beginAtZero: true, grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } },
927
+ x: { grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } }
928
+ },
929
+ plugins: { legend: { labels: { color:'#dfe7ff' } } }
930
+ }
931
+ });
932
+
933
+ const monCtx = $('#monitorChart').getContext('2d');
934
+ monitorChart = new Chart(monCtx, {
935
+ type: 'line',
936
+ data: {
937
+ labels: [],
938
+ datasets: [
939
+ { label:'Risk score', data: [], borderColor:'#5b8cff', backgroundColor:'rgba(91,140,255,.2)', tension:.25 },
940
+ { label:'Threshold', data: [], borderColor:'#ff6b6b', borderDash:[6,4], pointRadius:0 }
941
+ ]
942
+ },
943
+ options: {
944
+ responsive: true,
945
+ maintainAspectRatio: false,
946
+ scales: {
947
+ y: { min: 0, max: 1, grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } },
948
+ x: { grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab', autoSkip: true } }
949
+ },
950
+ plugins: { legend: { labels:{ color:'#dfe7ff' } } }
951
+ }
952
+ });
953
+ }
954
+ function updateLossChart(lossArr, accArr){
955
+ if (!lossChart) return;
956
+ lossChart.data.labels = lossArr.map((_,i)=>`Epoch ${i+1}`);
957
+ lossChart.data.datasets[0].data = lossArr;
958
+ lossChart.data.datasets[1].data = accArr;
959
+ lossChart.update();
960
+ }
961
+ function renderConfusionMatrix(cm){
962
+ if (!cmChart) return;
963
+ const tn = cm[0][0], fp = cm[0][1], fn = cm[1][0], tp = cm[1][1];
964
+ cmChart.data.datasets[0].data = [tn, fp, fn, tp];
965
+ cmChart.update();
966
+ }
967
+ function pushMonitorPoint(score){
968
+ if (!monitorChart) return;
969
+ const labels = monitorChart.data.labels;
970
+ labels.push(labels.length+1);
971
+ monitorChart.data.datasets[0].data.push(score);
972
+ monitorChart.data.datasets[1].push(0.5);
973
+ if (labels.length > 50){
974
+ labels.shift();
975
+ monitorChart.data.datasets.forEach(ds => ds.data.shift());
976
+ }
977
+ monitorChart.update();
978
+ }
979
+
980
+ // --- UI: sample parsing & prediction ---
981
+ function parseListNumberInput(el){
982
+ return el.value
983
+ .split(',')
984
+ .map(s => Number(s.trim()))
985
+ .filter(v => Number.isFinite(v));
986
+ }
987
+ function getSampleFromUI(){
988
+ const ssid = $('#ssid').value.trim() || 'Unknown';
989
+ const bssid = $('#bssid').value.trim() || '00:00:00:00:00:00';
990
+ const rssi = Number($('#rssi').value);
991
+ const channel = Number($('#channel').value);
992
+ const frequency = Number($('#frequency').value) || (2412 + (channel-1)*5);
993
+ const encryption = $('#encryption').value;
994
+ const hidden = $('#hidden').value === 'true';
995
+ const encMismatch = $('#enc-mismatch').value === 'true';
996
+ const expectedBSSIDs = $('#expected-bssids').value.split(',').map(s => s.trim()).filter(Boolean);
997
+ const knownChannels = parseListNumberInput($('#known-ch')) || [1,6,11];
998
+
999
+ return {
1000
+ ssid, bssid, rssi, channel, frequency,
1001
+ encryption, hidden, encMismatch, expectedBSSIDs, knownChannels
1002
+ };
1003
+ }
1004
+ function featuresFromUI(){
1005
+ const s = getSampleFromUI();
1006
+ return extractFeatures({
1007
+ rssi: s.rssi,
1008
+ channel: s.channel,
1009
+ knownChannels: s.knownChannels,
1010
+ encryption: s.encryption,
1011
+ encMismatch: s.encMismatch,
1012
+ hidden: s.hidden
1013
+ });
1014
+ }
1015
+ function explain(features, sample){
1016
+ const [sNorm, chDeltaNorm, mismatch] = features.map(v => Number(v.toFixed(2)));
1017
+ const nearest = sample.knownChannels.reduce((best, kc) => Math.min(best, Math.abs(kc - sample.channel)), 99);
1018
+ const exp = [];
1019
+ if (mismatch > 0.5) exp.push(`Security mismatch or weak encryption (${mismatch.toFixed(2)})`);
1020
+ if (sNorm < 0.5) exp.push(`Unusually strong signal (${sNorm.toFixed(2)})`);
1021
+ if (chDeltaNorm > 0.4) exp.push(`Channel far from known (Δ=${nearest})`);
1022
+ if (sample.hidden) exp.push(`Hidden SSID`);
1023
+ return exp.length ? exp.join('; ') : 'No strong anomalies detected';
1024
+ }
1025
+ function showPrediction(features){
1026
+ const sample = getSampleFromUI();
1027
+ const prob = predictProba(features);
1028
+ const label