UNPKG

@hclsoftware/secagent

Version:

IAST agent

149 lines (138 loc) 7.76 kB
//IASTIGNORE /* * **************************************************** * Licensed Materials - Property of HCL. * (c) Copyright HCL Technologies Ltd. 2017, 2025. * Note to U.S. Government Users *Restricted Rights. * **************************************************** */ const iastLogger = require("./Logger/IastLogger"); const {keys} = require("./AdditionalInfo"); const TaintTracker = require("./TaintTracker"); /** * Extracts the endpoints Route Object which is inside the bound dispatch layer of an express app middleware stack * and will use extractEndpointsFromLayerStack to extract the endpoints if there is a stack of layers again, * if not Endpoint will be added to the list in the end * @param {string} previousPath - the path of the previous layer * @param {Array} route - the route object of the bound dispatch layer * @param {Array} apiInfoMap - the list of endpoints detected so far */ function extractRouteEndpoints(previousPath, route, apiInfoMap) { let path = previousPath ? previousPath + ' -> ' + route.path : route.path; for (const method in route.methods) { // always contains at least one method // Route object can have a layer stack for deeper nested routes // path and method are sent (as args) and will be used to create Endpoint in case of they are not defined in the deeper levels if (route.stack) { extractEndpointsFromLayerStack(path, route.stack, apiInfoMap, route.methods[method] ? method : '') } else { // if no stack, then add the path and method to the list here itself apiInfoMap.push([method, path]) } } } /** * Extracts the endpoints from the middleware stack and * looks for the bound dispatch, router or mounter_app (layer for another app), and anonymous layers (usually at the end of the flow) * - for bound dispatch layer, we will route object, which is created when app.get or app.post is called in the user code. therefore we extract the endpoints from the route object. * - for router or mounted_app layer, we will have a stack of layers, so we recursively call the function to extract the endpoints from the stack. * - for anonymous layers, we will extract the path and method and add to the list if we find path and method. * In case we are inside route object stack, there was no end point information extracted from the its layer, we will be adding the information from route object in the end after * checking anonymous layers api info map is empty. * * @param previousPath - the path of the previous layer that will be appended to current path in some cases * @param stack - middleware stack of the express app or router layer or mounter_app layer * @param apiInfoMap - the list of endpoints detected so far * @param routeMethod - method info from the routeObject, since route object always contains the method info, in case there is information extracted from its layers, * we use it and add the endpoint */ function extractEndpointsFromLayerStack(previousPath, stack, apiInfoMap, routeMethod){ let anonLayerApiInfoMap = [] for (const layer of stack){ if (!layer) continue if (layer.name === "bound dispatch"){ // check layer name if it is bound dispatch, then it has route object if (layer.route) extractRouteEndpoints(previousPath, layer.route, apiInfoMap) } else if (layer.name === "router" || layer.name === "mounted_app") { // if it is router layer then there can be more nested layers in handle.stack if (layer.handle && layer.handle.stack) { let path = previousPath ? previousPath + ' -> ' + layer.regexp.toString() : layer.regexp.toString(); extractEndpointsFromLayerStack(path, layer.handle.stack, apiInfoMap) } } else if (layer.name === "<anonymous>" && previousPath){ // expecting most flows to end with this case // in case there is no layer, it goes to next case, and any duplicates will be removed later let currentPath = layer.path ? layer.path : layer.regexp.toString() let totalPath = previousPath + ' -> ' + currentPath let currentHttpMethod = layer.method ? layer.method : routeMethod ? routeMethod : '' anonLayerApiInfoMap.push([currentHttpMethod, totalPath]) } } // In case there is new Api info from the anonymous layers of the stack, we append to the list if (anonLayerApiInfoMap.length > 0){ apiInfoMap.push(...anonLayerApiInfoMap) } else if (routeMethod) { // if the api info from the anonymous layers is empty then we add the info received from the route object // Note: routeMethod argument is only sent from the extractRouteEndpoints function. // so we check for routeMethod, and if we are in the route object, and we report the EndPoint here // this will be added only once per route object apiInfoMap.push([routeMethod, previousPath]) } } /** * Reports the detected APIs for the hooked express app. * the call is places before the iast headers check so that multiple apps can be checked for end points in the same request * 1. first we check app._router.stack to get the middleware stack and call extractEndpointsFromLayerStack to extract the endpoints * 2. we return with print message if no endpoints are detected * 3. we then add addiontional details to the detectedApiInfoObj and sort it based on the path, and is appended to the method. * 4. numbered keys are added to the detectedApiInfoObj, and reported as DETECTED_APIS issue * 5. flag __iastEndpointsReported is set to true to avoid multiple reporting of the same issue * @param app * @param req */ function reportDetectedApis(app, req) { try { if (!app.__iastEndpointsChecked) { const apiInfoMap = []; if (app._router && app._router.stack) { extractEndpointsFromLayerStack('', app._router.stack, apiInfoMap); } if (apiInfoMap.length === 0) { iastLogger.eventLog.debug("No Endpoints detected for this Express App"); app.__iastEndpointsChecked = true; return; } const detectedApiInfoObj = { "App_name": `${app.get('name') || app.name || "undefined"}${req.hostname && req.socket.localPort ? ` (${req.hostname}:${req.socket.localPort})` : ""}` }; if (app.mountpath) { detectedApiInfoObj["App_mountpath"] = app.mountpath; } const apiSet = new Set(); apiInfoMap.sort((a, b) => a[1].localeCompare(b[1])).forEach(([method, path]) => { path = path.replace(/\)$/g, ''); // to clean the ')' in end of the path const endPointItem = `${method} ${path}`; if (!apiSet.has(endPointItem)) { // to remove duplicates detectedApiInfoObj[`${keys.DETECTED_APIS}_${String(apiSet.size).padStart(4, '0')}`] = endPointItem; apiSet.add(endPointItem); } }); TaintTracker.reportVulnerability( TaintTracker.Vulnerability.DETECTED_APIS, "", "", [], null, false, false, null, null, detectedApiInfoObj ); app.__iastEndpointsChecked = true; } } catch (err) { iastLogger.eventLog.error(`Cannot report detected end points issue ${err.toString()}`); app.__iastEndpointsChecked = true; } } module.exports.reportDetectedApis = reportDetectedApis