204 lines
5.9 KiB
JavaScript
204 lines
5.9 KiB
JavaScript
import test from 'brittle'
|
|
import fs from 'fs'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
import { Roles } from '../shared/protocol.js'
|
|
import { validateMethodArgs } from '../shared/schema.js'
|
|
import {
|
|
assertLogSourceAllowed,
|
|
buildJournalArgv,
|
|
clampLimit,
|
|
isJournalEnabled,
|
|
matchesFilters,
|
|
normalizeAnomaly,
|
|
normalizeAudit,
|
|
normalizeJournal,
|
|
parseTimeBound,
|
|
queryLogs,
|
|
readAuditFileLines,
|
|
} from '../server/services/logs.js'
|
|
|
|
test('clampLimit bounds', (t) => {
|
|
t.is(clampLimit(50), 50)
|
|
t.is(clampLimit(99999), 2000)
|
|
t.is(clampLimit(0), 200)
|
|
t.is(clampLimit('x'), 200)
|
|
})
|
|
|
|
test('parseTimeBound absolute and relative', (t) => {
|
|
const sec = 1_700_000_000
|
|
t.is(parseTimeBound(sec), sec * 1000)
|
|
t.ok(Math.abs((parseTimeBound('-1h') || 0) - (Date.now() - 3_600_000)) < 2000)
|
|
t.ok(parseTimeBound('2020-01-01T00:00:00.000Z') === Date.parse('2020-01-01T00:00:00.000Z'))
|
|
})
|
|
|
|
test('assertLogSourceAllowed roles', (t) => {
|
|
t.ok(assertLogSourceAllowed(Roles.viewer, 'anomaly').ok)
|
|
t.absent(assertLogSourceAllowed(Roles.viewer, 'audit').ok)
|
|
t.absent(assertLogSourceAllowed(Roles.operator, 'journal').ok)
|
|
t.ok(assertLogSourceAllowed(Roles.admin, 'audit').ok)
|
|
t.ok(assertLogSourceAllowed(Roles.admin, 'journal').ok)
|
|
t.is(assertLogSourceAllowed(Roles.viewer, 'audit').code, 'PERMISSION_DENIED')
|
|
})
|
|
|
|
test('buildJournalArgv fixed flags', (t) => {
|
|
const argv = buildJournalArgv({
|
|
limit: 50,
|
|
sinceMs: Date.parse('2024-01-01T00:00:00.000Z'),
|
|
untilMs: Date.parse('2024-01-01T01:00:00.000Z'),
|
|
priority: '3',
|
|
unit: 'peardata.service',
|
|
q: 'error',
|
|
})
|
|
t.ok(argv.includes('--output=json'))
|
|
t.ok(argv.includes('--no-pager'))
|
|
t.ok(argv.includes('-n'))
|
|
t.ok(argv.includes('50'))
|
|
t.ok(argv.includes('--since'))
|
|
t.ok(argv.includes('--until'))
|
|
t.ok(argv.includes('-p'))
|
|
t.ok(argv.includes('3'))
|
|
t.ok(argv.includes('-u'))
|
|
t.ok(argv.includes('peardata.service'))
|
|
t.ok(argv.includes('--grep'))
|
|
t.ok(argv.includes('error'))
|
|
t.absent(argv.some((a) => String(a).includes(';')))
|
|
})
|
|
|
|
test('normalize helpers', (t) => {
|
|
const a = normalizeAnomaly({
|
|
ts: 1_700_000_000_000,
|
|
chart: 'system.cpu',
|
|
severity: 'critical',
|
|
message: 'cpu high',
|
|
})
|
|
t.is(a.source, 'anomaly')
|
|
t.is(a.unit, 'system.cpu')
|
|
t.ok(a.message.includes('cpu'))
|
|
|
|
const audit = normalizeAudit({
|
|
ts: '2024-06-01T12:00:00.000Z',
|
|
method: 'mintInvite',
|
|
peerId: 'abcd',
|
|
role: 'admin',
|
|
ok: true,
|
|
})
|
|
t.is(audit.source, 'audit')
|
|
t.ok(audit.message.includes('mintInvite'))
|
|
|
|
const j = normalizeJournal({
|
|
__REALTIME_TIMESTAMP: String(1_700_000_000_000_000),
|
|
PRIORITY: '3',
|
|
_SYSTEMD_UNIT: 'sshd.service',
|
|
MESSAGE: 'Failed password',
|
|
})
|
|
t.is(j.source, 'journal')
|
|
t.is(j.severity, 'err')
|
|
t.is(j.unit, 'sshd.service')
|
|
})
|
|
|
|
test('matchesFilters q and window', (t) => {
|
|
const row = { ts: 1000, message: 'Hello World', unit: 'cpu', severity: 'warning' }
|
|
t.ok(matchesFilters({ q: 'hello' }, row))
|
|
t.absent(matchesFilters({ q: 'nope' }, row))
|
|
t.absent(matchesFilters({ since: 2000 }, row))
|
|
t.ok(matchesFilters({ since: 500, until: 1500 }, row))
|
|
})
|
|
|
|
test('queryLogs anomaly with mock engine', async (t) => {
|
|
const anomalies = {
|
|
listRecent() {
|
|
return [
|
|
{ ts: Date.now() - 1000, chart: 'system.cpu', severity: 'warning', message: 'cpu warn' },
|
|
{ ts: Date.now() - 500, chart: 'system.ram', severity: 'critical', message: 'ram crit' },
|
|
]
|
|
},
|
|
}
|
|
const res = await queryLogs(
|
|
{ source: 'anomaly', q: 'ram', role: Roles.viewer, limit: 50 },
|
|
{ anomalies }
|
|
)
|
|
t.ok(res.ok)
|
|
t.is(res.entries.length, 1)
|
|
t.is(res.entries[0].unit, 'system.ram')
|
|
})
|
|
|
|
test('queryLogs audit from temp file', async (t) => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'peardata-audit-'))
|
|
const file = path.join(dir, 'audit.log')
|
|
const lines = [
|
|
JSON.stringify({
|
|
ts: new Date(Date.now() - 60_000).toISOString(),
|
|
method: 'mintInvite',
|
|
peerId: 'aabb',
|
|
role: 'admin',
|
|
ok: true,
|
|
error: null,
|
|
}),
|
|
JSON.stringify({
|
|
ts: new Date().toISOString(),
|
|
method: 'runJob',
|
|
peerId: 'ccdd',
|
|
role: 'operator',
|
|
ok: false,
|
|
error: 'boom',
|
|
}),
|
|
]
|
|
fs.writeFileSync(file, lines.join('\n') + '\n')
|
|
t.is(readAuditFileLines(file).length, 2)
|
|
|
|
const denied = await queryLogs({ source: 'audit', role: Roles.viewer, auditPath: file })
|
|
t.absent(denied.ok)
|
|
t.is(denied.code, 'PERMISSION_DENIED')
|
|
|
|
const res = await queryLogs(
|
|
{ source: 'audit', role: Roles.admin, q: 'runJob', auditPath: file, limit: 20 },
|
|
{}
|
|
)
|
|
t.ok(res.ok)
|
|
t.is(res.entries.length, 1)
|
|
t.ok(res.entries[0].message.includes('runJob'))
|
|
})
|
|
|
|
test('queryLogs journal default on / disable / unsupported', async (t) => {
|
|
const prev = process.env.PEARDATA_JOURNAL
|
|
delete process.env.PEARDATA_JOURNAL
|
|
t.ok(isJournalEnabled(), 'journal enabled by default')
|
|
|
|
process.env.PEARDATA_JOURNAL = '0'
|
|
t.absent(isJournalEnabled())
|
|
const off = await queryLogs({ source: 'journal', role: Roles.admin })
|
|
t.absent(off.ok)
|
|
t.is(off.error, 'journal_disabled')
|
|
|
|
process.env.PEARDATA_JOURNAL = '1'
|
|
t.ok(isJournalEnabled())
|
|
if (os.platform() !== 'linux') {
|
|
const uns = await queryLogs({ source: 'journal', role: Roles.admin })
|
|
t.absent(uns.ok)
|
|
t.is(uns.error, 'unsupported')
|
|
} else {
|
|
const res = await queryLogs(
|
|
{ source: 'journal', role: Roles.admin, limit: 5 },
|
|
{
|
|
spawnJournal: async () => ({
|
|
stdout: '',
|
|
stderr: 'No journal files were found',
|
|
code: 1,
|
|
}),
|
|
}
|
|
)
|
|
t.absent(res.ok)
|
|
t.is(res.error, 'journalctl_failed')
|
|
}
|
|
if (prev == null) delete process.env.PEARDATA_JOURNAL
|
|
else process.env.PEARDATA_JOURNAL = prev
|
|
})
|
|
|
|
test('validateMethodArgs queryLogs', (t) => {
|
|
t.ok(validateMethodArgs('queryLogs', {}).ok)
|
|
t.ok(validateMethodArgs('queryLogs', { source: 'anomaly', limit: 10 }).ok)
|
|
t.absent(validateMethodArgs('queryLogs', { source: 'nope' }).ok)
|
|
t.absent(validateMethodArgs('queryLogs', { limit: 0 }).ok)
|
|
})
|