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