UNPKG

@hclsoftware/secagent

Version:

IAST agent

276 lines (244 loc) 10.9 kB
//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 }