cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
276 lines (275 loc) • 11.4 kB
JavaScript
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;