From 1cb693a1ebd6fbe514dc55914147d93b64d94311 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sun, 2 Aug 2026 18:45:16 -0400 Subject: [PATCH] fix: resolve discovery, plugins, and vault issues --- nodejs/models/plugin_instance.js | 1 + nodejs/models/resource.js | 4 ++- nodejs/plugins/discovery/nmap.js | 6 +++++ nodejs/plugins/discovery/proxmox.js | 2 +- nodejs/routes/api_directory_admin.js | 10 ++++++++ nodejs/routes/api_plugins.js | 2 +- nodejs/routes/discovery.js | 37 ++++++++++++++++++++++++++++ nodejs/routes/index.js | 3 ++- nodejs/services/scheduler.js | 12 ++++++--- nodejs/views/discovery.ejs | 10 ++------ nodejs/views/plugins.ejs | 22 +++++++++++------ 11 files changed, 86 insertions(+), 23 deletions(-) diff --git a/nodejs/models/plugin_instance.js b/nodejs/models/plugin_instance.js index f3c0ec8..82032ef 100644 --- a/nodejs/models/plugin_instance.js +++ b/nodejs/models/plugin_instance.js @@ -58,6 +58,7 @@ class PluginInstance extends Model { lastRunAt: { type: 'integer' }, lastStatus: { type: 'string' }, lastError: { type: 'text' }, + lastLog: { type: 'text' }, // Audit stamps (set by the route handler, not by an ORM hook). created_by: { type: 'string' }, created_on: { type: 'integer' }, diff --git a/nodejs/models/resource.js b/nodejs/models/resource.js index 42d1998..92f296e 100644 --- a/nodejs/models/resource.js +++ b/nodejs/models/resource.js @@ -96,11 +96,13 @@ class Resource extends Model { return false; } + let maxUpdated = 0; resObjs.forEach(r => { r.metadata.isProduction = checkProd(r.id); + if (r.updated_on && r.updated_on > maxUpdated) maxUpdated = r.updated_on; }); - return { resources: resObjs, edges }; + return { resources: resObjs, edges, updated_on: maxUpdated || Date.now() }; } // Stamp `resolvedAddress` on each resource: its own address/ip if it has one, diff --git a/nodejs/plugins/discovery/nmap.js b/nodejs/plugins/discovery/nmap.js index d0eba1e..2461bce 100644 --- a/nodejs/plugins/discovery/nmap.js +++ b/nodejs/plugins/discovery/nmap.js @@ -34,7 +34,13 @@ module.exports = { // OsAndPortScan requires root (for -O). NmapScan does a basic port scan (TCP connect if non-root). const scan = new nmap.NmapScan(targetRange); scan.command.push('-Pn'); + scan.command.push('-F'); // fast scan, 100 top ports + scan.command.push('--min-rate', '100'); // speed up the scan + + if (config.log) config.log(`Starting nmap scan: ${scan.command.join(' ')}`); + scan.on('complete', function(data) { + if (config.log) config.log(`Scan complete. Found ${data ? data.length : 0} hosts.`); const resources = []; const edges = []; diff --git a/nodejs/plugins/discovery/proxmox.js b/nodejs/plugins/discovery/proxmox.js index 8261bd0..e670116 100644 --- a/nodejs/plugins/discovery/proxmox.js +++ b/nodejs/plugins/discovery/proxmox.js @@ -35,7 +35,7 @@ module.exports = { }, discover: async (config) => { - const { url, tokenId, tokenSecret } = config; + let { url, tokenId, tokenSecret } = config; if (!url || !tokenId || !tokenSecret) { throw new Error("Missing Proxmox config"); } diff --git a/nodejs/routes/api_directory_admin.js b/nodejs/routes/api_directory_admin.js index 5b0c5de..bbf3c77 100644 --- a/nodejs/routes/api_directory_admin.js +++ b/nodejs/routes/api_directory_admin.js @@ -201,6 +201,16 @@ router.post('/resources/:id/rotate-secret', async (req, res, next) => { } }); +router.post('/resources/:id/service-token', async (req, res, next) => { + try { + const { ServiceToken } = require('../models/token'); + const token = await ServiceToken.issue(req.params.id, req.user.uid); + res.json({ results: { token: token.token } }); + } catch (err) { + next(err); + } +}); + router.delete('/resources/:id', async (req, res, next) => { try { const r = await Resource.get(req.params.id); diff --git a/nodejs/routes/api_plugins.js b/nodejs/routes/api_plugins.js index 23301a2..8b4aabc 100644 --- a/nodejs/routes/api_plugins.js +++ b/nodejs/routes/api_plugins.js @@ -274,7 +274,7 @@ router.get('/:id/runs', async (req, res, next) => { try { const inst = await PluginInstance.get(req.params.id); if (!inst) return res.status(404).json({ error: 'Not found' }); - res.json({ results: { lastRunAt: inst.lastRunAt, lastStatus: inst.lastStatus, lastError: inst.lastError } }); + res.json({ results: { lastRunAt: inst.lastRunAt, lastStatus: inst.lastStatus, lastError: inst.lastError, lastLog: inst.lastLog } }); } catch (err) { next(err); } }); diff --git a/nodejs/routes/discovery.js b/nodejs/routes/discovery.js index 09507c0..2fb1279 100644 --- a/nodejs/routes/discovery.js +++ b/nodejs/routes/discovery.js @@ -62,6 +62,7 @@ router.get('/graph', async (req, res, next) => { res.json(envelope({ resources: projectResources(graph.resources, { fullMetadata }), edges: graph.edges, + updated_on: graph.updated_on })); } catch (err) { next(err); } }); @@ -98,6 +99,42 @@ router.get('/me', async (req, res, next) => { } catch (err) { next(err); } }); +// GET /api/discovery/access/:uid[/:slug] +// Answers per-user access for a machine caller (e.g. jump-host). +router.get('/access/:uid/:slug?', async (req, res, next) => { + try { + const { fullMetadata } = await callerView(req); + if (!req.user || (!req.user.isMachine && !fullMetadata)) { + return res.status(403).json(envelope({ error: 'Only machine identities or admins may query access for other users.' })); + } + const { User } = require('../models/user_ldap'); + const { groupCns } = require('../utils/user_groups'); + + const targetUser = await User.get(req.params.uid).catch(() => null); + if (!targetUser) return res.status(404).json(envelope({ error: 'User not found' })); + + const groups = await groupCns(targetUser); + const ids = new Set(); + if (groups.length) { + const rgs = await ResourceGroup.list({ where: { groupCn: { in: groups } } }); + for (const rg of rgs) ids.add(rg.resourceId); + } + + let all = await Resource.list(); + if (req.params.slug) all = all.filter(r => r.slug === req.params.slug); + + let accessible = all.filter(r => { + const isAuto = r.metadata?.discovery_sources?.length > 0 && !r.metadata.discovery_sources.includes('manual'); + const isManaged = r.metadata?.managed === true; + if (isAuto && !isManaged) return false; + return ids.has(r.id) || (r.metadata && r.metadata.isPublic); + }); + + accessible = await Resource.withResolvedAddress(accessible); + res.json(envelope(projectResources(accessible, { fullMetadata }))); + } catch (err) { next(err); } +}); + // POST /api/discovery/sync // Used by external agents (e.g. ldap-client) to push discovery data. router.post('/sync', async (req, res, next) => { diff --git a/nodejs/routes/index.js b/nodejs/routes/index.js index 212f6f3..f021c89 100755 --- a/nodejs/routes/index.js +++ b/nodejs/routes/index.js @@ -90,7 +90,8 @@ router.get('/plugins', function(req, res, next) { // 'app_sso_directory_admin','admin']) and the /api/plugins endpoints enforce // the same server-side. Same header-vs-navigation auth model as /conf and // /vault (auth-token is a client-set header, not a cookie). - res.render('plugins', {...values}); + const registry = require('../services/plugin_registry'); + res.render('plugins', {...values, pluginTypes: registry.types }); }); router.get('/vault', function(req, res) { diff --git a/nodejs/services/scheduler.js b/nodejs/services/scheduler.js index d183145..0611b49 100644 --- a/nodejs/services/scheduler.js +++ b/nodejs/services/scheduler.js @@ -71,17 +71,23 @@ async function runPluginJob(instanceId) { } console.log(`[Scheduler] Running plugin: ${instance.slug} (${instance.pluginType})`); - await instance.update({ lastRunAt: Date.now(), lastStatus: STATUS.RUNNING, lastError: null }); + await instance.update({ lastRunAt: Date.now(), lastStatus: STATUS.RUNNING, lastError: null, lastLog: null }); + let logs = []; try { const cfg = await pluginSecrets.mergeForRun(instance); + cfg.log = (msg) => { + logs.push(`[${new Date().toISOString()}] ${msg}`); + console.log(`[Plugin ${instance.slug}] ${msg}`); + if (logs.length > 1000) logs.shift(); + }; const payload = await runFn(cfg); if (instance.category === 'discovery') { await DiscoveryReconciler.reconcile(instance.slug, payload); } - await instance.update({ lastStatus: STATUS.OK, lastError: null }); + await instance.update({ lastStatus: STATUS.OK, lastError: null, lastLog: logs.join('\n') }); } catch (err) { console.error(`[Scheduler] Plugin ${instance.slug} failed:`, err.message); - await instance.update({ lastStatus: STATUS.ERROR, lastError: String(err.message || err) }); + await instance.update({ lastStatus: STATUS.ERROR, lastError: String(err.message || err), lastLog: logs.join('\n') }); } } diff --git a/nodejs/views/discovery.ejs b/nodejs/views/discovery.ejs index 2992eea..ba45a20 100644 --- a/nodejs/views/discovery.ejs +++ b/nodejs/views/discovery.ejs @@ -21,11 +21,6 @@
-
@@ -132,10 +127,9 @@ // Name search if(search && !r.name.toLowerCase().includes(search) && !r.slug.toLowerCase().includes(search)) return false; - // Managed filter + // Always hide items that have been committed to the catalog (managed) const isManaged = !!(r.metadata && r.metadata.managed); - if(managedFilter === 'managed' && !isManaged) return false; - if(managedFilter === 'unmanaged' && isManaged) return false; + if(isManaged) return false; const isAuto = r.metadata && r.metadata.discovery_sources && r.metadata.discovery_sources.length > 0 && !r.metadata.discovery_sources.includes('manual'); if(!isAuto) return false; diff --git a/nodejs/views/plugins.ejs b/nodejs/views/plugins.ejs index ab691f3..49bdefb 100644 --- a/nodejs/views/plugins.ejs +++ b/nodejs/views/plugins.ejs @@ -57,7 +57,7 @@ - {{#lastError}}{{/lastError}} + {{#lastRunAt}}{{/lastRunAt}} {{#enabled}}{{/enabled}} {{^enabled}}{{/enabled}} @@ -370,14 +370,20 @@ } } - function showLogs(id) { + async function showLogs(id) { var p = pluginsById[id]; - if (!p || !p.lastError) return; - app.modal.open({ - title: 'Logs — ' + p.name, - bodyHtml: '
' + String(p.lastError).replace(//g, '>') + '
', - footer: { buttonsHtml: '' } - }); + if (!p) return; + try { + const res = await app.api.get('plugins/' + id + '/runs'); + const logText = (res.results && res.results.lastLog) || (res.results && res.results.lastError) || 'No logs available.'; + app.modal.open({ + title: 'Logs — ' + p.name, + bodyHtml: '
' + String(logText).replace(//g, '>') + '
', + footer: { buttonsHtml: '' } + }); + } catch (err) { + app.messages.toast('Failed to load logs: ' + (err.message || err), 'danger'); + } } async function togglePlugin(id, enable) {