vigile-scan 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/dist/index.js +1141 -10
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -25,8 +25,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
28
29
|
var import_ora = __toESM(require("ora"));
|
|
29
|
-
var
|
|
30
|
+
var import_promises4 = require("fs/promises");
|
|
30
31
|
|
|
31
32
|
// src/discovery/claude-desktop.ts
|
|
32
33
|
var import_path2 = require("path");
|
|
@@ -1310,6 +1311,577 @@ function deduplicateFindings2(findings) {
|
|
|
1310
1311
|
});
|
|
1311
1312
|
}
|
|
1312
1313
|
|
|
1314
|
+
// src/sentinel/sentinel.ts
|
|
1315
|
+
var import_child_process = require("child_process");
|
|
1316
|
+
|
|
1317
|
+
// src/sentinel/sentinel-patterns.ts
|
|
1318
|
+
var SUSPICIOUS_ENDPOINT_PATTERNS = [
|
|
1319
|
+
{
|
|
1320
|
+
id: "SN-001",
|
|
1321
|
+
severity: "critical",
|
|
1322
|
+
title: "Known data exfiltration endpoint",
|
|
1323
|
+
urlPattern: /https?:\/\/[^/]*(?:pastebin\.com|hastebin\.com|ghostbin\.co|paste\.ee|dpaste\.org|transfer\.sh|file\.io|0x0\.st|ix\.io)\/(?:api|raw|upload|documents)/i,
|
|
1324
|
+
description: "MCP server is sending data to a paste/file sharing service commonly used for data exfiltration.",
|
|
1325
|
+
recommendation: "CRITICAL: Stop this MCP server immediately. Legitimate tools should never upload to paste services."
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
id: "SN-002",
|
|
1329
|
+
severity: "critical",
|
|
1330
|
+
title: "Webhook exfiltration channel",
|
|
1331
|
+
urlPattern: /https?:\/\/(?:hooks\.slack\.com|discord(?:app)?\.com\/api\/webhooks|webhook\.site|pipedream\.net|requestbin\.|hookbin\.|beeceptor\.com)/i,
|
|
1332
|
+
description: "MCP server is sending data to a webhook endpoint. Attackers commonly use webhook services as low-noise exfiltration channels.",
|
|
1333
|
+
recommendation: "Investigate what data is being sent to this webhook. This is a common attacker exfiltration method."
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
id: "SN-003",
|
|
1337
|
+
severity: "high",
|
|
1338
|
+
title: "Dynamic DNS destination",
|
|
1339
|
+
urlPattern: /https?:\/\/[^/]*\.(?:duckdns\.org|no-ip\.com|ngrok\.io|ngrok-free\.app|serveo\.net|localhost\.run|bore\.digital|tailscale\.io)(?:\/|$)/i,
|
|
1340
|
+
description: "MCP server is connecting to a dynamic DNS or tunneling service, commonly used for C2 infrastructure.",
|
|
1341
|
+
recommendation: "Review this connection. Dynamic DNS is frequently used by attackers to rotate C2 endpoints."
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
id: "SN-004",
|
|
1345
|
+
severity: "high",
|
|
1346
|
+
title: "Cryptocurrency-related endpoint",
|
|
1347
|
+
urlPattern: /https?:\/\/[^/]*(?:blockchain\.info|etherscan\.io|bscscan\.com|solscan\.io|mempool\.space)\/(?:api|rawaddr|tx|address)/i,
|
|
1348
|
+
description: "MCP server is querying cryptocurrency blockchain APIs, which could indicate wallet scanning or theft.",
|
|
1349
|
+
recommendation: "Unless this is an explicitly crypto-related tool, this connection is highly suspicious."
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
id: "SN-005",
|
|
1353
|
+
severity: "medium",
|
|
1354
|
+
title: "Telemetry to unknown endpoint",
|
|
1355
|
+
urlPattern: /https?:\/\/[^/]*(?:\/(?:telemetry|analytics|tracking|pixel|beacon|collect|event|metrics|heartbeat))(?:\?|$|\/)/i,
|
|
1356
|
+
description: "MCP server is sending telemetry/analytics data. While sometimes legitimate, this can mask data exfiltration.",
|
|
1357
|
+
recommendation: "Verify the telemetry destination is expected. Compare against the MCP server's documentation."
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: "SN-006",
|
|
1361
|
+
severity: "critical",
|
|
1362
|
+
title: "Raw IP connection (no hostname)",
|
|
1363
|
+
urlPattern: /https?:\/\/(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?(?:\/|$)/,
|
|
1364
|
+
description: "MCP server is connecting directly to an IP address instead of a hostname. This bypasses DNS-based monitoring and is a strong indicator of C2 communication.",
|
|
1365
|
+
recommendation: "CRITICAL: Direct IP connections are almost never legitimate for MCP servers. Investigate immediately."
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
id: "SN-007",
|
|
1369
|
+
severity: "high",
|
|
1370
|
+
title: "Non-standard port connection",
|
|
1371
|
+
urlPattern: /https?:\/\/[^/]+:(?!80\b|443\b|8080\b|8443\b|3000\b|5000\b|8000\b)\d{4,5}(?:\/|$)/,
|
|
1372
|
+
description: "MCP server is connecting to a non-standard port. While sometimes legitimate, C2 infrastructure often uses unusual ports.",
|
|
1373
|
+
recommendation: "Verify this port is expected for the service being contacted."
|
|
1374
|
+
}
|
|
1375
|
+
];
|
|
1376
|
+
var BEHAVIORAL_PATTERNS = [
|
|
1377
|
+
{
|
|
1378
|
+
id: "SN-010",
|
|
1379
|
+
severity: "critical",
|
|
1380
|
+
title: "C2 beaconing detected (regular interval)",
|
|
1381
|
+
description: "MCP server is making network requests at regular intervals to the same destination, consistent with command-and-control beaconing. Malware phones home on a schedule to receive commands.",
|
|
1382
|
+
recommendation: "CRITICAL: This pattern strongly indicates the MCP server is compromised. Stop it immediately and investigate the destination.",
|
|
1383
|
+
minEvents: 5,
|
|
1384
|
+
timeWindowSeconds: 300,
|
|
1385
|
+
// 5 minutes
|
|
1386
|
+
detect: (events) => {
|
|
1387
|
+
const byDest = /* @__PURE__ */ new Map();
|
|
1388
|
+
for (const e of events) {
|
|
1389
|
+
const dest = new URL(e.url).hostname;
|
|
1390
|
+
if (!byDest.has(dest)) byDest.set(dest, []);
|
|
1391
|
+
byDest.get(dest).push(e.timestamp);
|
|
1392
|
+
}
|
|
1393
|
+
let maxConfidence = 0;
|
|
1394
|
+
for (const [, timestamps] of byDest) {
|
|
1395
|
+
if (timestamps.length < 5) continue;
|
|
1396
|
+
timestamps.sort((a, b) => a - b);
|
|
1397
|
+
const intervals = [];
|
|
1398
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
1399
|
+
intervals.push(timestamps[i] - timestamps[i - 1]);
|
|
1400
|
+
}
|
|
1401
|
+
const mean = intervals.reduce((s, v) => s + v, 0) / intervals.length;
|
|
1402
|
+
const std = Math.sqrt(
|
|
1403
|
+
intervals.reduce((s, v) => s + (v - mean) ** 2, 0) / intervals.length
|
|
1404
|
+
);
|
|
1405
|
+
const cv = mean > 0 ? std / mean : 1;
|
|
1406
|
+
if (cv < 0.15) maxConfidence = Math.max(maxConfidence, 95);
|
|
1407
|
+
else if (cv < 0.25) maxConfidence = Math.max(maxConfidence, 80);
|
|
1408
|
+
else if (cv < 0.35) maxConfidence = Math.max(maxConfidence, 60);
|
|
1409
|
+
}
|
|
1410
|
+
return maxConfidence;
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
id: "SN-011",
|
|
1415
|
+
severity: "critical",
|
|
1416
|
+
title: "Burst data exfiltration",
|
|
1417
|
+
description: "MCP server sent an unusually large amount of data in a short burst to an external endpoint. This pattern matches credential dump exfiltration and file theft.",
|
|
1418
|
+
recommendation: "CRITICAL: Investigate what data was transmitted. Check for stolen SSH keys, credentials, or source code.",
|
|
1419
|
+
minEvents: 1,
|
|
1420
|
+
timeWindowSeconds: 60,
|
|
1421
|
+
// 1 minute
|
|
1422
|
+
detect: (events) => {
|
|
1423
|
+
const LARGE_REQUEST_THRESHOLD = 5e4;
|
|
1424
|
+
const BURST_THRESHOLD = 2e5;
|
|
1425
|
+
let maxSingle = 0;
|
|
1426
|
+
let totalBytes = 0;
|
|
1427
|
+
for (const e of events) {
|
|
1428
|
+
maxSingle = Math.max(maxSingle, e.requestSize);
|
|
1429
|
+
totalBytes += e.requestSize;
|
|
1430
|
+
}
|
|
1431
|
+
if (maxSingle > LARGE_REQUEST_THRESHOLD) return 90;
|
|
1432
|
+
if (totalBytes > BURST_THRESHOLD) return 85;
|
|
1433
|
+
if (totalBytes > BURST_THRESHOLD / 2) return 60;
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
id: "SN-012",
|
|
1439
|
+
severity: "high",
|
|
1440
|
+
title: "DNS tunneling suspected",
|
|
1441
|
+
description: "MCP server is making an unusual number of DNS queries with high-entropy subdomains. This pattern matches DNS tunneling \u2014 a technique to exfiltrate data by encoding it in DNS queries, which often bypass firewalls.",
|
|
1442
|
+
recommendation: "Investigate the DNS queries. DNS tunneling uses encoded data in subdomain labels (e.g., aGVsbG8=.evil.com).",
|
|
1443
|
+
minEvents: 10,
|
|
1444
|
+
timeWindowSeconds: 120,
|
|
1445
|
+
detect: (events) => {
|
|
1446
|
+
const dnsEvents = events.filter((e) => e.dnsQueryType);
|
|
1447
|
+
if (dnsEvents.length < 10) return 0;
|
|
1448
|
+
let suspiciousCount = 0;
|
|
1449
|
+
for (const e of dnsEvents) {
|
|
1450
|
+
try {
|
|
1451
|
+
const hostname = new URL(e.url).hostname;
|
|
1452
|
+
const parts = hostname.split(".");
|
|
1453
|
+
for (const part of parts) {
|
|
1454
|
+
if (part.length > 30) suspiciousCount++;
|
|
1455
|
+
if (/^[A-Za-z0-9+/]{20,}={0,2}$/.test(part)) suspiciousCount++;
|
|
1456
|
+
}
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
const ratio = suspiciousCount / dnsEvents.length;
|
|
1461
|
+
if (ratio > 0.5) return 90;
|
|
1462
|
+
if (ratio > 0.3) return 70;
|
|
1463
|
+
if (ratio > 0.1) return 40;
|
|
1464
|
+
return 0;
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
id: "SN-013",
|
|
1469
|
+
severity: "high",
|
|
1470
|
+
title: "Multi-destination scatter exfiltration",
|
|
1471
|
+
description: "MCP server is distributing data across many different external destinations in a short period. Sophisticated exfiltration splits data across multiple endpoints to avoid size-based detection.",
|
|
1472
|
+
recommendation: "Review all destinations contacted. This pattern suggests deliberate evasion of single-destination monitoring.",
|
|
1473
|
+
minEvents: 5,
|
|
1474
|
+
timeWindowSeconds: 120,
|
|
1475
|
+
detect: (events) => {
|
|
1476
|
+
const destinations = /* @__PURE__ */ new Set();
|
|
1477
|
+
for (const e of events) {
|
|
1478
|
+
try {
|
|
1479
|
+
destinations.add(new URL(e.url).hostname);
|
|
1480
|
+
} catch {
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const withData = events.filter((e) => e.requestSize > 500);
|
|
1484
|
+
const destWithData = /* @__PURE__ */ new Set();
|
|
1485
|
+
for (const e of withData) {
|
|
1486
|
+
try {
|
|
1487
|
+
destWithData.add(new URL(e.url).hostname);
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (destWithData.size > 10) return 85;
|
|
1492
|
+
if (destWithData.size > 5) return 65;
|
|
1493
|
+
if (destWithData.size > 3 && withData.length > events.length * 0.5) return 50;
|
|
1494
|
+
return 0;
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
id: "SN-014",
|
|
1499
|
+
severity: "high",
|
|
1500
|
+
title: "High-entropy payload transmission",
|
|
1501
|
+
description: "MCP server is sending request bodies with unusually high entropy (randomness), suggesting encrypted or compressed data exfiltration. Legitimate API calls typically have structured, lower-entropy payloads.",
|
|
1502
|
+
recommendation: "Investigate the payload contents. High entropy in outbound data often indicates stolen credentials or encrypted exfiltration.",
|
|
1503
|
+
minEvents: 3,
|
|
1504
|
+
timeWindowSeconds: 180,
|
|
1505
|
+
detect: (events) => {
|
|
1506
|
+
const highEntropyEvents = events.filter(
|
|
1507
|
+
(e) => e.bodyEntropy !== void 0 && e.bodyEntropy > 7 && e.requestSize > 1e3
|
|
1508
|
+
);
|
|
1509
|
+
if (highEntropyEvents.length === 0) return 0;
|
|
1510
|
+
const ratio = highEntropyEvents.length / events.length;
|
|
1511
|
+
if (ratio > 0.5 && highEntropyEvents.length >= 5) return 90;
|
|
1512
|
+
if (ratio > 0.3) return 70;
|
|
1513
|
+
if (highEntropyEvents.length >= 3) return 55;
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
id: "SN-015",
|
|
1519
|
+
severity: "medium",
|
|
1520
|
+
title: "Unexpected outbound connection during idle",
|
|
1521
|
+
description: "MCP server made network requests when no user-initiated tool calls were active. Legitimate MCP servers should only make network requests in response to tool invocations.",
|
|
1522
|
+
recommendation: "Review why this MCP server is making network requests when idle. This could indicate background beaconing or telemetry.",
|
|
1523
|
+
minEvents: 2,
|
|
1524
|
+
timeWindowSeconds: 60,
|
|
1525
|
+
detect: (events) => {
|
|
1526
|
+
if (events.length >= 3) return 50;
|
|
1527
|
+
if (events.length >= 2) return 30;
|
|
1528
|
+
return 0;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
];
|
|
1532
|
+
var CREDENTIAL_EXFIL_PATTERNS = [
|
|
1533
|
+
{
|
|
1534
|
+
id: "SN-020",
|
|
1535
|
+
severity: "critical",
|
|
1536
|
+
title: "SSH key in outbound request",
|
|
1537
|
+
urlPattern: /[?&](?:key|data|payload|content|body)=[^&]*(?:ssh-rsa|ssh-ed25519|PRIVATE\s*KEY)/i,
|
|
1538
|
+
description: "MCP server appears to be transmitting SSH key material in a URL parameter. This matches the Invariant Labs exfiltration attack vector.",
|
|
1539
|
+
recommendation: "CRITICAL: Your SSH keys may be compromised. Rotate all SSH keys immediately."
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
id: "SN-021",
|
|
1543
|
+
severity: "critical",
|
|
1544
|
+
title: "API key/token in outbound URL",
|
|
1545
|
+
urlPattern: /[?&](?:key|token|secret|api_key|apikey|auth|password|credential)=(?!(?:test|demo|example|placeholder))[A-Za-z0-9_-]{20,}/i,
|
|
1546
|
+
description: "MCP server is transmitting what appears to be an API key or token in a URL parameter to an external destination.",
|
|
1547
|
+
recommendation: "CRITICAL: Rotate the compromised API key/token immediately. Check your .env files for exposed secrets."
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
id: "SN-022",
|
|
1551
|
+
severity: "critical",
|
|
1552
|
+
title: "AWS credential in outbound request",
|
|
1553
|
+
urlPattern: /(?:AKIA[0-9A-Z]{16}|(?:aws_secret_access_key|aws_access_key_id)\s*[:=]\s*[A-Za-z0-9/+=]{20,})/i,
|
|
1554
|
+
description: "MCP server is transmitting AWS credentials. AWS access keys follow a known format (AKIA...).",
|
|
1555
|
+
recommendation: "CRITICAL: Deactivate the compromised AWS keys immediately via IAM console."
|
|
1556
|
+
}
|
|
1557
|
+
];
|
|
1558
|
+
function calculateThreatScore(findings) {
|
|
1559
|
+
if (findings.length === 0) return 0;
|
|
1560
|
+
let score = 0;
|
|
1561
|
+
for (const f of findings) {
|
|
1562
|
+
const severityWeight = f.severity === "critical" ? 30 : f.severity === "high" ? 20 : f.severity === "medium" ? 10 : f.severity === "low" ? 5 : 2;
|
|
1563
|
+
score += severityWeight * (f.confidence / 100);
|
|
1564
|
+
}
|
|
1565
|
+
return Math.min(100, Math.round(score));
|
|
1566
|
+
}
|
|
1567
|
+
function threatLevelFromScore(score) {
|
|
1568
|
+
if (score >= 70) return "critical";
|
|
1569
|
+
if (score >= 40) return "malicious";
|
|
1570
|
+
if (score >= 15) return "suspicious";
|
|
1571
|
+
return "clean";
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/sentinel/sentinel.ts
|
|
1575
|
+
var SentinelEngine = class {
|
|
1576
|
+
events = [];
|
|
1577
|
+
findings = [];
|
|
1578
|
+
monitorProcess = null;
|
|
1579
|
+
startTime = 0;
|
|
1580
|
+
serverName;
|
|
1581
|
+
durationSeconds;
|
|
1582
|
+
onEvent;
|
|
1583
|
+
constructor(options) {
|
|
1584
|
+
this.serverName = options.serverName;
|
|
1585
|
+
this.durationSeconds = options.durationSeconds || 120;
|
|
1586
|
+
this.onEvent = options.onEvent;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Start monitoring network activity for the given MCP server.
|
|
1590
|
+
*
|
|
1591
|
+
* The monitor works in three modes depending on the OS and permissions:
|
|
1592
|
+
* 1. macOS: Uses `nettop` or `networksetup` + `tcpdump`
|
|
1593
|
+
* 2. Linux: Uses `ss` polling + optional `tcpdump`
|
|
1594
|
+
* 3. Fallback: Uses Node.js HTTP/HTTPS module monkey-patching
|
|
1595
|
+
* (only captures Node.js HTTP requests from the current process tree)
|
|
1596
|
+
*/
|
|
1597
|
+
async startMonitoring() {
|
|
1598
|
+
this.startTime = Date.now();
|
|
1599
|
+
this.events = [];
|
|
1600
|
+
this.findings = [];
|
|
1601
|
+
const method = this.detectMonitoringMethod();
|
|
1602
|
+
switch (method) {
|
|
1603
|
+
case "lsof-poll":
|
|
1604
|
+
await this.startLsofPolling();
|
|
1605
|
+
break;
|
|
1606
|
+
case "ss-poll":
|
|
1607
|
+
await this.startSsPolling();
|
|
1608
|
+
break;
|
|
1609
|
+
case "proxy":
|
|
1610
|
+
await this.startProxyCapture();
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Stop monitoring and generate the report.
|
|
1616
|
+
*/
|
|
1617
|
+
async stopMonitoring() {
|
|
1618
|
+
if (this.monitorProcess) {
|
|
1619
|
+
this.monitorProcess.kill("SIGTERM");
|
|
1620
|
+
this.monitorProcess = null;
|
|
1621
|
+
}
|
|
1622
|
+
this.analyzeEvents();
|
|
1623
|
+
const threatScore = calculateThreatScore(this.findings);
|
|
1624
|
+
const threatLevel = threatLevelFromScore(threatScore);
|
|
1625
|
+
const uniqueDestinations = [
|
|
1626
|
+
...new Set(
|
|
1627
|
+
this.events.map((e) => {
|
|
1628
|
+
try {
|
|
1629
|
+
return new URL(e.url).hostname;
|
|
1630
|
+
} catch {
|
|
1631
|
+
return e.url;
|
|
1632
|
+
}
|
|
1633
|
+
})
|
|
1634
|
+
)
|
|
1635
|
+
];
|
|
1636
|
+
return {
|
|
1637
|
+
serverName: this.serverName,
|
|
1638
|
+
monitoringDuration: (Date.now() - this.startTime) / 1e3,
|
|
1639
|
+
totalEvents: this.events.length,
|
|
1640
|
+
uniqueDestinations,
|
|
1641
|
+
findings: this.findings,
|
|
1642
|
+
threatLevel,
|
|
1643
|
+
threatScore,
|
|
1644
|
+
startedAt: new Date(this.startTime).toISOString(),
|
|
1645
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Feed a single network event into the engine (for real-time analysis).
|
|
1650
|
+
*/
|
|
1651
|
+
ingestEvent(event) {
|
|
1652
|
+
this.events.push(event);
|
|
1653
|
+
this.onEvent?.(event);
|
|
1654
|
+
this.checkEndpointPatterns(event);
|
|
1655
|
+
this.checkCredentialPatterns(event);
|
|
1656
|
+
}
|
|
1657
|
+
// ── Detection Methods ──
|
|
1658
|
+
/**
|
|
1659
|
+
* Run all analysis on collected events.
|
|
1660
|
+
*/
|
|
1661
|
+
analyzeEvents() {
|
|
1662
|
+
for (const event of this.events) {
|
|
1663
|
+
this.checkEndpointPatterns(event);
|
|
1664
|
+
this.checkCredentialPatterns(event);
|
|
1665
|
+
}
|
|
1666
|
+
for (const pattern of BEHAVIORAL_PATTERNS) {
|
|
1667
|
+
if (this.events.length < pattern.minEvents) continue;
|
|
1668
|
+
const now = Date.now();
|
|
1669
|
+
const windowStart = now - pattern.timeWindowSeconds * 1e3;
|
|
1670
|
+
const windowEvents = this.events.filter((e) => e.timestamp >= windowStart);
|
|
1671
|
+
if (windowEvents.length < pattern.minEvents) continue;
|
|
1672
|
+
const confidence = pattern.detect(windowEvents);
|
|
1673
|
+
if (confidence > 25) {
|
|
1674
|
+
if (!this.findings.some((f) => f.id === pattern.id)) {
|
|
1675
|
+
this.findings.push({
|
|
1676
|
+
id: pattern.id,
|
|
1677
|
+
category: pattern.id.startsWith("SN-01") ? "c2-beaconing" : pattern.id === "SN-012" ? "dns-tunneling" : pattern.id === "SN-013" ? "covert-channel" : "phone-home",
|
|
1678
|
+
severity: pattern.severity,
|
|
1679
|
+
title: pattern.title,
|
|
1680
|
+
description: pattern.description,
|
|
1681
|
+
serverName: this.serverName,
|
|
1682
|
+
evidence: windowEvents.slice(0, 10),
|
|
1683
|
+
// Cap evidence at 10 events
|
|
1684
|
+
recommendation: pattern.recommendation,
|
|
1685
|
+
confidence
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1691
|
+
for (const f of this.findings) {
|
|
1692
|
+
const existing = seen.get(f.id);
|
|
1693
|
+
if (!existing || f.confidence > existing.confidence) {
|
|
1694
|
+
seen.set(f.id, f);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
this.findings = Array.from(seen.values());
|
|
1698
|
+
}
|
|
1699
|
+
checkEndpointPatterns(event) {
|
|
1700
|
+
for (const pattern of SUSPICIOUS_ENDPOINT_PATTERNS) {
|
|
1701
|
+
if (pattern.urlPattern.test(event.url)) {
|
|
1702
|
+
if (!this.findings.some((f) => f.id === pattern.id && f.evidence[0]?.url === event.url)) {
|
|
1703
|
+
this.findings.push({
|
|
1704
|
+
id: pattern.id,
|
|
1705
|
+
category: "phone-home",
|
|
1706
|
+
severity: pattern.severity,
|
|
1707
|
+
title: pattern.title,
|
|
1708
|
+
description: pattern.description,
|
|
1709
|
+
serverName: this.serverName,
|
|
1710
|
+
evidence: [event],
|
|
1711
|
+
recommendation: pattern.recommendation,
|
|
1712
|
+
confidence: 95
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
checkCredentialPatterns(event) {
|
|
1719
|
+
for (const pattern of CREDENTIAL_EXFIL_PATTERNS) {
|
|
1720
|
+
if (pattern.urlPattern.test(event.url)) {
|
|
1721
|
+
this.findings.push({
|
|
1722
|
+
id: pattern.id,
|
|
1723
|
+
category: "data-exfiltration",
|
|
1724
|
+
severity: pattern.severity,
|
|
1725
|
+
title: pattern.title,
|
|
1726
|
+
description: pattern.description,
|
|
1727
|
+
serverName: this.serverName,
|
|
1728
|
+
evidence: [event],
|
|
1729
|
+
recommendation: pattern.recommendation,
|
|
1730
|
+
confidence: 98
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
// ── Monitoring Methods ──
|
|
1736
|
+
detectMonitoringMethod() {
|
|
1737
|
+
try {
|
|
1738
|
+
(0, import_child_process.execSync)("which lsof", { stdio: "pipe" });
|
|
1739
|
+
return "lsof-poll";
|
|
1740
|
+
} catch {
|
|
1741
|
+
try {
|
|
1742
|
+
(0, import_child_process.execSync)("which ss", { stdio: "pipe" });
|
|
1743
|
+
return "ss-poll";
|
|
1744
|
+
} catch {
|
|
1745
|
+
return "proxy";
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* macOS/Linux: Poll `lsof` to capture network connections for a process.
|
|
1751
|
+
* This is the lowest-privilege method — no root needed.
|
|
1752
|
+
*/
|
|
1753
|
+
async startLsofPolling() {
|
|
1754
|
+
const pollInterval = setInterval(() => {
|
|
1755
|
+
try {
|
|
1756
|
+
const output = (0, import_child_process.execSync)(
|
|
1757
|
+
`lsof -i -n -P 2>/dev/null | grep -i "${this.serverName}" || true`,
|
|
1758
|
+
{ timeout: 5e3, encoding: "utf-8" }
|
|
1759
|
+
);
|
|
1760
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
1761
|
+
const event = this.parseLsofLine(line);
|
|
1762
|
+
if (event) this.ingestEvent(event);
|
|
1763
|
+
}
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
}, 2e3);
|
|
1767
|
+
this.monitorProcess = {
|
|
1768
|
+
kill: () => clearInterval(pollInterval)
|
|
1769
|
+
};
|
|
1770
|
+
setTimeout(() => {
|
|
1771
|
+
clearInterval(pollInterval);
|
|
1772
|
+
}, this.durationSeconds * 1e3);
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Linux: Poll `ss` (socket statistics) for network connections.
|
|
1776
|
+
*/
|
|
1777
|
+
async startSsPolling() {
|
|
1778
|
+
const pollInterval = setInterval(() => {
|
|
1779
|
+
try {
|
|
1780
|
+
const output = (0, import_child_process.execSync)(
|
|
1781
|
+
`ss -tnp 2>/dev/null | grep "${this.serverName}" || true`,
|
|
1782
|
+
{ timeout: 5e3, encoding: "utf-8" }
|
|
1783
|
+
);
|
|
1784
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
1785
|
+
const event = this.parseSsLine(line);
|
|
1786
|
+
if (event) this.ingestEvent(event);
|
|
1787
|
+
}
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
}, 2e3);
|
|
1791
|
+
this.monitorProcess = {
|
|
1792
|
+
kill: () => clearInterval(pollInterval)
|
|
1793
|
+
};
|
|
1794
|
+
setTimeout(() => {
|
|
1795
|
+
clearInterval(pollInterval);
|
|
1796
|
+
}, this.durationSeconds * 1e3);
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Fallback: Start a lightweight MITM proxy that MCP traffic routes through.
|
|
1800
|
+
* This requires the runtime proxy (Phase 3) to be active.
|
|
1801
|
+
*/
|
|
1802
|
+
async startProxyCapture() {
|
|
1803
|
+
console.warn(
|
|
1804
|
+
"[Sentinel] Proxy capture requires the runtime proxy (coming soon). Using manual event ingestion mode."
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
// ── Parsers ──
|
|
1808
|
+
parseLsofLine(line) {
|
|
1809
|
+
const match = line.match(
|
|
1810
|
+
/(\S+)\s+(\d+)\s+\S+\s+\S+\s+IPv[46]\s+\S+\s+\S+\s+TCP\s+\S+->(\S+):(\d+)\s/
|
|
1811
|
+
);
|
|
1812
|
+
if (!match) return null;
|
|
1813
|
+
const [, _command, _pid, remoteHost, remotePort] = match;
|
|
1814
|
+
return {
|
|
1815
|
+
timestamp: Date.now(),
|
|
1816
|
+
serverName: this.serverName,
|
|
1817
|
+
method: "TCP",
|
|
1818
|
+
url: `https://${remoteHost}:${remotePort}/`,
|
|
1819
|
+
destinationIp: remoteHost,
|
|
1820
|
+
port: parseInt(remotePort, 10),
|
|
1821
|
+
requestSize: 0,
|
|
1822
|
+
tls: parseInt(remotePort, 10) === 443
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
parseSsLine(line) {
|
|
1826
|
+
const match = line.match(
|
|
1827
|
+
/ESTAB\s+\d+\s+(\d+)\s+\S+\s+(\S+):(\d+)/
|
|
1828
|
+
);
|
|
1829
|
+
if (!match) return null;
|
|
1830
|
+
const [, sendQueue, remoteHost, remotePort] = match;
|
|
1831
|
+
return {
|
|
1832
|
+
timestamp: Date.now(),
|
|
1833
|
+
serverName: this.serverName,
|
|
1834
|
+
method: "TCP",
|
|
1835
|
+
url: `https://${remoteHost}:${remotePort}/`,
|
|
1836
|
+
destinationIp: remoteHost,
|
|
1837
|
+
port: parseInt(remotePort, 10),
|
|
1838
|
+
requestSize: parseInt(sendQueue, 10),
|
|
1839
|
+
tls: parseInt(remotePort, 10) === 443
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
function getSentinelFeatures(tier) {
|
|
1844
|
+
switch (tier) {
|
|
1845
|
+
case "free":
|
|
1846
|
+
return {
|
|
1847
|
+
monitoringEnabled: false,
|
|
1848
|
+
// Upgrade required
|
|
1849
|
+
maxDurationSeconds: 0,
|
|
1850
|
+
maxConcurrentServers: 0,
|
|
1851
|
+
behavioralDetection: false,
|
|
1852
|
+
realTimeAlerts: false,
|
|
1853
|
+
historyRetentionDays: 0,
|
|
1854
|
+
apiAccess: false
|
|
1855
|
+
};
|
|
1856
|
+
case "pro":
|
|
1857
|
+
return {
|
|
1858
|
+
monitoringEnabled: true,
|
|
1859
|
+
maxDurationSeconds: 300,
|
|
1860
|
+
// 5 minutes
|
|
1861
|
+
maxConcurrentServers: 3,
|
|
1862
|
+
behavioralDetection: true,
|
|
1863
|
+
realTimeAlerts: false,
|
|
1864
|
+
// Future: Team+ only
|
|
1865
|
+
historyRetentionDays: 7,
|
|
1866
|
+
apiAccess: true
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
var SENTINEL_MARKETING = {
|
|
1871
|
+
tagline: "Always watching what your tools are doing.",
|
|
1872
|
+
featureName: "Vigile Sentinel",
|
|
1873
|
+
tierName: "Sentinel Protection",
|
|
1874
|
+
upgradePrompt: "\u{1F6E1}\uFE0F Vigile Sentinel is a Pro feature. Upgrade at https://vigile.dev/pricing to monitor your MCP servers for real-time phone-home detection.",
|
|
1875
|
+
categories: [
|
|
1876
|
+
{ icon: "\u{1F4E1}", name: "C2 Beaconing Detection", desc: "Catches tools phoning home on a schedule" },
|
|
1877
|
+
{ icon: "\u{1F510}", name: "Credential Theft Alerts", desc: "Detects SSH keys & API tokens leaving your machine" },
|
|
1878
|
+
{ icon: "\u{1F575}\uFE0F", name: "DNS Tunneling Detection", desc: "Spots data hidden in DNS queries" },
|
|
1879
|
+
{ icon: "\u{1F4CA}", name: "Behavioral Analysis", desc: "Machine-learning-ready traffic pattern analysis" },
|
|
1880
|
+
{ icon: "\u26A1", name: "Real-Time Alerts", desc: "Instant notification when threats are detected" },
|
|
1881
|
+
{ icon: "\u{1F6E1}\uFE0F", name: "Continuous Monitoring", desc: "24/7 protection for enterprise environments" }
|
|
1882
|
+
]
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1313
1885
|
// src/output/terminal.ts
|
|
1314
1886
|
var import_chalk = __toESM(require("chalk"));
|
|
1315
1887
|
var TRUST_COLORS = {
|
|
@@ -1348,9 +1920,9 @@ var FILE_TYPE_LABELS = {
|
|
|
1348
1920
|
};
|
|
1349
1921
|
function printBanner() {
|
|
1350
1922
|
console.log("");
|
|
1351
|
-
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u2566 \u2566\u2566\u2554\u2550\u2557\u2566\u2566 "));
|
|
1352
|
-
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u255A\u2557\u2554\u255D\u2551\u2551 \u2566\u2551\u2551 "));
|
|
1353
|
-
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u255A\u255D \u2569\u255A\u2550\u255D\u2569\u2569\u2550\u255D"));
|
|
1923
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u2566 \u2566\u2566\u2554\u2550\u2557\u2566\u2566 \u2554\u2550\u2557"));
|
|
1924
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u255A\u2557\u2554\u255D\u2551\u2551 \u2566\u2551\u2551 \u2551\u2563 "));
|
|
1925
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u255A\u255D \u2569\u255A\u2550\u255D\u2569\u2569\u2550\u255D\u255A\u2550\u255D"));
|
|
1354
1926
|
console.log(import_chalk.default.gray(" AI Agent Security Scanner"));
|
|
1355
1927
|
console.log("");
|
|
1356
1928
|
}
|
|
@@ -1506,14 +2078,309 @@ function scoreBar(score) {
|
|
|
1506
2078
|
const color = score >= 80 ? import_chalk.default.green : score >= 60 ? import_chalk.default.yellow : score >= 40 ? import_chalk.default.hex("#FF8C00") : import_chalk.default.red;
|
|
1507
2079
|
return color("\u2588".repeat(filled)) + import_chalk.default.gray("\u2591".repeat(empty));
|
|
1508
2080
|
}
|
|
2081
|
+
var SENTINEL_THREAT_COLORS = {
|
|
2082
|
+
clean: import_chalk.default.green,
|
|
2083
|
+
suspicious: import_chalk.default.yellow,
|
|
2084
|
+
malicious: import_chalk.default.red,
|
|
2085
|
+
critical: import_chalk.default.bgRed.white.bold
|
|
2086
|
+
};
|
|
2087
|
+
var SENTINEL_THREAT_ICONS = {
|
|
2088
|
+
clean: "\u2713",
|
|
2089
|
+
suspicious: "\u26A0",
|
|
2090
|
+
malicious: "\u2717",
|
|
2091
|
+
critical: "!!!"
|
|
2092
|
+
};
|
|
2093
|
+
function printSentinelReport(report) {
|
|
2094
|
+
const color = SENTINEL_THREAT_COLORS[report.threatLevel];
|
|
2095
|
+
const icon = SENTINEL_THREAT_ICONS[report.threatLevel];
|
|
2096
|
+
console.log("");
|
|
2097
|
+
console.log(
|
|
2098
|
+
import_chalk.default.bold(` ${icon} `) + import_chalk.default.bold(`Sentinel: ${report.serverName}`) + " " + color(`[Threat: ${report.threatScore}/100 ${report.threatLevel.toUpperCase()}]`)
|
|
2099
|
+
);
|
|
2100
|
+
console.log(import_chalk.default.gray(` Monitored: ${report.monitoringDuration.toFixed(0)}s | Events: ${report.totalEvents} | Destinations: ${report.uniqueDestinations.length}`));
|
|
2101
|
+
if (report.uniqueDestinations.length > 0) {
|
|
2102
|
+
console.log(import_chalk.default.gray(` Destinations:`));
|
|
2103
|
+
for (const dest of report.uniqueDestinations.slice(0, 10)) {
|
|
2104
|
+
console.log(import_chalk.default.gray(` \u2192 ${dest}`));
|
|
2105
|
+
}
|
|
2106
|
+
if (report.uniqueDestinations.length > 10) {
|
|
2107
|
+
console.log(import_chalk.default.gray(` ... and ${report.uniqueDestinations.length - 10} more`));
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
if (report.findings.length === 0) {
|
|
2111
|
+
console.log(import_chalk.default.green(" No suspicious network behavior detected."));
|
|
2112
|
+
} else {
|
|
2113
|
+
console.log(import_chalk.default.dim(` ${report.findings.length} finding(s):`));
|
|
2114
|
+
for (const finding of report.findings) {
|
|
2115
|
+
const fColor = SEVERITY_COLORS[finding.severity];
|
|
2116
|
+
const fIcon = SEVERITY_ICONS[finding.severity];
|
|
2117
|
+
console.log(
|
|
2118
|
+
` ${fColor(`[${fIcon}]`)} ${fColor(finding.severity.toUpperCase())} ` + import_chalk.default.white(finding.title) + import_chalk.default.gray(` (${finding.id})`)
|
|
2119
|
+
);
|
|
2120
|
+
console.log(import_chalk.default.gray(` ${finding.description}`));
|
|
2121
|
+
if (finding.evidence && finding.evidence.length > 0) {
|
|
2122
|
+
console.log(import_chalk.default.gray(` Evidence: ${finding.evidence.length} network event(s)`));
|
|
2123
|
+
}
|
|
2124
|
+
console.log(import_chalk.default.cyan(` \u2192 ${finding.recommendation}`));
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
console.log(import_chalk.default.gray(` ${report.startedAt} \u2192 ${report.endedAt}`));
|
|
2128
|
+
console.log("");
|
|
2129
|
+
}
|
|
2130
|
+
function printSentinelUpgrade() {
|
|
2131
|
+
console.log("");
|
|
2132
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(` \u{1F6E1}\uFE0F ${SENTINEL_MARKETING.featureName}`));
|
|
2133
|
+
console.log(import_chalk.default.white(` ${SENTINEL_MARKETING.tagline}`));
|
|
2134
|
+
console.log("");
|
|
2135
|
+
for (const cat of SENTINEL_MARKETING.categories) {
|
|
2136
|
+
console.log(import_chalk.default.white(` ${cat.icon} ${import_chalk.default.bold(cat.name)}`));
|
|
2137
|
+
console.log(import_chalk.default.gray(` ${cat.desc}`));
|
|
2138
|
+
}
|
|
2139
|
+
console.log("");
|
|
2140
|
+
console.log(import_chalk.default.yellow(` \u26A1 Sentinel is a Pro feature. Upgrade to unlock runtime monitoring:`));
|
|
2141
|
+
console.log(import_chalk.default.cyan(` https://vigile.dev/pricing`));
|
|
2142
|
+
console.log("");
|
|
2143
|
+
console.log(import_chalk.default.gray(` Pro ($19/mo) \u2014 5-min sessions, 3 servers`));
|
|
2144
|
+
console.log(import_chalk.default.gray(` Team ($99/mo) \u2014 30-min sessions, 10 servers, real-time alerts`));
|
|
2145
|
+
console.log(import_chalk.default.gray(` Enterprise ($999+) \u2014 Unlimited, custom rules, SLA`));
|
|
2146
|
+
console.log("");
|
|
2147
|
+
}
|
|
2148
|
+
function printAuthStatus(info) {
|
|
2149
|
+
if (info.authenticated) {
|
|
2150
|
+
console.log(import_chalk.default.green(` Authenticated as ${info.email}`));
|
|
2151
|
+
console.log(import_chalk.default.gray(` Tier: ${(info.tier || "free").toUpperCase()}`));
|
|
2152
|
+
if (info.name) {
|
|
2153
|
+
console.log(import_chalk.default.gray(` Name: ${info.name}`));
|
|
2154
|
+
}
|
|
2155
|
+
console.log(
|
|
2156
|
+
import_chalk.default.gray(
|
|
2157
|
+
` Source: ${info.source === "env" ? "VIGILE_TOKEN env var" : "~/.vigile/config.json"}`
|
|
2158
|
+
)
|
|
2159
|
+
);
|
|
2160
|
+
} else {
|
|
2161
|
+
console.log(import_chalk.default.yellow(" Not authenticated."));
|
|
2162
|
+
if (info.error) {
|
|
2163
|
+
console.log(import_chalk.default.red(` Error: ${info.error}`));
|
|
2164
|
+
}
|
|
2165
|
+
console.log(import_chalk.default.gray(" Run `vigile-scan auth login` or set VIGILE_TOKEN to authenticate."));
|
|
2166
|
+
}
|
|
2167
|
+
console.log("");
|
|
2168
|
+
}
|
|
2169
|
+
function printAuthLoginSuccess(email, tier) {
|
|
2170
|
+
console.log("");
|
|
2171
|
+
console.log(import_chalk.default.green(" Authenticated successfully!"));
|
|
2172
|
+
console.log(import_chalk.default.gray(` Email: ${email}`));
|
|
2173
|
+
console.log(import_chalk.default.gray(` Tier: ${tier.toUpperCase()}`));
|
|
2174
|
+
console.log(import_chalk.default.gray(" Token stored in ~/.vigile/config.json"));
|
|
2175
|
+
console.log("");
|
|
2176
|
+
}
|
|
2177
|
+
function printUploadSuccess(summary) {
|
|
2178
|
+
const total = summary.mcpUploaded + summary.skillsUploaded;
|
|
2179
|
+
if (total > 0) {
|
|
2180
|
+
console.log(import_chalk.default.green(` Uploaded ${total} result(s) to Vigile registry.`));
|
|
2181
|
+
if (summary.mcpUploaded > 0) {
|
|
2182
|
+
console.log(import_chalk.default.gray(` MCP servers: ${summary.mcpUploaded}`));
|
|
2183
|
+
}
|
|
2184
|
+
if (summary.skillsUploaded > 0) {
|
|
2185
|
+
console.log(import_chalk.default.gray(` Skills: ${summary.skillsUploaded}`));
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (summary.failures > 0) {
|
|
2189
|
+
console.log(import_chalk.default.yellow(` ${summary.failures} upload(s) failed (results saved locally).`));
|
|
2190
|
+
}
|
|
2191
|
+
console.log("");
|
|
2192
|
+
}
|
|
2193
|
+
function printUploadSkipped(reason) {
|
|
2194
|
+
if (reason === "not-authenticated") {
|
|
2195
|
+
console.log(
|
|
2196
|
+
import_chalk.default.gray(" Tip: Run `vigile-scan auth login` to upload results to the Vigile registry.")
|
|
2197
|
+
);
|
|
2198
|
+
console.log("");
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
1509
2201
|
|
|
1510
2202
|
// src/output/json.ts
|
|
1511
2203
|
function formatJSON(summary) {
|
|
1512
2204
|
return JSON.stringify(summary, null, 2);
|
|
1513
2205
|
}
|
|
1514
2206
|
|
|
2207
|
+
// src/api/auth.ts
|
|
2208
|
+
var import_promises3 = require("fs/promises");
|
|
2209
|
+
var import_path8 = require("path");
|
|
2210
|
+
var import_os2 = require("os");
|
|
2211
|
+
|
|
2212
|
+
// src/api/client.ts
|
|
2213
|
+
var DEFAULT_API_URL = "https://api.vigile.dev";
|
|
2214
|
+
var API_TIMEOUT_MS = 15e3;
|
|
2215
|
+
var CLI_VERSION = "0.2.0";
|
|
2216
|
+
var VigileApiClient = class {
|
|
2217
|
+
baseUrl;
|
|
2218
|
+
token;
|
|
2219
|
+
constructor(baseUrl, token) {
|
|
2220
|
+
this.baseUrl = (baseUrl || process.env.VIGILE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
2221
|
+
this.token = token || null;
|
|
2222
|
+
}
|
|
2223
|
+
get isAuthenticated() {
|
|
2224
|
+
return this.token !== null && this.token.length > 0;
|
|
2225
|
+
}
|
|
2226
|
+
// ── Private helpers ──
|
|
2227
|
+
async request(method, path, body) {
|
|
2228
|
+
const controller = new AbortController();
|
|
2229
|
+
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
2230
|
+
const headers = {
|
|
2231
|
+
"Content-Type": "application/json",
|
|
2232
|
+
"User-Agent": `vigile-cli/${CLI_VERSION}`
|
|
2233
|
+
};
|
|
2234
|
+
if (this.token) {
|
|
2235
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
2236
|
+
}
|
|
2237
|
+
try {
|
|
2238
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2239
|
+
method,
|
|
2240
|
+
headers,
|
|
2241
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
2242
|
+
signal: controller.signal
|
|
2243
|
+
});
|
|
2244
|
+
clearTimeout(timeoutId);
|
|
2245
|
+
if (!response.ok) {
|
|
2246
|
+
const errorBody = await response.text();
|
|
2247
|
+
let errorMessage;
|
|
2248
|
+
try {
|
|
2249
|
+
const parsed = JSON.parse(errorBody);
|
|
2250
|
+
errorMessage = typeof parsed.detail === "string" ? parsed.detail : JSON.stringify(parsed.detail);
|
|
2251
|
+
} catch {
|
|
2252
|
+
errorMessage = errorBody || `HTTP ${response.status}`;
|
|
2253
|
+
}
|
|
2254
|
+
return { ok: false, error: errorMessage, status: response.status };
|
|
2255
|
+
}
|
|
2256
|
+
const data = await response.json();
|
|
2257
|
+
return { ok: true, data };
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
clearTimeout(timeoutId);
|
|
2260
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
2261
|
+
return { ok: false, error: "Request timed out", status: 0 };
|
|
2262
|
+
}
|
|
2263
|
+
return {
|
|
2264
|
+
ok: false,
|
|
2265
|
+
error: err instanceof Error ? err.message : "Network error",
|
|
2266
|
+
status: 0
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
// ── Auth ──
|
|
2271
|
+
async getMe() {
|
|
2272
|
+
return this.request("GET", "/api/v1/auth/me");
|
|
2273
|
+
}
|
|
2274
|
+
// ── Scanning ──
|
|
2275
|
+
async submitMCPScan(payload) {
|
|
2276
|
+
return this.request("POST", "/api/v1/scan/", payload);
|
|
2277
|
+
}
|
|
2278
|
+
async submitSkillScan(payload) {
|
|
2279
|
+
return this.request("POST", "/api/v1/scan/skill", payload);
|
|
2280
|
+
}
|
|
2281
|
+
// ── Sentinel ──
|
|
2282
|
+
async createSentinelSession(serverName, durationSeconds, client = "cli") {
|
|
2283
|
+
return this.request("POST", "/api/v1/sentinel/sessions", {
|
|
2284
|
+
server_name: serverName,
|
|
2285
|
+
duration_seconds: durationSeconds,
|
|
2286
|
+
client
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
async submitSentinelEvents(sessionId, events) {
|
|
2290
|
+
return this.request(
|
|
2291
|
+
"POST",
|
|
2292
|
+
`/api/v1/sentinel/sessions/${sessionId}/events`,
|
|
2293
|
+
{ session_id: sessionId, events }
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
async analyzeSentinelSession(sessionId) {
|
|
2297
|
+
return this.request(
|
|
2298
|
+
"POST",
|
|
2299
|
+
`/api/v1/sentinel/sessions/${sessionId}/analyze`
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
|
|
2304
|
+
// src/api/auth.ts
|
|
2305
|
+
var CONFIG_DIR = (0, import_path8.join)((0, import_os2.homedir)(), ".vigile");
|
|
2306
|
+
var CONFIG_FILE = (0, import_path8.join)(CONFIG_DIR, "config.json");
|
|
2307
|
+
async function resolveToken() {
|
|
2308
|
+
const envToken = process.env.VIGILE_TOKEN;
|
|
2309
|
+
if (envToken && envToken.length > 0) {
|
|
2310
|
+
return envToken;
|
|
2311
|
+
}
|
|
2312
|
+
const config = await loadConfig();
|
|
2313
|
+
return config.token || null;
|
|
2314
|
+
}
|
|
2315
|
+
async function resolveApiUrl() {
|
|
2316
|
+
const envUrl = process.env.VIGILE_API_URL;
|
|
2317
|
+
if (envUrl) return envUrl;
|
|
2318
|
+
const config = await loadConfig();
|
|
2319
|
+
return config.api_url || "https://api.vigile.dev";
|
|
2320
|
+
}
|
|
2321
|
+
async function loadConfig() {
|
|
2322
|
+
try {
|
|
2323
|
+
const raw = await (0, import_promises3.readFile)(CONFIG_FILE, "utf-8");
|
|
2324
|
+
return JSON.parse(raw);
|
|
2325
|
+
} catch {
|
|
2326
|
+
return {};
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
async function saveConfig(config) {
|
|
2330
|
+
await (0, import_promises3.mkdir)(CONFIG_DIR, { recursive: true });
|
|
2331
|
+
await (0, import_promises3.writeFile)(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
2332
|
+
mode: 384
|
|
2333
|
+
// Owner read/write only
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
async function authLogin(token) {
|
|
2337
|
+
const apiUrl = await resolveApiUrl();
|
|
2338
|
+
const client = new VigileApiClient(apiUrl, token);
|
|
2339
|
+
const result = await client.getMe();
|
|
2340
|
+
if (!result.ok) {
|
|
2341
|
+
return { success: false, error: result.error };
|
|
2342
|
+
}
|
|
2343
|
+
const config = await loadConfig();
|
|
2344
|
+
config.token = token;
|
|
2345
|
+
config.user = {
|
|
2346
|
+
email: result.data.email,
|
|
2347
|
+
tier: result.data.tier,
|
|
2348
|
+
name: result.data.name || void 0
|
|
2349
|
+
};
|
|
2350
|
+
await saveConfig(config);
|
|
2351
|
+
return { success: true, user: result.data };
|
|
2352
|
+
}
|
|
2353
|
+
async function authStatus() {
|
|
2354
|
+
const envToken = process.env.VIGILE_TOKEN;
|
|
2355
|
+
const config = await loadConfig();
|
|
2356
|
+
const token = envToken || config.token;
|
|
2357
|
+
if (!token) {
|
|
2358
|
+
return { authenticated: false };
|
|
2359
|
+
}
|
|
2360
|
+
const source = envToken ? "env" : "config";
|
|
2361
|
+
const apiUrl = await resolveApiUrl();
|
|
2362
|
+
const client = new VigileApiClient(apiUrl, token);
|
|
2363
|
+
const result = await client.getMe();
|
|
2364
|
+
if (!result.ok) {
|
|
2365
|
+
return { authenticated: false, source, error: result.error };
|
|
2366
|
+
}
|
|
2367
|
+
return { authenticated: true, source, user: result.data };
|
|
2368
|
+
}
|
|
2369
|
+
async function authLogout() {
|
|
2370
|
+
const config = await loadConfig();
|
|
2371
|
+
delete config.token;
|
|
2372
|
+
delete config.user;
|
|
2373
|
+
await saveConfig(config);
|
|
2374
|
+
}
|
|
2375
|
+
async function getAuthenticatedClient() {
|
|
2376
|
+
const token = await resolveToken();
|
|
2377
|
+
if (!token) return null;
|
|
2378
|
+
const apiUrl = await resolveApiUrl();
|
|
2379
|
+
return new VigileApiClient(apiUrl, token);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
1515
2382
|
// src/index.ts
|
|
1516
|
-
var VERSION = "0.
|
|
2383
|
+
var VERSION = "0.2.0";
|
|
1517
2384
|
var program = new import_commander.Command();
|
|
1518
2385
|
program.name("vigile-scan").description(
|
|
1519
2386
|
"Security scanner for AI agent tools \u2014 detect tool poisoning, permission abuse, and supply chain attacks in MCP servers and agent skills"
|
|
@@ -1522,7 +2389,7 @@ function addScanOptions(cmd) {
|
|
|
1522
2389
|
return cmd.option("-j, --json", "Output results as JSON").option("-v, --verbose", "Show detailed findings and score breakdown").option("-c, --config <path>", "Path to a custom MCP config file").option("-o, --output <path>", "Write results to a file").option(
|
|
1523
2390
|
"--client <client>",
|
|
1524
2391
|
"Only scan a specific client (claude-desktop, cursor, claude-code, windsurf, vscode)"
|
|
1525
|
-
).option("-s, --skills", "Scan agent skills only (SKILL.md, .mdc rules, CLAUDE.md, etc.)").option("-a, --all", "Scan both MCP servers and agent skills");
|
|
2392
|
+
).option("-s, --skills", "Scan agent skills only (SKILL.md, .mdc rules, CLAUDE.md, etc.)").option("-a, --all", "Scan both MCP servers and agent skills").option("--sentinel", "Enable Sentinel runtime monitoring (Pro+ feature)").option("--sentinel-server <name>", "Monitor a specific MCP server by name").option("--sentinel-duration <seconds>", "Monitoring duration in seconds (default: 120)", parseInt).option("--no-upload", "Skip uploading scan results to Vigile API");
|
|
1526
2393
|
}
|
|
1527
2394
|
addScanOptions(
|
|
1528
2395
|
program.command("scan").description("Scan MCP server configurations and agent skill files on this machine")
|
|
@@ -1530,12 +2397,48 @@ addScanOptions(
|
|
|
1530
2397
|
await runScan(options);
|
|
1531
2398
|
});
|
|
1532
2399
|
addScanOptions(program).action(async (options) => {
|
|
1533
|
-
if (!process.argv.slice(2).includes("scan")) {
|
|
2400
|
+
if (!process.argv.slice(2).includes("scan") && !process.argv.slice(2).includes("auth")) {
|
|
1534
2401
|
await runScan(options);
|
|
1535
2402
|
}
|
|
1536
2403
|
});
|
|
2404
|
+
var authCmd = program.command("auth").description("Manage Vigile API authentication");
|
|
2405
|
+
authCmd.command("login").description("Authenticate with your Vigile API key").argument("[token]", "API key (vgl_...) or JWT token. If omitted, reads from VIGILE_TOKEN env var.").action(async (token) => {
|
|
2406
|
+
const resolvedToken = token || process.env.VIGILE_TOKEN;
|
|
2407
|
+
if (!resolvedToken) {
|
|
2408
|
+
console.log(import_chalk2.default.red(" No token provided. Pass a token argument or set VIGILE_TOKEN env var."));
|
|
2409
|
+
console.log(import_chalk2.default.gray(" Usage: vigile-scan auth login <vgl_your_api_key>"));
|
|
2410
|
+
console.log(import_chalk2.default.gray(" Get an API key at https://vigile.dev/account"));
|
|
2411
|
+
process.exit(1);
|
|
2412
|
+
}
|
|
2413
|
+
const spinner = (0, import_ora.default)("Validating token...").start();
|
|
2414
|
+
const result = await authLogin(resolvedToken);
|
|
2415
|
+
if (result.success && result.user) {
|
|
2416
|
+
spinner.succeed("Token validated");
|
|
2417
|
+
printAuthLoginSuccess(result.user.email, result.user.tier);
|
|
2418
|
+
} else {
|
|
2419
|
+
spinner.fail("Authentication failed");
|
|
2420
|
+
console.log(import_chalk2.default.red(` Error: ${result.error || "Unknown error"}`));
|
|
2421
|
+
process.exit(1);
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
authCmd.command("status").description("Show current authentication status").action(async () => {
|
|
2425
|
+
const result = await authStatus();
|
|
2426
|
+
printAuthStatus({
|
|
2427
|
+
authenticated: result.authenticated,
|
|
2428
|
+
source: result.source,
|
|
2429
|
+
email: result.user?.email,
|
|
2430
|
+
tier: result.user?.tier,
|
|
2431
|
+
name: result.user?.name || void 0,
|
|
2432
|
+
error: result.error
|
|
2433
|
+
});
|
|
2434
|
+
});
|
|
2435
|
+
authCmd.command("logout").description("Clear stored credentials").action(async () => {
|
|
2436
|
+
await authLogout();
|
|
2437
|
+
console.log(import_chalk2.default.green(" Logged out. Credentials removed from ~/.vigile/config.json"));
|
|
2438
|
+
console.log("");
|
|
2439
|
+
});
|
|
1537
2440
|
async function runScan(options) {
|
|
1538
|
-
const isJSON = options.json;
|
|
2441
|
+
const isJSON = options.json ?? false;
|
|
1539
2442
|
const scanMCP = !options.skills;
|
|
1540
2443
|
const scanSkills = options.skills || options.all;
|
|
1541
2444
|
if (!isJSON) {
|
|
@@ -1625,7 +2528,7 @@ async function runScan(options) {
|
|
|
1625
2528
|
if (isJSON) {
|
|
1626
2529
|
const jsonOutput = formatJSON(summary);
|
|
1627
2530
|
if (options.output) {
|
|
1628
|
-
await (0,
|
|
2531
|
+
await (0, import_promises4.writeFile)(options.output, jsonOutput);
|
|
1629
2532
|
} else {
|
|
1630
2533
|
console.log(jsonOutput);
|
|
1631
2534
|
}
|
|
@@ -1643,13 +2546,241 @@ async function runScan(options) {
|
|
|
1643
2546
|
}
|
|
1644
2547
|
printSummary(summary);
|
|
1645
2548
|
if (options.output) {
|
|
1646
|
-
await (0,
|
|
2549
|
+
await (0, import_promises4.writeFile)(options.output, formatJSON(summary));
|
|
1647
2550
|
console.log(` Results saved to ${options.output}`);
|
|
1648
2551
|
}
|
|
1649
2552
|
}
|
|
2553
|
+
if (options.noUpload !== true) {
|
|
2554
|
+
await uploadResults(results, skillResults, isJSON);
|
|
2555
|
+
}
|
|
2556
|
+
if (options.sentinel) {
|
|
2557
|
+
await runSentinel(options, results, isJSON);
|
|
2558
|
+
}
|
|
1650
2559
|
if (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0) {
|
|
1651
2560
|
process.exit(1);
|
|
1652
2561
|
}
|
|
1653
2562
|
}
|
|
2563
|
+
async function uploadResults(mcpResults, skillResults, isJSON) {
|
|
2564
|
+
const client = await getAuthenticatedClient();
|
|
2565
|
+
if (!client) {
|
|
2566
|
+
if (!isJSON) {
|
|
2567
|
+
printUploadSkipped("not-authenticated");
|
|
2568
|
+
}
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const summary = {
|
|
2572
|
+
mcpUploaded: 0,
|
|
2573
|
+
skillsUploaded: 0,
|
|
2574
|
+
failures: 0,
|
|
2575
|
+
errors: []
|
|
2576
|
+
};
|
|
2577
|
+
const spinner = isJSON ? null : (0, import_ora.default)("Uploading results to Vigile registry...").start();
|
|
2578
|
+
for (const result of mcpResults) {
|
|
2579
|
+
const payload = mapMCPResultToApiPayload(result);
|
|
2580
|
+
const response = await client.submitMCPScan(payload);
|
|
2581
|
+
if (response.ok) {
|
|
2582
|
+
summary.mcpUploaded++;
|
|
2583
|
+
} else {
|
|
2584
|
+
summary.failures++;
|
|
2585
|
+
summary.errors.push(`${result.server.name}: ${response.error}`);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
for (const result of skillResults) {
|
|
2589
|
+
const payload = mapSkillResultToApiPayload(result);
|
|
2590
|
+
const response = await client.submitSkillScan(payload);
|
|
2591
|
+
if (response.ok) {
|
|
2592
|
+
summary.skillsUploaded++;
|
|
2593
|
+
} else {
|
|
2594
|
+
summary.failures++;
|
|
2595
|
+
summary.errors.push(`${result.skill.name}: ${response.error}`);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
if (spinner) {
|
|
2599
|
+
const total = summary.mcpUploaded + summary.skillsUploaded;
|
|
2600
|
+
if (total > 0 && summary.failures === 0) {
|
|
2601
|
+
spinner.succeed(`Uploaded ${total} result(s) to Vigile registry`);
|
|
2602
|
+
} else if (total > 0 && summary.failures > 0) {
|
|
2603
|
+
spinner.warn(`Uploaded ${total} result(s), ${summary.failures} failed`);
|
|
2604
|
+
} else {
|
|
2605
|
+
spinner.fail("Upload failed");
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
if (!isJSON) {
|
|
2609
|
+
printUploadSuccess(summary);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
function mapMCPResultToApiPayload(result) {
|
|
2613
|
+
let packageUrl;
|
|
2614
|
+
if (result.server.command === "npx") {
|
|
2615
|
+
const packageName = result.server.args.find((a) => !a.startsWith("-"));
|
|
2616
|
+
if (packageName) {
|
|
2617
|
+
packageUrl = `https://www.npmjs.com/package/${packageName}`;
|
|
2618
|
+
}
|
|
2619
|
+
} else if (result.server.command === "uvx" || result.server.command === "pip") {
|
|
2620
|
+
const packageName = result.server.args.find((a) => !a.startsWith("-"));
|
|
2621
|
+
if (packageName) {
|
|
2622
|
+
packageUrl = `https://pypi.org/project/${packageName}/`;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
const toolDescriptions = result.server.args.filter((a) => !a.startsWith("-"));
|
|
2626
|
+
return {
|
|
2627
|
+
server_name: result.server.name,
|
|
2628
|
+
source: "manual",
|
|
2629
|
+
// CLI-discovered servers are always manual source
|
|
2630
|
+
package_url: packageUrl,
|
|
2631
|
+
description: `MCP server discovered from ${result.server.source} config`,
|
|
2632
|
+
tool_descriptions: toolDescriptions.length > 0 ? toolDescriptions : void 0
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function mapSkillResultToApiPayload(result) {
|
|
2636
|
+
const platformMap = {
|
|
2637
|
+
"claude-code": "claude-code",
|
|
2638
|
+
"github-copilot": "copilot",
|
|
2639
|
+
"cursor": "cursor",
|
|
2640
|
+
"memory-file": "unknown",
|
|
2641
|
+
"custom": "unknown"
|
|
2642
|
+
};
|
|
2643
|
+
return {
|
|
2644
|
+
skill_name: result.skill.name,
|
|
2645
|
+
content: result.skill.content,
|
|
2646
|
+
file_type: result.skill.fileType,
|
|
2647
|
+
platform: platformMap[result.skill.source] || "unknown",
|
|
2648
|
+
source: "manual"
|
|
2649
|
+
// CLI submissions are always manual source
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
function mapNetworkEventToApi(event) {
|
|
2653
|
+
return {
|
|
2654
|
+
timestamp: event.timestamp,
|
|
2655
|
+
server_name: event.serverName,
|
|
2656
|
+
method: event.method,
|
|
2657
|
+
url: event.url,
|
|
2658
|
+
destination_ip: event.destinationIp || null,
|
|
2659
|
+
port: event.port,
|
|
2660
|
+
request_size: event.requestSize,
|
|
2661
|
+
response_size: event.responseSize ?? null,
|
|
2662
|
+
status_code: event.statusCode ?? null,
|
|
2663
|
+
headers: event.headers || null,
|
|
2664
|
+
dns_query_type: event.dnsQueryType ?? null,
|
|
2665
|
+
tls: event.tls,
|
|
2666
|
+
body_hash: event.bodyHash ?? null,
|
|
2667
|
+
body_entropy: event.bodyEntropy ?? null
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
async function runSentinel(options, scanResults, isJSON) {
|
|
2671
|
+
const client = await getAuthenticatedClient();
|
|
2672
|
+
let tier = "free";
|
|
2673
|
+
if (client) {
|
|
2674
|
+
const meResult = await client.getMe();
|
|
2675
|
+
if (meResult.ok) {
|
|
2676
|
+
tier = meResult.data.tier;
|
|
2677
|
+
}
|
|
2678
|
+
} else {
|
|
2679
|
+
tier = process.env.VIGILE_TIER || "free";
|
|
2680
|
+
}
|
|
2681
|
+
const features = getSentinelFeatures(tier);
|
|
2682
|
+
if (!features.monitoringEnabled) {
|
|
2683
|
+
if (!isJSON) {
|
|
2684
|
+
printSentinelUpgrade();
|
|
2685
|
+
} else {
|
|
2686
|
+
console.log(
|
|
2687
|
+
JSON.stringify({
|
|
2688
|
+
sentinel: { error: "upgrade_required", message: SENTINEL_MARKETING.upgradePrompt }
|
|
2689
|
+
})
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const serversToMonitor = [];
|
|
2695
|
+
if (options.sentinelServer) {
|
|
2696
|
+
serversToMonitor.push(options.sentinelServer);
|
|
2697
|
+
} else if (scanResults.length > 0) {
|
|
2698
|
+
const limit = features.maxConcurrentServers === -1 ? scanResults.length : Math.min(scanResults.length, features.maxConcurrentServers);
|
|
2699
|
+
for (let i = 0; i < limit; i++) {
|
|
2700
|
+
serversToMonitor.push(scanResults[i].server.name);
|
|
2701
|
+
}
|
|
2702
|
+
} else {
|
|
2703
|
+
if (!isJSON) {
|
|
2704
|
+
console.log(
|
|
2705
|
+
import_chalk2.default.yellow(" No MCP servers to monitor. Run a scan first or specify --sentinel-server <name>.")
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
const requestedDuration = options.sentinelDuration || 120;
|
|
2711
|
+
const maxDuration = features.maxDurationSeconds === -1 ? requestedDuration : features.maxDurationSeconds;
|
|
2712
|
+
const duration = Math.min(requestedDuration, maxDuration);
|
|
2713
|
+
if (!isJSON) {
|
|
2714
|
+
console.log("");
|
|
2715
|
+
console.log(import_chalk2.default.bold.hex("#2C4A7C")(" \u{1F6E1}\uFE0F Vigile Sentinel \u2014 Runtime Monitor"));
|
|
2716
|
+
console.log(
|
|
2717
|
+
import_chalk2.default.gray(` Tier: ${tier.toUpperCase()} | Duration: ${duration}s | Servers: ${serversToMonitor.length}`)
|
|
2718
|
+
);
|
|
2719
|
+
console.log("");
|
|
2720
|
+
}
|
|
2721
|
+
const sentinelReports = [];
|
|
2722
|
+
for (const serverName of serversToMonitor) {
|
|
2723
|
+
let apiSessionId = null;
|
|
2724
|
+
if (client) {
|
|
2725
|
+
const sessionResult = await client.createSentinelSession(serverName, duration);
|
|
2726
|
+
if (sessionResult.ok) {
|
|
2727
|
+
apiSessionId = sessionResult.data.session_id;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
const spinner = isJSON ? null : (0, import_ora.default)(`Monitoring ${serverName} for ${duration}s...`).start();
|
|
2731
|
+
let pendingApiEvents = [];
|
|
2732
|
+
let lastApiSubmit = Date.now();
|
|
2733
|
+
const engine = new SentinelEngine({
|
|
2734
|
+
serverName,
|
|
2735
|
+
durationSeconds: duration,
|
|
2736
|
+
onEvent: (event) => {
|
|
2737
|
+
if (!isJSON && spinner) {
|
|
2738
|
+
spinner.text = `Monitoring ${serverName} \u2014 ${engine.events.length} events captured...`;
|
|
2739
|
+
}
|
|
2740
|
+
if (apiSessionId !== null && client) {
|
|
2741
|
+
pendingApiEvents.push(event);
|
|
2742
|
+
const now = Date.now();
|
|
2743
|
+
if (now - lastApiSubmit > 5e3 && pendingApiEvents.length > 0) {
|
|
2744
|
+
const eventsToSubmit = pendingApiEvents.map(mapNetworkEventToApi);
|
|
2745
|
+
pendingApiEvents = [];
|
|
2746
|
+
lastApiSubmit = now;
|
|
2747
|
+
client.submitSentinelEvents(apiSessionId, eventsToSubmit).catch(() => {
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
await engine.startMonitoring();
|
|
2754
|
+
await new Promise((resolve) => setTimeout(resolve, duration * 1e3));
|
|
2755
|
+
const report = await engine.stopMonitoring();
|
|
2756
|
+
sentinelReports.push(report);
|
|
2757
|
+
if (apiSessionId !== null && client) {
|
|
2758
|
+
if (pendingApiEvents.length > 0) {
|
|
2759
|
+
const finalEvents = pendingApiEvents.map(mapNetworkEventToApi);
|
|
2760
|
+
await client.submitSentinelEvents(apiSessionId, finalEvents);
|
|
2761
|
+
}
|
|
2762
|
+
const apiReport = await client.analyzeSentinelSession(apiSessionId);
|
|
2763
|
+
if (apiReport.ok && !isJSON) {
|
|
2764
|
+
console.log(
|
|
2765
|
+
import_chalk2.default.gray(
|
|
2766
|
+
` API Analysis: ${apiReport.data.findings.length} findings, threat: ${apiReport.data.threat_level}`
|
|
2767
|
+
)
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
if (spinner) {
|
|
2772
|
+
spinner.succeed(
|
|
2773
|
+
`${serverName}: ${report.totalEvents} events, ${report.findings.length} findings [${report.threatLevel.toUpperCase()}]`
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
if (isJSON) {
|
|
2778
|
+
console.log(JSON.stringify({ sentinel: sentinelReports }, null, 2));
|
|
2779
|
+
} else {
|
|
2780
|
+
for (const report of sentinelReports) {
|
|
2781
|
+
printSentinelReport(report);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
1654
2785
|
program.parse();
|
|
1655
2786
|
//# sourceMappingURL=index.js.map
|