From b60ede178a261bc99daf9b4a562d8ec6548d6386 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 5 Feb 2024 10:46:53 +0000 Subject: [PATCH 1/6] Add device audit logging route --- forge/auditLog/device.js | 3 ++ forge/routes/logging/index.js | 96 ++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/forge/auditLog/device.js b/forge/auditLog/device.js index b3978b0a1e..455633a403 100644 --- a/forge/auditLog/device.js +++ b/forge/auditLog/device.js @@ -22,6 +22,9 @@ module.exports = { } await log('device.unassigned', actionedBy, device.id, generateBody(bodyData)) }, + async startFailed (actionedBy, error, device) { + await log('device.start-failed', actionedBy || 0, device?.id, generateBody({ error })) + }, credentials: { async generated (actionedBy, error, device) { await log('device.credential.generated', actionedBy, device?.id, generateBody({ error, device })) diff --git a/forge/routes/logging/index.js b/forge/routes/logging/index.js index 50ebf23222..0b7ec4791f 100644 --- a/forge/routes/logging/index.js +++ b/forge/routes/logging/index.js @@ -1,4 +1,5 @@ -const { getLoggers } = require('../../auditLog/project') +const { getLoggers: getDeviceLogger } = require('../../auditLog/device') +const { getLoggers: getProjectLogger } = require('../../auditLog/project') /** Node-RED Audit Logging backend * @@ -9,23 +10,36 @@ const { getLoggers } = require('../../auditLog/project') */ module.exports = async function (app) { - const logger = getLoggers(app) + const deviceAuditLogger = getDeviceLogger(app) + const projectAuditLogger = getProjectLogger(app) + /** @type {import('../../db/controllers/AuditLog')} */ + const auditLogController = app.db.controllers.AuditLog + app.addHook('preHandler', app.verifySession) - app.addHook('preHandler', async (request, response) => { - // The request has a valid token, but need to check the token is allowed - // to access the project - const id = request.params.projectId - // Check if the project exists first - const project = await app.db.models.Project.byId(id) - if (project && request.session.ownerType === 'project' && request.session.ownerId === id) { - // Project exists and the auth token is for this project - request.project = project - return + /** + * Post route for node-red _cloud_ instance audit log events + * @method POST + * @name /logging/:projectId/audit + * @memberof forge.routes.logging + */ + app.post('/:projectId/audit', { + preHandler: async (request, response) => { + // The request has a valid token, but need to check the token is allowed + // to access the project + + const id = request.params.projectId + // Check if the project exists first + const project = await app.db.models.Project.byId(id) + if (project && request.session.ownerType === 'project' && request.session.ownerId === id) { + // Project exists and the auth token is for this project + request.project = project + return + } + response.status(404).send({ code: 'not_found', error: 'Not Found' }) } - response.status(404).send({ code: 'not_found', error: 'Not Found' }) - }) - app.post('/:projectId/audit', async (request, response) => { + }, + async (request, response) => { const projectId = request.params.projectId const auditEvent = request.body const event = auditEvent.event @@ -34,7 +48,7 @@ module.exports = async function (app) { // first check to see if the event is a known structured event if (event === 'start-failed') { - await logger.project.startFailed(userId || 'system', error, { id: projectId }) + await projectAuditLogger.project.startFailed(userId || 'system', error, { id: projectId }) } else { // otherwise, just log it delete auditEvent.event @@ -42,7 +56,7 @@ module.exports = async function (app) { delete auditEvent.path delete auditEvent.timestamp - await app.db.controllers.AuditLog.projectLog( + await auditLogController.projectLog( projectId, userId, event, @@ -63,4 +77,52 @@ module.exports = async function (app) { response.status(200).send() }) + + /** + * Post route for node_red device audit log events + * @method POST + * @name /logging/device/:deviceId/audit + * @memberof forge.routes.logging + */ + app.post('/device/:deviceId/audit', { + preHandler: async (request, response) => { + // The request has a valid token, but need to check the token is allowed + // to access the device + const id = request.params.deviceId + // Check if the device exists first + const device = await app.db.models.Device.byId(id) + if (device && request.session.ownerType === 'device' && +request.session.ownerId === device.id) { + // device exists and the auth token is for this device + request.device = device + return + } + response.status(404).send({ code: 'not_found', error: 'Not Found' }) + } + }, async (request, response) => { + const deviceId = request.params.deviceId + const auditEvent = request.body + const event = auditEvent.event + const error = auditEvent.error + const userId = auditEvent.user ? app.db.models.User.decodeHashid(auditEvent.user) : undefined + + // first check to see if the event is a known structured event + if (event === 'start-failed') { + await deviceAuditLogger.device.startFailed(userId || 'system', error, { id: deviceId }) + } else { + // otherwise, just log it + delete auditEvent.event + delete auditEvent.user + delete auditEvent.path + delete auditEvent.timestamp + + await auditLogController.deviceLog( + request.device.id, + userId, + event, + auditEvent + ) + } + + response.status(200).send() + }) } From a6aa0730f966f906818d3e9a71dbcdc49f24a92e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 5 Feb 2024 12:18:19 +0000 Subject: [PATCH 2/6] handle NR audit events sent by a device --- frontend/src/data/audit-events.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/data/audit-events.json b/frontend/src/data/audit-events.json index 347cfd2d39..da4de09c20 100644 --- a/frontend/src/data/audit-events.json +++ b/frontend/src/data/audit-events.json @@ -138,9 +138,13 @@ "device.developer-mode.disabled": "Developer Mode Disabled", "device.remote-access.enabled": "Remote Access Enabled", "device.remote-access.disabled": "Remote Access Disabled", - "device.developer-mode.enabled": "Developer Mode Enabled", - "device.developer-mode.disabled": "Developer Mode Disabled", - "device.remote-access.enabled": "Remote Access Enabled", - "device.remote-access.disabled": "Remote Access Disabled" + "device.start-failed": "Device Start Failed", + "safe-mode": "Node-RED has been placed in Safe Mode", + "settings.update": "Node-RED Settings Updated", + "flows.set": "Flow Deployed", + "library.set": "Saved to Library", + "nodes.install": "Third-Party Nodes Installed", + "nodes.remove": "Third-Party Nodes Removed", + "context.delete": "Context Key Deleted" } } \ No newline at end of file From 3dbfdeb268e426596c6c5f5f7be360478d74e6f9 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 5 Feb 2024 12:19:42 +0000 Subject: [PATCH 3/6] clarification - separate deploy from reload --- forge/auditLog/formatters.js | 13 +++++++++++++ .../src/components/audit-log/AuditEntryVerbose.vue | 13 +++++++++++-- frontend/src/data/audit-events.json | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/forge/auditLog/formatters.js b/forge/auditLog/formatters.js index ae0ec9475a..3c9e48b2da 100644 --- a/forge/auditLog/formatters.js +++ b/forge/auditLog/formatters.js @@ -171,6 +171,19 @@ const formatLogEntry = (auditLogDbRow) => { formatted.body.context = { key: body.key, scope: body.scope, store: body.store } } + // format log entries for know Node-RED audit events + if (formatted.event === 'flows.set') { + formatted.body = formatted.body || {} + formatted.body.flowsSet = formatted.body?.flowsSet || { type: body.type } + } + // TODO: Add other known Node-RED audit events + // including: 'nodes.install', 'nodes.remove', 'library.set' + // this will permit audit viewer to access the details of the event + // via the body.xxx object and thus permit the UI to display the details + // instead of the current generic message + // e.g. to show _which_ module was installed or removed + // e.g. to show _which_ library was set + const roleObj = body?.role && roleObject(body.role) if (roleObj) { if (formatted.body?.user) { diff --git a/frontend/src/components/audit-log/AuditEntryVerbose.vue b/frontend/src/components/audit-log/AuditEntryVerbose.vue index 33ef1edc2c..0bfd8e7816 100644 --- a/frontend/src/components/audit-log/AuditEntryVerbose.vue +++ b/frontend/src/components/audit-log/AuditEntryVerbose.vue @@ -522,8 +522,17 @@ Node-RED editor user settings have been updated.