@sonde/agent 0.0.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/cli/packs.d.ts +23 -0
- package/dist/cli/packs.d.ts.map +1 -0
- package/dist/cli/packs.js +172 -0
- package/dist/cli/packs.js.map +1 -0
- package/dist/cli/packs.test.d.ts +2 -0
- package/dist/cli/packs.test.d.ts.map +1 -0
- package/dist/cli/packs.test.js +171 -0
- package/dist/cli/packs.test.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/attestation.d.ts +9 -0
- package/dist/runtime/attestation.d.ts.map +1 -0
- package/dist/runtime/attestation.js +32 -0
- package/dist/runtime/attestation.js.map +1 -0
- package/dist/runtime/attestation.test.d.ts +2 -0
- package/dist/runtime/attestation.test.d.ts.map +1 -0
- package/dist/runtime/attestation.test.js +59 -0
- package/dist/runtime/attestation.test.js.map +1 -0
- package/dist/runtime/audit.d.ts +19 -0
- package/dist/runtime/audit.d.ts.map +1 -0
- package/dist/runtime/audit.js +52 -0
- package/dist/runtime/audit.js.map +1 -0
- package/dist/runtime/audit.test.d.ts +2 -0
- package/dist/runtime/audit.test.d.ts.map +1 -0
- package/dist/runtime/audit.test.js +53 -0
- package/dist/runtime/audit.test.js.map +1 -0
- package/dist/runtime/connection.d.ts +55 -0
- package/dist/runtime/connection.d.ts.map +1 -0
- package/dist/runtime/connection.js +325 -0
- package/dist/runtime/connection.js.map +1 -0
- package/dist/runtime/connection.test.d.ts +2 -0
- package/dist/runtime/connection.test.d.ts.map +1 -0
- package/dist/runtime/connection.test.js +221 -0
- package/dist/runtime/connection.test.js.map +1 -0
- package/dist/runtime/executor.d.ts +21 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +89 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/executor.test.d.ts +2 -0
- package/dist/runtime/executor.test.d.ts.map +1 -0
- package/dist/runtime/executor.test.js +88 -0
- package/dist/runtime/executor.test.js.map +1 -0
- package/dist/runtime/privilege.d.ts +9 -0
- package/dist/runtime/privilege.d.ts.map +1 -0
- package/dist/runtime/privilege.js +35 -0
- package/dist/runtime/privilege.js.map +1 -0
- package/dist/runtime/privilege.test.d.ts +2 -0
- package/dist/runtime/privilege.test.d.ts.map +1 -0
- package/dist/runtime/privilege.test.js +22 -0
- package/dist/runtime/privilege.test.js.map +1 -0
- package/dist/runtime/scrubber.d.ts +17 -0
- package/dist/runtime/scrubber.d.ts.map +1 -0
- package/dist/runtime/scrubber.js +84 -0
- package/dist/runtime/scrubber.js.map +1 -0
- package/dist/runtime/scrubber.test.d.ts +2 -0
- package/dist/runtime/scrubber.test.d.ts.map +1 -0
- package/dist/runtime/scrubber.test.js +72 -0
- package/dist/runtime/scrubber.test.js.map +1 -0
- package/dist/system/scanner.d.ts +32 -0
- package/dist/system/scanner.d.ts.map +1 -0
- package/dist/system/scanner.js +90 -0
- package/dist/system/scanner.js.map +1 -0
- package/dist/system/scanner.test.d.ts +2 -0
- package/dist/system/scanner.test.d.ts.map +1 -0
- package/dist/system/scanner.test.js +121 -0
- package/dist/system/scanner.test.js.map +1 -0
- package/dist/tui/installer/InstallerApp.d.ts +11 -0
- package/dist/tui/installer/InstallerApp.d.ts.map +1 -0
- package/dist/tui/installer/InstallerApp.js +32 -0
- package/dist/tui/installer/InstallerApp.js.map +1 -0
- package/dist/tui/installer/StepComplete.d.ts +9 -0
- package/dist/tui/installer/StepComplete.d.ts.map +1 -0
- package/dist/tui/installer/StepComplete.js +46 -0
- package/dist/tui/installer/StepComplete.js.map +1 -0
- package/dist/tui/installer/StepHub.d.ts +8 -0
- package/dist/tui/installer/StepHub.d.ts.map +1 -0
- package/dist/tui/installer/StepHub.js +65 -0
- package/dist/tui/installer/StepHub.js.map +1 -0
- package/dist/tui/installer/StepPacks.d.ts +9 -0
- package/dist/tui/installer/StepPacks.d.ts.map +1 -0
- package/dist/tui/installer/StepPacks.js +35 -0
- package/dist/tui/installer/StepPacks.js.map +1 -0
- package/dist/tui/installer/StepPermissions.d.ts +9 -0
- package/dist/tui/installer/StepPermissions.d.ts.map +1 -0
- package/dist/tui/installer/StepPermissions.js +39 -0
- package/dist/tui/installer/StepPermissions.js.map +1 -0
- package/dist/tui/installer/StepScan.d.ts +7 -0
- package/dist/tui/installer/StepScan.d.ts.map +1 -0
- package/dist/tui/installer/StepScan.js +38 -0
- package/dist/tui/installer/StepScan.js.map +1 -0
- package/dist/tui/manager/ActivityLog.d.ts +7 -0
- package/dist/tui/manager/ActivityLog.d.ts.map +1 -0
- package/dist/tui/manager/ActivityLog.js +25 -0
- package/dist/tui/manager/ActivityLog.js.map +1 -0
- package/dist/tui/manager/AuditView.d.ts +7 -0
- package/dist/tui/manager/AuditView.d.ts.map +1 -0
- package/dist/tui/manager/AuditView.js +32 -0
- package/dist/tui/manager/AuditView.js.map +1 -0
- package/dist/tui/manager/ManagerApp.d.ts +20 -0
- package/dist/tui/manager/ManagerApp.d.ts.map +1 -0
- package/dist/tui/manager/ManagerApp.js +79 -0
- package/dist/tui/manager/ManagerApp.js.map +1 -0
- package/dist/tui/manager/PackManager.d.ts +7 -0
- package/dist/tui/manager/PackManager.d.ts.map +1 -0
- package/dist/tui/manager/PackManager.js +22 -0
- package/dist/tui/manager/PackManager.js.map +1 -0
- package/dist/tui/manager/StatusView.d.ts +15 -0
- package/dist/tui/manager/StatusView.d.ts.map +1 -0
- package/dist/tui/manager/StatusView.js +10 -0
- package/dist/tui/manager/StatusView.js.map +1 -0
- package/package.json +45 -0
- package/scripts/install.sh +11 -0
- package/src/cli/packs.test.ts +213 -0
- package/src/cli/packs.ts +214 -0
- package/src/config.ts +62 -0
- package/src/index.ts +218 -0
- package/src/runtime/attestation.test.ts +69 -0
- package/src/runtime/attestation.ts +36 -0
- package/src/runtime/audit.test.ts +64 -0
- package/src/runtime/audit.ts +70 -0
- package/src/runtime/connection.test.ts +303 -0
- package/src/runtime/connection.ts +389 -0
- package/src/runtime/executor.test.ts +112 -0
- package/src/runtime/executor.ts +107 -0
- package/src/runtime/privilege.test.ts +25 -0
- package/src/runtime/privilege.ts +36 -0
- package/src/runtime/scrubber.test.ts +84 -0
- package/src/runtime/scrubber.ts +96 -0
- package/src/system/scanner.test.ts +154 -0
- package/src/system/scanner.ts +133 -0
- package/src/tui/installer/InstallerApp.tsx +86 -0
- package/src/tui/installer/StepComplete.tsx +94 -0
- package/src/tui/installer/StepHub.tsx +111 -0
- package/src/tui/installer/StepPacks.tsx +73 -0
- package/src/tui/installer/StepPermissions.tsx +104 -0
- package/src/tui/installer/StepScan.tsx +82 -0
- package/src/tui/manager/ActivityLog.tsx +57 -0
- package/src/tui/manager/AuditView.tsx +73 -0
- package/src/tui/manager/ManagerApp.tsx +157 -0
- package/src/tui/manager/PackManager.tsx +71 -0
- package/src/tui/manager/StatusView.tsx +103 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import type { HubConfig } from './InstallerApp.js';
|
|
6
|
+
|
|
7
|
+
interface StepHubProps {
|
|
8
|
+
onNext: (config: HubConfig) => void;
|
|
9
|
+
initialHubUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Field = 'hubUrl' | 'apiKey' | 'agentName';
|
|
13
|
+
const FIELDS: Field[] = ['hubUrl', 'apiKey', 'agentName'];
|
|
14
|
+
const FIELD_LABELS: Record<Field, string> = {
|
|
15
|
+
hubUrl: 'Hub URL',
|
|
16
|
+
apiKey: 'API Key',
|
|
17
|
+
agentName: 'Agent Name',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function StepHub({ onNext, initialHubUrl }: StepHubProps): JSX.Element {
|
|
21
|
+
const [activeField, setActiveField] = useState<Field>(initialHubUrl ? 'apiKey' : 'hubUrl');
|
|
22
|
+
const [hubUrl, setHubUrl] = useState(initialHubUrl ?? '');
|
|
23
|
+
const [apiKey, setApiKey] = useState('');
|
|
24
|
+
const [agentName, setAgentName] = useState(os.hostname());
|
|
25
|
+
const [error, setError] = useState('');
|
|
26
|
+
|
|
27
|
+
const values: Record<Field, string> = { hubUrl, apiKey, agentName };
|
|
28
|
+
const setters: Record<Field, (v: string) => void> = {
|
|
29
|
+
hubUrl: setHubUrl,
|
|
30
|
+
apiKey: setApiKey,
|
|
31
|
+
agentName: setAgentName,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
useInput((_input, key) => {
|
|
35
|
+
if (key.tab || (key.return && activeField !== FIELDS[FIELDS.length - 1])) {
|
|
36
|
+
const currentIdx = FIELDS.indexOf(activeField);
|
|
37
|
+
const nextIdx = (currentIdx + 1) % FIELDS.length;
|
|
38
|
+
const nextField = FIELDS[nextIdx];
|
|
39
|
+
if (nextField) setActiveField(nextField);
|
|
40
|
+
setError('');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (key.return && activeField === FIELDS[FIELDS.length - 1]) {
|
|
45
|
+
// Validate and submit
|
|
46
|
+
if (!hubUrl.trim()) {
|
|
47
|
+
setError('Hub URL is required');
|
|
48
|
+
setActiveField('hubUrl');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
new URL(hubUrl.trim());
|
|
53
|
+
} catch {
|
|
54
|
+
setError('Invalid URL format');
|
|
55
|
+
setActiveField('hubUrl');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!apiKey.trim()) {
|
|
59
|
+
setError('API Key is required');
|
|
60
|
+
setActiveField('apiKey');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
onNext({
|
|
64
|
+
hubUrl: hubUrl.trim(),
|
|
65
|
+
apiKey: apiKey.trim(),
|
|
66
|
+
agentName: agentName.trim() || os.hostname(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Box flexDirection="column">
|
|
73
|
+
<Text color="gray">Enter your hub connection details. Tab to move between fields.</Text>
|
|
74
|
+
<Box marginTop={1} flexDirection="column">
|
|
75
|
+
{FIELDS.map((field) => (
|
|
76
|
+
<Box key={field}>
|
|
77
|
+
<Box width={14}>
|
|
78
|
+
<Text color={activeField === field ? 'cyan' : 'white'}>
|
|
79
|
+
{activeField === field ? '> ' : ' '}
|
|
80
|
+
{FIELD_LABELS[field]}:
|
|
81
|
+
</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
<Box>
|
|
84
|
+
{activeField === field ? (
|
|
85
|
+
<TextInput
|
|
86
|
+
value={values[field]}
|
|
87
|
+
onChange={setters[field]}
|
|
88
|
+
placeholder={field === 'hubUrl' ? 'http://localhost:3000' : ''}
|
|
89
|
+
/>
|
|
90
|
+
) : (
|
|
91
|
+
<Text color="gray">
|
|
92
|
+
{field === 'apiKey' && values[field]
|
|
93
|
+
? '*'.repeat(values[field].length)
|
|
94
|
+
: values[field] || '(empty)'}
|
|
95
|
+
</Text>
|
|
96
|
+
)}
|
|
97
|
+
</Box>
|
|
98
|
+
</Box>
|
|
99
|
+
))}
|
|
100
|
+
</Box>
|
|
101
|
+
{error && (
|
|
102
|
+
<Box marginTop={1}>
|
|
103
|
+
<Text color="red">{error}</Text>
|
|
104
|
+
</Box>
|
|
105
|
+
)}
|
|
106
|
+
<Box marginTop={1}>
|
|
107
|
+
<Text color="gray">Tab: next field | Enter: submit</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { packRegistry } from '@sonde/packs';
|
|
2
|
+
import type { PackManifest } from '@sonde/shared';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import type { ScanResult } from '../../system/scanner.js';
|
|
6
|
+
|
|
7
|
+
interface StepPacksProps {
|
|
8
|
+
scanResults: ScanResult[];
|
|
9
|
+
onNext: (packs: PackManifest[]) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PackRow {
|
|
13
|
+
manifest: PackManifest;
|
|
14
|
+
detected: boolean;
|
|
15
|
+
selected: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function StepPacks({ scanResults, onNext }: StepPacksProps): JSX.Element {
|
|
19
|
+
const detectedNames = new Set(scanResults.filter((r) => r.detected).map((r) => r.packName));
|
|
20
|
+
|
|
21
|
+
const [rows, setRows] = useState<PackRow[]>(() =>
|
|
22
|
+
[...packRegistry.values()].map((pack) => ({
|
|
23
|
+
manifest: pack.manifest,
|
|
24
|
+
detected: detectedNames.has(pack.manifest.name),
|
|
25
|
+
selected: detectedNames.has(pack.manifest.name),
|
|
26
|
+
})),
|
|
27
|
+
);
|
|
28
|
+
const [cursor, setCursor] = useState(0);
|
|
29
|
+
|
|
30
|
+
useInput((_input, key) => {
|
|
31
|
+
if (key.upArrow) {
|
|
32
|
+
setCursor((prev) => (prev > 0 ? prev - 1 : rows.length - 1));
|
|
33
|
+
} else if (key.downArrow) {
|
|
34
|
+
setCursor((prev) => (prev < rows.length - 1 ? prev + 1 : 0));
|
|
35
|
+
} else if (_input === ' ') {
|
|
36
|
+
setRows((prev) =>
|
|
37
|
+
prev.map((row, i) => (i === cursor ? { ...row, selected: !row.selected } : row)),
|
|
38
|
+
);
|
|
39
|
+
} else if (key.return) {
|
|
40
|
+
const selected = rows.filter((r) => r.selected).map((r) => r.manifest);
|
|
41
|
+
onNext(selected);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box flexDirection="column">
|
|
47
|
+
<Text color="gray">Select packs to install. Detected software is pre-selected.</Text>
|
|
48
|
+
<Box marginTop={1} flexDirection="column">
|
|
49
|
+
{rows.map((row, i) => {
|
|
50
|
+
const isCursor = i === cursor;
|
|
51
|
+
const checkbox = row.selected ? '[x]' : '[ ]';
|
|
52
|
+
const probeCount = row.manifest.probes.length;
|
|
53
|
+
return (
|
|
54
|
+
<Box key={row.manifest.name}>
|
|
55
|
+
<Text color={isCursor ? 'cyan' : 'white'} bold={isCursor}>
|
|
56
|
+
{isCursor ? '> ' : ' '}
|
|
57
|
+
{checkbox} {row.manifest.name}
|
|
58
|
+
</Text>
|
|
59
|
+
<Text color="gray">
|
|
60
|
+
{' '}
|
|
61
|
+
({probeCount} probes) — {row.manifest.description}
|
|
62
|
+
</Text>
|
|
63
|
+
{row.detected && <Text color="green"> [detected]</Text>}
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</Box>
|
|
68
|
+
<Box marginTop={1}>
|
|
69
|
+
<Text color="gray">Up/Down: navigate | Space: toggle | Enter: confirm</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
</Box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PackManifest } from '@sonde/shared';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
type PermissionCheck,
|
|
6
|
+
checkPackPermissions,
|
|
7
|
+
createSystemChecker,
|
|
8
|
+
} from '../../system/scanner.js';
|
|
9
|
+
|
|
10
|
+
interface StepPermissionsProps {
|
|
11
|
+
selectedPacks: PackManifest[];
|
|
12
|
+
onNext: () => void;
|
|
13
|
+
onBack: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PackPermission {
|
|
17
|
+
name: string;
|
|
18
|
+
check: PermissionCheck;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getUserGroups(): string[] {
|
|
22
|
+
try {
|
|
23
|
+
if (typeof process.getgroups === 'function') {
|
|
24
|
+
return process.getgroups().map(String);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Not available on all platforms
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function StepPermissions({
|
|
33
|
+
selectedPacks,
|
|
34
|
+
onNext,
|
|
35
|
+
onBack,
|
|
36
|
+
}: StepPermissionsProps): JSX.Element {
|
|
37
|
+
const [results, setResults] = useState<PackPermission[]>([]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const checker = createSystemChecker();
|
|
41
|
+
const groups = getUserGroups();
|
|
42
|
+
const checks = selectedPacks.map((manifest) => ({
|
|
43
|
+
name: manifest.name,
|
|
44
|
+
check: checkPackPermissions(manifest, checker, groups),
|
|
45
|
+
}));
|
|
46
|
+
setResults(checks);
|
|
47
|
+
}, [selectedPacks]);
|
|
48
|
+
|
|
49
|
+
useInput((input, key) => {
|
|
50
|
+
if (key.return) {
|
|
51
|
+
onNext();
|
|
52
|
+
} else if (input === 'b') {
|
|
53
|
+
onBack();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const issues = results.filter((r) => !r.check.satisfied);
|
|
58
|
+
const allGood = issues.length === 0;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Box flexDirection="column">
|
|
62
|
+
<Text bold>Permission Review</Text>
|
|
63
|
+
<Box marginTop={1} flexDirection="column">
|
|
64
|
+
{results.map((r) => (
|
|
65
|
+
<Box key={r.name} flexDirection="column">
|
|
66
|
+
<Text color={r.check.satisfied ? 'green' : 'yellow'}>
|
|
67
|
+
{r.check.satisfied ? ' OK' : ' !!'} {r.name}
|
|
68
|
+
</Text>
|
|
69
|
+
{!r.check.satisfied && (
|
|
70
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
71
|
+
{r.check.missingGroups.length > 0 && (
|
|
72
|
+
<>
|
|
73
|
+
<Text color="yellow">Missing groups: {r.check.missingGroups.join(', ')}</Text>
|
|
74
|
+
{r.check.missingGroups.map((g) => (
|
|
75
|
+
<Text key={g} color="gray">
|
|
76
|
+
{' '}sudo usermod -aG {g} $(whoami)
|
|
77
|
+
</Text>
|
|
78
|
+
))}
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
{r.check.missingCommands.length > 0 && (
|
|
82
|
+
<Text color="yellow">Missing commands: {r.check.missingCommands.join(', ')}</Text>
|
|
83
|
+
)}
|
|
84
|
+
{r.check.missingFiles.length > 0 && (
|
|
85
|
+
<Text color="yellow">Missing files: {r.check.missingFiles.join(', ')}</Text>
|
|
86
|
+
)}
|
|
87
|
+
</Box>
|
|
88
|
+
)}
|
|
89
|
+
</Box>
|
|
90
|
+
))}
|
|
91
|
+
</Box>
|
|
92
|
+
{!allGood && (
|
|
93
|
+
<Box marginTop={1}>
|
|
94
|
+
<Text color="yellow">
|
|
95
|
+
Some packs have missing permissions. They may not work correctly.
|
|
96
|
+
</Text>
|
|
97
|
+
</Box>
|
|
98
|
+
)}
|
|
99
|
+
<Box marginTop={1}>
|
|
100
|
+
<Text color="gray">Enter: proceed | b: back to pack selection</Text>
|
|
101
|
+
</Box>
|
|
102
|
+
</Box>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { packRegistry } from '@sonde/packs';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { type ScanResult, createSystemChecker, scanForSoftware } from '../../system/scanner.js';
|
|
6
|
+
|
|
7
|
+
interface StepScanProps {
|
|
8
|
+
onNext: (results: ScanResult[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function StepScan({ onNext }: StepScanProps): JSX.Element {
|
|
12
|
+
const [scanning, setScanning] = useState(true);
|
|
13
|
+
const [results, setResults] = useState<ScanResult[]>([]);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const manifests = [...packRegistry.values()].map((p) => p.manifest);
|
|
17
|
+
const checker = createSystemChecker();
|
|
18
|
+
const scanResults = scanForSoftware(manifests, checker);
|
|
19
|
+
setResults(scanResults);
|
|
20
|
+
setScanning(false);
|
|
21
|
+
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
onNext(scanResults);
|
|
24
|
+
}, 1500);
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
}, [onNext]);
|
|
27
|
+
|
|
28
|
+
if (scanning) {
|
|
29
|
+
return (
|
|
30
|
+
<Box>
|
|
31
|
+
<Text color="cyan">
|
|
32
|
+
<Spinner type="dots" />
|
|
33
|
+
</Text>
|
|
34
|
+
<Text> Scanning system for known software...</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const detected = results.filter((r) => r.detected);
|
|
40
|
+
const notDetected = results.filter((r) => !r.detected);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box flexDirection="column">
|
|
44
|
+
<Text color="green">Scan complete!</Text>
|
|
45
|
+
<Box marginTop={1} flexDirection="column">
|
|
46
|
+
{detected.length > 0 && (
|
|
47
|
+
<>
|
|
48
|
+
<Text bold>Detected:</Text>
|
|
49
|
+
{detected.map((r) => (
|
|
50
|
+
<Text key={r.packName} color="green">
|
|
51
|
+
{' '}
|
|
52
|
+
{r.packName} — {formatMatches(r)}
|
|
53
|
+
</Text>
|
|
54
|
+
))}
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
{notDetected.length > 0 && (
|
|
58
|
+
<Box marginTop={detected.length > 0 ? 1 : 0} flexDirection="column">
|
|
59
|
+
<Text bold>Not detected:</Text>
|
|
60
|
+
{notDetected.map((r) => (
|
|
61
|
+
<Text key={r.packName} color="gray">
|
|
62
|
+
{' '}
|
|
63
|
+
{r.packName}
|
|
64
|
+
</Text>
|
|
65
|
+
))}
|
|
66
|
+
</Box>
|
|
67
|
+
)}
|
|
68
|
+
</Box>
|
|
69
|
+
<Box marginTop={1}>
|
|
70
|
+
<Text color="gray">Continuing to pack selection...</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatMatches(r: ScanResult): string {
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
if (r.matchedCommands.length > 0) parts.push(`commands: ${r.matchedCommands.join(', ')}`);
|
|
79
|
+
if (r.matchedFiles.length > 0) parts.push(`files: ${r.matchedFiles.join(', ')}`);
|
|
80
|
+
if (r.matchedServices.length > 0) parts.push(`services: ${r.matchedServices.join(', ')}`);
|
|
81
|
+
return parts.join('; ');
|
|
82
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import type { ActivityEntry } from './ManagerApp.js';
|
|
3
|
+
|
|
4
|
+
interface ActivityLogProps {
|
|
5
|
+
activity: ActivityEntry[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatTime(isoString: string): string {
|
|
9
|
+
const date = new Date(isoString);
|
|
10
|
+
return date.toLocaleTimeString('en-GB', { hour12: false });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function statusColor(status: string): string {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case 'success':
|
|
16
|
+
return 'green';
|
|
17
|
+
case 'error':
|
|
18
|
+
return 'red';
|
|
19
|
+
case 'timeout':
|
|
20
|
+
return 'yellow';
|
|
21
|
+
default:
|
|
22
|
+
return 'gray';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ActivityLog({ activity }: ActivityLogProps): JSX.Element {
|
|
27
|
+
if (activity.length === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<Box flexDirection="column">
|
|
30
|
+
<Text bold color="white">
|
|
31
|
+
Activity
|
|
32
|
+
</Text>
|
|
33
|
+
<Box marginTop={1}>
|
|
34
|
+
<Text color="gray">Waiting for probe activity...</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Box flexDirection="column">
|
|
42
|
+
<Text bold color="white">
|
|
43
|
+
Activity ({activity.length} probes)
|
|
44
|
+
</Text>
|
|
45
|
+
<Box marginTop={1} flexDirection="column">
|
|
46
|
+
{activity.map((entry, i) => (
|
|
47
|
+
<Box key={`${entry.timestamp}-${i}`}>
|
|
48
|
+
<Text color="gray">{formatTime(entry.timestamp)} </Text>
|
|
49
|
+
<Text color="white">{entry.probe.padEnd(25)}</Text>
|
|
50
|
+
<Text color={statusColor(entry.status)}>{entry.status.padEnd(10)}</Text>
|
|
51
|
+
<Text color="gray">{entry.durationMs}ms</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
))}
|
|
54
|
+
</Box>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import type { AgentAuditEntry, AgentAuditLog } from '../../runtime/audit.js';
|
|
4
|
+
|
|
5
|
+
interface AuditViewProps {
|
|
6
|
+
auditLog: AgentAuditLog;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatTime(isoString: string): string {
|
|
10
|
+
const date = new Date(isoString);
|
|
11
|
+
return date.toLocaleTimeString('en-GB', { hour12: false });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function statusColor(status: string): string {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case 'success':
|
|
17
|
+
return 'green';
|
|
18
|
+
case 'error':
|
|
19
|
+
return 'red';
|
|
20
|
+
case 'timeout':
|
|
21
|
+
return 'yellow';
|
|
22
|
+
default:
|
|
23
|
+
return 'gray';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AuditView({ auditLog }: AuditViewProps): JSX.Element {
|
|
28
|
+
const [entries, setEntries] = useState<AgentAuditEntry[]>([]);
|
|
29
|
+
const [chainValid, setChainValid] = useState(true);
|
|
30
|
+
const [totalEntries, setTotalEntries] = useState(0);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const all = auditLog.getRecent();
|
|
34
|
+
setTotalEntries(all.length);
|
|
35
|
+
setEntries(auditLog.getRecent(100));
|
|
36
|
+
setChainValid(auditLog.verifyChain().valid);
|
|
37
|
+
}, [auditLog]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Box flexDirection="column">
|
|
41
|
+
<Text bold color="white">
|
|
42
|
+
Audit Chain:{' '}
|
|
43
|
+
<Text color={chainValid ? 'green' : 'red'}>{chainValid ? 'Valid' : 'Broken'}</Text>
|
|
44
|
+
<Text color="gray"> ({totalEntries} entries)</Text>
|
|
45
|
+
</Text>
|
|
46
|
+
|
|
47
|
+
{entries.length === 0 ? (
|
|
48
|
+
<Box marginTop={1}>
|
|
49
|
+
<Text color="gray">No audit entries yet.</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
) : (
|
|
52
|
+
<Box marginTop={1} flexDirection="column">
|
|
53
|
+
<Box>
|
|
54
|
+
<Text bold color="gray">
|
|
55
|
+
{'Time Probe Status Duration Hash'}
|
|
56
|
+
</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
{entries.map((entry, i) => (
|
|
59
|
+
<Box key={`${entry.timestamp}-${i}`}>
|
|
60
|
+
<Text color="gray">{formatTime(entry.timestamp)} </Text>
|
|
61
|
+
<Text color="white">{entry.probe.padEnd(25)}</Text>
|
|
62
|
+
<Text color={statusColor(entry.status)}>{entry.status.padEnd(11)}</Text>
|
|
63
|
+
<Text color="gray">{String(entry.durationMs).padStart(4)}ms </Text>
|
|
64
|
+
<Text color="gray">
|
|
65
|
+
{entry.prevHash ? `${entry.prevHash.slice(0, 7)}...` : '(genesis)'}
|
|
66
|
+
</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
))}
|
|
69
|
+
</Box>
|
|
70
|
+
)}
|
|
71
|
+
</Box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import type { AgentConfig } from '../../config.js';
|
|
4
|
+
import { saveConfig } from '../../config.js';
|
|
5
|
+
import type { AgentConnection, ConnectionEvents } from '../../runtime/connection.js';
|
|
6
|
+
import type { ProbeExecutor } from '../../runtime/executor.js';
|
|
7
|
+
import { ActivityLog } from './ActivityLog.js';
|
|
8
|
+
import { AuditView } from './AuditView.js';
|
|
9
|
+
import { PackManager } from './PackManager.js';
|
|
10
|
+
import { StatusView } from './StatusView.js';
|
|
11
|
+
|
|
12
|
+
type View = 'status' | 'packs' | 'activity' | 'audit';
|
|
13
|
+
|
|
14
|
+
export interface ActivityEntry {
|
|
15
|
+
timestamp: string;
|
|
16
|
+
probe: string;
|
|
17
|
+
status: string;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Runtime {
|
|
22
|
+
config: AgentConfig;
|
|
23
|
+
executor: ProbeExecutor;
|
|
24
|
+
connection: AgentConnection;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ManagerAppProps {
|
|
28
|
+
createRuntime: (events: ConnectionEvents) => Runtime;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAX_ACTIVITY = 50;
|
|
32
|
+
|
|
33
|
+
export function ManagerApp({ createRuntime }: ManagerAppProps): JSX.Element {
|
|
34
|
+
const { exit } = useApp();
|
|
35
|
+
const [view, setView] = useState<View>('status');
|
|
36
|
+
const [connected, setConnected] = useState(false);
|
|
37
|
+
const [agentId, setAgentId] = useState<string | undefined>();
|
|
38
|
+
const [activity, setActivity] = useState<ActivityEntry[]>([]);
|
|
39
|
+
|
|
40
|
+
const runtimeRef = useRef<Runtime | null>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const runtime = createRuntime({
|
|
44
|
+
onConnected: (id) => {
|
|
45
|
+
setConnected(true);
|
|
46
|
+
setAgentId(id);
|
|
47
|
+
},
|
|
48
|
+
onDisconnected: () => {
|
|
49
|
+
setConnected(false);
|
|
50
|
+
},
|
|
51
|
+
onError: () => {},
|
|
52
|
+
onRegistered: (id) => {
|
|
53
|
+
runtime.config.agentId = id;
|
|
54
|
+
saveConfig(runtime.config);
|
|
55
|
+
},
|
|
56
|
+
onProbeCompleted: (probe, status, durationMs) => {
|
|
57
|
+
setActivity((prev) => {
|
|
58
|
+
const entry: ActivityEntry = {
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
probe,
|
|
61
|
+
status,
|
|
62
|
+
durationMs,
|
|
63
|
+
};
|
|
64
|
+
const next = [...prev, entry];
|
|
65
|
+
return next.length > MAX_ACTIVITY ? next.slice(-MAX_ACTIVITY) : next;
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
runtimeRef.current = runtime;
|
|
71
|
+
runtime.connection.start();
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
runtime.connection.stop();
|
|
75
|
+
};
|
|
76
|
+
}, [createRuntime]);
|
|
77
|
+
|
|
78
|
+
useInput((input) => {
|
|
79
|
+
switch (input) {
|
|
80
|
+
case 's':
|
|
81
|
+
setView('status');
|
|
82
|
+
break;
|
|
83
|
+
case 'p':
|
|
84
|
+
setView('packs');
|
|
85
|
+
break;
|
|
86
|
+
case 'l':
|
|
87
|
+
setView('activity');
|
|
88
|
+
break;
|
|
89
|
+
case 'a':
|
|
90
|
+
setView('audit');
|
|
91
|
+
break;
|
|
92
|
+
case 'q':
|
|
93
|
+
runtimeRef.current?.connection.stop();
|
|
94
|
+
exit();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const runtime = runtimeRef.current;
|
|
100
|
+
const config = runtime?.config;
|
|
101
|
+
const executor = runtime?.executor;
|
|
102
|
+
const auditLog = runtime?.connection.getAuditLog();
|
|
103
|
+
|
|
104
|
+
const statusColor = connected ? 'green' : 'yellow';
|
|
105
|
+
const statusText = connected ? 'Connected' : 'Connecting...';
|
|
106
|
+
const displayId = agentId ? ` (${agentId})` : '';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
|
|
110
|
+
<Box marginBottom={1}>
|
|
111
|
+
<Text bold color="cyan">
|
|
112
|
+
Sonde Agent
|
|
113
|
+
</Text>
|
|
114
|
+
<Text> </Text>
|
|
115
|
+
<Text color={statusColor}>{statusText}</Text>
|
|
116
|
+
<Text color="gray">{displayId}</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
|
|
119
|
+
{view === 'status' && config && executor && auditLog && (
|
|
120
|
+
<StatusView
|
|
121
|
+
config={config}
|
|
122
|
+
connected={connected}
|
|
123
|
+
agentId={agentId}
|
|
124
|
+
executor={executor}
|
|
125
|
+
auditLog={auditLog}
|
|
126
|
+
activity={activity}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{view === 'packs' && executor && <PackManager executor={executor} />}
|
|
131
|
+
|
|
132
|
+
{view === 'activity' && <ActivityLog activity={activity} />}
|
|
133
|
+
|
|
134
|
+
{view === 'audit' && auditLog && <AuditView auditLog={auditLog} />}
|
|
135
|
+
|
|
136
|
+
<Box marginTop={1}>
|
|
137
|
+
<Text color={view === 'status' ? 'cyan' : 'gray'} bold={view === 'status'}>
|
|
138
|
+
s:status
|
|
139
|
+
</Text>
|
|
140
|
+
<Text> </Text>
|
|
141
|
+
<Text color={view === 'packs' ? 'cyan' : 'gray'} bold={view === 'packs'}>
|
|
142
|
+
p:packs
|
|
143
|
+
</Text>
|
|
144
|
+
<Text> </Text>
|
|
145
|
+
<Text color={view === 'activity' ? 'cyan' : 'gray'} bold={view === 'activity'}>
|
|
146
|
+
l:activity
|
|
147
|
+
</Text>
|
|
148
|
+
<Text> </Text>
|
|
149
|
+
<Text color={view === 'audit' ? 'cyan' : 'gray'} bold={view === 'audit'}>
|
|
150
|
+
a:audit
|
|
151
|
+
</Text>
|
|
152
|
+
<Text> </Text>
|
|
153
|
+
<Text color="gray">q:quit</Text>
|
|
154
|
+
</Box>
|
|
155
|
+
</Box>
|
|
156
|
+
);
|
|
157
|
+
}
|