@retracedhq/retraced
Version:
The official NodeJS client for interacting with the Retraced audit logging API.
86 lines (85 loc) • 3.62 kB
JavaScript
;
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;
}