@tracehound/cli 1.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/LICENSE +190 -0
- package/README.md +17 -0
- package/dist/commands/inspect.d.ts +6 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +108 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +80 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +98 -0
- package/dist/commands/watch.d.ts +45 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +184 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/theme.d.ts +39 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +95 -0
- package/dist/tui/App.d.ts +10 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +19 -0
- package/dist/tui/hooks/useSnapshot.d.ts +29 -0
- package/dist/tui/hooks/useSnapshot.d.ts.map +1 -0
- package/dist/tui/hooks/useSnapshot.js +41 -0
- package/dist/tui/panels/Audit.d.ts +15 -0
- package/dist/tui/panels/Audit.d.ts.map +1 -0
- package/dist/tui/panels/Audit.js +9 -0
- package/dist/tui/panels/HoundPool.d.ts +16 -0
- package/dist/tui/panels/HoundPool.d.ts.map +1 -0
- package/dist/tui/panels/HoundPool.js +15 -0
- package/dist/tui/panels/Quarantine.d.ts +21 -0
- package/dist/tui/panels/Quarantine.d.ts.map +1 -0
- package/dist/tui/panels/Quarantine.js +18 -0
- package/package.json +48 -0
- package/src/commands/inspect.ts +142 -0
- package/src/commands/stats.ts +124 -0
- package/src/commands/status.ts +144 -0
- package/src/commands/watch.ts +273 -0
- package/src/index.ts +40 -0
- package/src/lib/theme.ts +117 -0
- package/tests/commands.test.ts +226 -0
- package/tests/smoke.test.ts +27 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspect command - Inspect quarantine contents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Table from 'cli-table3'
|
|
6
|
+
import { Command } from 'commander'
|
|
7
|
+
|
|
8
|
+
export const inspectCommand = new Command('inspect')
|
|
9
|
+
.description('Inspect quarantine contents')
|
|
10
|
+
.option('-s, --signature <sig>', 'Inspect specific signature')
|
|
11
|
+
.option('-l, --limit <n>', 'Limit results', '10')
|
|
12
|
+
.option('-j, --json', 'Output as JSON')
|
|
13
|
+
.action((options) => {
|
|
14
|
+
if (options.signature) {
|
|
15
|
+
inspectSingle(options.signature, options.json)
|
|
16
|
+
} else {
|
|
17
|
+
inspectList(parseInt(options.limit), options.json)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
interface QuarantineEntry {
|
|
22
|
+
signature: string
|
|
23
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
24
|
+
category: string
|
|
25
|
+
size: number
|
|
26
|
+
captured: number
|
|
27
|
+
source: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getQuarantineEntries(limit: number): QuarantineEntry[] {
|
|
31
|
+
// TODO: Connect to real core when available
|
|
32
|
+
void limit
|
|
33
|
+
return []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getEntry(signature: string): QuarantineEntry | null {
|
|
37
|
+
// TODO: Connect to real core when available
|
|
38
|
+
void signature
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inspectSingle(signature: string, json: boolean): void {
|
|
43
|
+
const entry = getEntry(signature)
|
|
44
|
+
|
|
45
|
+
if (!entry) {
|
|
46
|
+
console.log(`\n ❌ Evidence not found: ${signature}\n`)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (json) {
|
|
51
|
+
console.log(JSON.stringify(entry, null, 2))
|
|
52
|
+
} else {
|
|
53
|
+
printEntry(entry)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function inspectList(limit: number, json: boolean): void {
|
|
58
|
+
const entries = getQuarantineEntries(limit)
|
|
59
|
+
|
|
60
|
+
if (json) {
|
|
61
|
+
console.log(JSON.stringify(entries, null, 2))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
console.log('\n 📭 Quarantine is empty\n')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Header
|
|
71
|
+
console.log('\n ╔══════════════════════════════════════════════════════════════╗')
|
|
72
|
+
console.log(' ║ QUARANTINE CONTENTS ║')
|
|
73
|
+
console.log(' ╚══════════════════════════════════════════════════════════════╝\n')
|
|
74
|
+
|
|
75
|
+
const table = new Table({
|
|
76
|
+
head: ['Signature', 'Severity', 'Category', 'Size', 'Source'],
|
|
77
|
+
style: { head: ['cyan'], border: ['gray'] },
|
|
78
|
+
colWidths: [16, 12, 12, 12, 18],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const severityIcon = getSeverityIcon(entry.severity)
|
|
83
|
+
table.push([
|
|
84
|
+
entry.signature.slice(0, 12) + '...',
|
|
85
|
+
`${severityIcon} ${entry.severity}`,
|
|
86
|
+
entry.category,
|
|
87
|
+
formatBytes(entry.size),
|
|
88
|
+
entry.source.slice(0, 15),
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(table.toString())
|
|
93
|
+
console.log()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printEntry(entry: QuarantineEntry): void {
|
|
97
|
+
const severityIcon = getSeverityIcon(entry.severity)
|
|
98
|
+
|
|
99
|
+
// Header
|
|
100
|
+
console.log('\n ╔══════════════════════════════════════════════════════════════╗')
|
|
101
|
+
console.log(' ║ EVIDENCE DETAILS ║')
|
|
102
|
+
console.log(' ╚══════════════════════════════════════════════════════════════╝\n')
|
|
103
|
+
|
|
104
|
+
const table = new Table({
|
|
105
|
+
style: { border: ['gray'] },
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
table.push(
|
|
109
|
+
{ Signature: entry.signature },
|
|
110
|
+
{ Severity: `${severityIcon} ${entry.severity}` },
|
|
111
|
+
{ Category: entry.category },
|
|
112
|
+
{ Size: formatBytes(entry.size) },
|
|
113
|
+
{ Source: entry.source },
|
|
114
|
+
{ Captured: new Date(entry.captured).toISOString() }
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
console.log(table.toString())
|
|
118
|
+
console.log()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getSeverityIcon(severity: string): string {
|
|
122
|
+
switch (severity) {
|
|
123
|
+
case 'critical':
|
|
124
|
+
return '🔴'
|
|
125
|
+
case 'high':
|
|
126
|
+
return '🟠'
|
|
127
|
+
case 'medium':
|
|
128
|
+
return '🟡'
|
|
129
|
+
case 'low':
|
|
130
|
+
return '🟢'
|
|
131
|
+
default:
|
|
132
|
+
return '⚪'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatBytes(bytes: number): string {
|
|
137
|
+
if (bytes === 0) return '0 B'
|
|
138
|
+
const k = 1024
|
|
139
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
140
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
141
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
142
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats command - Show threat statistics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Table from 'cli-table3'
|
|
6
|
+
import { Command } from 'commander'
|
|
7
|
+
|
|
8
|
+
export const statsCommand = new Command('stats')
|
|
9
|
+
.description('Show threat statistics')
|
|
10
|
+
.option('-j, --json', 'Output as JSON')
|
|
11
|
+
.option('--since <duration>', 'Time window (e.g., 1h, 24h, 7d)', '24h')
|
|
12
|
+
.action((options) => {
|
|
13
|
+
const stats = getStats(options.since)
|
|
14
|
+
|
|
15
|
+
if (options.json) {
|
|
16
|
+
console.log(JSON.stringify(stats, null, 2))
|
|
17
|
+
} else {
|
|
18
|
+
printStats(stats)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
interface ThreatStats {
|
|
23
|
+
window: string
|
|
24
|
+
total: number
|
|
25
|
+
bySeverity: {
|
|
26
|
+
critical: number
|
|
27
|
+
high: number
|
|
28
|
+
medium: number
|
|
29
|
+
low: number
|
|
30
|
+
}
|
|
31
|
+
byCategory: {
|
|
32
|
+
injection: number
|
|
33
|
+
ddos: number
|
|
34
|
+
other: number
|
|
35
|
+
}
|
|
36
|
+
outcomes: {
|
|
37
|
+
quarantined: number
|
|
38
|
+
rateLimited: number
|
|
39
|
+
clean: number
|
|
40
|
+
ignored: number
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getStats(_since: string): ThreatStats {
|
|
45
|
+
// TODO: Connect to real core when available
|
|
46
|
+
return {
|
|
47
|
+
window: _since,
|
|
48
|
+
total: 0,
|
|
49
|
+
bySeverity: {
|
|
50
|
+
critical: 0,
|
|
51
|
+
high: 0,
|
|
52
|
+
medium: 0,
|
|
53
|
+
low: 0,
|
|
54
|
+
},
|
|
55
|
+
byCategory: {
|
|
56
|
+
injection: 0,
|
|
57
|
+
ddos: 0,
|
|
58
|
+
other: 0,
|
|
59
|
+
},
|
|
60
|
+
outcomes: {
|
|
61
|
+
quarantined: 0,
|
|
62
|
+
rateLimited: 0,
|
|
63
|
+
clean: 0,
|
|
64
|
+
ignored: 0,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printStats(stats: ThreatStats): void {
|
|
70
|
+
// Header
|
|
71
|
+
console.log('\n ╔══════════════════════════════════════════════════════════════╗')
|
|
72
|
+
console.log(` ║ THREAT STATISTICS (${stats.window.padEnd(24)}) ║`)
|
|
73
|
+
console.log(' ╚══════════════════════════════════════════════════════════════╝\n')
|
|
74
|
+
|
|
75
|
+
// Summary
|
|
76
|
+
const summaryTable = new Table({
|
|
77
|
+
head: ['Metric', 'Value'],
|
|
78
|
+
style: { head: ['cyan'], border: ['gray'] },
|
|
79
|
+
})
|
|
80
|
+
summaryTable.push(['Total Threats', String(stats.total)], ['Time Window', stats.window])
|
|
81
|
+
console.log(summaryTable.toString())
|
|
82
|
+
console.log()
|
|
83
|
+
|
|
84
|
+
// By Severity
|
|
85
|
+
const severityTable = new Table({
|
|
86
|
+
head: ['Severity', 'Count'],
|
|
87
|
+
style: { head: ['red'], border: ['gray'] },
|
|
88
|
+
})
|
|
89
|
+
severityTable.push(
|
|
90
|
+
['🔴 Critical', String(stats.bySeverity.critical)],
|
|
91
|
+
['🟠 High', String(stats.bySeverity.high)],
|
|
92
|
+
['🟡 Medium', String(stats.bySeverity.medium)],
|
|
93
|
+
['🟢 Low', String(stats.bySeverity.low)]
|
|
94
|
+
)
|
|
95
|
+
console.log(severityTable.toString())
|
|
96
|
+
console.log()
|
|
97
|
+
|
|
98
|
+
// By Category
|
|
99
|
+
const categoryTable = new Table({
|
|
100
|
+
head: ['Category', 'Count'],
|
|
101
|
+
style: { head: ['yellow'], border: ['gray'] },
|
|
102
|
+
})
|
|
103
|
+
categoryTable.push(
|
|
104
|
+
['💉 Injection', String(stats.byCategory.injection)],
|
|
105
|
+
['🌊 DDoS', String(stats.byCategory.ddos)],
|
|
106
|
+
['❓ Other', String(stats.byCategory.other)]
|
|
107
|
+
)
|
|
108
|
+
console.log(categoryTable.toString())
|
|
109
|
+
console.log()
|
|
110
|
+
|
|
111
|
+
// Outcomes
|
|
112
|
+
const outcomesTable = new Table({
|
|
113
|
+
head: ['Outcome', 'Count'],
|
|
114
|
+
style: { head: ['green'], border: ['gray'] },
|
|
115
|
+
})
|
|
116
|
+
outcomesTable.push(
|
|
117
|
+
['🔒 Quarantined', String(stats.outcomes.quarantined)],
|
|
118
|
+
['⏱️ Rate Limited', String(stats.outcomes.rateLimited)],
|
|
119
|
+
['✅ Clean', String(stats.outcomes.clean)],
|
|
120
|
+
['⏭️ Ignored', String(stats.outcomes.ignored)]
|
|
121
|
+
)
|
|
122
|
+
console.log(outcomesTable.toString())
|
|
123
|
+
console.log()
|
|
124
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status command - Show current system status
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Table from 'cli-table3'
|
|
6
|
+
import { Command } from 'commander'
|
|
7
|
+
import { createRequire } from 'module'
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
9
|
+
const { version } = require('../../package.json')
|
|
10
|
+
|
|
11
|
+
export const statusCommand = new Command('status')
|
|
12
|
+
.description('Show current Tracehound system status')
|
|
13
|
+
.option('-j, --json', 'Output as JSON')
|
|
14
|
+
.action((options) => {
|
|
15
|
+
const status = getSystemStatus()
|
|
16
|
+
|
|
17
|
+
if (options.json) {
|
|
18
|
+
console.log(JSON.stringify(status, null, 2))
|
|
19
|
+
} else {
|
|
20
|
+
printStatus(status)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
interface SystemStatus {
|
|
25
|
+
version: string
|
|
26
|
+
uptime: number
|
|
27
|
+
health: 'healthy' | 'degraded' | 'critical'
|
|
28
|
+
quarantine: {
|
|
29
|
+
count: number
|
|
30
|
+
bytes: number
|
|
31
|
+
capacity: number
|
|
32
|
+
}
|
|
33
|
+
rateLimit: {
|
|
34
|
+
blocked: number
|
|
35
|
+
active: number
|
|
36
|
+
}
|
|
37
|
+
houndPool: {
|
|
38
|
+
active: number
|
|
39
|
+
dormant: number
|
|
40
|
+
total: number
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getSystemStatus(): SystemStatus {
|
|
45
|
+
// TODO: Connect to real core when available
|
|
46
|
+
return {
|
|
47
|
+
version: version,
|
|
48
|
+
uptime: Math.floor(process.uptime()),
|
|
49
|
+
health: 'healthy',
|
|
50
|
+
quarantine: {
|
|
51
|
+
count: 0,
|
|
52
|
+
bytes: 0,
|
|
53
|
+
capacity: 1000,
|
|
54
|
+
},
|
|
55
|
+
rateLimit: {
|
|
56
|
+
blocked: 0,
|
|
57
|
+
active: 0,
|
|
58
|
+
},
|
|
59
|
+
houndPool: {
|
|
60
|
+
active: 0,
|
|
61
|
+
dormant: 0,
|
|
62
|
+
total: 0,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printStatus(status: SystemStatus): void {
|
|
68
|
+
const healthIcon = status.health === 'healthy' ? '✅' : status.health === 'degraded' ? '⚠️' : '🔴'
|
|
69
|
+
|
|
70
|
+
// Header
|
|
71
|
+
console.log('\n ╔══════════════════════════════════════════════════════════════╗')
|
|
72
|
+
console.log(' ║ TRACEHOUND STATUS ║')
|
|
73
|
+
console.log(' ╚══════════════════════════════════════════════════════════════╝\n')
|
|
74
|
+
|
|
75
|
+
// System Info Table
|
|
76
|
+
const systemTable = new Table({
|
|
77
|
+
head: ['Property', 'Value'],
|
|
78
|
+
style: { head: ['cyan'], border: ['gray'] },
|
|
79
|
+
})
|
|
80
|
+
systemTable.push(
|
|
81
|
+
['Version', status.version],
|
|
82
|
+
['Uptime', formatUptime(status.uptime)],
|
|
83
|
+
['Health', `${healthIcon} ${status.health}`],
|
|
84
|
+
)
|
|
85
|
+
console.log(systemTable.toString())
|
|
86
|
+
console.log()
|
|
87
|
+
|
|
88
|
+
// Quarantine Table
|
|
89
|
+
const quarantineTable = new Table({
|
|
90
|
+
head: ['QUARANTINE', 'Value'],
|
|
91
|
+
style: { head: ['yellow'], border: ['gray'] },
|
|
92
|
+
})
|
|
93
|
+
const usage =
|
|
94
|
+
status.quarantine.capacity > 0
|
|
95
|
+
? ((status.quarantine.count / status.quarantine.capacity) * 100).toFixed(1)
|
|
96
|
+
: '0.0'
|
|
97
|
+
quarantineTable.push(
|
|
98
|
+
['Count', `${status.quarantine.count} / ${status.quarantine.capacity}`],
|
|
99
|
+
['Usage', `${usage}%`],
|
|
100
|
+
['Bytes', formatBytes(status.quarantine.bytes)],
|
|
101
|
+
)
|
|
102
|
+
console.log(quarantineTable.toString())
|
|
103
|
+
console.log()
|
|
104
|
+
|
|
105
|
+
// Rate Limit Table
|
|
106
|
+
const rateLimitTable = new Table({
|
|
107
|
+
head: ['RATE LIMIT', 'Value'],
|
|
108
|
+
style: { head: ['magenta'], border: ['gray'] },
|
|
109
|
+
})
|
|
110
|
+
rateLimitTable.push(
|
|
111
|
+
['Blocked', String(status.rateLimit.blocked)],
|
|
112
|
+
['Active', String(status.rateLimit.active)],
|
|
113
|
+
)
|
|
114
|
+
console.log(rateLimitTable.toString())
|
|
115
|
+
console.log()
|
|
116
|
+
|
|
117
|
+
// Hound Pool Table
|
|
118
|
+
const poolTable = new Table({
|
|
119
|
+
head: ['HOUND POOL', 'Value'],
|
|
120
|
+
style: { head: ['green'], border: ['gray'] },
|
|
121
|
+
})
|
|
122
|
+
poolTable.push(
|
|
123
|
+
['Active', String(status.houndPool.active)],
|
|
124
|
+
['Dormant', String(status.houndPool.dormant)],
|
|
125
|
+
['Total', String(status.houndPool.total)],
|
|
126
|
+
)
|
|
127
|
+
console.log(poolTable.toString())
|
|
128
|
+
console.log()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatUptime(seconds: number): string {
|
|
132
|
+
const h = Math.floor(seconds / 3600)
|
|
133
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
134
|
+
const s = seconds % 60
|
|
135
|
+
return `${h}h ${m}m ${s}s`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatBytes(bytes: number): string {
|
|
139
|
+
if (bytes === 0) return '0 B'
|
|
140
|
+
const k = 1024
|
|
141
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
142
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
143
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
144
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch command - Live dashboard (Pure ANSI, no React)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Table from 'cli-table3'
|
|
6
|
+
import { Command } from 'commander'
|
|
7
|
+
import { createRequire } from 'module'
|
|
8
|
+
import {
|
|
9
|
+
bold,
|
|
10
|
+
clearScreen,
|
|
11
|
+
hideCursor,
|
|
12
|
+
muted,
|
|
13
|
+
primary,
|
|
14
|
+
progressBar,
|
|
15
|
+
secondary,
|
|
16
|
+
severity,
|
|
17
|
+
showCursor,
|
|
18
|
+
theme,
|
|
19
|
+
} from '../lib/theme.js'
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url)
|
|
22
|
+
const { version } = require('../../package.json')
|
|
23
|
+
|
|
24
|
+
export const watchCommand = new Command('watch')
|
|
25
|
+
.description('Launch live dashboard')
|
|
26
|
+
.option('-r, --refresh <ms>', 'Refresh interval in ms', '1000')
|
|
27
|
+
.action((options) => {
|
|
28
|
+
const refreshMs = parseInt(options.refresh)
|
|
29
|
+
startDashboard(refreshMs)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
interface Snapshot {
|
|
33
|
+
timestamp: string
|
|
34
|
+
system: {
|
|
35
|
+
version: string
|
|
36
|
+
uptime: string
|
|
37
|
+
health: 'healthy' | 'degraded' | 'critical'
|
|
38
|
+
memory: { used: number; total: number }
|
|
39
|
+
}
|
|
40
|
+
quarantine: {
|
|
41
|
+
count: number
|
|
42
|
+
capacity: number
|
|
43
|
+
bytes: number
|
|
44
|
+
bySeverity: { critical: number; high: number; medium: number; low: number }
|
|
45
|
+
}
|
|
46
|
+
houndPool: {
|
|
47
|
+
active: number
|
|
48
|
+
dormant: number
|
|
49
|
+
total: number
|
|
50
|
+
status: 'ok' | 'exhausted'
|
|
51
|
+
}
|
|
52
|
+
recentThreats: Array<{
|
|
53
|
+
signature: string
|
|
54
|
+
severity: string
|
|
55
|
+
category: string
|
|
56
|
+
size: string
|
|
57
|
+
time: string
|
|
58
|
+
}>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getSnapshot(): Snapshot {
|
|
62
|
+
// TODO: Connect to real core
|
|
63
|
+
return {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
system: {
|
|
66
|
+
version: version,
|
|
67
|
+
uptime: formatUptime(Math.floor(process.uptime())),
|
|
68
|
+
health: 'healthy',
|
|
69
|
+
memory: { used: 45, total: 256 },
|
|
70
|
+
},
|
|
71
|
+
quarantine: {
|
|
72
|
+
count: 0,
|
|
73
|
+
capacity: 1000,
|
|
74
|
+
bytes: 0,
|
|
75
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
76
|
+
},
|
|
77
|
+
houndPool: {
|
|
78
|
+
active: 0,
|
|
79
|
+
dormant: 0,
|
|
80
|
+
total: 0,
|
|
81
|
+
status: 'ok',
|
|
82
|
+
},
|
|
83
|
+
recentThreats: [],
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function startDashboard(refreshMs: number): void {
|
|
88
|
+
hideCursor()
|
|
89
|
+
|
|
90
|
+
// Handle Ctrl+C gracefully
|
|
91
|
+
process.on('SIGINT', () => {
|
|
92
|
+
showCursor()
|
|
93
|
+
clearScreen()
|
|
94
|
+
console.log(muted('\n Dashboard closed.\n'))
|
|
95
|
+
process.exit(0)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const render = () => {
|
|
99
|
+
clearScreen()
|
|
100
|
+
const snapshot = getSnapshot()
|
|
101
|
+
renderDashboard(snapshot, refreshMs)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render()
|
|
105
|
+
setInterval(render, refreshMs)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderDashboard(s: Snapshot, refreshMs: number): void {
|
|
109
|
+
const width = 76
|
|
110
|
+
|
|
111
|
+
// Header
|
|
112
|
+
console.log()
|
|
113
|
+
console.log(primary(` ╔${'═'.repeat(width)}╗`))
|
|
114
|
+
console.log(
|
|
115
|
+
primary(` ║${' '.repeat(20)}`) +
|
|
116
|
+
bold('🐕 TRACEHOUND LIVE DASHBOARD') +
|
|
117
|
+
primary(`${' '.repeat(28)}║`),
|
|
118
|
+
)
|
|
119
|
+
console.log(primary(` ║${' '.repeat(24)}`) + muted(s.timestamp) + primary(`${' '.repeat(28)}║`))
|
|
120
|
+
console.log(primary(` ╚${'═'.repeat(width)}╝`))
|
|
121
|
+
console.log()
|
|
122
|
+
|
|
123
|
+
// System Status
|
|
124
|
+
const systemTable = new Table({
|
|
125
|
+
chars: getTableChars(),
|
|
126
|
+
style: { head: [], border: [] },
|
|
127
|
+
head: [secondary('Version'), secondary('Uptime'), secondary('Health'), secondary('Memory')],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const healthIcon =
|
|
131
|
+
s.system.health === 'healthy' ? '✅' : s.system.health === 'degraded' ? '⚠️' : '🔴'
|
|
132
|
+
const memBar = progressBar(s.system.memory.used, s.system.memory.total, 10)
|
|
133
|
+
|
|
134
|
+
systemTable.push([
|
|
135
|
+
s.system.version,
|
|
136
|
+
s.system.uptime,
|
|
137
|
+
`${healthIcon} ${s.system.health}`,
|
|
138
|
+
`${memBar} ${s.system.memory.used}/${s.system.memory.total} MB`,
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
console.log(muted(' SYSTEM'))
|
|
142
|
+
console.log(indent(systemTable.toString()))
|
|
143
|
+
console.log()
|
|
144
|
+
|
|
145
|
+
// Quarantine & Hound Pool side by side
|
|
146
|
+
const quarantineTable = new Table({
|
|
147
|
+
chars: getTableChars(),
|
|
148
|
+
style: { head: [], border: [] },
|
|
149
|
+
head: [secondary('QUARANTINE'), secondary('Value')],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const qUsage = s.quarantine.capacity > 0 ? (s.quarantine.count / s.quarantine.capacity) * 100 : 0
|
|
153
|
+
const qBar = progressBar(s.quarantine.count, s.quarantine.capacity, 8)
|
|
154
|
+
|
|
155
|
+
quarantineTable.push(
|
|
156
|
+
['Count', `${s.quarantine.count} / ${s.quarantine.capacity}`],
|
|
157
|
+
['Usage', `${qBar} ${qUsage.toFixed(1)}%`],
|
|
158
|
+
['Bytes', formatBytes(s.quarantine.bytes)],
|
|
159
|
+
[
|
|
160
|
+
'Split',
|
|
161
|
+
`${severity('critical').slice(0, 15)} ${s.quarantine.bySeverity.critical} ${severity(
|
|
162
|
+
'high',
|
|
163
|
+
).slice(0, 12)} ${s.quarantine.bySeverity.high} ${severity('medium').slice(0, 12)} ${
|
|
164
|
+
s.quarantine.bySeverity.medium
|
|
165
|
+
} ${severity('low').slice(0, 10)} ${s.quarantine.bySeverity.low}`,
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const poolTable = new Table({
|
|
170
|
+
chars: getTableChars(),
|
|
171
|
+
style: { head: [], border: [] },
|
|
172
|
+
head: [secondary('HOUND POOL'), secondary('Value')],
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const poolBar = progressBar(s.houndPool.active, s.houndPool.total || 1, 8)
|
|
176
|
+
const poolStatus = s.houndPool.status === 'ok' ? '✅ OK' : '🔴 EXHAUSTED'
|
|
177
|
+
|
|
178
|
+
poolTable.push(
|
|
179
|
+
['Active', `${poolBar} ${s.houndPool.active}/${s.houndPool.total}`],
|
|
180
|
+
['Dormant', String(s.houndPool.dormant)],
|
|
181
|
+
['Status', poolStatus],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.log(indent(quarantineTable.toString()))
|
|
185
|
+
console.log()
|
|
186
|
+
console.log(indent(poolTable.toString()))
|
|
187
|
+
console.log()
|
|
188
|
+
|
|
189
|
+
// Recent Threats
|
|
190
|
+
if (s.recentThreats.length > 0) {
|
|
191
|
+
const threatTable = new Table({
|
|
192
|
+
chars: getTableChars(),
|
|
193
|
+
style: { head: [], border: [] },
|
|
194
|
+
head: [
|
|
195
|
+
secondary('Signature'),
|
|
196
|
+
secondary('Severity'),
|
|
197
|
+
secondary('Category'),
|
|
198
|
+
secondary('Size'),
|
|
199
|
+
secondary('Time'),
|
|
200
|
+
],
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
for (const t of s.recentThreats.slice(0, 5)) {
|
|
204
|
+
threatTable.push([
|
|
205
|
+
t.signature.slice(0, 12) + '...',
|
|
206
|
+
severity(t.severity),
|
|
207
|
+
t.category,
|
|
208
|
+
t.size,
|
|
209
|
+
t.time,
|
|
210
|
+
])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(muted(' RECENT THREATS'))
|
|
214
|
+
console.log(indent(threatTable.toString()))
|
|
215
|
+
console.log()
|
|
216
|
+
} else {
|
|
217
|
+
console.log(muted(' RECENT THREATS'))
|
|
218
|
+
console.log(muted(' 📭 No recent threats'))
|
|
219
|
+
console.log()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Footer
|
|
223
|
+
console.log(muted(` ${'─'.repeat(width)}`))
|
|
224
|
+
console.log(
|
|
225
|
+
muted(
|
|
226
|
+
` Press Ctrl+C to exit │ Refresh: ${refreshMs}ms │ ${theme.reset}${secondary(
|
|
227
|
+
new Date().toLocaleTimeString(),
|
|
228
|
+
)}`,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getTableChars() {
|
|
234
|
+
return {
|
|
235
|
+
top: '─',
|
|
236
|
+
'top-mid': '┬',
|
|
237
|
+
'top-left': '┌',
|
|
238
|
+
'top-right': '┐',
|
|
239
|
+
bottom: '─',
|
|
240
|
+
'bottom-mid': '┴',
|
|
241
|
+
'bottom-left': '└',
|
|
242
|
+
'bottom-right': '┘',
|
|
243
|
+
left: '│',
|
|
244
|
+
'left-mid': '├',
|
|
245
|
+
mid: '─',
|
|
246
|
+
'mid-mid': '┼',
|
|
247
|
+
right: '│',
|
|
248
|
+
'right-mid': '┤',
|
|
249
|
+
middle: '│',
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function indent(text: string, spaces = 2): string {
|
|
254
|
+
return text
|
|
255
|
+
.split('\n')
|
|
256
|
+
.map((line) => ' '.repeat(spaces) + line)
|
|
257
|
+
.join('\n')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function formatUptime(seconds: number): string {
|
|
261
|
+
const h = Math.floor(seconds / 3600)
|
|
262
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
263
|
+
const s = seconds % 60
|
|
264
|
+
return `${h}h ${m}m ${s}s`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatBytes(bytes: number): string {
|
|
268
|
+
if (bytes === 0) return '0 B'
|
|
269
|
+
const k = 1024
|
|
270
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
271
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
272
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
273
|
+
}
|