UNPKG

cumulocity-cypress

Version:
276 lines (275 loc) 11.4 kB
import _ from "lodash"; import * as setCookieParser from "set-cookie-parser"; import * as libCookie from "cookie"; import { toSensitiveObjectKeyPath } from "../util"; export const C8yPactPreprocessorDefaultOptions = { ignore: [ "request.headers.accept-encoding", "response.headers.cache-control", "response.headers.content-length", "response.headers.content-encoding", "response.headers.transfer-encoding", "response.headers.keep-alive", ], obfuscate: [ "request.headers.cookie.authorization", "request.headers.cookie.XSRF-TOKEN", "request.headers.authorization", "request.headers.X-XSRF-TOKEN", "response.headers.set-cookie.authorization", "response.headers.set-cookie.XSRF-TOKEN", "response.body.password", "response.body.users.password", ], obfuscationPattern: "****", ignoreCase: true, }; /** * Default implementation of C8yPactPreprocessor. Preprocessor for C8yPact objects * that can be used to obfuscate or remove sensitive data from the pact objects. * Use C8ypactPreprocessorOptions to configure the preprocessor. * * Removes cookies and set-cookie headers by appending the key to the `cookie` or `set-cookie` * key as for example `headers.cookie.authorization` or `headers.set-cookie.authorization`. */ export class C8yDefaultPactPreprocessor { constructor(options) { this.reservedKeys = ["id", "pact", "info", "records"]; this.options = this.resolveOptions(options); } apply(obj, options) { if (!obj || !_.isObjectLike(obj)) return; const objs = "records" in obj ? _.get(obj, "records") : [obj]; if (!_.isArray(objs)) return; const o = this.resolveOptions(options); const ignoreCase = o.ignoreCase; const obfuscationPattern = o.obfuscationPattern; const mapSensitiveKeys = (mapObject, keys) => keys.map((k) => ignoreCase === true ? toSensitiveObjectKeyPath(mapObject, k) ?? k : k); objs.forEach((obj) => { if (o?.pick != null) { const keepPaths = []; if (_.isPlainObject(o.pick)) { Object.entries(o.pick ?? {}).forEach(([parentKey, childKeys]) => { if (_.isEmpty(childKeys)) keepPaths.push(parentKey); childKeys.forEach((childKey) => { keepPaths.push(`${parentKey}.${childKey}`); }); }); this.filterObjectByKeepPaths(obj, keepPaths, ignoreCase); } else if (_.isArray(o.pick)) { this.applyKeepArray(obj, o.pick); } } const keysToObfuscate = mapSensitiveKeys(obj, o.obfuscate ?? []); const keysToRemove = mapSensitiveKeys(obj, o.ignore ?? []); this.handleObfuscation(obj, keysToObfuscate, obfuscationPattern); this.handleRemoval(obj, keysToRemove); }); } filterObjectByKeepPaths(obj, keepPaths, ignoreCase = false) { const prepKey = (key) => key != null && ignoreCase === true ? key.toLowerCase() : key; const shouldKeep = (keyPath) => { return keepPaths .map((k) => prepKey(k)) .some((keepPath) => prepKey(keyPath) === keepPath || keepPath?.startsWith(`${prepKey(keyPath)}.`)); }; const recursiveFilter = (currentObj, currentPath) => { if (!_.isObject(currentObj)) return; Object.keys(currentObj).forEach((key) => { const fullPath = currentPath ? `${currentPath}.${key}` : key; if (!shouldKeep(fullPath)) { _.unset(obj, fullPath); } else if (!keepPaths.includes(fullPath)) { recursiveFilter(_.get(currentObj, key), fullPath); } }); }; recursiveFilter(obj, ""); } applyKeepArray(obj, keep) { if (keep == null || _.isEmpty(keep)) return; if (_.isObjectLike(obj)) { const keysToRemove = Object.keys(obj).filter((childKey) => !keep.includes(childKey.toLowerCase())); keysToRemove.forEach((childKey) => { _.unset(obj, childKey); }); } } handleObfuscation(obj, keysToObfuscate, obfuscationPattern) { const validKeys = this.filterValidKeys(obj, keysToObfuscate); validKeys.forEach((key) => { this.obfuscateKey(obj, key, obfuscationPattern); }); } handleRemoval(obj, keysToRemove) { const validKeys = this.filterValidKeys(obj, keysToRemove); validKeys.forEach((key) => { this.removeKey(obj, key); }); } removeKey(obj, key) { const keyPath = key.split("."); if (this.hasKey(keyPath, "set-cookie")) { this.removeSetCookie(obj, keyPath); } else if (this.hasKey(keyPath, "cookie")) { this.removeCookie(obj, keyPath); } else { const processKeyPath = (currentObj, remainingKeyParts) => { if (!currentObj || remainingKeyParts.length === 0) return; const [_currentKey, ...restKeys] = remainingKeyParts; const currentKey = toSensitiveObjectKeyPath(currentObj, _currentKey) ?? _currentKey; const target = _.get(currentObj, currentKey); if (_.isArray(target)) { // If the current key points to an array, process each element target.forEach((item) => processKeyPath(item, restKeys)); } else if (restKeys.length === 0) { _.unset(currentObj, currentKey); } else { processKeyPath(target, restKeys); } }; processKeyPath(obj, keyPath); } } removeSetCookie(obj, keyParts) { const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts); if (!cookieHeader) return; if (!name) { _.unset(obj, keyPath); return; } const cookies = setCookieParser.parse(cookieHeader, { decodeValues: false }) ?? []; if (cookies.length) { const filteredCookies = cookies .filter((cookie) => cookie.name.toLowerCase() !== name.toLowerCase()) .map((cookie) => libCookie.serialize(cookie.name, cookie.value, cookie)); if (filteredCookies.length === 0) { _.unset(obj, keyPath); } else { _.set(obj, keyPath, filteredCookies); } } } removeCookie(obj, keyParts) { const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts); if (!cookieHeader) return; if (!name) { _.unset(obj, keyPath); return; } const cookies = libCookie.parse(cookieHeader); delete cookies[name]; const remainingCookies = Object.entries(cookies); if (remainingCookies.length === 0) { _.unset(obj, keyPath); } else { const v = remainingCookies .map(([name, value]) => `${name}=${value}`) .join("; "); _.set(obj, keyPath, v); } } filterValidKeys(obj, keys) { return _.without(keys, ...this.reservedKeys); } obfuscateKey(obj, key, pattern) { const keyParts = key.split("."); const p = pattern ?? C8yDefaultPactPreprocessor.defaultObfuscationPattern; if (this.hasKey(keyParts, "set-cookie")) { this.obfuscateSetCookie(obj, keyParts, p); } else if (this.hasKey(keyParts, "cookie")) { this.obfuscateCookie(obj, keyParts, p); } else { const processKeyPath = (currentObj, remainingKeyParts) => { if (!currentObj || remainingKeyParts.length === 0) return; const [_currentKey, ...restKeys] = remainingKeyParts; const currentKey = toSensitiveObjectKeyPath(currentObj, _currentKey) ?? _currentKey; const target = _.get(currentObj, currentKey); if (_.isArray(target)) { // If the current key points to an array, process each element target.forEach((item) => processKeyPath(item, restKeys)); } else if (restKeys.length === 0) { if (_.get(currentObj, currentKey) != null) { _.set(currentObj, currentKey, p); } } else { processKeyPath(target, restKeys); } }; processKeyPath(obj, keyParts); } } obfuscateSetCookie(obj, keyParts, obfuscationPattern) { const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts); if (!cookieHeader) return; const cookies = setCookieParser.parse(cookieHeader, { decodeValues: false }) ?? []; if (cookies.length) { const fixedCookies = cookies.reduce((acc, cookie) => { const n = name?.toLowerCase(); const shouldObfuscate = !n || (n && n === cookie.name?.toLowerCase()); const cookieValue = shouldObfuscate ? obfuscationPattern ?? "" : cookie.value; acc.push(libCookie.serialize(cookie.name, cookieValue, cookie)); return acc; }, []); _.set(obj, keyPath, fixedCookies); } } obfuscateCookie(obj, keyParts, obfuscationPattern) { const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts); if (!cookieHeader) return; const cookies = libCookie.parse(cookieHeader); Object.keys(cookies).forEach((cookieName) => { if (name != null && cookieName.toLowerCase() !== name.toLowerCase()) return; cookies[cookieName] = obfuscationPattern; }); const result = Object.entries(cookies) .map(([n, v]) => `${n}=${v}`) .join("; "); _.set(obj, keyPath, result); } resolveOptions(options) { return _.defaults(options, this.options, C8yPactPreprocessorDefaultOptions); } hasKey(keyPath, key) { return ((_.isArray(keyPath) ? keyPath : keyPath.split(".")).filter((k) => k.toLowerCase() === key.toLowerCase()).length > 0); } getCookieObject(obj, keyParts) { let name = undefined; const l = _.last(keyParts)?.toLowerCase(); if (l !== "cookie" && l !== "set-cookie") { name = _.last(keyParts); keyParts = keyParts.slice(0, -1); } const keyPath = keyParts.join("."); const cookieHeader = _.get(obj, keyPath); return { name, keyPath, cookieHeader }; } } C8yDefaultPactPreprocessor.defaultObfuscationPattern = C8yPactPreprocessorDefaultOptions.obfuscationPattern;