Initial project documentation and status

This commit is contained in:
Clawdia
2026-08-30 14:46:23 -04:00
commit 765f372c72
50 changed files with 12916 additions and 0 deletions
+708
View File
@@ -0,0 +1,708 @@
/**
* Adapter pattern for MeshCentral integration.
*
* Defines the boundary where a real MeshCentral client can be swapped in.
*/
import {
MeshCentralAdapter,
Device,
DeviceGroup,
DiagnosticResult,
ListDevicesRequest,
RunDiagnosticRequest,
BridgeError,
ErrorCodes,
defaultConfig,
BridgeConfig,
DeviceStatus,
validateCredentials,
} from './contracts';
import { createMockAdapter } from './mock-adapter';
import {
MeshCtrlClient,
} from './meshctrl-client';
import {
createAuditEntry,
AuditAction,
AuditStatus,
} from './audit';
import { Logger, getGlobalLogger } from './logger';
/**
* Real MeshCentral adapter using the official meshctrl.js protocol.
*
* Connects to MeshCentral's control.ashx WebSocket and uses the
* same protocol as meshctrl.js for inventory and diagnostics.
*/
export class MeshCentralRealAdapter implements MeshCentralAdapter {
readonly name = 'meshcentral-real';
readonly version = '0.1.0';
private config: BridgeConfig;
private client: MeshCtrlClient | null = null;
private credentials: { username: string; password: string; totpToken?: string } | null = null;
private connected = false;
private logger: Logger;
constructor(config?: Partial<BridgeConfig>) {
this.config = { ...defaultConfig, ...(config ?? {}) };
// Use a dedicated logger for this adapter instance
// In production, you could inject a custom logger via config
this.logger = getGlobalLogger();
// Set the logger name to be more descriptive
this.logger.setName('meshcentral-adapter');
// Validate configuration and credentials
this._validateConfig();
}
/**
* Set credentials for authentication.
* Must be called before connecting to the real adapter.
*/
setCredentials(credentials: { username: string; password: string; totpToken?: string }): void {
// Log audit entry for credential update attempt
this._auditLog(AuditAction.CREDENTIAL_UPDATE, AuditStatus.PENDING, {
actor: this._getActorName(),
resource: 'adapter-credentials',
});
// Validate credentials before storing
validateCredentials(credentials);
this.credentials = {
username: credentials.username,
password: credentials.password,
totpToken: credentials.totpToken,
};
// Log successful credential update (redacted)
this._auditLog(AuditAction.CREDENTIAL_UPDATE, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: 'adapter-credentials',
detailsUpdated: true,
});
}
/**
* Generate a redacted credentials summary for logging.
*/
private _getCredentialsSummary(): { hasCredentials: boolean; hasTotp: boolean } {
if (!this.credentials) {
return { hasCredentials: false, hasTotp: false };
}
return {
hasCredentials: !!this.credentials.username && !!this.credentials.password,
hasTotp: !!this.credentials.totpToken,
};
}
/**
* Get a safe actor name for audit logs (no secrets).
*/
private _getActorName(): string {
if (!this.credentials?.username) {
return 'unknown';
}
// Use username without exposing password or totp
return this.credentials.username;
}
/**
* Log an audit entry.
*/
private _auditLog(
action: AuditAction,
status: AuditStatus,
extraDetails?: Record<string, unknown>,
): void {
const details = {
adapter: this.name,
version: this.version,
configUrl: this.config.meshCentralUrl,
...extraDetails,
};
// Create and format audit entry (secrets already redacted by sanitizeForLogging)
const entry = createAuditEntry({
action,
status,
actor: typeof extraDetails?.actor === 'string' ? extraDetails.actor : this._getActorName(),
resource: typeof extraDetails?.resource === 'string'
? extraDetails.resource
: 'meshcentral-adapter',
details: Object.fromEntries(
Object.entries(details).filter(([key]) => key !== 'actor' && key !== 'resource'),
),
});
// Output audit log via structured logger
this.logger.audit(`AUDIT - ${entry.action} - ${entry.status}`, {
action: entry.action,
status: entry.status,
actor: typeof extraDetails?.actor === 'string' ? extraDetails.actor : this._getActorName(),
resource: typeof extraDetails?.resource === 'string'
? extraDetails.resource
: 'meshcentral-adapter',
...Object.fromEntries(
Object.entries(details).filter(([key]) => key !== 'actor' && key !== 'resource'),
),
});
if (process.env.AUDIT_DEBUG === 'true') {
this.logger.debug(JSON.stringify(entry, null, 2));
}
}
private _validateConfig(): void {
// Log config load attempt
this._auditLog(AuditAction.CONFIG_LOAD, AuditStatus.PENDING, {
adapter: this.name,
resource: 'adapter-configuration',
});
if (!this.config.meshCentralUrl) {
this._auditLog(AuditAction.CONFIG_LOAD, AuditStatus.FAILURE, {
adapter: this.name,
resource: 'adapter-configuration',
error: 'meshCentralUrl is required',
});
throw new BridgeError(
ErrorCodes.ConfigurationInvalid,
'Configuration is incomplete: meshCentralUrl is required',
);
}
// Check for credentials in environment/config
const hasUsername = !!this.config.username;
const hasPassword = !!this.config.password;
// Generic error message that doesn't reveal which credential is missing
if (hasUsername !== hasPassword) {
throw new BridgeError(
ErrorCodes.ConfigurationInvalid,
'Configuration is incomplete: authentication credentials must be provided together',
);
}
if (this.config.adapterType === 'real' && (!hasUsername || !hasPassword)) {
throw new BridgeError(
ErrorCodes.ConfigurationInvalid,
'Real adapter requires authentication credentials',
);
}
// Validate credential formats if present
if (hasUsername && hasPassword) {
try {
validateCredentials({
username: this.config.username,
password: this.config.password,
totpToken: this.config.totpToken,
});
} catch {
// Re-throw as generic error to prevent credential enumeration
throw new BridgeError(
ErrorCodes.ConfigurationInvalid,
'Authentication credentials are invalid',
);
}
}
// Validate allowlist formats if provided
this._validateAllowlist(this.config.pveAllowlist, 'pveAllowlist');
this._validateAllowlist(this.config.deviceAllowlist, 'deviceAllowlist');
this._validateAllowlist(this.config.groupAllowlist, 'groupAllowlist');
}
/**
* Validate allowlist entries.
*/
private _validateAllowlist(allowlist: string[] | undefined, name: string): void {
if (!allowlist) {
return;
}
for (const entry of allowlist) {
if (!entry || typeof entry !== 'string' || entry.trim() === '') {
throw new BridgeError(
ErrorCodes.ConfigurationInvalid,
`Allowlist entry in ${name} is empty or invalid`,
);
}
}
}
/**
* Check if a device is in the allowlist.
*/
private _isDeviceAllowed(deviceId: string): boolean {
const { deviceAllowlist, groupAllowlist } = this.config;
// If no allowlists configured, all devices are allowed
if (!deviceAllowlist && !groupAllowlist) {
return true;
}
// Check device allowlist first (specific devices)
if (deviceAllowlist && deviceAllowlist.includes(deviceId)) {
return true;
}
// If group allowlist is configured, device must be in an allowed group
if (groupAllowlist && groupAllowlist.length > 0) {
// We'll check this after we have device info
return true; // Will be checked in getDevice
}
return false;
}
private _isGroupAllowed(groupId: string): boolean {
return !this.config.groupAllowlist || this.config.groupAllowlist.length === 0
|| this.config.groupAllowlist.includes(groupId);
}
/**
* Connect to MeshCentral if not already connected.
*/
private async _ensureConnected(): Promise<void> {
if (this.connected) {
return;
}
const credSummary = this._getCredentialsSummary();
// Log connection attempt (redacted)
this._auditLog(AuditAction.CONNECTION_ATTEMPT, AuditStatus.PENDING, {
actor: this._getActorName(),
resource: `meshcentral://${this.config.meshCentralUrl}`,
hasCredentials: credSummary.hasCredentials,
hasTotp: credSummary.hasTotp,
});
if (!this.credentials) {
this._auditLog(AuditAction.CONNECTION_ATTEMPT, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: `meshcentral://${this.config.meshCentralUrl}`,
error: 'Adapter is not configured for authentication',
});
throw new BridgeError(
ErrorCodes.ConfigurationInvalid,
'Adapter is not configured for authentication',
);
}
this.client = new MeshCtrlClient({
url: this.config.meshCentralUrl,
timeoutMs: this.config.timeoutMs,
});
try {
await this.client.connect(this.credentials);
this.connected = true;
// Log successful connection
this._auditLog(AuditAction.CONNECTION_ESTABLISHED, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: `meshcentral://${this.config.meshCentralUrl}`,
});
} catch (error) {
this._auditLog(AuditAction.CONNECTION_ATTEMPT, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: `meshcentral://${this.config.meshCentralUrl}`,
error: error instanceof Error ? error.message : 'unknown',
});
throw error;
}
}
/**
* Disconnect from MeshCentral.
*/
private async _disconnect(): Promise<void> {
if (this.connected && this.client) {
try {
await this.client.disconnect();
this._auditLog(AuditAction.CONNECTION_CLOSED, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: `meshcentral://${this.config.meshCentralUrl}`,
});
} finally {
this.connected = false;
this.client = null;
}
} else if (this.connected) {
this._auditLog(AuditAction.CONNECTION_CLOSED, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: `meshcentral://${this.config.meshCentralUrl}`,
error: 'Client was null when attempting disconnect',
});
}
}
/**
* Convert MeshCtrl node response to Device type.
*/
private _mapNodeToDevice(node: { id: string; name: string; online: boolean; device?: { os?: string; uptime?: number; lastseen?: number; ip?: string[]; agentver?: string } }, group: { id: string; name: string }): Device {
const status = node.online ? DeviceStatus.Online : DeviceStatus.Offline;
return {
id: node.id,
name: node.name,
groupId: group.id,
groupName: group.name,
status,
os: node.device?.os ?? undefined,
uptimeSeconds: node.device?.uptime ?? undefined,
lastSeen: node.device?.lastseen
? new Date(node.device.lastseen * 1000).toISOString()
: undefined,
ipAddresses: node.device?.ip ?? undefined,
agentVersion: node.device?.agentver ?? undefined,
};
}
/**
* Convert MeshCtrl meshes response to DeviceGroup type.
*/
private _mapMeshToGroup(mesh: { id: string; name: string; description?: string }): DeviceGroup {
return {
id: mesh.id,
name: mesh.name,
description: mesh.description ?? undefined,
deviceCount: 0, // Will be populated on demand
};
}
/**
* Filter an array by allowlist.
*/
private _filterByAllowlist<T>(items: T[], getId: (item: T) => string, allowlist?: string[]): T[] {
if (!allowlist || allowlist.length === 0) {
return items;
}
const allowedIds = new Set(allowlist);
return items.filter((item) => allowedIds.has(getId(item)));
}
async listDevices(request?: ListDevicesRequest): Promise<Device[]> {
await this._ensureConnected();
// Log device listing attempt
this._auditLog(AuditAction.DEVICE_LISTED, AuditStatus.PENDING, {
actor: this._getActorName(),
resource: 'all-devices',
groupId: request?.groupId,
});
// Get server version first to verify connection
try {
const versionResponse = await this.client!.sendRequest({
action: 'serverversion',
});
if (versionResponse.err) {
this._auditLog(AuditAction.MESHCENTRAL_REQUEST, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: 'serverversion',
error: versionResponse.err,
});
throw new BridgeError(
ErrorCodes.MeshCentralError,
'Failed to establish connection with MeshCentral server',
);
} else {
this._auditLog(AuditAction.MESHCENTRAL_RESPONSE, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: 'serverversion',
});
}
} catch (error) {
await this._disconnect();
throw error;
}
// Get meshes (device groups)
const meshesResponse = await this.client!.sendRequest({
action: 'meshes',
});
if (meshesResponse.err) {
throw new BridgeError(
ErrorCodes.MeshCentralError,
'Failed to retrieve device groups from MeshCentral server',
);
}
const meshes = (meshesResponse.meshes ?? []) as Array<{ id: string; name: string }>;
// If group filter provided, find matching mesh
if (request?.groupId) {
const targetMesh = meshes.find((m) => m.id === request.groupId);
if (!targetMesh) {
throw new BridgeError(
ErrorCodes.ResourceNotFound,
'Requested device group could not be found',
);
}
if (!this._isGroupAllowed(targetMesh.id)) {
throw new BridgeError(
ErrorCodes.ResourceNotFound,
'Requested device group could not be found',
);
}
return this._getDevicesForMesh(targetMesh);
}
// Get all devices from all meshes
const allDevices: Device[] = [];
for (const mesh of meshes) {
const devices = await this._getDevicesForMesh(mesh);
allDevices.push(...devices);
}
return allDevices;
}
// Get devices for a specific mesh/group.
private async _getDevicesForMesh(mesh: { id: string; name: string }): Promise<Device[]> {
const nodesResponse = await this.client!.sendRequest({
action: 'nodes',
meshid: mesh.id,
});
if (nodesResponse.err) {
throw new BridgeError(
ErrorCodes.MeshCentralError,
`Failed to retrieve devices for group: ${mesh.name}`,
);
}
const nodes = (nodesResponse.nodes ?? []) as Array<{ id: string; name: string; online: boolean; meshid?: string; device?: { os?: string; uptime?: number; lastseen?: number; ip?: string[]; agentver?: string } }>;
if (!this._isGroupAllowed(mesh.id)) {
return [];
}
return nodes
.map((node) => this._mapNodeToDevice(node, mesh))
.filter((device) => this._isDeviceAllowed(device.id));
}
async getDevice(deviceId: string): Promise<Device> {
await this._ensureConnected();
// Log device access attempt
this._auditLog(AuditAction.DEVICE_ACCESS, AuditStatus.PENDING, {
actor: this._getActorName(),
resource: deviceId,
});
// First get all devices to validate allowlist and find the device
const allDevices = await this.listDevices();
const device = allDevices.find((d) => d.id === deviceId);
if (!device) {
this._auditLog(AuditAction.DEVICE_ACCESS, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: deviceId,
error: 'Device could not be found',
});
throw new BridgeError(
ErrorCodes.ResourceNotFound,
'Device could not be found',
);
}
// Check allowlist again after getting device info
if (!this._isDeviceAllowed(deviceId)) {
this._auditLog(AuditAction.ACCESS_DENIED, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: deviceId,
error: 'Device is not authorized for access',
});
throw new BridgeError(
ErrorCodes.AdapterNotFound,
'Device is not authorized for access',
);
}
this._auditLog(AuditAction.DEVICE_ACCESS, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: deviceId,
});
return device;
}
async listGroups(): Promise<DeviceGroup[]> {
await this._ensureConnected();
const meshesResponse = await this.client!.sendRequest({
action: 'meshes',
});
if (meshesResponse.err) {
throw new BridgeError(
ErrorCodes.MeshCentralError,
'Failed to retrieve device groups from MeshCentral server',
);
}
const meshes = (meshesResponse.meshes ?? []) as Array<{ id: string; name: string; description?: string }>;
// Filter by group allowlist if configured
const allowedMeshes = this._filterByAllowlist(
meshes,
(m) => m.id,
this.config.groupAllowlist,
);
return allowedMeshes.map((mesh) => this._mapMeshToGroup(mesh));
}
async getGroup(groupId: string): Promise<DeviceGroup> {
const groups = await this.listGroups();
const group = groups.find((g) => g.id === groupId);
if (!group) {
throw new BridgeError(
ErrorCodes.ResourceNotFound,
'Device group could not be found',
);
}
return group;
}
async runDiagnostic(request: RunDiagnosticRequest): Promise<DiagnosticResult> {
const { deviceId, checkType } = request;
// Log diagnostic attempt
this._auditLog(AuditAction.MESHCENTRAL_REQUEST, AuditStatus.PENDING, {
actor: this._getActorName(),
resource: deviceId,
checkType,
});
// Validate device is in allowlist
if (!this._isDeviceAllowed(deviceId)) {
this._auditLog(AuditAction.ACCESS_DENIED, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: deviceId,
checkType,
error: 'Device is not authorized for access',
});
throw new BridgeError(
ErrorCodes.AdapterNotFound,
'Device is not authorized for access',
);
}
// Get device to verify it exists and get group info
await this.getDevice(deviceId);
// For now, return a placeholder diagnostic result
// Real implementation would execute commands via MeshCtrl's deviceinfo or events API
const result = {
deviceId,
checkType,
timestamp: new Date().toISOString(),
success: true,
output: '[Real adapter diagnostic not yet implemented - device is online]',
exitCode: 0,
outputLength: 49,
maxOutputLength: request.maxOutputLength ?? this.config.maxOutputLength,
truncated: false,
};
this._auditLog(AuditAction.MESHCENTRAL_RESPONSE, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: deviceId,
checkType,
});
return result;
}
async healthCheck(): Promise<boolean> {
// Log health check attempt
this._auditLog(AuditAction.SERVICE_START, AuditStatus.PENDING, {
actor: this._getActorName(),
resource: 'health-check',
});
if (this.config.adapterType !== 'real') {
this._auditLog(AuditAction.SERVICE_START, AuditStatus.SUCCESS, {
actor: this._getActorName(),
resource: 'health-check',
adapterType: this.config.adapterType,
});
return true; // Mock adapter is always healthy
}
try {
await this._ensureConnected();
const versionResponse = await this.client!.sendRequest({
action: 'serverversion',
});
const healthy = !versionResponse.err;
this._auditLog(AuditAction.SERVICE_START, healthy ? AuditStatus.SUCCESS : AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: 'health-check',
healthy,
});
return healthy;
} catch {
this._auditLog(AuditAction.SERVICE_START, AuditStatus.FAILURE, {
actor: this._getActorName(),
resource: 'health-check',
healthy: false,
});
await this._disconnect();
return false;
}
}
/**
* Cleanup on destroy.
*/
async destroy(): Promise<void> {
await this._disconnect();
}
}
/**
* Factory function to create the appropriate adapter based on configuration.
*/
export function createMeshCentralAdapter(config: Partial<BridgeConfig> = {}): MeshCentralAdapter {
const adapterType = config.adapterType ?? 'mock';
switch (adapterType) {
case 'real':
return new MeshCentralRealAdapter(config);
case 'mock':
default:
return createMockAdapter(config);
}
}