36e9d5b0b3
An SSH jump host that authenticates users against the shared LDAP directory, authorizes them from the SSO Manager's inventory graph, and bridges them to downstream hosts — auditing everything. - Username-grammar routing (uid_-_target@jump) + interactive TUI picker - Inbound LDAP auth (publickey / password with off|local|all policy) - Directory-driven access (LDAP groups x /api/discovery/resources?group=) - Per-user key injection into sshPublicKey, connects downstream as the user - Shell / exec / SFTP-subsystem bridging (WinSCP works) - Web UI + HTTP API (:3002) for audit + metrics; LDAP-admin gated - Packaged like proxy: ops/install.sh + systemd, all-in-one Docker, compose - Tests: 23 unit + 3 integration (node --test), all green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.0 KiB
JavaScript
103 lines
3.0 KiB
JavaScript
'use strict';
|
|
|
|
// Audit trail of every connection attempt/session. Stored as one redis hash
|
|
// per event plus a sorted-set index (score = timestamp) for paged reads,
|
|
// trimmed to conf.audit.maxEvents. Raw redis (not the Table model) because we
|
|
// want the zset index and cheap range reads.
|
|
|
|
const crypto = require('crypto');
|
|
const conf = require('@simpleworkjs/conf');
|
|
const { getRedis } = require('./index');
|
|
|
|
const P = () => conf.redis.prefix;
|
|
const idxKey = () => `${P()}audit_index`;
|
|
const evtKey = (id) => `${P()}audit_${id}`;
|
|
|
|
// A live event: create() returns a handle you finish() when the session ends.
|
|
async function create(fields) {
|
|
const id = crypto.randomUUID();
|
|
const ts = Date.now();
|
|
const event = {
|
|
id, ts,
|
|
uid: '', authMethod: '', mode: '',
|
|
targetSlug: '', targetAddr: '', targetPort: '',
|
|
channel: '', clientIp: '',
|
|
success: false, failReason: '',
|
|
hostKeyFp: '', startedAt: ts, endedAt: '', durationMs: '',
|
|
bytesIn: 0, bytesOut: 0,
|
|
...fields,
|
|
};
|
|
await write(event);
|
|
return {
|
|
id,
|
|
event,
|
|
async patch(update) {
|
|
Object.assign(event, update);
|
|
await write(event);
|
|
},
|
|
async finish(update = {}) {
|
|
Object.assign(event, update, {
|
|
endedAt: Date.now(),
|
|
durationMs: Date.now() - event.startedAt,
|
|
});
|
|
await write(event);
|
|
},
|
|
};
|
|
}
|
|
|
|
async function write(event) {
|
|
const redis = await getRedis();
|
|
await redis.hSet(evtKey(event.id), serialize(event));
|
|
await redis.zAdd(idxKey(), { score: event.ts, value: event.id });
|
|
// Trim oldest beyond the cap.
|
|
const max = (conf.audit && conf.audit.maxEvents) || 50000;
|
|
const count = await redis.zCard(idxKey());
|
|
if (count > max) {
|
|
const stale = await redis.zRange(idxKey(), 0, count - max - 1);
|
|
if (stale.length) {
|
|
await redis.zRem(idxKey(), stale);
|
|
await redis.del(stale.map(evtKey));
|
|
}
|
|
}
|
|
}
|
|
|
|
function serialize(event) {
|
|
const out = {};
|
|
for (const [k, v] of Object.entries(event)) {
|
|
out[k] = typeof v === 'boolean' ? (v ? '1' : '0') : String(v == null ? '' : v);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function deserialize(h) {
|
|
if (!h || !h.id) return null;
|
|
return {
|
|
...h,
|
|
ts: Number(h.ts),
|
|
success: h.success === '1',
|
|
bytesIn: Number(h.bytesIn || 0),
|
|
bytesOut: Number(h.bytesOut || 0),
|
|
durationMs: h.durationMs === '' ? null : Number(h.durationMs),
|
|
};
|
|
}
|
|
|
|
// Newest-first paged read with optional filters.
|
|
async function list({ page = 0, pageSize = 50, uid, target, status } = {}) {
|
|
const redis = await getRedis();
|
|
const ids = await redis.zRange(idxKey(), 0, -1, { REV: true });
|
|
const events = [];
|
|
for (const id of ids) {
|
|
const e = deserialize(await redis.hGetAll(evtKey(id)));
|
|
if (!e) continue;
|
|
if (uid && e.uid !== uid) continue;
|
|
if (target && e.targetSlug !== target && e.targetAddr !== target) continue;
|
|
if (status === 'success' && !e.success) continue;
|
|
if (status === 'fail' && e.success) continue;
|
|
events.push(e);
|
|
}
|
|
const start = page * pageSize;
|
|
return { total: events.length, page, pageSize, results: events.slice(start, start + pageSize) };
|
|
}
|
|
|
|
module.exports = { create, list };
|