UNPKG

@retracedhq/retraced

Version:

The official NodeJS client for interacting with the Retraced audit logging API.

86 lines (85 loc) 3.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyHash = verifyHash; exports.computeHash = computeHash; exports.buildHashTarget = buildHashTarget; const _ = require("lodash"); const crypto = require("crypto"); const requiredFields = ["action"]; const requiredSubfields = [ ["group", "group.id"], ["target", "target.id"], ["actor", "actor.id"], ]; function verifyHash(event, newEvent) { const { hashResult, hashTarget } = computeHash(event, newEvent.id); if (hashResult !== newEvent.hash) { throw new Error(`hash mismatch, local=${hashResult}, remote=${newEvent.hash}, target=${hashTarget}`); } return hashResult; } function computeHash(event, id) { if (!id) { throw new Error("Canonicalization failed: missing required event attribute 'id'"); } for (const fieldName of requiredFields) { if (_.isEmpty(_.get(event, fieldName))) { throw new Error(`Canonicalization failed: missing required event attribute '${fieldName}'`); } } for (const [fieldName, requiredSubfield] of requiredSubfields) { const hasField = !_.isEmpty(_.get(event, fieldName)); const missingSubfield = hasField && _.isEmpty(_.get(event, requiredSubfield)); if (missingSubfield) { throw new Error(`Canonicalization failed: missing attribute '${requiredSubfield}' which is required when '${fieldName}' is present.`); } } const hashTarget = buildHashTarget(event, id); const hasher = crypto.createHash("sha256"); hasher.update(hashTarget); const hashResult = hasher.digest("hex"); return { hashResult, hashTarget }; } function buildHashTarget(event, id) { let canonicalString = ""; canonicalString += `${encodePassOne(id)}:`; canonicalString += `${encodePassOne(event.action)}:`; canonicalString += _.isEmpty(event.target) ? ":" : `${encodePassOne(event.target.id)}:`; canonicalString += _.isEmpty(event.actor) ? ":" : `${encodePassOne(event.actor.id)}:`; canonicalString += _.isEmpty(event.group) ? ":" : `${encodePassOne(event.group.id)}:`; canonicalString += _.isEmpty(event.source_ip) ? ":" : `${encodePassOne(event.source_ip)}:`; canonicalString += event.is_failure ? "1:" : "0:"; canonicalString += event.is_anonymous ? "1:" : "0:"; if (!event.fields) { canonicalString += ":"; } else { const sortedKeys = _.keys(event.fields).sort(); for (const key of sortedKeys) { const value = event.fields[key]; const encodedKey = encodePassTwo(encodePassOne(key)); const encodedValue = encodePassTwo(encodePassOne(value)); canonicalString += `${encodedKey}=${encodedValue};`; } } if (event.external_id) { canonicalString += `:${encodePassOne(event.external_id)}`; } if (event.metadata) { canonicalString += ":"; const sortedKeys = _.keys(event.metadata).sort(); for (const key of sortedKeys) { const value = event.metadata[key]; const encodedKey = encodePassTwo(encodePassOne(key)); const encodedValue = encodePassTwo(encodePassOne(value)); canonicalString += `${encodedKey}=${encodedValue};`; } } return canonicalString; } function encodePassOne(valueIn) { return valueIn ? (valueIn.replace ? valueIn.replace(/%/g, "%25").replace(/:/g, "%3A") : valueIn) : valueIn; } function encodePassTwo(valueIn) { return valueIn ? (valueIn.replace ? valueIn.replace(/=/g, "%3D").replace(/;/g, "%3B") : valueIn) : valueIn; }