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 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 import_promises3 = require("fs/promises");
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.1.2";
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, import_promises3.writeFile)(options.output, jsonOutput);
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, import_promises3.writeFile)(options.output, formatJSON(summary));
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