dd-trace
Version:
Datadog APM tracing client for JavaScript
204 lines (172 loc) • 7.67 kB
JavaScript
const crypto = require('crypto')
const { defaults } = require('../../../config/defaults')
const STRINGIFY_RANGE_KEY = 'DD_' + crypto.randomBytes(20).toString('hex')
const STRINGIFY_SENSITIVE_KEY = STRINGIFY_RANGE_KEY + 'SENSITIVE'
const STRINGIFY_SENSITIVE_NOT_STRING_KEY = STRINGIFY_SENSITIVE_KEY + 'NOTSTRING'
// eslint-disable-next-line @stylistic/max-len
const REGEX_FOR_STRINGIFY_SENSITIVE_NOT_STRING = new RegExp(String.raw`"${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_\d+_([\s\-+0-9.a-zA-Z]*)"`)
const REGEX_FOR_STRINGIFY_SENSITIVE = new RegExp(String.raw`${STRINGIFY_SENSITIVE_KEY}_\d+_(\d+)_`)
const REGEX_FOR_STRINGIFY_RANGE = new RegExp(String.raw`(${STRINGIFY_RANGE_KEY}_\d+_)`)
const sensitiveValueRegex = new RegExp(/** @type {string} */ (defaults['iast.redactionValuePattern']), 'gmi')
function iterateObject (target, fn, levelKeys = [], depth = 10, visited = new Set()) {
for (const key of Object.keys(target)) {
const nextLevelKeys = [...levelKeys, key]
const val = target[key]
if (typeof val !== 'object' || !visited.has(val)) {
visited.add(val)
fn(val, nextLevelKeys, target, key)
if (val !== null && typeof val === 'object' && depth > 0) {
iterateObject(val, fn, nextLevelKeys, depth - 1, visited)
}
}
}
}
function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
let value
const ranges = []
const sensitiveRanges = []
objRanges = objRanges || {}
if (objRanges || loadSensitiveRanges) {
const cloneObj = Array.isArray(obj) ? [] : {}
let counter = 0
const allRanges = {}
const sensitiveKeysMapping = {}
iterateObject(obj, (val, levelKeys, parent, key) => {
let currentLevelClone = cloneObj
for (let i = 0; i < levelKeys.length - 1; i++) {
let levelKey = levelKeys[i]
if (!currentLevelClone[levelKey]) {
const sensitiveKey = sensitiveKeysMapping[levelKey]
if (currentLevelClone[sensitiveKey]) {
levelKey = sensitiveKey
}
}
currentLevelClone = currentLevelClone[levelKey]
}
if (loadSensitiveRanges) {
const sensitiveKey = sensitiveKeysMapping[key]
if (sensitiveKey) {
key = sensitiveKey
} else {
sensitiveValueRegex.lastIndex = 0
if (sensitiveValueRegex.test(key)) {
const current = counter++
const id = `${STRINGIFY_SENSITIVE_KEY}_${current}_${key.length}_`
key = `${id}${key}`
}
}
}
if (typeof val === 'string') {
const ranges = objRanges[levelKeys.join('.')]
if (ranges) {
const current = counter++
const id = `${STRINGIFY_RANGE_KEY}_${current}_`
allRanges[id] = ranges
currentLevelClone[key] = `${id}${val}`
} else {
currentLevelClone[key] = val
}
if (loadSensitiveRanges) {
const current = counter++
const id = `${STRINGIFY_SENSITIVE_KEY}_${current}_${val.length}_`
currentLevelClone[key] = `${id}${currentLevelClone[key]}`
}
} else if (typeof val !== 'object' || val === null) {
if (loadSensitiveRanges) {
const current = counter++
const id = `${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_${current}_`
// this is special, in the final string we should modify "key_value_[null|false|true]..."
// by null|false|..... ignoring the beginning and ending quotes
currentLevelClone[key] = id + val
} else {
currentLevelClone[key] = val
}
} else {
currentLevelClone[key] = Array.isArray(val) ? [] : {}
}
})
value = JSON.stringify(cloneObj, null, 2)
if (counter > 0) {
const segments = []
let outputLength = 0
let pos = 0
let rangeKeyIndex = value.indexOf(STRINGIFY_RANGE_KEY)
while (rangeKeyIndex > -1) {
let remainingStringValue = value.slice(rangeKeyIndex)
let cleanLength = rangeKeyIndex - pos
if (remainingStringValue.startsWith(STRINGIFY_SENSITIVE_NOT_STRING_KEY)) {
// In this case, we want to remove also the previous " char, because the value is not an string
rangeKeyIndex--
cleanLength--
remainingStringValue = value.slice(rangeKeyIndex)
const regexRes = REGEX_FOR_STRINGIFY_SENSITIVE_NOT_STRING.exec(remainingStringValue)
if (regexRes?.index === 0) {
const matchValue = regexRes[0]
const originalValue = regexRes[1]
const start = outputLength + cleanLength
sensitiveRanges.push({
start,
end: start + originalValue.length,
})
segments.push(value.slice(pos, rangeKeyIndex), originalValue)
outputLength += cleanLength + originalValue.length
pos = rangeKeyIndex + matchValue.length
} else {
// can't happen, the only way to this to happen is
// if the JSON has a value starting with the value of STRINGIFY_SENSITIVE_NOT_STRING_KEY
segments.push(value.slice(pos, rangeKeyIndex + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1))
outputLength += cleanLength + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1
pos = rangeKeyIndex + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1
}
} else if (remainingStringValue.startsWith(STRINGIFY_SENSITIVE_KEY)) {
const regexRes = REGEX_FOR_STRINGIFY_SENSITIVE.exec(remainingStringValue)
if (regexRes?.index === 0) {
const start = outputLength + cleanLength
sensitiveRanges.push({
start,
end: start + Number.parseInt(regexRes[1]),
})
segments.push(value.slice(pos, rangeKeyIndex))
outputLength += cleanLength
pos = rangeKeyIndex + regexRes[0].length
} else {
// can't happen, the only way to this to happen is
// if the JSON has a value starting with the value of STRINGIFY_SENSITIVE_KEY
segments.push(value.slice(pos, rangeKeyIndex + STRINGIFY_SENSITIVE_KEY.length))
outputLength += cleanLength + STRINGIFY_SENSITIVE_KEY.length
pos = rangeKeyIndex + STRINGIFY_SENSITIVE_KEY.length
}
} else {
const regexRes = REGEX_FOR_STRINGIFY_RANGE.exec(remainingStringValue)
if (regexRes?.index === 0) {
const start = outputLength + cleanLength
const rangesId = regexRes[1]
const updatedRanges = allRanges[rangesId].map(range => ({
...range,
start: range.start + start,
end: range.end + start,
}))
ranges.push(...updatedRanges)
segments.push(value.slice(pos, rangeKeyIndex))
outputLength += cleanLength
pos = rangeKeyIndex + regexRes[0].length
} else {
// can't happen, the only way to this to happen is
// if the JSON has a value starting with the value of STRINGIFY_RANGE_KEY
segments.push(value.slice(pos, rangeKeyIndex + STRINGIFY_RANGE_KEY.length))
outputLength += cleanLength + STRINGIFY_RANGE_KEY.length
pos = rangeKeyIndex + STRINGIFY_RANGE_KEY.length
}
}
rangeKeyIndex = value.indexOf(STRINGIFY_RANGE_KEY, pos)
}
segments.push(value.slice(pos))
value = segments.join('')
}
} else {
value = JSON.stringify(obj, null, 2)
}
return { value, ranges, sensitiveRanges }
}
module.exports = { stringifyWithRanges }