@hclsoftware/secagent
Version:
IAST agent
276 lines (244 loc) • 10.9 kB
JavaScript
//IASTIGNORE
/* eslint-disable no-new-wrappers */
/*
* ****************************************************
* Licensed Materials - Property of HCL.
* (c) Copyright HCL Technologies Ltd. 2017, 2025.
* Note to U.S. Government Users *Restricted Rights.
* ****************************************************
*/
const HookRuleFactory = require('../Rules/HookRules/HookRuleFactory')
const BeforeRuleFactory = require('../Rules/BeforeRules/BeforeRuleFactory')
const hookRule = require('../Rules/HookRules/HookRule')
const HookValues = require('./HookValues')
const logger = require('../Logger/IastLogger').eventLog
const regularHooks = require('./Hooks')
const requireHooks = require('./RequireHooks')
const readOnlyHooks = require('./ReadOnlyHooks')
const TaintTracker = require('../TaintTracker')
const IastProperties = require('./IastProperties')
module.exports.loadHooks = () => {
module.exports.hooksActive = false
setRegularHooks()
loadHooks(readOnlyHooks)
loadHooks(requireHooks)
module.exports.hooksActive = true
}
module.exports.hooksActive = true
const toActualHook = {}
// Set of module names which should not be included in methodSignature
const ignoredModuleNames = new Set(['global', 'axios']);
/*
called at start up.
Populates an object with hook functions which are generated according to the hook list.
*/
function loadHooks(hooks) {
for (const moduleName in hooks) {
hooks[moduleName].forEach(hook => {
const methodNames = getMethodNames(moduleName, hook)
for (const {methodName, methodSignature, fullName} of methodNames) {
const hookFunction = parseHook(hook, methodSignature)
toActualHook[fullName] = hookFunction
}
})
}
}
/*
Given a module, it overrides the relevant original functions by appropriate hook functions which loaded at start up.
*/
module.exports.setRequireHook = (origModule, moduleName) => {
const hooks = moduleName in requireHooks ? requireHooks[moduleName] : []
hooks.forEach(hookObj => {
// get hook function from cache:
const methodNames = getMethodNames(moduleName, hookObj)
for (const {methodName, methodSignature, fullName} of methodNames) {
const hookFunction = toActualHook[fullName]
// need to fetch the hooked module in run time because it might be of different version in different requires:
const hookedModule = getHookedModule(hookObj, origModule)
if (hookedModule == null) { // module may not be present, e.g. fs.promises
//logger.debug(`skipping undefined module ${origModule}, scope: ${JSON.stringify(hookObj.scopes)}`)
return
}
const origFunction = hookedModule[methodName]
if (origFunction != null) { // we may hook functions that do not exist in certain library versions
setProxyOnFunction(hookedModule, moduleName, methodName, origFunction, hookFunction)
}
// else {
// logger.debug(`skipping undefined function ${moduleName}.${functionName}`)
// }
}
})
}
function setRegularHooks() {
for (const moduleName in regularHooks) {
const baseModule = moduleName === 'global' ? global : require(moduleName)
regularHooks[moduleName].forEach(hookObj => {
const moduleObj = getHookedModule(hookObj, baseModule)
if (moduleObj == null) { // module may not be present, e.g. fs.promises
//logger.debug(`skipping undefined module ${baseModule}, scope: ${JSON.stringify(hookObj.scopes)}`)
return
}
const methodNames = getMethodNames(moduleName, hookObj)
for (const {methodName, methodSignature, fullName} of methodNames) {
const hookFunction = parseHook(hookObj, methodSignature)
const origFunction = moduleObj[methodName]
if (origFunction != null) { // we may hook functions that do not exist in certain library versions
setProxyOnFunction(moduleObj, moduleName, methodName, origFunction, hookFunction)
}
// else {
// logger.debug(`skipping undefined function ${moduleName}.${functionName}`)
// }
}
})
}
}
/* This function is used for adding hooks by proxy. There are functions that we cannot override by our hook,
* but with proxy because they contain additional properties that would be removed in the regular overriding way.
*/
function setProxyOnFunction(module, moduleName, functionName, origFunction, hookFunction) {
// we store a flag for each function that indicates whether the function has already wrapped by a proxy, to prevent
// a proxy chain, which might make the program crash.
if (origFunction == null || (module[functionName] != null && module[functionName][IastProperties.property.FUNCTION_PROXY])) {
return
}
module[functionName] = new Proxy(origFunction, {
apply(target, thisArg, argArray) { // trap regular function call, e.g. Error(message)
const that = thisArg == null || (!Buffer.isBuffer(thisArg)) && (!Array.isArray(thisArg) && thisArg.toString() === '[object global]') ? moduleName : thisArg
return hookFunction(that, argArray, target)
},
construct(target, argArray, newTarget) { // trap constructor call, e.g. new Error(message)
return hookFunction('global', argArray, target, newTarget)
},
get(target, p, receiver) {
if (p === IastProperties.property.FUNCTION_PROXY) {
return true;
}
return Reflect.get(target, p, receiver)
}
})
}
function getHookedModule(hookObj, baseModule) {
let currentScope = baseModule
const scopes = 'scopes' in hookObj ? hookObj.scopes : []
scopes.forEach(scope => {
if (currentScope != null) {
currentScope = currentScope[scope]
}
})
return currentScope
}
function getMethodNames(moduleName, hook) {
let result = [];
const originalScopes = 'scopes' in hook ? [...hook.scopes] : [];
const fullScopesStr = originalScopes.join('.');
// Create filtered scopes (without 'prototype') for methodSignature
const filteredScopes = originalScopes.filter(scope => scope !== 'prototype');
const filteredScopesStr = filteredScopes.join('.');
const methods = Array.isArray(hook.methodName) ? hook.methodName : [hook.methodName];
for (const methodName of methods) {
// For fullName, always include moduleName and original scopes (with prototype)
const fullName = fullScopesStr ? `${moduleName}.${fullScopesStr}.${methodName}` : `${moduleName}.${methodName}`;
// For methodSignature, only include moduleName if it's not in ignoredModuleNames
let methodSignature;
if (ignoredModuleNames.has(moduleName)) {
methodSignature = filteredScopesStr ? `${filteredScopesStr}.${methodName}` : methodName;
} else {
methodSignature = filteredScopesStr ? `${moduleName}.${filteredScopesStr}.${methodName}` : `${moduleName}.${methodName}`;
}
result.push({
methodName: methodName,
methodSignature: methodSignature,
fullName: fullName
});
}
return result;
}
module.exports.getHookFunctionFor = (fullMethodName) => {
return toActualHook[fullMethodName]
}
/*
This function Generates rules for required hook object.
It returns a function which would be called instead of the original method.
*/
function parseHook(hookObj, methodSignature) {
const beforeRules = hookObj.beforeRules != null ? parseRules(hookObj.beforeRules, BeforeRuleFactory) : []
const callbackRules = hookObj.callbackRules != null ? parseRules(hookObj.callbackRules, HookRuleFactory) : []
const hookRules = hookObj.rules != null ? parseRules(hookObj.rules, HookRuleFactory) : []
// this function would be called at run time instead of the original one:
return function (that, origArgs, method, constructorProxy=null) {
const hookValues = new HookValues(origArgs, that, method, methodSignature, hookObj.additionalInfo, callbackRules, constructorProxy)
return doRules(hookObj, beforeRules, hookRules, hookValues)
}
}
/*
Manages the hook execution:
1. executes before rules,
2 calls to the original method
3. executes the hook rules.
*/
function doRules(hookObj, beforeRules, hookRules, hookValues) {
let thrownError
try {
beforeRules.forEach(rule => {
try {
if (rule.runWhenInactive() || module.exports.hooksActive)
rule.doRule(hookValues)
} catch (e) {
logger.error(e)
}
})
if (hookValues.runOrigMethod) {
try {
if (hookValues.isConstructor) {
// hookValues.ret = new (Function.bind.apply(hookValues.origMethod, [null, ...hookValues.updatedArgs]))
hookValues.ret = Reflect.construct(hookValues.origMethod, hookValues.updatedArgs, hookValues.newTarget)
}
else {
hookValues.ret = hookValues.origMethod.apply(hookValues.updatedThat, hookValues.updatedArgs)
}
} catch (error) {
thrownError = error
}
}
if (module.exports.hooksActive && shouldRunHookRules(hookObj, hookRules, hookValues)) {
hookRules.forEach(rule => {
try {
rule.doHook(hookValues)
} catch (e) {
logger.error(e)
}
})
}
} catch (e) {
logger.error(e)
}
if (thrownError != null) {
throw thrownError
}
return hookValues.ret
}
function shouldRunHookRules(hookObj, hookRules, hookValues) {
if (hookRules.length === 0) {
return false
}
if (hookObj.taintCondition != null) {
return hookObj.taintCondition.some(p => isSpecifiedPositionTainted(p, hookValues))
}
return true
}
function isSpecifiedPositionTainted (position, hookValues) {
if (position === 'args') {
for (let i = 0; i < hookValues.args.length; i++) {
if (TaintTracker.isObjectTainted(hookRule.getActualParam(i, hookValues))) return true
}
return false
}
return TaintTracker.isObjectTainted(hookRule.getActualParam(position, hookValues))
}
function parseRules(rulesObj, ruleFactory) {
const rules = []
for (const ruleObj of rulesObj) {
rules.push(ruleFactory.createRule(ruleObj))
}
return rules
}