@samimcodes/ai-shield 0.1.1 → 0.2.0
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.
- package/README.md +135 -29
- package/dist/index.cjs +826 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +826 -2
- package/dist/index.js.map +1 -1
- package/dist/{middleware-DNRM5IlO.d.cts → middleware-C7DQhFkV.d.cts} +104 -2
- package/dist/{middleware-DNRM5IlO.d.ts → middleware-C7DQhFkV.d.ts} +104 -2
- package/dist/middleware.cjs +826 -1
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.d.cts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +826 -2
- package/dist/middleware.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1316,6 +1316,787 @@ var TelemetryEmitter = class {
|
|
|
1316
1316
|
await this.flush();
|
|
1317
1317
|
}
|
|
1318
1318
|
};
|
|
1319
|
+
var DashboardManager = class {
|
|
1320
|
+
basePath;
|
|
1321
|
+
auth;
|
|
1322
|
+
maxStoredEvents;
|
|
1323
|
+
debug;
|
|
1324
|
+
events = [];
|
|
1325
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
1326
|
+
heartbeatTimer = null;
|
|
1327
|
+
stats = {
|
|
1328
|
+
totalRequests: 0,
|
|
1329
|
+
botsIntercepted: 0,
|
|
1330
|
+
goodBotsAllowed: 0,
|
|
1331
|
+
honeypotTriggers: 0,
|
|
1332
|
+
poisonPayloadsServed: 0,
|
|
1333
|
+
estimatedFakeTokens: 0,
|
|
1334
|
+
topBots: {},
|
|
1335
|
+
topRoutes: {}
|
|
1336
|
+
};
|
|
1337
|
+
constructor(options = {}) {
|
|
1338
|
+
this.basePath = options.basePath ?? "/admin/ai-shield";
|
|
1339
|
+
this.auth = options.auth;
|
|
1340
|
+
this.maxStoredEvents = options.maxStoredEvents ?? 100;
|
|
1341
|
+
this.debug = options.debug ?? false;
|
|
1342
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1343
|
+
this.broadcastPing();
|
|
1344
|
+
}, 15e3);
|
|
1345
|
+
if (this.heartbeatTimer.unref) {
|
|
1346
|
+
this.heartbeatTimer.unref();
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Stop background timers and disconnect SSE clients (useful during teardown).
|
|
1351
|
+
*/
|
|
1352
|
+
close() {
|
|
1353
|
+
if (this.heartbeatTimer !== null) {
|
|
1354
|
+
clearInterval(this.heartbeatTimer);
|
|
1355
|
+
this.heartbeatTimer = null;
|
|
1356
|
+
}
|
|
1357
|
+
for (const client of this.sseClients) {
|
|
1358
|
+
try {
|
|
1359
|
+
client.end();
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
this.sseClients.clear();
|
|
1364
|
+
}
|
|
1365
|
+
/** Record a normal or bot request to update total counters. */
|
|
1366
|
+
recordRequest() {
|
|
1367
|
+
this.stats.totalRequests++;
|
|
1368
|
+
}
|
|
1369
|
+
/** Record a legitimate search engine pass-through (Googlebot, Bingbot, etc). */
|
|
1370
|
+
recordGoodBot(botName, path) {
|
|
1371
|
+
this.stats.totalRequests++;
|
|
1372
|
+
this.stats.goodBotsAllowed++;
|
|
1373
|
+
this.stats.topBots[botName] = (this.stats.topBots[botName] ?? 0) + 1;
|
|
1374
|
+
this.stats.topRoutes[path] = (this.stats.topRoutes[path] ?? 0) + 1;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Record an intercepted AI crawler or scraper, update stats,
|
|
1378
|
+
* prepend to event log, and push to all active SSE browser tabs.
|
|
1379
|
+
*/
|
|
1380
|
+
recordEvent(event) {
|
|
1381
|
+
this.stats.totalRequests++;
|
|
1382
|
+
this.stats.botsIntercepted++;
|
|
1383
|
+
if (event.action === "HONEYPOT_TRAP") {
|
|
1384
|
+
this.stats.honeypotTriggers++;
|
|
1385
|
+
} else {
|
|
1386
|
+
this.stats.poisonPayloadsServed++;
|
|
1387
|
+
this.stats.estimatedFakeTokens += 480;
|
|
1388
|
+
}
|
|
1389
|
+
this.stats.topBots[event.botName] = (this.stats.topBots[event.botName] ?? 0) + 1;
|
|
1390
|
+
this.stats.topRoutes[event.path] = (this.stats.topRoutes[event.path] ?? 0) + 1;
|
|
1391
|
+
const fullEvent = {
|
|
1392
|
+
id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1393
|
+
timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1394
|
+
botName: event.botName,
|
|
1395
|
+
category: event.category,
|
|
1396
|
+
path: event.path,
|
|
1397
|
+
score: event.score,
|
|
1398
|
+
signals: event.signals,
|
|
1399
|
+
ipHash: event.ipHash,
|
|
1400
|
+
userAgent: event.userAgent,
|
|
1401
|
+
action: event.action
|
|
1402
|
+
};
|
|
1403
|
+
this.events.unshift(fullEvent);
|
|
1404
|
+
if (this.events.length > this.maxStoredEvents) {
|
|
1405
|
+
this.events.length = this.maxStoredEvents;
|
|
1406
|
+
}
|
|
1407
|
+
this.broadcastEvent(fullEvent);
|
|
1408
|
+
return fullEvent;
|
|
1409
|
+
}
|
|
1410
|
+
/** Retrieve snapshot of current metrics. */
|
|
1411
|
+
getStats() {
|
|
1412
|
+
return { ...this.stats, topBots: { ...this.stats.topBots }, topRoutes: { ...this.stats.topRoutes } };
|
|
1413
|
+
}
|
|
1414
|
+
/** Retrieve recent security events. */
|
|
1415
|
+
getEvents() {
|
|
1416
|
+
return [...this.events];
|
|
1417
|
+
}
|
|
1418
|
+
/** Broadcast SSE event to all connected dashboard tabs. */
|
|
1419
|
+
broadcastEvent(event) {
|
|
1420
|
+
const payload = `data: ${JSON.stringify({ type: "event", event, stats: this.getStats() })}
|
|
1421
|
+
|
|
1422
|
+
`;
|
|
1423
|
+
for (const client of this.sseClients) {
|
|
1424
|
+
try {
|
|
1425
|
+
client.write(payload);
|
|
1426
|
+
} catch {
|
|
1427
|
+
this.sseClients.delete(client);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/** Broadcast periodic ping to keep SSE connections open through proxies. */
|
|
1432
|
+
broadcastPing() {
|
|
1433
|
+
if (this.sseClients.size === 0) return;
|
|
1434
|
+
const payload = `: ping
|
|
1435
|
+
|
|
1436
|
+
`;
|
|
1437
|
+
for (const client of this.sseClients) {
|
|
1438
|
+
try {
|
|
1439
|
+
client.write(payload);
|
|
1440
|
+
} catch {
|
|
1441
|
+
this.sseClients.delete(client);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
/** Authenticate request if dashboardAuth is configured. */
|
|
1446
|
+
checkAuth(req, res, next) {
|
|
1447
|
+
if (!this.auth) {
|
|
1448
|
+
next();
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
if (this.auth.token) {
|
|
1452
|
+
const authHeader = req.headers["authorization"];
|
|
1453
|
+
const apiKeyHeader = req.headers["x-ai-shield-key"];
|
|
1454
|
+
const tokenQuery = req.query["token"];
|
|
1455
|
+
if (tokenQuery === this.auth.token || apiKeyHeader === this.auth.token) {
|
|
1456
|
+
next();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (authHeader && authHeader.startsWith("Bearer ") && authHeader.slice(7) === this.auth.token) {
|
|
1460
|
+
next();
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (this.auth.username && this.auth.password) {
|
|
1465
|
+
const authHeader = req.headers["authorization"];
|
|
1466
|
+
if (authHeader && authHeader.startsWith("Basic ")) {
|
|
1467
|
+
const credentials = Buffer.from(authHeader.slice(6), "base64").toString("utf8");
|
|
1468
|
+
const [user, pass] = credentials.split(":");
|
|
1469
|
+
if (user === this.auth.username && pass === this.auth.password) {
|
|
1470
|
+
next();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
res.setHeader("WWW-Authenticate", 'Basic realm="AI Shield Security Dashboard"');
|
|
1475
|
+
res.status(401).send("401 Unauthorized: Invalid AI Shield dashboard credentials.");
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (this.auth.token) {
|
|
1479
|
+
res.status(401).json({ error: "Unauthorized: Missing or invalid token." });
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
next();
|
|
1483
|
+
}
|
|
1484
|
+
/** Mount and return Express router serving the live dashboard and API. */
|
|
1485
|
+
buildRouter() {
|
|
1486
|
+
const router = express.Router();
|
|
1487
|
+
router.use((req, res, next) => this.checkAuth(req, res, next));
|
|
1488
|
+
router.get("/api/stats", (_req, res) => {
|
|
1489
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1490
|
+
res.json({
|
|
1491
|
+
success: true,
|
|
1492
|
+
stats: this.getStats(),
|
|
1493
|
+
recentEvents: this.getEvents()
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
router.get("/events", (req, res) => {
|
|
1497
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1498
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1499
|
+
res.setHeader("Connection", "keep-alive");
|
|
1500
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1501
|
+
res.flushHeaders?.();
|
|
1502
|
+
res.write(`data: ${JSON.stringify({ type: "init", stats: this.getStats(), events: this.getEvents() })}
|
|
1503
|
+
|
|
1504
|
+
`);
|
|
1505
|
+
this.sseClients.add(res);
|
|
1506
|
+
req.on("close", () => {
|
|
1507
|
+
this.sseClients.delete(res);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
router.post("/api/test-event", (_req, res) => {
|
|
1511
|
+
const defaultChoice = {
|
|
1512
|
+
name: "GPTBot (OpenAI)",
|
|
1513
|
+
path: "/api/v1/products",
|
|
1514
|
+
cat: "AI_CRAWLER" /* AI_CRAWLER */,
|
|
1515
|
+
signals: ["UA_PATTERN_MATCH", "MISSING_ACCEPT_LANGUAGE"]
|
|
1516
|
+
};
|
|
1517
|
+
const testBots = [
|
|
1518
|
+
defaultChoice,
|
|
1519
|
+
{ name: "Claude-Web (Anthropic)", path: "/catalog/pricing", cat: "AI_CRAWLER" /* AI_CRAWLER */, signals: ["UA_PATTERN_MATCH"] },
|
|
1520
|
+
{ name: "ByteSpider (TikTok)", path: "/content/articles", cat: "SCRAPER" /* SCRAPER */, signals: ["UA_PATTERN_MATCH", "SUSPICIOUS_UA"] },
|
|
1521
|
+
{ name: "Honeypot Trap Route", path: "/api/internal/telemetry-sync", cat: "AI_CRAWLER" /* AI_CRAWLER */, signals: ["HONEYPOT_TRIGGERED"] }
|
|
1522
|
+
];
|
|
1523
|
+
const choice = testBots[Math.floor(Math.random() * testBots.length)] ?? defaultChoice;
|
|
1524
|
+
const event = this.recordEvent({
|
|
1525
|
+
botName: choice.name,
|
|
1526
|
+
category: choice.cat,
|
|
1527
|
+
path: choice.path,
|
|
1528
|
+
score: choice.name.includes("Honeypot") ? 100 : 95,
|
|
1529
|
+
signals: choice.signals,
|
|
1530
|
+
ipHash: Math.random().toString(16).slice(2, 10) + "...",
|
|
1531
|
+
userAgent: `Simulated/${choice.name.split(" ")[0]} Bot Simulator`,
|
|
1532
|
+
action: choice.name.includes("Honeypot") ? "HONEYPOT_TRAP" : "POISON_JSON"
|
|
1533
|
+
});
|
|
1534
|
+
res.json({ success: true, event });
|
|
1535
|
+
});
|
|
1536
|
+
router.get("/", (_req, res) => {
|
|
1537
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1538
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1539
|
+
res.send(this.renderHtml());
|
|
1540
|
+
});
|
|
1541
|
+
return router;
|
|
1542
|
+
}
|
|
1543
|
+
/** Render self-contained, responsive, dark-mode single-page HTML dashboard. */
|
|
1544
|
+
renderHtml() {
|
|
1545
|
+
const basePath = this.basePath;
|
|
1546
|
+
return `<!DOCTYPE html>
|
|
1547
|
+
<html lang="en">
|
|
1548
|
+
<head>
|
|
1549
|
+
<meta charset="UTF-8">
|
|
1550
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1551
|
+
<title>AI Shield \u2014 Live Security Monitor</title>
|
|
1552
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1553
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1554
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
1555
|
+
<style>
|
|
1556
|
+
:root {
|
|
1557
|
+
--bg: #090d16;
|
|
1558
|
+
--card-bg: rgba(17, 24, 39, 0.75);
|
|
1559
|
+
--card-border: rgba(255, 255, 255, 0.08);
|
|
1560
|
+
--primary: #6366f1;
|
|
1561
|
+
--primary-hover: #4f46e5;
|
|
1562
|
+
--accent: #10b981;
|
|
1563
|
+
--danger: #f43f5e;
|
|
1564
|
+
--warning: #f59e0b;
|
|
1565
|
+
--text: #f3f4f6;
|
|
1566
|
+
--text-muted: #9ca3af;
|
|
1567
|
+
}
|
|
1568
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1569
|
+
body {
|
|
1570
|
+
background-color: var(--bg);
|
|
1571
|
+
color: var(--text);
|
|
1572
|
+
font-family: 'Inter', sans-serif;
|
|
1573
|
+
min-height: 100vh;
|
|
1574
|
+
display: flex;
|
|
1575
|
+
flex-direction: column;
|
|
1576
|
+
overflow-x: hidden;
|
|
1577
|
+
background-image: radial-gradient(circle at 50% 0%, rgba(99, 102, 241, 0.12) 0%, transparent 60%);
|
|
1578
|
+
}
|
|
1579
|
+
header {
|
|
1580
|
+
padding: 1.25rem 2rem;
|
|
1581
|
+
border-bottom: 1px solid var(--card-border);
|
|
1582
|
+
display: flex;
|
|
1583
|
+
justify-content: space-between;
|
|
1584
|
+
align-items: center;
|
|
1585
|
+
background: rgba(9, 13, 22, 0.85);
|
|
1586
|
+
backdrop-filter: blur(12px);
|
|
1587
|
+
position: sticky;
|
|
1588
|
+
top: 0;
|
|
1589
|
+
z-index: 50;
|
|
1590
|
+
}
|
|
1591
|
+
.brand {
|
|
1592
|
+
display: flex;
|
|
1593
|
+
align-items: center;
|
|
1594
|
+
gap: 0.75rem;
|
|
1595
|
+
font-weight: 700;
|
|
1596
|
+
font-size: 1.25rem;
|
|
1597
|
+
letter-spacing: -0.02em;
|
|
1598
|
+
}
|
|
1599
|
+
.shield-icon {
|
|
1600
|
+
width: 32px;
|
|
1601
|
+
height: 32px;
|
|
1602
|
+
background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
1603
|
+
border-radius: 8px;
|
|
1604
|
+
display: flex;
|
|
1605
|
+
align-items: center;
|
|
1606
|
+
justify-content: center;
|
|
1607
|
+
box-shadow: 0 0 16px rgba(99, 102, 241, 0.4);
|
|
1608
|
+
}
|
|
1609
|
+
.badge-live {
|
|
1610
|
+
display: inline-flex;
|
|
1611
|
+
align-items: center;
|
|
1612
|
+
gap: 0.5rem;
|
|
1613
|
+
padding: 0.25rem 0.75rem;
|
|
1614
|
+
border-radius: 9999px;
|
|
1615
|
+
font-size: 0.75rem;
|
|
1616
|
+
font-weight: 600;
|
|
1617
|
+
background: rgba(16, 185, 129, 0.12);
|
|
1618
|
+
color: #34d399;
|
|
1619
|
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
1620
|
+
}
|
|
1621
|
+
.pulse-dot {
|
|
1622
|
+
width: 8px;
|
|
1623
|
+
height: 8px;
|
|
1624
|
+
border-radius: 50%;
|
|
1625
|
+
background: #10b981;
|
|
1626
|
+
box-shadow: 0 0 8px #10b981;
|
|
1627
|
+
animation: pulse 1.8s infinite;
|
|
1628
|
+
}
|
|
1629
|
+
@keyframes pulse {
|
|
1630
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
1631
|
+
50% { opacity: 0.4; transform: scale(0.85); }
|
|
1632
|
+
}
|
|
1633
|
+
.header-actions {
|
|
1634
|
+
display: flex;
|
|
1635
|
+
gap: 0.75rem;
|
|
1636
|
+
align-items: center;
|
|
1637
|
+
}
|
|
1638
|
+
.btn {
|
|
1639
|
+
padding: 0.5rem 1rem;
|
|
1640
|
+
border-radius: 8px;
|
|
1641
|
+
font-size: 0.875rem;
|
|
1642
|
+
font-weight: 600;
|
|
1643
|
+
cursor: pointer;
|
|
1644
|
+
transition: all 0.2s ease;
|
|
1645
|
+
display: inline-flex;
|
|
1646
|
+
align-items: center;
|
|
1647
|
+
gap: 0.5rem;
|
|
1648
|
+
border: 1px solid transparent;
|
|
1649
|
+
}
|
|
1650
|
+
.btn-primary {
|
|
1651
|
+
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
1652
|
+
color: white;
|
|
1653
|
+
box-shadow: 0 0 15px rgba(99, 102, 241, 0.3);
|
|
1654
|
+
}
|
|
1655
|
+
.btn-primary:hover {
|
|
1656
|
+
opacity: 0.92;
|
|
1657
|
+
transform: translateY(-1px);
|
|
1658
|
+
}
|
|
1659
|
+
.btn-outline {
|
|
1660
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1661
|
+
border-color: var(--card-border);
|
|
1662
|
+
color: var(--text);
|
|
1663
|
+
}
|
|
1664
|
+
.btn-outline:hover {
|
|
1665
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1666
|
+
}
|
|
1667
|
+
main {
|
|
1668
|
+
flex: 1;
|
|
1669
|
+
padding: 2rem;
|
|
1670
|
+
max-width: 1400px;
|
|
1671
|
+
width: 100%;
|
|
1672
|
+
margin: 0 auto;
|
|
1673
|
+
display: flex;
|
|
1674
|
+
flex-direction: column;
|
|
1675
|
+
gap: 2rem;
|
|
1676
|
+
}
|
|
1677
|
+
.metrics-grid {
|
|
1678
|
+
display: grid;
|
|
1679
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
1680
|
+
gap: 1.25rem;
|
|
1681
|
+
}
|
|
1682
|
+
.card {
|
|
1683
|
+
background: var(--card-bg);
|
|
1684
|
+
border: 1px solid var(--card-border);
|
|
1685
|
+
border-radius: 12px;
|
|
1686
|
+
padding: 1.25rem 1.5rem;
|
|
1687
|
+
backdrop-filter: blur(16px);
|
|
1688
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24);
|
|
1689
|
+
display: flex;
|
|
1690
|
+
flex-direction: column;
|
|
1691
|
+
gap: 0.5rem;
|
|
1692
|
+
position: relative;
|
|
1693
|
+
overflow: hidden;
|
|
1694
|
+
}
|
|
1695
|
+
.card::before {
|
|
1696
|
+
content: '';
|
|
1697
|
+
position: absolute;
|
|
1698
|
+
top: 0; left: 0; right: 0;
|
|
1699
|
+
height: 2px;
|
|
1700
|
+
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.4), transparent);
|
|
1701
|
+
}
|
|
1702
|
+
.card-title {
|
|
1703
|
+
font-size: 0.8125rem;
|
|
1704
|
+
font-weight: 600;
|
|
1705
|
+
color: var(--text-muted);
|
|
1706
|
+
text-transform: uppercase;
|
|
1707
|
+
letter-spacing: 0.05em;
|
|
1708
|
+
}
|
|
1709
|
+
.card-val {
|
|
1710
|
+
font-size: 2rem;
|
|
1711
|
+
font-weight: 700;
|
|
1712
|
+
color: #fff;
|
|
1713
|
+
font-variant-numeric: tabular-nums;
|
|
1714
|
+
}
|
|
1715
|
+
.card-sub {
|
|
1716
|
+
font-size: 0.75rem;
|
|
1717
|
+
color: var(--text-muted);
|
|
1718
|
+
display: flex;
|
|
1719
|
+
align-items: center;
|
|
1720
|
+
gap: 0.25rem;
|
|
1721
|
+
}
|
|
1722
|
+
.text-emerald { color: #34d399; }
|
|
1723
|
+
.text-rose { color: #fb7185; }
|
|
1724
|
+
.text-amber { color: #fbbf24; }
|
|
1725
|
+
.text-indigo { color: #818cf8; }
|
|
1726
|
+
|
|
1727
|
+
/* Alert Banner Toast */
|
|
1728
|
+
#alert-toast {
|
|
1729
|
+
position: fixed;
|
|
1730
|
+
bottom: 2rem;
|
|
1731
|
+
right: 2rem;
|
|
1732
|
+
max-width: 440px;
|
|
1733
|
+
width: calc(100% - 4rem);
|
|
1734
|
+
background: rgba(17, 24, 39, 0.95);
|
|
1735
|
+
border: 1px solid #f43f5e;
|
|
1736
|
+
border-radius: 12px;
|
|
1737
|
+
padding: 1rem 1.25rem;
|
|
1738
|
+
box-shadow: 0 12px 40px rgba(244, 63, 94, 0.35);
|
|
1739
|
+
backdrop-filter: blur(20px);
|
|
1740
|
+
z-index: 100;
|
|
1741
|
+
display: none;
|
|
1742
|
+
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1743
|
+
}
|
|
1744
|
+
@keyframes slideIn {
|
|
1745
|
+
from { transform: translateY(20px); opacity: 0; }
|
|
1746
|
+
to { transform: translateY(0); opacity: 1; }
|
|
1747
|
+
}
|
|
1748
|
+
.toast-header {
|
|
1749
|
+
display: flex;
|
|
1750
|
+
align-items: center;
|
|
1751
|
+
justify-content: space-between;
|
|
1752
|
+
margin-bottom: 0.25rem;
|
|
1753
|
+
}
|
|
1754
|
+
.toast-tag {
|
|
1755
|
+
font-size: 0.75rem;
|
|
1756
|
+
font-weight: 700;
|
|
1757
|
+
color: #f43f5e;
|
|
1758
|
+
text-transform: uppercase;
|
|
1759
|
+
letter-spacing: 0.05em;
|
|
1760
|
+
}
|
|
1761
|
+
.toast-body {
|
|
1762
|
+
font-size: 0.875rem;
|
|
1763
|
+
line-height: 1.4;
|
|
1764
|
+
color: #e5e7eb;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/* Feed Table */
|
|
1768
|
+
.table-container {
|
|
1769
|
+
background: var(--card-bg);
|
|
1770
|
+
border: 1px solid var(--card-border);
|
|
1771
|
+
border-radius: 12px;
|
|
1772
|
+
overflow: hidden;
|
|
1773
|
+
backdrop-filter: blur(16px);
|
|
1774
|
+
}
|
|
1775
|
+
.table-header {
|
|
1776
|
+
padding: 1.25rem 1.5rem;
|
|
1777
|
+
border-bottom: 1px solid var(--card-border);
|
|
1778
|
+
display: flex;
|
|
1779
|
+
justify-content: space-between;
|
|
1780
|
+
align-items: center;
|
|
1781
|
+
}
|
|
1782
|
+
.table-title {
|
|
1783
|
+
font-size: 1.125rem;
|
|
1784
|
+
font-weight: 600;
|
|
1785
|
+
}
|
|
1786
|
+
table {
|
|
1787
|
+
width: 100%;
|
|
1788
|
+
border-collapse: collapse;
|
|
1789
|
+
text-align: left;
|
|
1790
|
+
font-size: 0.875rem;
|
|
1791
|
+
}
|
|
1792
|
+
th {
|
|
1793
|
+
padding: 0.875rem 1.25rem;
|
|
1794
|
+
background: rgba(255, 255, 255, 0.02);
|
|
1795
|
+
color: var(--text-muted);
|
|
1796
|
+
font-weight: 600;
|
|
1797
|
+
font-size: 0.75rem;
|
|
1798
|
+
text-transform: uppercase;
|
|
1799
|
+
letter-spacing: 0.05em;
|
|
1800
|
+
border-bottom: 1px solid var(--card-border);
|
|
1801
|
+
}
|
|
1802
|
+
td {
|
|
1803
|
+
padding: 1rem 1.25rem;
|
|
1804
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
1805
|
+
color: #d1d5db;
|
|
1806
|
+
}
|
|
1807
|
+
tr:last-child td { border-bottom: none; }
|
|
1808
|
+
tr:hover td { background: rgba(255, 255, 255, 0.02); }
|
|
1809
|
+
.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.8125rem; }
|
|
1810
|
+
.badge {
|
|
1811
|
+
display: inline-flex;
|
|
1812
|
+
align-items: center;
|
|
1813
|
+
padding: 0.2rem 0.6rem;
|
|
1814
|
+
border-radius: 6px;
|
|
1815
|
+
font-size: 0.75rem;
|
|
1816
|
+
font-weight: 600;
|
|
1817
|
+
}
|
|
1818
|
+
.badge-danger { background: rgba(244, 63, 94, 0.15); color: #fb7185; border: 1px solid rgba(244, 63, 94, 0.3); }
|
|
1819
|
+
.badge-amber { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.3); }
|
|
1820
|
+
.badge-emerald { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); }
|
|
1821
|
+
.empty-state {
|
|
1822
|
+
padding: 4rem 2rem;
|
|
1823
|
+
text-align: center;
|
|
1824
|
+
color: var(--text-muted);
|
|
1825
|
+
}
|
|
1826
|
+
@keyframes flashRow {
|
|
1827
|
+
0% { background: rgba(99, 102, 241, 0.25); }
|
|
1828
|
+
100% { background: transparent; }
|
|
1829
|
+
}
|
|
1830
|
+
.new-row {
|
|
1831
|
+
animation: flashRow 2s ease-out;
|
|
1832
|
+
}
|
|
1833
|
+
</style>
|
|
1834
|
+
</head>
|
|
1835
|
+
<body>
|
|
1836
|
+
|
|
1837
|
+
<header>
|
|
1838
|
+
<div class="brand">
|
|
1839
|
+
<div class="shield-icon">
|
|
1840
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1841
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
1842
|
+
</svg>
|
|
1843
|
+
</div>
|
|
1844
|
+
<span>ai-shield <span style="font-weight:400; opacity:0.6; font-size: 0.9rem;">Live Monitor</span></span>
|
|
1845
|
+
<div class="badge-live">
|
|
1846
|
+
<div class="pulse-dot"></div>
|
|
1847
|
+
ACTIVE DEFENSE
|
|
1848
|
+
</div>
|
|
1849
|
+
</div>
|
|
1850
|
+
<div class="header-actions">
|
|
1851
|
+
<button class="btn btn-outline" id="audioToggle">
|
|
1852
|
+
<span id="audioIcon">\u{1F514} Sound ON</span>
|
|
1853
|
+
</button>
|
|
1854
|
+
<button class="btn btn-primary" id="testAttackBtn">
|
|
1855
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
|
1856
|
+
Simulate Bot Attack
|
|
1857
|
+
</button>
|
|
1858
|
+
</div>
|
|
1859
|
+
</header>
|
|
1860
|
+
|
|
1861
|
+
<main>
|
|
1862
|
+
<!-- Metrics Row -->
|
|
1863
|
+
<div class="metrics-grid">
|
|
1864
|
+
<div class="card">
|
|
1865
|
+
<div class="card-title">Bots Intercepted</div>
|
|
1866
|
+
<div class="card-val text-rose" id="m-intercepted">0</div>
|
|
1867
|
+
<div class="card-sub text-rose">
|
|
1868
|
+
<span>\u25CF</span> Fed synthetic poisoned responses (HTTP 200)
|
|
1869
|
+
</div>
|
|
1870
|
+
</div>
|
|
1871
|
+
<div class="card">
|
|
1872
|
+
<div class="card-title">Honeypot Traps Sprung</div>
|
|
1873
|
+
<div class="card-val text-amber" id="m-honeypot">0</div>
|
|
1874
|
+
<div class="card-sub text-amber">
|
|
1875
|
+
<span>\u25CF</span> Scrapers quarantined via hidden routes
|
|
1876
|
+
</div>
|
|
1877
|
+
</div>
|
|
1878
|
+
<div class="card">
|
|
1879
|
+
<div class="card-title">Search Engines Allowed (SEO)</div>
|
|
1880
|
+
<div class="card-val text-emerald" id="m-goodbots">0</div>
|
|
1881
|
+
<div class="card-sub text-emerald">
|
|
1882
|
+
<span>\u25CF</span> 0% false positives, 100% crawl integrity
|
|
1883
|
+
</div>
|
|
1884
|
+
</div>
|
|
1885
|
+
<div class="card">
|
|
1886
|
+
<div class="card-title">Fake Tokens Injected</div>
|
|
1887
|
+
<div class="card-val text-indigo" id="m-tokens">0</div>
|
|
1888
|
+
<div class="card-sub text-indigo">
|
|
1889
|
+
<span>\u25CF</span> AI scrapers polluted with synthetic catalog data
|
|
1890
|
+
</div>
|
|
1891
|
+
</div>
|
|
1892
|
+
</div>
|
|
1893
|
+
|
|
1894
|
+
<!-- Live Event Feed Table -->
|
|
1895
|
+
<div class="table-container">
|
|
1896
|
+
<div class="table-header">
|
|
1897
|
+
<div class="table-title">Real-Time Threat Interception Stream</div>
|
|
1898
|
+
<div class="badge-live" style="font-size: 0.7rem;">
|
|
1899
|
+
<span id="conn-status">Connecting to stream...</span>
|
|
1900
|
+
</div>
|
|
1901
|
+
</div>
|
|
1902
|
+
<table>
|
|
1903
|
+
<thead>
|
|
1904
|
+
<tr>
|
|
1905
|
+
<th>Time</th>
|
|
1906
|
+
<th>Bot / Persona</th>
|
|
1907
|
+
<th>Threat Score</th>
|
|
1908
|
+
<th>Route Attacked</th>
|
|
1909
|
+
<th>Defense Action</th>
|
|
1910
|
+
<th>Client Hash</th>
|
|
1911
|
+
</tr>
|
|
1912
|
+
</thead>
|
|
1913
|
+
<tbody id="event-rows">
|
|
1914
|
+
<tr>
|
|
1915
|
+
<td colspan="6" class="empty-state">
|
|
1916
|
+
Listening for incoming crawlers... Waiting for bot traffic.
|
|
1917
|
+
</td>
|
|
1918
|
+
</tr>
|
|
1919
|
+
</tbody>
|
|
1920
|
+
</table>
|
|
1921
|
+
</div>
|
|
1922
|
+
</main>
|
|
1923
|
+
|
|
1924
|
+
<!-- Live Pop-up Alert Toast -->
|
|
1925
|
+
<div id="alert-toast">
|
|
1926
|
+
<div class="toast-header">
|
|
1927
|
+
<span class="toast-tag">\u{1F6A8} AI Crawler Intercepted</span>
|
|
1928
|
+
<span class="mono" style="font-size: 0.75rem; color: #9ca3af;" id="toast-time">Just now</span>
|
|
1929
|
+
</div>
|
|
1930
|
+
<div class="toast-body" id="toast-body">
|
|
1931
|
+
<strong>GPTBot</strong> attempted to scrape <span class="mono">/api/products</span>.
|
|
1932
|
+
Synthetic poisoned JSON served with HTTP 200.
|
|
1933
|
+
</div>
|
|
1934
|
+
</div>
|
|
1935
|
+
|
|
1936
|
+
<script>
|
|
1937
|
+
const basePath = '${basePath}';
|
|
1938
|
+
let soundEnabled = true;
|
|
1939
|
+
let audioCtx = null;
|
|
1940
|
+
|
|
1941
|
+
// Synthesize gentle sci-fi alert chime via Web Audio API (no external file dependencies)
|
|
1942
|
+
function playAlertChime() {
|
|
1943
|
+
if (!soundEnabled) return;
|
|
1944
|
+
try {
|
|
1945
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
1946
|
+
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
1947
|
+
const now = audioCtx.currentTime;
|
|
1948
|
+
const osc = audioCtx.createOscillator();
|
|
1949
|
+
const gain = audioCtx.createGain();
|
|
1950
|
+
osc.type = 'sine';
|
|
1951
|
+
osc.frequency.setValueAtTime(587.33, now); // D5
|
|
1952
|
+
osc.frequency.exponentialRampToValueAtTime(880, now + 0.12); // A5
|
|
1953
|
+
gain.gain.setValueAtTime(0.08, now);
|
|
1954
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
|
|
1955
|
+
osc.connect(gain);
|
|
1956
|
+
gain.connect(audioCtx.destination);
|
|
1957
|
+
osc.start(now);
|
|
1958
|
+
osc.stop(now + 0.35);
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
console.warn('Audio notice:', e);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Toggle audio
|
|
1965
|
+
document.getElementById('audioToggle').addEventListener('click', () => {
|
|
1966
|
+
soundEnabled = !soundEnabled;
|
|
1967
|
+
document.getElementById('audioIcon').innerText = soundEnabled ? '\u{1F514} Sound ON' : '\u{1F515} Sound OFF';
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
// Simulate bot attack button
|
|
1971
|
+
document.getElementById('testAttackBtn').addEventListener('click', async () => {
|
|
1972
|
+
try {
|
|
1973
|
+
const res = await fetch(basePath + '/api/test-event', { method: 'POST' });
|
|
1974
|
+
const data = await res.json();
|
|
1975
|
+
console.log('Simulated attack triggered:', data);
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
console.error('Test attack failed:', err);
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
// Update metric cards
|
|
1982
|
+
function updateStats(stats) {
|
|
1983
|
+
if (!stats) return;
|
|
1984
|
+
document.getElementById('m-intercepted').innerText = Number(stats.botsIntercepted || 0).toLocaleString();
|
|
1985
|
+
document.getElementById('m-honeypot').innerText = Number(stats.honeypotTriggers || 0).toLocaleString();
|
|
1986
|
+
document.getElementById('m-goodbots').innerText = Number(stats.goodBotsAllowed || 0).toLocaleString();
|
|
1987
|
+
document.getElementById('m-tokens').innerText = Number(stats.estimatedFakeTokens || 0).toLocaleString();
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Show floating alert toast
|
|
1991
|
+
let toastTimer = null;
|
|
1992
|
+
function triggerAlertToast(evt) {
|
|
1993
|
+
const toast = document.getElementById('alert-toast');
|
|
1994
|
+
const body = document.getElementById('toast-body');
|
|
1995
|
+
const time = document.getElementById('toast-time');
|
|
1996
|
+
|
|
1997
|
+
time.innerText = new Date(evt.timestamp).toLocaleTimeString();
|
|
1998
|
+
body.innerHTML = '<strong>' + escapeHtml(evt.botName) + '</strong> targeted <span class="mono">' + escapeHtml(evt.path) + '</span>.<br>' +
|
|
1999
|
+
'Action: <span class="badge badge-danger">' + escapeHtml(evt.action) + '</span> (HTTP 200 OK)';
|
|
2000
|
+
|
|
2001
|
+
toast.style.display = 'block';
|
|
2002
|
+
playAlertChime();
|
|
2003
|
+
|
|
2004
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
2005
|
+
toastTimer = setTimeout(() => {
|
|
2006
|
+
toast.style.display = 'none';
|
|
2007
|
+
}, 4500);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function escapeHtml(str) {
|
|
2011
|
+
return String(str || '').replace(/[&<>'"]/g, tag => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[tag] || tag));
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Render table rows
|
|
2015
|
+
function renderRows(events, isNew) {
|
|
2016
|
+
const tbody = document.getElementById('event-rows');
|
|
2017
|
+
if (!events || events.length === 0) return;
|
|
2018
|
+
|
|
2019
|
+
tbody.innerHTML = events.map((evt, idx) => {
|
|
2020
|
+
const timeStr = new Date(evt.timestamp).toLocaleTimeString();
|
|
2021
|
+
const badgeClass = evt.action.includes('HONEYPOT') ? 'badge-amber' : 'badge-danger';
|
|
2022
|
+
const flashClass = isNew && idx === 0 ? 'new-row' : '';
|
|
2023
|
+
|
|
2024
|
+
return '<tr class="' + flashClass + '">' +
|
|
2025
|
+
'<td class="mono">' + timeStr + '</td>' +
|
|
2026
|
+
'<td><strong>' + escapeHtml(evt.botName) + '</strong></td>' +
|
|
2027
|
+
'<td><span class="mono">' + evt.score + '/100</span></td>' +
|
|
2028
|
+
'<td class="mono text-indigo">' + escapeHtml(evt.path) + '</td>' +
|
|
2029
|
+
'<td><span class="badge ' + badgeClass + '">' + escapeHtml(evt.action) + '</span></td>' +
|
|
2030
|
+
'<td class="mono" style="opacity:0.7;">' + escapeHtml(evt.ipHash) + '</td>' +
|
|
2031
|
+
'</tr>';
|
|
2032
|
+
}).join('');
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Connect SSE stream
|
|
2036
|
+
function initSSE() {
|
|
2037
|
+
const connStatus = document.getElementById('conn-status');
|
|
2038
|
+
let es = null;
|
|
2039
|
+
|
|
2040
|
+
try {
|
|
2041
|
+
es = new EventSource(basePath + '/events');
|
|
2042
|
+
|
|
2043
|
+
es.onopen = () => {
|
|
2044
|
+
connStatus.innerText = '\u25CF Live EventSource Active';
|
|
2045
|
+
connStatus.style.color = '#34d399';
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
es.onmessage = (msg) => {
|
|
2049
|
+
try {
|
|
2050
|
+
const data = JSON.parse(msg.data);
|
|
2051
|
+
if (data.type === 'init') {
|
|
2052
|
+
updateStats(data.stats);
|
|
2053
|
+
renderRows(data.events, false);
|
|
2054
|
+
} else if (data.type === 'event') {
|
|
2055
|
+
updateStats(data.stats);
|
|
2056
|
+
triggerAlertToast(data.event);
|
|
2057
|
+
// Fetch latest list or unshift
|
|
2058
|
+
fetch(basePath + '/api/stats')
|
|
2059
|
+
.then(r => r.json())
|
|
2060
|
+
.then(res => {
|
|
2061
|
+
if (res.recentEvents) renderRows(res.recentEvents, true);
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
} catch (e) {
|
|
2065
|
+
console.error('SSE parse error:', e);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
es.onerror = () => {
|
|
2070
|
+
connStatus.innerText = '\u25CF Polling Fallback';
|
|
2071
|
+
connStatus.style.color = '#fbbf24';
|
|
2072
|
+
es.close();
|
|
2073
|
+
// Fallback polling every 3 seconds
|
|
2074
|
+
setInterval(fetchLatestStats, 3000);
|
|
2075
|
+
};
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
connStatus.innerText = '\u25CF Polling Mode';
|
|
2078
|
+
setInterval(fetchLatestStats, 3000);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
async function fetchLatestStats() {
|
|
2083
|
+
try {
|
|
2084
|
+
const res = await fetch(basePath + '/api/stats');
|
|
2085
|
+
const data = await res.json();
|
|
2086
|
+
if (data.stats) updateStats(data.stats);
|
|
2087
|
+
if (data.recentEvents) renderRows(data.recentEvents, false);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
// silent
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// Start on page load
|
|
2094
|
+
initSSE();
|
|
2095
|
+
</script>
|
|
2096
|
+
</body>
|
|
2097
|
+
</html>`;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
1319
2100
|
|
|
1320
2101
|
// src/middleware.ts
|
|
1321
2102
|
var DEFAULT_SCORE_THRESHOLD = 50;
|
|
@@ -1332,11 +2113,16 @@ function createAiShield(options = {}) {
|
|
|
1332
2113
|
honeypotTtlMs = 24 * 60 * 60 * 1e3,
|
|
1333
2114
|
debug = false,
|
|
1334
2115
|
apiKey,
|
|
1335
|
-
dashboardUrl
|
|
2116
|
+
dashboardUrl,
|
|
2117
|
+
enableDashboard = false,
|
|
2118
|
+
dashboardPath = "/admin/ai-shield",
|
|
2119
|
+
dashboardAuth
|
|
1336
2120
|
} = options;
|
|
1337
2121
|
const detector = new BotDetector(customFingerprints, signalWeights);
|
|
1338
2122
|
const poisonEngine = new PoisonEngine(poisonContentType);
|
|
1339
2123
|
const telemetry = apiKey !== void 0 ? new TelemetryEmitter({ apiKey, dashboardUrl }) : void 0;
|
|
2124
|
+
const dashboard = enableDashboard ? new DashboardManager({ basePath: dashboardPath, auth: dashboardAuth, debug }) : void 0;
|
|
2125
|
+
const dashboardRouter = dashboard ? dashboard.buildRouter() : void 0;
|
|
1340
2126
|
const honeypot = new HoneypotManager(poisonEngine, {
|
|
1341
2127
|
customRoutes: honeypotPaths,
|
|
1342
2128
|
ttlMs: honeypotTtlMs,
|
|
@@ -1347,6 +2133,18 @@ function createAiShield(options = {}) {
|
|
|
1347
2133
|
`[ai-shield] Honeypot trap: "${route.label}" | IP: ${ip} | UA: ${ua.slice(0, 80)}`
|
|
1348
2134
|
);
|
|
1349
2135
|
}
|
|
2136
|
+
if (dashboard !== void 0) {
|
|
2137
|
+
dashboard.recordEvent({
|
|
2138
|
+
botName: `HoneypotTrap:${route.label}`,
|
|
2139
|
+
category: "AI_CRAWLER" /* AI_CRAWLER */,
|
|
2140
|
+
path: route.path,
|
|
2141
|
+
score: 100,
|
|
2142
|
+
signals: ["HONEYPOT_TRIGGERED"],
|
|
2143
|
+
ipHash: hashClientIp(ip),
|
|
2144
|
+
userAgent: ua,
|
|
2145
|
+
action: "HONEYPOT_TRAP"
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
1350
2148
|
if (telemetry !== void 0) {
|
|
1351
2149
|
telemetry.emit({
|
|
1352
2150
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1370,6 +2168,17 @@ function createAiShield(options = {}) {
|
|
|
1370
2168
|
const honeypotRouter = honeypot.buildRouter();
|
|
1371
2169
|
const excludeSet = new Set(excludePaths);
|
|
1372
2170
|
const shield = (req, res, next) => {
|
|
2171
|
+
if (dashboardRouter !== void 0 && (req.path === dashboardPath || req.path.startsWith(dashboardPath + "/"))) {
|
|
2172
|
+
const originalUrl = req.url;
|
|
2173
|
+
const subPath = req.path.slice(dashboardPath.length) || "/";
|
|
2174
|
+
const query = req.url.includes("?") ? req.url.slice(req.url.indexOf("?")) : "";
|
|
2175
|
+
req.url = subPath + query;
|
|
2176
|
+
dashboardRouter(req, res, (err) => {
|
|
2177
|
+
req.url = originalUrl;
|
|
2178
|
+
next(err);
|
|
2179
|
+
});
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
1373
2182
|
if (excludeSet.has(req.path)) {
|
|
1374
2183
|
next();
|
|
1375
2184
|
return;
|
|
@@ -1384,6 +2193,7 @@ function createAiShield(options = {}) {
|
|
|
1384
2193
|
const meta = { detection, poisoned: false };
|
|
1385
2194
|
req.aiShield = meta;
|
|
1386
2195
|
if (detection.category === "GOOD_BOT" /* GOOD_BOT */) {
|
|
2196
|
+
dashboard?.recordGoodBot(detection.label, req.path);
|
|
1387
2197
|
if (debug) {
|
|
1388
2198
|
console.info(
|
|
1389
2199
|
`[ai-shield] Good bot allowed: ${detection.label} | ${req.method} ${req.path}`
|
|
@@ -1393,6 +2203,7 @@ function createAiShield(options = {}) {
|
|
|
1393
2203
|
return;
|
|
1394
2204
|
}
|
|
1395
2205
|
if (detection.score < scoreThreshold && !isHoneypotFlagged) {
|
|
2206
|
+
dashboard?.recordRequest();
|
|
1396
2207
|
if (debug && detection.score > 0) {
|
|
1397
2208
|
console.info(
|
|
1398
2209
|
`[ai-shield] Below threshold (${detection.score} < ${scoreThreshold}): ${detection.label} | ${req.path}`
|
|
@@ -1428,6 +2239,18 @@ function createAiShield(options = {}) {
|
|
|
1428
2239
|
} else if (accept.includes("application/json") && poisonContentType === "HTML") {
|
|
1429
2240
|
effectiveType = "JSON";
|
|
1430
2241
|
}
|
|
2242
|
+
if (dashboard !== void 0) {
|
|
2243
|
+
dashboard.recordEvent({
|
|
2244
|
+
botName: detection.label,
|
|
2245
|
+
category: detection.category,
|
|
2246
|
+
path: req.path,
|
|
2247
|
+
score: detection.score,
|
|
2248
|
+
signals: detection.signals,
|
|
2249
|
+
ipHash: hashClientIp(clientIp),
|
|
2250
|
+
userAgent: req.headers["user-agent"] ?? "",
|
|
2251
|
+
action: effectiveType === "JSON" ? "POISON_JSON" : effectiveType === "SITEMAP" ? "POISON_SITEMAP" : "POISON_HTML"
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
1431
2254
|
const payload = poisonEngine.generate(req.path, effectiveType);
|
|
1432
2255
|
let responseBody = payload.body;
|
|
1433
2256
|
if (injectHoneypotLinks && effectiveType === "HTML") {
|
|
@@ -1442,6 +2265,7 @@ function createAiShield(options = {}) {
|
|
|
1442
2265
|
});
|
|
1443
2266
|
};
|
|
1444
2267
|
shield.telemetry = telemetry;
|
|
2268
|
+
shield.dashboard = dashboard;
|
|
1445
2269
|
return shield;
|
|
1446
2270
|
}
|
|
1447
2271
|
var aiShieldMiddleware = createAiShield;
|
|
@@ -1474,6 +2298,7 @@ exports.BotDetector = BotDetector;
|
|
|
1474
2298
|
exports.CANONICAL_TRAP = CANONICAL_TRAP;
|
|
1475
2299
|
exports.Confidence = Confidence;
|
|
1476
2300
|
exports.DEFAULT_SIGNAL_WEIGHTS = DEFAULT_SIGNAL_WEIGHTS;
|
|
2301
|
+
exports.DashboardManager = DashboardManager;
|
|
1477
2302
|
exports.HoneypotManager = HoneypotManager;
|
|
1478
2303
|
exports.IpTrapCache = IpTrapCache;
|
|
1479
2304
|
exports.PoisonEngine = PoisonEngine;
|