@hclsoftware/secagent
Version:
IAST agent
149 lines (138 loc) • 7.76 kB
JavaScript
//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