@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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch command - Live dashboard (Pure ANSI, no React)
|
|
3
|
+
*/
|
|
4
|
+
import Table from 'cli-table3';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
import { bold, clearScreen, hideCursor, muted, primary, progressBar, secondary, severity, showCursor, theme, } from '../lib/theme.js';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version } = require('../../package.json');
|
|
10
|
+
export const watchCommand = new Command('watch')
|
|
11
|
+
.description('Launch live dashboard')
|
|
12
|
+
.option('-r, --refresh <ms>', 'Refresh interval in ms', '1000')
|
|
13
|
+
.action((options) => {
|
|
14
|
+
const refreshMs = parseInt(options.refresh);
|
|
15
|
+
startDashboard(refreshMs);
|
|
16
|
+
});
|
|
17
|
+
export function getSnapshot() {
|
|
18
|
+
// TODO: Connect to real core
|
|
19
|
+
return {
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
system: {
|
|
22
|
+
version: version,
|
|
23
|
+
uptime: formatUptime(Math.floor(process.uptime())),
|
|
24
|
+
health: 'healthy',
|
|
25
|
+
memory: { used: 45, total: 256 },
|
|
26
|
+
},
|
|
27
|
+
quarantine: {
|
|
28
|
+
count: 0,
|
|
29
|
+
capacity: 1000,
|
|
30
|
+
bytes: 0,
|
|
31
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
32
|
+
},
|
|
33
|
+
houndPool: {
|
|
34
|
+
active: 0,
|
|
35
|
+
dormant: 0,
|
|
36
|
+
total: 0,
|
|
37
|
+
status: 'ok',
|
|
38
|
+
},
|
|
39
|
+
recentThreats: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function startDashboard(refreshMs) {
|
|
43
|
+
hideCursor();
|
|
44
|
+
// Handle Ctrl+C gracefully
|
|
45
|
+
process.on('SIGINT', () => {
|
|
46
|
+
showCursor();
|
|
47
|
+
clearScreen();
|
|
48
|
+
console.log(muted('\n Dashboard closed.\n'));
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
const render = () => {
|
|
52
|
+
clearScreen();
|
|
53
|
+
const snapshot = getSnapshot();
|
|
54
|
+
renderDashboard(snapshot, refreshMs);
|
|
55
|
+
};
|
|
56
|
+
render();
|
|
57
|
+
setInterval(render, refreshMs);
|
|
58
|
+
}
|
|
59
|
+
export function renderDashboard(s, refreshMs) {
|
|
60
|
+
const width = 76;
|
|
61
|
+
// Header
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(primary(` ╔${'═'.repeat(width)}╗`));
|
|
64
|
+
console.log(primary(` ║${' '.repeat(20)}`) +
|
|
65
|
+
bold('🐕 TRACEHOUND LIVE DASHBOARD') +
|
|
66
|
+
primary(`${' '.repeat(28)}║`));
|
|
67
|
+
console.log(primary(` ║${' '.repeat(24)}`) + muted(s.timestamp) + primary(`${' '.repeat(28)}║`));
|
|
68
|
+
console.log(primary(` ╚${'═'.repeat(width)}╝`));
|
|
69
|
+
console.log();
|
|
70
|
+
// System Status
|
|
71
|
+
const systemTable = new Table({
|
|
72
|
+
chars: getTableChars(),
|
|
73
|
+
style: { head: [], border: [] },
|
|
74
|
+
head: [secondary('Version'), secondary('Uptime'), secondary('Health'), secondary('Memory')],
|
|
75
|
+
});
|
|
76
|
+
const healthIcon = s.system.health === 'healthy' ? '✅' : s.system.health === 'degraded' ? '⚠️' : '🔴';
|
|
77
|
+
const memBar = progressBar(s.system.memory.used, s.system.memory.total, 10);
|
|
78
|
+
systemTable.push([
|
|
79
|
+
s.system.version,
|
|
80
|
+
s.system.uptime,
|
|
81
|
+
`${healthIcon} ${s.system.health}`,
|
|
82
|
+
`${memBar} ${s.system.memory.used}/${s.system.memory.total} MB`,
|
|
83
|
+
]);
|
|
84
|
+
console.log(muted(' SYSTEM'));
|
|
85
|
+
console.log(indent(systemTable.toString()));
|
|
86
|
+
console.log();
|
|
87
|
+
// Quarantine & Hound Pool side by side
|
|
88
|
+
const quarantineTable = new Table({
|
|
89
|
+
chars: getTableChars(),
|
|
90
|
+
style: { head: [], border: [] },
|
|
91
|
+
head: [secondary('QUARANTINE'), secondary('Value')],
|
|
92
|
+
});
|
|
93
|
+
const qUsage = s.quarantine.capacity > 0 ? (s.quarantine.count / s.quarantine.capacity) * 100 : 0;
|
|
94
|
+
const qBar = progressBar(s.quarantine.count, s.quarantine.capacity, 8);
|
|
95
|
+
quarantineTable.push(['Count', `${s.quarantine.count} / ${s.quarantine.capacity}`], ['Usage', `${qBar} ${qUsage.toFixed(1)}%`], ['Bytes', formatBytes(s.quarantine.bytes)], [
|
|
96
|
+
'Split',
|
|
97
|
+
`${severity('critical').slice(0, 15)} ${s.quarantine.bySeverity.critical} ${severity('high').slice(0, 12)} ${s.quarantine.bySeverity.high} ${severity('medium').slice(0, 12)} ${s.quarantine.bySeverity.medium} ${severity('low').slice(0, 10)} ${s.quarantine.bySeverity.low}`,
|
|
98
|
+
]);
|
|
99
|
+
const poolTable = new Table({
|
|
100
|
+
chars: getTableChars(),
|
|
101
|
+
style: { head: [], border: [] },
|
|
102
|
+
head: [secondary('HOUND POOL'), secondary('Value')],
|
|
103
|
+
});
|
|
104
|
+
const poolBar = progressBar(s.houndPool.active, s.houndPool.total || 1, 8);
|
|
105
|
+
const poolStatus = s.houndPool.status === 'ok' ? '✅ OK' : '🔴 EXHAUSTED';
|
|
106
|
+
poolTable.push(['Active', `${poolBar} ${s.houndPool.active}/${s.houndPool.total}`], ['Dormant', String(s.houndPool.dormant)], ['Status', poolStatus]);
|
|
107
|
+
console.log(indent(quarantineTable.toString()));
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(indent(poolTable.toString()));
|
|
110
|
+
console.log();
|
|
111
|
+
// Recent Threats
|
|
112
|
+
if (s.recentThreats.length > 0) {
|
|
113
|
+
const threatTable = new Table({
|
|
114
|
+
chars: getTableChars(),
|
|
115
|
+
style: { head: [], border: [] },
|
|
116
|
+
head: [
|
|
117
|
+
secondary('Signature'),
|
|
118
|
+
secondary('Severity'),
|
|
119
|
+
secondary('Category'),
|
|
120
|
+
secondary('Size'),
|
|
121
|
+
secondary('Time'),
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
for (const t of s.recentThreats.slice(0, 5)) {
|
|
125
|
+
threatTable.push([
|
|
126
|
+
t.signature.slice(0, 12) + '...',
|
|
127
|
+
severity(t.severity),
|
|
128
|
+
t.category,
|
|
129
|
+
t.size,
|
|
130
|
+
t.time,
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
console.log(muted(' RECENT THREATS'));
|
|
134
|
+
console.log(indent(threatTable.toString()));
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(muted(' RECENT THREATS'));
|
|
139
|
+
console.log(muted(' 📭 No recent threats'));
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
// Footer
|
|
143
|
+
console.log(muted(` ${'─'.repeat(width)}`));
|
|
144
|
+
console.log(muted(` Press Ctrl+C to exit │ Refresh: ${refreshMs}ms │ ${theme.reset}${secondary(new Date().toLocaleTimeString())}`));
|
|
145
|
+
}
|
|
146
|
+
function getTableChars() {
|
|
147
|
+
return {
|
|
148
|
+
top: '─',
|
|
149
|
+
'top-mid': '┬',
|
|
150
|
+
'top-left': '┌',
|
|
151
|
+
'top-right': '┐',
|
|
152
|
+
bottom: '─',
|
|
153
|
+
'bottom-mid': '┴',
|
|
154
|
+
'bottom-left': '└',
|
|
155
|
+
'bottom-right': '┘',
|
|
156
|
+
left: '│',
|
|
157
|
+
'left-mid': '├',
|
|
158
|
+
mid: '─',
|
|
159
|
+
'mid-mid': '┼',
|
|
160
|
+
right: '│',
|
|
161
|
+
'right-mid': '┤',
|
|
162
|
+
middle: '│',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function indent(text, spaces = 2) {
|
|
166
|
+
return text
|
|
167
|
+
.split('\n')
|
|
168
|
+
.map((line) => ' '.repeat(spaces) + line)
|
|
169
|
+
.join('\n');
|
|
170
|
+
}
|
|
171
|
+
function formatUptime(seconds) {
|
|
172
|
+
const h = Math.floor(seconds / 3600);
|
|
173
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
174
|
+
const s = seconds % 60;
|
|
175
|
+
return `${h}h ${m}m ${s}s`;
|
|
176
|
+
}
|
|
177
|
+
function formatBytes(bytes) {
|
|
178
|
+
if (bytes === 0)
|
|
179
|
+
return '0 B';
|
|
180
|
+
const k = 1024;
|
|
181
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
182
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
183
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
184
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tracehound CLI - Evaluation Runtime
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* - tracehound status : Show current system status
|
|
7
|
+
* - tracehound stats : Show threat statistics
|
|
8
|
+
* - tracehound inspect : Inspect quarantine
|
|
9
|
+
* - tracehound watch : Live TUI dashboard
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
export declare const program: Command;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAYnC,eAAO,MAAM,OAAO,SAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tracehound CLI - Evaluation Runtime
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* - tracehound status : Show current system status
|
|
7
|
+
* - tracehound stats : Show threat statistics
|
|
8
|
+
* - tracehound inspect : Inspect quarantine
|
|
9
|
+
* - tracehound watch : Live TUI dashboard
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { inspectCommand } from './commands/inspect.js';
|
|
13
|
+
import { statsCommand } from './commands/stats.js';
|
|
14
|
+
import { statusCommand } from './commands/status.js';
|
|
15
|
+
import { watchCommand } from './commands/watch.js';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { version } = require('../package.json');
|
|
20
|
+
export const program = new Command();
|
|
21
|
+
program.name('tracehound').description('Tracehound CLI - Runtime Security Buffer').version(version);
|
|
22
|
+
// Register commands
|
|
23
|
+
program.addCommand(statusCommand);
|
|
24
|
+
program.addCommand(statsCommand);
|
|
25
|
+
program.addCommand(inspectCommand);
|
|
26
|
+
program.addCommand(watchCommand);
|
|
27
|
+
// Only parse if executed directly
|
|
28
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url).endsWith(process.argv[1].replace(/\\/g, '/'));
|
|
29
|
+
if (isMain || process.env.NODE_ENV === 'cli-run') {
|
|
30
|
+
program.parse();
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI Theme - Soft Dark Material
|
|
3
|
+
*
|
|
4
|
+
* Terminal color utilities with a beautiful dark theme
|
|
5
|
+
*/
|
|
6
|
+
export declare const theme: {
|
|
7
|
+
bg: string;
|
|
8
|
+
fg: string;
|
|
9
|
+
primary: string;
|
|
10
|
+
secondary: string;
|
|
11
|
+
accent: string;
|
|
12
|
+
critical: string;
|
|
13
|
+
high: string;
|
|
14
|
+
medium: string;
|
|
15
|
+
low: string;
|
|
16
|
+
border: string;
|
|
17
|
+
muted: string;
|
|
18
|
+
success: string;
|
|
19
|
+
warning: string;
|
|
20
|
+
error: string;
|
|
21
|
+
bold: string;
|
|
22
|
+
dim: string;
|
|
23
|
+
reset: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function colorize(text: string, ...styles: string[]): string;
|
|
26
|
+
export declare function primary(text: string): string;
|
|
27
|
+
export declare function secondary(text: string): string;
|
|
28
|
+
export declare function accent(text: string): string;
|
|
29
|
+
export declare function muted(text: string): string;
|
|
30
|
+
export declare function bold(text: string): string;
|
|
31
|
+
export declare function success(text: string): string;
|
|
32
|
+
export declare function warning(text: string): string;
|
|
33
|
+
export declare function error(text: string): string;
|
|
34
|
+
export declare function severity(level: string): string;
|
|
35
|
+
export declare function progressBar(current: number, max: number, width?: number): string;
|
|
36
|
+
export declare function clearScreen(): void;
|
|
37
|
+
export declare function hideCursor(): void;
|
|
38
|
+
export declare function showCursor(): void;
|
|
39
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/lib/theme.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;CA2BjB,CAAA;AAGD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAElE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAa9C;AAGD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,CAS5E;AAGD,wBAAgB,WAAW,IAAI,IAAI,CAElC;AAGD,wBAAgB,UAAU,IAAI,IAAI,CAEjC;AAED,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI Theme - Soft Dark Material
|
|
3
|
+
*
|
|
4
|
+
* Terminal color utilities with a beautiful dark theme
|
|
5
|
+
*/
|
|
6
|
+
// ANSI escape codes
|
|
7
|
+
const ESC = '\x1b[';
|
|
8
|
+
const RESET = `${ESC}0m`;
|
|
9
|
+
// 256-color palette for soft dark material theme
|
|
10
|
+
export const theme = {
|
|
11
|
+
// Base colors
|
|
12
|
+
bg: `${ESC}48;5;235m`, // Soft dark gray background
|
|
13
|
+
fg: `${ESC}38;5;253m`, // Light gray text
|
|
14
|
+
// Accent colors (Material Design inspired)
|
|
15
|
+
primary: `${ESC}38;5;75m`, // Soft blue
|
|
16
|
+
secondary: `${ESC}38;5;183m`, // Soft purple
|
|
17
|
+
accent: `${ESC}38;5;114m`, // Soft green
|
|
18
|
+
// Severity colors
|
|
19
|
+
critical: `${ESC}38;5;203m`, // Soft red
|
|
20
|
+
high: `${ESC}38;5;215m`, // Soft orange
|
|
21
|
+
medium: `${ESC}38;5;221m`, // Soft yellow
|
|
22
|
+
low: `${ESC}38;5;114m`, // Soft green
|
|
23
|
+
// UI elements
|
|
24
|
+
border: `${ESC}38;5;240m`, // Dim gray for borders
|
|
25
|
+
muted: `${ESC}38;5;245m`, // Muted text
|
|
26
|
+
success: `${ESC}38;5;114m`, // Green
|
|
27
|
+
warning: `${ESC}38;5;215m`, // Orange
|
|
28
|
+
error: `${ESC}38;5;203m`, // Red
|
|
29
|
+
// Styles
|
|
30
|
+
bold: `${ESC}1m`,
|
|
31
|
+
dim: `${ESC}2m`,
|
|
32
|
+
reset: RESET,
|
|
33
|
+
};
|
|
34
|
+
// Color helper functions
|
|
35
|
+
export function colorize(text, ...styles) {
|
|
36
|
+
return `${styles.join('')}${text}${RESET}`;
|
|
37
|
+
}
|
|
38
|
+
export function primary(text) {
|
|
39
|
+
return colorize(text, theme.primary);
|
|
40
|
+
}
|
|
41
|
+
export function secondary(text) {
|
|
42
|
+
return colorize(text, theme.secondary);
|
|
43
|
+
}
|
|
44
|
+
export function accent(text) {
|
|
45
|
+
return colorize(text, theme.accent);
|
|
46
|
+
}
|
|
47
|
+
export function muted(text) {
|
|
48
|
+
return colorize(text, theme.muted);
|
|
49
|
+
}
|
|
50
|
+
export function bold(text) {
|
|
51
|
+
return colorize(text, theme.bold);
|
|
52
|
+
}
|
|
53
|
+
export function success(text) {
|
|
54
|
+
return colorize(text, theme.success);
|
|
55
|
+
}
|
|
56
|
+
export function warning(text) {
|
|
57
|
+
return colorize(text, theme.warning);
|
|
58
|
+
}
|
|
59
|
+
export function error(text) {
|
|
60
|
+
return colorize(text, theme.error);
|
|
61
|
+
}
|
|
62
|
+
export function severity(level) {
|
|
63
|
+
switch (level) {
|
|
64
|
+
case 'critical':
|
|
65
|
+
return colorize(`● ${level}`, theme.critical);
|
|
66
|
+
case 'high':
|
|
67
|
+
return colorize(`● ${level}`, theme.high);
|
|
68
|
+
case 'medium':
|
|
69
|
+
return colorize(`● ${level}`, theme.medium);
|
|
70
|
+
case 'low':
|
|
71
|
+
return colorize(`● ${level}`, theme.low);
|
|
72
|
+
default:
|
|
73
|
+
return level;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Progress bar
|
|
77
|
+
export function progressBar(current, max, width = 20) {
|
|
78
|
+
const ratio = max > 0 ? current / max : 0;
|
|
79
|
+
const filled = Math.round(ratio * width);
|
|
80
|
+
const empty = width - filled;
|
|
81
|
+
const filledColor = ratio > 0.9 ? theme.critical : ratio > 0.7 ? theme.warning : theme.accent;
|
|
82
|
+
const bar = `${filledColor}${'█'.repeat(filled)}${theme.muted}${'░'.repeat(empty)}${RESET}`;
|
|
83
|
+
return bar;
|
|
84
|
+
}
|
|
85
|
+
// Clear screen
|
|
86
|
+
export function clearScreen() {
|
|
87
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
88
|
+
}
|
|
89
|
+
// Hide/show cursor
|
|
90
|
+
export function hideCursor() {
|
|
91
|
+
process.stdout.write('\x1b[?25l');
|
|
92
|
+
}
|
|
93
|
+
export function showCursor() {
|
|
94
|
+
process.stdout.write('\x1b[?25h');
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAMlD,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,GAAG,CAAC,EAAE,SAAS,EAAE,EAAE,QAAQ,GAAG,KAAK,CAAC,YAAY,CAiD/D"}
|
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TUI Application - Main Ink component
|
|
4
|
+
*/
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
import { useSnapshot } from './hooks/useSnapshot.js';
|
|
8
|
+
import { AuditPanel } from './panels/Audit.js';
|
|
9
|
+
import { HoundPoolPanel } from './panels/HoundPool.js';
|
|
10
|
+
import { QuarantinePanel } from './panels/Quarantine.js';
|
|
11
|
+
export function App({ refreshMs }) {
|
|
12
|
+
const snapshot = useSnapshot(refreshMs);
|
|
13
|
+
const [time, setTime] = useState(new Date());
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timer = setInterval(() => setTime(new Date()), 1000);
|
|
16
|
+
return () => clearInterval(timer);
|
|
17
|
+
}, []);
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557" }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2551" }), _jsxs(Text, { bold: true, color: "white", children: [' ', "TRACEHOUND LIVE", ' '] }), _jsx(Text, { color: "gray", children: time.toISOString() }), _jsxs(Text, { bold: true, color: "cyan", children: [' ', "\u2551"] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D" }) }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsx(QuarantinePanel, { data: snapshot.quarantine }), _jsx(HoundPoolPanel, { data: snapshot.houndPool }), _jsx(AuditPanel, { data: snapshot.audit })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: ["Press Ctrl+C to exit | Refresh: ", refreshMs, "ms"] }) })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSnapshot hook - Fetches system snapshot at interval
|
|
3
|
+
*/
|
|
4
|
+
export interface Snapshot {
|
|
5
|
+
quarantine: {
|
|
6
|
+
count: number;
|
|
7
|
+
bytes: number;
|
|
8
|
+
capacity: number;
|
|
9
|
+
bySeverity: {
|
|
10
|
+
critical: number;
|
|
11
|
+
high: number;
|
|
12
|
+
medium: number;
|
|
13
|
+
low: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
houndPool: {
|
|
17
|
+
active: number;
|
|
18
|
+
dormant: number;
|
|
19
|
+
total: number;
|
|
20
|
+
exhausted: boolean;
|
|
21
|
+
};
|
|
22
|
+
audit: {
|
|
23
|
+
records: number;
|
|
24
|
+
lastHash: string;
|
|
25
|
+
integrity: 'valid' | 'invalid' | 'empty';
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function useSnapshot(refreshMs: number): Snapshot;
|
|
29
|
+
//# sourceMappingURL=useSnapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSnapshot.d.ts","sourceRoot":"","sources":["../../../src/tui/hooks/useSnapshot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE;QACV,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,MAAM,CAAA;QAChB,UAAU,EAAE;YACV,QAAQ,EAAE,MAAM,CAAA;YAChB,IAAI,EAAE,MAAM,CAAA;YACZ,MAAM,EAAE,MAAM,CAAA;YACd,GAAG,EAAE,MAAM,CAAA;SACZ,CAAA;KACF,CAAA;IACD,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,OAAO,CAAA;KACnB,CAAA;IACD,KAAK,EAAE;QACL,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAA;KACzC,CAAA;CACF;AA8BD,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAYvD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSnapshot hook - Fetches system snapshot at interval
|
|
3
|
+
*/
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
function getSnapshot() {
|
|
6
|
+
// TODO: Connect to real core when available
|
|
7
|
+
return {
|
|
8
|
+
quarantine: {
|
|
9
|
+
count: 0,
|
|
10
|
+
bytes: 0,
|
|
11
|
+
capacity: 1000,
|
|
12
|
+
bySeverity: {
|
|
13
|
+
critical: 0,
|
|
14
|
+
high: 0,
|
|
15
|
+
medium: 0,
|
|
16
|
+
low: 0,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
houndPool: {
|
|
20
|
+
active: 0,
|
|
21
|
+
dormant: 0,
|
|
22
|
+
total: 0,
|
|
23
|
+
exhausted: false,
|
|
24
|
+
},
|
|
25
|
+
audit: {
|
|
26
|
+
records: 0,
|
|
27
|
+
lastHash: '',
|
|
28
|
+
integrity: 'empty',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function useSnapshot(refreshMs) {
|
|
33
|
+
const [snapshot, setSnapshot] = useState(getSnapshot);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const interval = setInterval(() => {
|
|
36
|
+
setSnapshot(getSnapshot());
|
|
37
|
+
}, refreshMs);
|
|
38
|
+
return () => clearInterval(interval);
|
|
39
|
+
}, [refreshMs]);
|
|
40
|
+
return snapshot;
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Panel - Shows audit chain status
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
interface AuditData {
|
|
6
|
+
records: number;
|
|
7
|
+
lastHash: string;
|
|
8
|
+
integrity: 'valid' | 'invalid' | 'empty';
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
data: AuditData;
|
|
12
|
+
}
|
|
13
|
+
export declare function AuditPanel({ data }: Props): React.ReactElement;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=Audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Audit.d.ts","sourceRoot":"","sources":["../../../src/tui/panels/Audit.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAA;CACzC;AAED,UAAU,KAAK;IACb,IAAI,EAAE,SAAS,CAAA;CAChB;AAED,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,YAAY,CA6B9D"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Audit Panel - Shows audit chain status
|
|
4
|
+
*/
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
export function AuditPanel({ data }) {
|
|
7
|
+
const integrityColor = data.integrity === 'valid' ? 'green' : data.integrity === 'invalid' ? 'red' : 'gray';
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", padding: 1, width: 25, children: [_jsx(Text, { bold: true, color: "cyan", children: "AUDIT CHAIN" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Records: " }), _jsx(Text, { bold: true, children: data.records })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Integrity: " }), _jsx(Text, { color: integrityColor, bold: true, children: data.integrity.toUpperCase() })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "Last Hash:" }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [data.lastHash.slice(0, 16) || '(empty)', "..."] }) })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hound Pool Panel - Shows pool status
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
interface HoundPoolData {
|
|
6
|
+
active: number;
|
|
7
|
+
dormant: number;
|
|
8
|
+
total: number;
|
|
9
|
+
exhausted: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface Props {
|
|
12
|
+
data: HoundPoolData;
|
|
13
|
+
}
|
|
14
|
+
export declare function HoundPoolPanel({ data }: Props): React.ReactElement;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=HoundPool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HoundPool.d.ts","sourceRoot":"","sources":["../../../src/tui/panels/HoundPool.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,UAAU,KAAK;IACb,IAAI,EAAE,aAAa,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,YAAY,CAuClE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Hound Pool Panel - Shows pool status
|
|
4
|
+
*/
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
export function HoundPoolPanel({ data }) {
|
|
7
|
+
const healthColor = data.exhausted ? 'red' : data.active === data.total ? 'yellow' : 'green';
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", padding: 1, width: 25, children: [_jsx(Text, { bold: true, color: "cyan", children: "HOUND POOL" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Status: " }), _jsx(Text, { color: healthColor, bold: true, children: data.exhausted ? 'EXHAUSTED' : 'OK' })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "\u25CF Active: " }), _jsx(Text, { bold: true, children: data.active })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "\u25CB Dormant: " }), _jsx(Text, { children: data.dormant })] }), _jsxs(Box, { children: [_jsx(Text, { children: "Total: " }), _jsx(Text, { children: data.total })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "Utilization:" }) }), _jsx(Box, { children: _jsx(Text, { children: renderBar(data.active, data.total) }) })] }));
|
|
9
|
+
}
|
|
10
|
+
function renderBar(current, max) {
|
|
11
|
+
const width = 15;
|
|
12
|
+
const filled = max > 0 ? Math.round((current / max) * width) : 0;
|
|
13
|
+
const empty = width - filled;
|
|
14
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quarantine Panel - Shows quarantine status
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
interface QuarantineData {
|
|
6
|
+
count: number;
|
|
7
|
+
bytes: number;
|
|
8
|
+
capacity: number;
|
|
9
|
+
bySeverity: {
|
|
10
|
+
critical: number;
|
|
11
|
+
high: number;
|
|
12
|
+
medium: number;
|
|
13
|
+
low: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface Props {
|
|
17
|
+
data: QuarantineData;
|
|
18
|
+
}
|
|
19
|
+
export declare function QuarantinePanel({ data }: Props): React.ReactElement;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=Quarantine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Quarantine.d.ts","sourceRoot":"","sources":["../../../src/tui/panels/Quarantine.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,MAAM,EAAE,MAAM,CAAA;QACd,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;CACF;AAED,UAAU,KAAK;IACb,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,YAAY,CAuCnE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Quarantine Panel - Shows quarantine status
|
|
4
|
+
*/
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
export function QuarantinePanel({ data }) {
|
|
7
|
+
const usage = data.capacity > 0 ? (data.count / data.capacity) * 100 : 0;
|
|
8
|
+
const usageColor = usage > 90 ? 'red' : usage > 70 ? 'yellow' : 'green';
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", padding: 1, width: 30, children: [_jsx(Text, { bold: true, color: "cyan", children: "QUARANTINE" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Count: " }), _jsx(Text, { color: usageColor, bold: true, children: data.count }), _jsxs(Text, { color: "gray", children: [" / ", data.capacity] })] }), _jsxs(Box, { children: [_jsx(Text, { children: "Usage: " }), _jsxs(Text, { color: usageColor, children: [usage.toFixed(1), "%"] })] }), _jsxs(Box, { children: [_jsx(Text, { children: "Bytes: " }), _jsx(Text, { children: formatBytes(data.bytes) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "By Severity:" }) }), _jsxs(Box, { children: [_jsxs(Text, { color: "red", children: ["\u25CF ", data.bySeverity.critical, " "] }), _jsxs(Text, { color: "yellow", children: ["\u25CF ", data.bySeverity.high, " "] }), _jsxs(Text, { color: "cyan", children: ["\u25CF ", data.bySeverity.medium, " "] }), _jsxs(Text, { color: "green", children: ["\u25CF ", data.bySeverity.low] })] })] }));
|
|
10
|
+
}
|
|
11
|
+
function formatBytes(bytes) {
|
|
12
|
+
if (bytes === 0)
|
|
13
|
+
return '0 B';
|
|
14
|
+
const k = 1024;
|
|
15
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
16
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
17
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tracehound/cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Tracehound CLI evaluation runtime",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Erdem Arslan <me@erdem.work>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/tracehound/tracehound.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/tracehound/tracehound#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/tracehound/tracehound/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"security",
|
|
18
|
+
"runtime",
|
|
19
|
+
"cli",
|
|
20
|
+
"forensics",
|
|
21
|
+
"audit"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"bin": {
|
|
26
|
+
"tracehound": "dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"cli-table3": "^0.6.5",
|
|
30
|
+
"commander": "^12.0.0",
|
|
31
|
+
"@tracehound/core": "1.2.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vitest": "^2.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"dev": "tsx src/index.ts",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"lint": "tsc --noEmit"
|
|
47
|
+
}
|
|
48
|
+
}
|