@hclsoftware/secagent
Version:
IAST agent
164 lines (150 loc) • 7.08 kB
JavaScript
//IASTIGNORE
/*
* ****************************************************
* Licensed Materials - Property of HCL.
* (c) Copyright HCL Technologies Ltd. 2017, 2025.
* Note to U.S. Government Users *Restricted Rights.
* ****************************************************
*/
const TaintTracker = require("../../TaintTracker");
const {Entity, EntityType} = require("../../Entity");
const {keys} = require("../../AdditionalInfo");
const StackInfo = require("../../StackInfo");
const {ConfigInfo} = require("../../ConfigFile/ConfigInfo");
const Utils = require("../../Utils/Utils");
const AdditionalInfo = require("../../AdditionalInfo");
const IastProperties = require("../../Hooks/IastProperties");
const _ = require('deepdash')(require('lodash'));
function sinkTrigger(obj, vulnerabilities, parameters, origPropertyName, entityName = "") {
const telemetryDataList = createTelemetryDataForObject(origPropertyName, obj, entityName)
for (const telemetryData of telemetryDataList) {
if (telemetryData.additionalInfo != null) {
TaintTracker.getTaintedData(telemetryData.taintedItem).addAdditionalInfoToFlows(telemetryData.additionalInfo)
}
for (const vulnerability of vulnerabilities) {
TaintTracker.sinkTrigger(telemetryData.taintedItem, vulnerability, parameters)
}
}
}
function addSinkAdditionalInfo(source, methodName) {
let from = StackInfo.asString(source)
if (ConfigInfo.ConfigInfo.hidePasswords && StackInfo.hasPasswordFlow(source)) {
from = Utils.PASSWORD_TEXT
}
const sinkObj = {method: methodName, from: from}
AdditionalInfo.addAdditionalInfoToFlows(source, {[keys.K8S_SINK]: JSON.origStringify(sinkObj)})
}
/*
Recursively iterating over the given object properties to create Telemetry data for each tainted item found.
The Telemetry data is an entity-like, converted to string.
Returns the found tainted items as an array of objects containing the tainted item and the additional info when needed.
*/
function createTelemetryDataForObject(origPropertyName, obj, entityName="", taintedItems=[]) {
if (TaintTracker.hasTaintedData(obj) && (obj instanceof String || Buffer.isBuffer(obj))) {
const entityType = createEntityType(obj, entityName, origPropertyName)
let objString = obj.origToString();
let parsedJson;
// we try to parse the object as JSON, if it succeeds, we can extract the tainted data for each flow and add it as the telemetry data.
try {
// try to parse the object as JSON
parsedJson = JSON.origParse(objString);
// only proceed if parsedJson is an object
if (typeof parsedJson !== 'object') {
throw new Error('Parsed value is not an object'); // throw exception to handle non-object cases
}
} catch (e) {
// if the object is not a valid JSON, we set the telemetry data to be the value itself
const additionalInfo = createTelemetryDataEntity(entityName, objString, entityType)
taintedItems.push({taintedItem: obj, additionalInfo: additionalInfo})
return taintedItems;
}
let objectMap = null;
// if we reach here, the object is a valid JSON, we can extract the tainted data for each flow
for (const flow of obj[IastProperties.property.TAINTED_DATA].flows) {
let value, path;
if (flow.storedDataForTelemetryIssue !== null) {
// if the flow has stored value for telemetry data, we can use it directly and skip the search in the stack info list
({ value, path } = flow.storedDataForTelemetryIssue);
} else {
if (!objectMap) {
objectMap = mapObject(parsedJson); // create a map of the object properties to their paths
}
({value, path} = getFlowValueFromJson(flow, objectMap));
if (value === null) { // if no value was found, we use the original json string as the value
value = objString;
path = '';
}
}
const additionalInfo = createTelemetryDataEntity(path, value, entityType)
flow.addAdditionalInfo(additionalInfo)
}
taintedItems.push({taintedItem: obj, additionalInfo: null})
}
else if (typeof obj === "object") {
const isArray = Array.isArray(obj);
for (const innerProperty in obj) {
const path = isArray ? `${entityName}[${innerProperty}]` : `${entityName}.${innerProperty}`
const currentEntityName = entityName === "" ? innerProperty : path
createTelemetryDataForObject(origPropertyName, obj[innerProperty], currentEntityName, taintedItems)
}
}
return taintedItems
}
function createTelemetryDataEntity(entityName, entityValue, entityType) {
const entity = new Entity(entityName, entityValue, entityType)
return {[keys.TELEMETRY_DATA]: JSON.origStringify(entity)}
}
function getFlowValueFromJson(flow, objectMap) {
// look for the tainted data in the stack info list
let targetValue = null;
const stackInfoList = flow.stackInfoList;
// walk through the stack info list in reverse order to find the first occurrence of the tainted data as it's defined in the json
for (const item of [...stackInfoList].reverse()) {
switch (item.type) {
case TaintTracker.HookRuleType.SOURCE:
targetValue = flow.entity.value;
break;
case TaintTracker.HookRuleType.PROPAGATOR:
targetValue = item.propagatorTargetAsString;
break;
}
if (targetValue === null || targetValue === undefined) continue;
if (objectMap.has(targetValue)) {
const path = objectMap.get(targetValue);
return {value: targetValue, path: path};
}
}
return null;
}
function mapObject(obj) {
const map = new Map();
_.eachDeep(obj, (value, key, parent, context) => {
map.set(value, context.path); // context.path is the full path to the value in the object
}, { checkCircular: true, leavesOnly: true });
return map;
}
function createEntityType(obj, entityName, propertyName) {
switch (propertyName) {
case "data":
if (obj instanceof String || Buffer.isBuffer(obj)) {
if (entityName === "body"){
return EntityType.BODY;
} else {
return EntityType.BODY_PARAMETER
}
}
return EntityType.PARAMETER
case "headers":
return EntityType.HEADER
case "cookies":
return EntityType.COOKIE
case "response":
return EntityType.RESPONSE
case "message":
return EntityType.MESSAGE
default:
return EntityType.PARAMETER
}
}
module.exports.sinkTrigger = sinkTrigger
module.exports.addSinkAdditionalInfo = addSinkAdditionalInfo