@sonde/packs 0.1.1 → 0.1.3
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/.turbo/turbo-build.log +4 -6
- package/.turbo/turbo-test.log +57 -814
- package/CHANGELOG.md +18 -0
- package/dist/docker/manifest (# Edit conflict 2026-02-19 LIl7ilN #).js +54 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/a10.d.ts +3 -0
- package/dist/integrations/a10.d.ts.map +1 -0
- package/dist/integrations/a10.js +218 -0
- package/dist/integrations/a10.js.map +1 -0
- package/dist/integrations/checkpoint.d.ts +3 -0
- package/dist/integrations/checkpoint.d.ts.map +1 -0
- package/dist/integrations/checkpoint.js +249 -0
- package/dist/integrations/checkpoint.js.map +1 -0
- package/dist/integrations/citrix.d.ts.map +1 -1
- package/dist/integrations/citrix.js +14 -19
- package/dist/integrations/citrix.js.map +1 -1
- package/dist/integrations/citrix.test.js +3 -3
- package/dist/integrations/citrix.test.js.map +1 -1
- package/dist/integrations/datadog.d.ts +3 -0
- package/dist/integrations/datadog.d.ts.map +1 -0
- package/dist/integrations/datadog.js +195 -0
- package/dist/integrations/datadog.js.map +1 -0
- package/dist/integrations/graph.d.ts.map +1 -1
- package/dist/integrations/graph.js +10 -15
- package/dist/integrations/graph.js.map +1 -1
- package/dist/integrations/graph.test.js +3 -3
- package/dist/integrations/graph.test.js.map +1 -1
- package/dist/integrations/httpbin.d.ts.map +1 -1
- package/dist/integrations/httpbin.js +2 -7
- package/dist/integrations/httpbin.js.map +1 -1
- package/dist/integrations/jira.d.ts +3 -0
- package/dist/integrations/jira.d.ts.map +1 -0
- package/dist/integrations/jira.js +199 -0
- package/dist/integrations/jira.js.map +1 -0
- package/dist/integrations/loki.d.ts +3 -0
- package/dist/integrations/loki.d.ts.map +1 -0
- package/dist/integrations/loki.js +178 -0
- package/dist/integrations/loki.js.map +1 -0
- package/dist/integrations/meraki.d.ts +3 -0
- package/dist/integrations/meraki.d.ts.map +1 -0
- package/dist/integrations/meraki.js +238 -0
- package/dist/integrations/meraki.js.map +1 -0
- package/dist/integrations/nutanix.d.ts.map +1 -1
- package/dist/integrations/nutanix.js +12 -17
- package/dist/integrations/nutanix.js.map +1 -1
- package/dist/integrations/nutanix.test.js +3 -3
- package/dist/integrations/nutanix.test.js.map +1 -1
- package/dist/integrations/pagerduty.d.ts +3 -0
- package/dist/integrations/pagerduty.d.ts.map +1 -0
- package/dist/integrations/pagerduty.js +229 -0
- package/dist/integrations/pagerduty.js.map +1 -0
- package/dist/integrations/proxmox.d.ts.map +1 -1
- package/dist/integrations/proxmox.js +8 -13
- package/dist/integrations/proxmox.js.map +1 -1
- package/dist/integrations/proxmox.test.js +3 -3
- package/dist/integrations/proxmox.test.js.map +1 -1
- package/dist/integrations/servicenow.d.ts.map +1 -1
- package/dist/integrations/servicenow.js +10 -16
- package/dist/integrations/servicenow.js.map +1 -1
- package/dist/integrations/servicenow.test.js +3 -3
- package/dist/integrations/servicenow.test.js.map +1 -1
- package/dist/integrations/splunk.d.ts.map +1 -1
- package/dist/integrations/splunk.js +8 -13
- package/dist/integrations/splunk.js.map +1 -1
- package/dist/integrations/splunk.test.js +3 -3
- package/dist/integrations/splunk.test.js.map +1 -1
- package/dist/integrations/thousandeyes.d.ts +3 -0
- package/dist/integrations/thousandeyes.d.ts.map +1 -0
- package/dist/integrations/thousandeyes.js +263 -0
- package/dist/integrations/thousandeyes.js.map +1 -0
- package/dist/integrations/vcenter.d.ts +3 -0
- package/dist/integrations/vcenter.d.ts.map +1 -0
- package/dist/integrations/vcenter.js +190 -0
- package/dist/integrations/vcenter.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts (# Edit conflict 2026-02-19 N25hAvJ #).map +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +10 -0
- package/dist/system/index.js.map +1 -1
- package/dist/system/manifest.d.ts.map +1 -1
- package/dist/system/manifest.js +100 -3
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/probes/logs-dmesg.d.ts +13 -0
- package/dist/system/probes/logs-dmesg.d.ts.map +1 -0
- package/dist/system/probes/logs-dmesg.js +22 -0
- package/dist/system/probes/logs-dmesg.js.map +1 -0
- package/dist/system/probes/logs-dmesg.test.d.ts +2 -0
- package/dist/system/probes/logs-dmesg.test.d.ts.map +1 -0
- package/dist/system/probes/logs-dmesg.test.js +55 -0
- package/dist/system/probes/logs-dmesg.test.js.map +1 -0
- package/dist/system/probes/logs-journal.d.ts +21 -0
- package/dist/system/probes/logs-journal.d.ts.map +1 -0
- package/dist/system/probes/logs-journal.js +70 -0
- package/dist/system/probes/logs-journal.js.map +1 -0
- package/dist/system/probes/logs-journal.test.d.ts +2 -0
- package/dist/system/probes/logs-journal.test.d.ts.map +1 -0
- package/dist/system/probes/logs-journal.test.js +113 -0
- package/dist/system/probes/logs-journal.test.js.map +1 -0
- package/dist/system/probes/logs-tail.d.ts +14 -0
- package/dist/system/probes/logs-tail.d.ts.map +1 -0
- package/dist/system/probes/logs-tail.js +40 -0
- package/dist/system/probes/logs-tail.js.map +1 -0
- package/dist/system/probes/logs-tail.test.d.ts +2 -0
- package/dist/system/probes/logs-tail.test.d.ts.map +1 -0
- package/dist/system/probes/logs-tail.test.js +82 -0
- package/dist/system/probes/logs-tail.test.js.map +1 -0
- package/dist/system/probes/ping.d.ts +20 -0
- package/dist/system/probes/ping.d.ts.map +1 -0
- package/dist/system/probes/ping.js +54 -0
- package/dist/system/probes/ping.js.map +1 -0
- package/dist/system/probes/ping.test.d.ts +2 -0
- package/dist/system/probes/ping.test.d.ts.map +1 -0
- package/dist/system/probes/ping.test.js +127 -0
- package/dist/system/probes/ping.test.js.map +1 -0
- package/dist/system/probes/traceroute.d.ts +17 -0
- package/dist/system/probes/traceroute.d.ts.map +1 -0
- package/dist/system/probes/traceroute.js +72 -0
- package/dist/system/probes/traceroute.js.map +1 -0
- package/dist/system/probes/traceroute.test.d.ts +2 -0
- package/dist/system/probes/traceroute.test.d.ts.map +1 -0
- package/dist/system/probes/traceroute.test.js +98 -0
- package/dist/system/probes/traceroute.test.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +9 -0
- package/src/integrations/a10.ts +370 -0
- package/src/integrations/checkpoint.ts +381 -0
- package/src/integrations/citrix.test.ts +3 -3
- package/src/integrations/citrix.ts +17 -21
- package/src/integrations/datadog.ts +281 -0
- package/src/integrations/graph.test.ts +3 -3
- package/src/integrations/graph.ts +10 -14
- package/src/integrations/httpbin.ts +2 -6
- package/src/integrations/jira.ts +272 -0
- package/src/integrations/loki.ts +228 -0
- package/src/integrations/meraki.ts +344 -0
- package/src/integrations/nutanix.test.ts +3 -3
- package/src/integrations/nutanix.ts +13 -17
- package/src/integrations/pagerduty.ts +319 -0
- package/src/integrations/proxmox.test.ts +3 -3
- package/src/integrations/proxmox.ts +8 -12
- package/src/integrations/servicenow.test.ts +3 -3
- package/src/integrations/servicenow.ts +10 -15
- package/src/integrations/splunk.test.ts +3 -3
- package/src/integrations/splunk.ts +9 -13
- package/src/integrations/thousandeyes.ts +353 -0
- package/src/integrations/vcenter.ts +261 -0
- package/src/system/index.ts +10 -0
- package/src/system/manifest.ts +113 -3
- package/src/system/probes/logs-dmesg.test.ts +83 -0
- package/src/system/probes/logs-dmesg.ts +38 -0
- package/src/system/probes/logs-journal.test.ts +142 -0
- package/src/system/probes/logs-journal.ts +103 -0
- package/src/system/probes/logs-tail.test.ts +140 -0
- package/src/system/probes/logs-tail.ts +70 -0
- package/src/system/probes/ping.test.ts +163 -0
- package/src/system/probes/ping.ts +89 -0
- package/src/system/probes/traceroute.test.ts +149 -0
- package/src/system/probes/traceroute.ts +99 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { platform } from 'node:os';
|
|
2
|
+
import type { ProbeHandler } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export interface JournalEntry {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
priority: number;
|
|
7
|
+
message: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
uid: number;
|
|
10
|
+
unit?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface JournalResult {
|
|
14
|
+
entries: JournalEntry[];
|
|
15
|
+
entryCount: number;
|
|
16
|
+
unit?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads recent systemd journal entries via `journalctl -o json`.
|
|
21
|
+
* Linux only — fails with a clear message on macOS.
|
|
22
|
+
*/
|
|
23
|
+
export const logsJournal: ProbeHandler = async (params, exec) => {
|
|
24
|
+
if (platform() === 'darwin') {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'system.logs.journal requires systemd (Linux only)',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = Math.min(
|
|
31
|
+
Math.max(Number(params?.lines ?? 50), 1),
|
|
32
|
+
500,
|
|
33
|
+
);
|
|
34
|
+
const unit = params?.unit as string | undefined;
|
|
35
|
+
const priority = params?.priority as string | undefined;
|
|
36
|
+
|
|
37
|
+
const args = [
|
|
38
|
+
'-n',
|
|
39
|
+
String(lines),
|
|
40
|
+
'--no-pager',
|
|
41
|
+
'-o',
|
|
42
|
+
'json',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (unit) {
|
|
46
|
+
args.push('-u', unit);
|
|
47
|
+
}
|
|
48
|
+
if (priority) {
|
|
49
|
+
args.push('-p', priority);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const output = await exec('journalctl', args);
|
|
53
|
+
return parseJournalOutput(output, unit);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function parseJournalOutput(
|
|
57
|
+
raw: string,
|
|
58
|
+
unit?: string,
|
|
59
|
+
): JournalResult {
|
|
60
|
+
const entries: JournalEntry[] = [];
|
|
61
|
+
|
|
62
|
+
for (const line of raw.trim().split('\n')) {
|
|
63
|
+
if (!line) continue;
|
|
64
|
+
|
|
65
|
+
let obj: Record<string, unknown>;
|
|
66
|
+
try {
|
|
67
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const timestamp =
|
|
73
|
+
typeof obj.__REALTIME_TIMESTAMP === 'string'
|
|
74
|
+
? formatUsecTimestamp(obj.__REALTIME_TIMESTAMP)
|
|
75
|
+
: String(obj.__REALTIME_TIMESTAMP ?? '');
|
|
76
|
+
|
|
77
|
+
entries.push({
|
|
78
|
+
timestamp,
|
|
79
|
+
priority: Number(obj.PRIORITY ?? 6),
|
|
80
|
+
message: String(obj.MESSAGE ?? ''),
|
|
81
|
+
pid: Number(obj._PID ?? 0),
|
|
82
|
+
uid: Number(obj._UID ?? 0),
|
|
83
|
+
unit: obj._SYSTEMD_UNIT
|
|
84
|
+
? String(obj._SYSTEMD_UNIT)
|
|
85
|
+
: undefined,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result: JournalResult = {
|
|
90
|
+
entries,
|
|
91
|
+
entryCount: entries.length,
|
|
92
|
+
};
|
|
93
|
+
if (unit) {
|
|
94
|
+
result.unit = unit;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatUsecTimestamp(usec: string): string {
|
|
100
|
+
const ms = Math.floor(Number(usec) / 1000);
|
|
101
|
+
if (Number.isNaN(ms)) return usec;
|
|
102
|
+
return new Date(ms).toISOString();
|
|
103
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ExecFn } from '../../types.js';
|
|
3
|
+
import type { LogTailResult } from './logs-tail.js';
|
|
4
|
+
import {
|
|
5
|
+
logsTail,
|
|
6
|
+
parseLogTailOutput,
|
|
7
|
+
validateLogPath,
|
|
8
|
+
} from './logs-tail.js';
|
|
9
|
+
|
|
10
|
+
const SYSLOG_OUTPUT = `Jan 15 10:00:01 host systemd[1]: Started Session 1
|
|
11
|
+
Jan 15 10:00:02 host sshd[512]: Accepted publickey
|
|
12
|
+
Jan 15 10:00:03 host kernel: audit: type=1400`;
|
|
13
|
+
|
|
14
|
+
describe('validateLogPath', () => {
|
|
15
|
+
it('accepts valid /var/log/ paths', () => {
|
|
16
|
+
expect(() =>
|
|
17
|
+
validateLogPath('/var/log/syslog'),
|
|
18
|
+
).not.toThrow();
|
|
19
|
+
expect(() =>
|
|
20
|
+
validateLogPath('/var/log/nginx/access.log'),
|
|
21
|
+
).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('accepts valid /tmp/ paths', () => {
|
|
25
|
+
expect(() =>
|
|
26
|
+
validateLogPath('/tmp/debug.log'),
|
|
27
|
+
).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects relative paths', () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
validateLogPath('var/log/syslog'),
|
|
33
|
+
).toThrow('Path must be absolute');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects paths with ..', () => {
|
|
37
|
+
expect(() =>
|
|
38
|
+
validateLogPath('/var/log/../../etc/shadow'),
|
|
39
|
+
).toThrow("must not contain '..'");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects paths outside allowed prefixes', () => {
|
|
43
|
+
expect(() =>
|
|
44
|
+
validateLogPath('/etc/shadow'),
|
|
45
|
+
).toThrow('must start with one of');
|
|
46
|
+
expect(() =>
|
|
47
|
+
validateLogPath('/home/user/.bash_history'),
|
|
48
|
+
).toThrow('must start with one of');
|
|
49
|
+
expect(() =>
|
|
50
|
+
validateLogPath('/root/.ssh/id_rsa'),
|
|
51
|
+
).toThrow('must start with one of');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('parseLogTailOutput', () => {
|
|
56
|
+
it('parses multi-line log output', () => {
|
|
57
|
+
const result = parseLogTailOutput(
|
|
58
|
+
SYSLOG_OUTPUT,
|
|
59
|
+
'/var/log/syslog',
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(result.logPath).toBe('/var/log/syslog');
|
|
63
|
+
expect(result.lineCount).toBe(3);
|
|
64
|
+
expect(result.lines[0]).toContain('Started Session');
|
|
65
|
+
expect(result.lines[2]).toContain('audit');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('handles empty output', () => {
|
|
69
|
+
const result = parseLogTailOutput('', '/var/log/empty.log');
|
|
70
|
+
|
|
71
|
+
expect(result.lineCount).toBe(0);
|
|
72
|
+
expect(result.lines).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('logsTail handler', () => {
|
|
77
|
+
it('throws when path is missing', async () => {
|
|
78
|
+
const mockExec: ExecFn = async () => '';
|
|
79
|
+
await expect(
|
|
80
|
+
logsTail(undefined, mockExec),
|
|
81
|
+
).rejects.toThrow('Missing required parameter: path');
|
|
82
|
+
await expect(
|
|
83
|
+
logsTail({}, mockExec),
|
|
84
|
+
).rejects.toThrow('Missing required parameter: path');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('throws for invalid paths before calling exec', async () => {
|
|
88
|
+
const calls: string[] = [];
|
|
89
|
+
const mockExec: ExecFn = async (cmd) => {
|
|
90
|
+
calls.push(cmd);
|
|
91
|
+
return '';
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
logsTail({ path: '/etc/shadow' }, mockExec),
|
|
96
|
+
).rejects.toThrow('must start with one of');
|
|
97
|
+
expect(calls).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('passes correct args with defaults', async () => {
|
|
101
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
102
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
103
|
+
calls.push({ cmd, args });
|
|
104
|
+
return SYSLOG_OUTPUT;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = (await logsTail(
|
|
108
|
+
{ path: '/var/log/syslog' },
|
|
109
|
+
mockExec,
|
|
110
|
+
)) as LogTailResult;
|
|
111
|
+
|
|
112
|
+
expect(calls).toHaveLength(1);
|
|
113
|
+
expect(calls[0]?.cmd).toBe('tail');
|
|
114
|
+
expect(calls[0]?.args).toEqual([
|
|
115
|
+
'-n', '50', '/var/log/syslog',
|
|
116
|
+
]);
|
|
117
|
+
expect(result.logPath).toBe('/var/log/syslog');
|
|
118
|
+
expect(result.lineCount).toBe(3);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('clamps lines to valid range', async () => {
|
|
122
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
123
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
124
|
+
calls.push({ cmd, args });
|
|
125
|
+
return '';
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
await logsTail(
|
|
129
|
+
{ path: '/var/log/syslog', lines: 9999 },
|
|
130
|
+
mockExec,
|
|
131
|
+
);
|
|
132
|
+
expect(calls[0]?.args).toContain('500');
|
|
133
|
+
|
|
134
|
+
await logsTail(
|
|
135
|
+
{ path: '/var/log/syslog', lines: 0 },
|
|
136
|
+
mockExec,
|
|
137
|
+
);
|
|
138
|
+
expect(calls[1]?.args).toContain('1');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ProbeHandler } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
export interface LogTailResult {
|
|
4
|
+
logPath: string;
|
|
5
|
+
lines: string[];
|
|
6
|
+
lineCount: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ALLOWED_PREFIXES = ['/var/log/', '/tmp/'];
|
|
10
|
+
|
|
11
|
+
export function validateLogPath(path: string): void {
|
|
12
|
+
if (!path.startsWith('/')) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Path must be absolute (start with /): ${path}`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (path.includes('..')) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Path must not contain '..': ${path}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const allowed = ALLOWED_PREFIXES.some((prefix) =>
|
|
25
|
+
path.startsWith(prefix),
|
|
26
|
+
);
|
|
27
|
+
if (!allowed) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Path must start with one of: ${ALLOWED_PREFIXES.join(', ')} — got: ${path}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tails a log file at a given absolute path.
|
|
36
|
+
* Restricted to /var/log/ and /tmp/ for security.
|
|
37
|
+
*/
|
|
38
|
+
export const logsTail: ProbeHandler = async (params, exec) => {
|
|
39
|
+
const path = params?.path as string | undefined;
|
|
40
|
+
if (!path) {
|
|
41
|
+
throw new Error('Missing required parameter: path');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
validateLogPath(path);
|
|
45
|
+
|
|
46
|
+
const lines = Math.min(
|
|
47
|
+
Math.max(Number(params?.lines ?? 50), 1),
|
|
48
|
+
500,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const output = await exec('tail', [
|
|
52
|
+
'-n',
|
|
53
|
+
String(lines),
|
|
54
|
+
path,
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
return parseLogTailOutput(output, path);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function parseLogTailOutput(
|
|
61
|
+
raw: string,
|
|
62
|
+
logPath: string,
|
|
63
|
+
): LogTailResult {
|
|
64
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
65
|
+
return {
|
|
66
|
+
logPath,
|
|
67
|
+
lines,
|
|
68
|
+
lineCount: lines.length,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ExecFn } from '../../types.js';
|
|
3
|
+
import type { PingResult } from './ping.js';
|
|
4
|
+
import { parsePingOutput, ping } from './ping.js';
|
|
5
|
+
|
|
6
|
+
const LINUX_OUTPUT = `PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
|
|
7
|
+
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.543 ms
|
|
8
|
+
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.401 ms
|
|
9
|
+
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=0.387 ms
|
|
10
|
+
64 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=0.392 ms
|
|
11
|
+
|
|
12
|
+
--- 10.0.0.1 ping statistics ---
|
|
13
|
+
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
|
|
14
|
+
rtt min/avg/max/mdev = 0.387/0.430/0.543/0.065 ms
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const MACOS_OUTPUT = `PING 192.168.1.1 (192.168.1.1): 56 data bytes
|
|
18
|
+
64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.234 ms
|
|
19
|
+
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=1.456 ms
|
|
20
|
+
64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=1.123 ms
|
|
21
|
+
64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=1.345 ms
|
|
22
|
+
|
|
23
|
+
--- 192.168.1.1 ping statistics ---
|
|
24
|
+
4 packets transmitted, 4 packets received, 0.0% packet loss
|
|
25
|
+
round-trip min/avg/max/stddev = 1.123/1.289/1.456/0.122 ms
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const FULL_LOSS_OUTPUT = `PING 10.99.99.99 (10.99.99.99) 56(84) bytes of data.
|
|
29
|
+
|
|
30
|
+
--- 10.99.99.99 ping statistics ---
|
|
31
|
+
4 packets transmitted, 0 received, 100% packet loss, time 3003ms
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const PARTIAL_LOSS_OUTPUT = `PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data.
|
|
35
|
+
64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.500 ms
|
|
36
|
+
64 bytes from 10.0.0.5: icmp_seq=3 ttl=64 time=0.600 ms
|
|
37
|
+
|
|
38
|
+
--- 10.0.0.5 ping statistics ---
|
|
39
|
+
4 packets transmitted, 2 received, 50% packet loss, time 3004ms
|
|
40
|
+
rtt min/avg/max/mdev = 0.500/0.550/0.600/0.050 ms
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
describe('parsePingOutput', () => {
|
|
44
|
+
it('parses valid Linux ping output', () => {
|
|
45
|
+
const result = parsePingOutput(LINUX_OUTPUT, '10.0.0.1');
|
|
46
|
+
|
|
47
|
+
expect(result.host).toBe('10.0.0.1');
|
|
48
|
+
expect(result.packetsTransmitted).toBe(4);
|
|
49
|
+
expect(result.packetsReceived).toBe(4);
|
|
50
|
+
expect(result.packetLossPercent).toBe(0);
|
|
51
|
+
expect(result.rttMs).toEqual({
|
|
52
|
+
min: 0.387,
|
|
53
|
+
avg: 0.43,
|
|
54
|
+
max: 0.543,
|
|
55
|
+
stddev: 0.065,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('parses valid macOS ping output', () => {
|
|
60
|
+
const result = parsePingOutput(MACOS_OUTPUT, '192.168.1.1');
|
|
61
|
+
|
|
62
|
+
expect(result.host).toBe('192.168.1.1');
|
|
63
|
+
expect(result.packetsTransmitted).toBe(4);
|
|
64
|
+
expect(result.packetsReceived).toBe(4);
|
|
65
|
+
expect(result.packetLossPercent).toBe(0);
|
|
66
|
+
expect(result.rttMs).toEqual({
|
|
67
|
+
min: 1.123,
|
|
68
|
+
avg: 1.289,
|
|
69
|
+
max: 1.456,
|
|
70
|
+
stddev: 0.122,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles 100% packet loss', () => {
|
|
75
|
+
const result = parsePingOutput(FULL_LOSS_OUTPUT, '10.99.99.99');
|
|
76
|
+
|
|
77
|
+
expect(result.packetsTransmitted).toBe(4);
|
|
78
|
+
expect(result.packetsReceived).toBe(0);
|
|
79
|
+
expect(result.packetLossPercent).toBe(100);
|
|
80
|
+
expect(result.rttMs).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('handles partial packet loss with RTT', () => {
|
|
84
|
+
const result = parsePingOutput(PARTIAL_LOSS_OUTPUT, '10.0.0.5');
|
|
85
|
+
|
|
86
|
+
expect(result.packetsTransmitted).toBe(4);
|
|
87
|
+
expect(result.packetsReceived).toBe(2);
|
|
88
|
+
expect(result.packetLossPercent).toBe(50);
|
|
89
|
+
expect(result.rttMs).toBeDefined();
|
|
90
|
+
expect(result.rttMs?.avg).toBeCloseTo(0.55);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('ping handler', () => {
|
|
95
|
+
it('throws when host param is missing', async () => {
|
|
96
|
+
const mockExec: ExecFn = async () => '';
|
|
97
|
+
await expect(ping(undefined, mockExec)).rejects.toThrow(
|
|
98
|
+
'Missing required parameter: host',
|
|
99
|
+
);
|
|
100
|
+
await expect(ping({}, mockExec)).rejects.toThrow(
|
|
101
|
+
'Missing required parameter: host',
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('passes correct args to exec with default count', async () => {
|
|
106
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
107
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
108
|
+
calls.push({ cmd, args });
|
|
109
|
+
return LINUX_OUTPUT;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = (await ping(
|
|
113
|
+
{ host: '10.0.0.1' },
|
|
114
|
+
mockExec,
|
|
115
|
+
)) as PingResult;
|
|
116
|
+
|
|
117
|
+
expect(calls).toHaveLength(1);
|
|
118
|
+
expect(calls[0]?.cmd).toBe('ping');
|
|
119
|
+
expect(calls[0]?.args).toContain('-c');
|
|
120
|
+
expect(calls[0]?.args).toContain('4');
|
|
121
|
+
expect(calls[0]?.args).toContain('10.0.0.1');
|
|
122
|
+
expect(result.host).toBe('10.0.0.1');
|
|
123
|
+
expect(result.packetsTransmitted).toBe(4);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('respects custom count param', async () => {
|
|
127
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
128
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
129
|
+
calls.push({ cmd, args });
|
|
130
|
+
return LINUX_OUTPUT;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await ping({ host: '10.0.0.1', count: 2 }, mockExec);
|
|
134
|
+
|
|
135
|
+
expect(calls[0]?.args).toContain('2');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('clamps count to valid range', async () => {
|
|
139
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
140
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
141
|
+
calls.push({ cmd, args });
|
|
142
|
+
return LINUX_OUTPUT;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await ping({ host: '10.0.0.1', count: 100 }, mockExec);
|
|
146
|
+
expect(calls[0]?.args).toContain('20');
|
|
147
|
+
|
|
148
|
+
await ping({ host: '10.0.0.1', count: 0 }, mockExec);
|
|
149
|
+
expect(calls[1]?.args).toContain('1');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('handles exec failure (DNS resolution error)', async () => {
|
|
153
|
+
const mockExec: ExecFn = async () => {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'ping: unknown host nosuchhost.invalid',
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await expect(
|
|
160
|
+
ping({ host: 'nosuchhost.invalid' }, mockExec),
|
|
161
|
+
).rejects.toThrow('unknown host');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { platform } from 'node:os';
|
|
2
|
+
import type { ProbeHandler } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export interface PingResult {
|
|
5
|
+
host: string;
|
|
6
|
+
packetsTransmitted: number;
|
|
7
|
+
packetsReceived: number;
|
|
8
|
+
packetLossPercent: number;
|
|
9
|
+
rttMs?: {
|
|
10
|
+
min: number;
|
|
11
|
+
avg: number;
|
|
12
|
+
max: number;
|
|
13
|
+
stddev: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pings a remote host and returns packet loss and RTT statistics.
|
|
19
|
+
* Cross-platform: uses -W (Linux) or -t (macOS) for per-packet timeout.
|
|
20
|
+
*/
|
|
21
|
+
export const ping: ProbeHandler = async (params, exec) => {
|
|
22
|
+
const host = params?.host as string | undefined;
|
|
23
|
+
if (!host) {
|
|
24
|
+
throw new Error('Missing required parameter: host');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const count = Math.min(
|
|
28
|
+
Math.max(Number(params?.count ?? 4), 1),
|
|
29
|
+
20,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const isMac = platform() === 'darwin';
|
|
33
|
+
const timeoutFlag = isMac ? '-t' : '-W';
|
|
34
|
+
|
|
35
|
+
const output = await exec('ping', [
|
|
36
|
+
'-c',
|
|
37
|
+
String(count),
|
|
38
|
+
timeoutFlag,
|
|
39
|
+
'3',
|
|
40
|
+
host,
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
return parsePingOutput(output, host);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function parsePingOutput(
|
|
47
|
+
raw: string,
|
|
48
|
+
host: string,
|
|
49
|
+
): PingResult {
|
|
50
|
+
const result: PingResult = {
|
|
51
|
+
host,
|
|
52
|
+
packetsTransmitted: 0,
|
|
53
|
+
packetsReceived: 0,
|
|
54
|
+
packetLossPercent: 100,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Match "X packets transmitted, Y received" or "Y packets received"
|
|
58
|
+
const statsMatch = raw.match(
|
|
59
|
+
/(\d+)\s+packets?\s+transmitted,\s+(\d+)\s+(?:packets?\s+)?received/,
|
|
60
|
+
);
|
|
61
|
+
if (statsMatch) {
|
|
62
|
+
result.packetsTransmitted = Number(statsMatch[1]);
|
|
63
|
+
result.packetsReceived = Number(statsMatch[2]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Match "X% packet loss"
|
|
67
|
+
const lossMatch = raw.match(
|
|
68
|
+
/([\d.]+)%\s+packet\s+loss/,
|
|
69
|
+
);
|
|
70
|
+
if (lossMatch) {
|
|
71
|
+
result.packetLossPercent = Number(lossMatch[1]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Match RTT line: "min/avg/max/stddev = X/X/X/X ms"
|
|
75
|
+
// or "min/avg/max/mdev = X/X/X/X ms" (Linux)
|
|
76
|
+
const rttMatch = raw.match(
|
|
77
|
+
/=\s+([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)\s+ms/,
|
|
78
|
+
);
|
|
79
|
+
if (rttMatch) {
|
|
80
|
+
result.rttMs = {
|
|
81
|
+
min: Number(rttMatch[1]),
|
|
82
|
+
avg: Number(rttMatch[2]),
|
|
83
|
+
max: Number(rttMatch[3]),
|
|
84
|
+
stddev: Number(rttMatch[4]),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|