UNPKG

@hclsoftware/secagent

Version:

IAST agent

243 lines (213 loc) 9.85 kB
//IASTIGNORE /* * **************************************************** * Licensed Materials - Property of HCL. * (c) Copyright HCL Technologies Ltd. 2017, 2025. * Note to U.S. Government Users *Restricted Rights. * **************************************************** */ 'use strict' const path = require('path') const Vulnerability = require('../Vulnerability') const LoggerInfo = require('../Logger/LoggerConstants') // key names const IgnoredMethods = 'ignoredMethods' const SafeHeaders = 'safeHeaders' const safeCookies = 'safeCookies' const IgnoredNonSecureCookies = 'ignoredNonSecureCookies' const IgnoredNonHttponlyCookies = 'ignoredNonHttpOnlyCookies' const Packages = 'packages' const SecurityRules = 'securityRules' const CsrfRequests = 'csrfRequests' const DefaultConfigInfo = () => { return { packages: [], ignoredMethods: [], ignoredNonSecureCookies: [], ignoredNonHttpOnlyCookies: [], safeHeaders:[], safeCookies:[], internalIgnoredMethods: new Map([ [Vulnerability.WEAK_RANDOM, [ path.join('swig','lib','tags','for.js'), // avoid getting FP, triggered by a rendering library (https://github.com/paularmstrong/swig/blob/master/lib/tags/for.js) The library seems popular but not maintained for over 4 years. path.join('form-data','lib','form_data.js'), // avoid getting FP from form-data lib (random is used to generate boundary). path.join('bson','lib','bson','objectid.js'), // avoid getting FP from bson lib (random is used to generate index). path.join('source-map','lib','quick-sort.js'), // FP - random is used for quick sort algorithm. path.join('redux','lib','redux.js'), // TODO: find the source in order to determine if it's FP or not. path.join('react-dom','cjs','react-dom.development.js'), // TODO: find the source in order to determine if it's FP or not. path.join('nanoid','non-secure','index.cjs'), // non secure variant of nanoid https://github.com/ai/nanoid/tree/46a116a98efd25b171885f0c8fc5d3d8abd1e2a6#non-secure path.join('brace-expansion', 'index.js'), // FP - random is used for escaping: https://github.com/juliangruber/brace-expansion/blob/bc482fba0a225be53f5ba92e62f6c9255a933261/index.js path.join('knex', 'lib', 'util', 'nanoid.js'), // FP - random is used to generate index: https://github.com/knex/knex/blob/92d8f49724493d25cad8224fe9831f51fbb7f6c9/lib/util/nanoid.js path.join('bson-objectid', 'objectid.js'), // FP - random is used to generate index/id: https://github.com/williamkapke/bson-objectid/blob/af908646e0e5618302d303bcc9e406e81f5bf006/objectid.js path.join('express-hbs', 'lib', 'generate-id.js'), // FP - random is used to choose array index. path.join('ldapjs', 'lib', 'client', 'index.js'), // FP - random is used to generate index/id. path.join('internal', 'process', 'next_tick.js'), // FP - node internals path.join('got', 'source', 'normalize-arguments.js'), // FP - TODO: find the source in order to determine if it's FP or not. "runMicrotasks", // "at runMicrotasks (<anonymous>)" ]], [Vulnerability.PATH_TRAVERSAL, [ // i18n lib triggers call to renameSync. // TODO: check if we need more specific rule for rename method (e.g. same taint at the end of both args is FP?). path.join('i18n', 'i18n.js') ]], // currently createHash hook is not added due to FP // ,[Vulnerability.WEAK_HASH, [ // path.join('object-hash','index.js'), // path.join('express-session','index.js') // ]] ]), singleLineIgnoredMethods: new Map([ [Vulnerability.WEAK_RANDOM, [ "internal/process/task_queues", // FP - node internals ]]]), securityRules: { CheckCsrf: true, CheckServerHeader: true, CheckXPoweredBy: true }, logging: { stdoutLogLevel: LoggerInfo.defaultEventsStdLogLevel, logLevel: LoggerInfo.defaultEventsLogLevel, findingsLogLevel: LoggerInfo.defaultFindingsLogLevel, maxSizeLogFileMB: LoggerInfo.defaultMaxSizeFileInMbB }, csrfRequests: [], inSessionPatterns: undefined, asoc: { asocPollingIntervalInSec: 10, testmode: false, reportToAsoc: true }, memoryThreshold: 0.95, hidePasswords: false, enableDastCommunication: true } } const securityRuleToVulnerability = { CheckCsrf: Vulnerability.CSRF, CheckServerHeader: Vulnerability.SERVER_HEADER, CheckXPoweredBy: Vulnerability.XPOWEREDBY_HEADER } let isEnableRuntimeScaInitialized = false let enableRuntimeSca = false class UserCsrfRequest { constructor (methodName, url, parameterNames) { this.constData = { HttpMethod: 'method', Url: 'url', Parameters: 'parameterNames' } this.methodName = methodName.origToUpperCase() this.url = url this.parameterNames = parameterNames } equalsToActualRequest (requestInfo) { const allParameterNames = Object.keys(requestInfo.allParameters) const containsAll = (obj, arr) => arr.every(item => Object.prototype.hasOwnProperty.call(obj, item)) return containsAll(allParameterNames, this.parameterNames) && requestInfo.url.origStringIncludes(this.url) && requestInfo.method === this.methodName } } UserCsrfRequest.urlRegex = /(^(https?|ftp|file):\/\/)([^/\s]+)(\/[^?#]*)(\?([^#]*))?/ UserCsrfRequest.urlPathRegex = /(^\/)([^/\s]+)([/^?#]*)(\?([^#]*))?/ class ConfigInfo { constructor () { this.resetToDefault() } loadFromFile (configObj) { this.ConfigInfo = DefaultConfigInfo() this.overrideConfig(this.ConfigInfo, configObj) this.finishSettingConfigInfo() } finishSettingConfigInfo () { this.ConfigInfo.ignoredMethods = this.loadValuesToArray(IgnoredMethods, true) this.ConfigInfo.packages = this.loadValuesToArray(Packages, true).map(element => element.origReplace(/\./g, path.sep)) this.ConfigInfo.safeHeaders = this.loadValuesToArray(SafeHeaders).map(element => element.origToLowerCase()) this.ConfigInfo.safeCookies = this.loadValuesToArray(safeCookies) this.ConfigInfo.ignoredNonSecureCookies = this.loadValuesToArray(IgnoredNonSecureCookies) this.ConfigInfo.ignoredNonHttpOnlyCookies = this.loadValuesToArray(IgnoredNonHttponlyCookies) this.ConfigInfo.securityRules = this.loadIgnoredVulnerabilities() this.ConfigInfo.csrfRequests = this.loadConfigFileRequests() } overrideConfig (defaultConfig, newConfig) { for (const prop in newConfig) { const value = newConfig[prop] if (!Array.isArray(value) && typeof value === 'object') { this.overrideConfig(defaultConfig[prop], newConfig[prop]) // <- recursive call } else { defaultConfig[prop] = newConfig[prop] } } } resetToDefault () { this.ConfigInfo = DefaultConfigInfo() this.finishSettingConfigInfo() } isSafeHeaderOrSafeCookie(entityName, entityType){ return (entityType === 'header' && this.ConfigInfo.safeHeaders.origArrayIncludes(entityName.origToLowerCase())) || (entityType === 'cookie' && this.ConfigInfo.safeCookies.origArrayIncludes(entityName)) } isIgnoredMethod (stack, v) { const actualInternalMethods = this.ConfigInfo.internalIgnoredMethods.get(v) || [] return this.isAnyElementInStackTrace(actualInternalMethods, stack) || this.isAnyElementInStackTrace(this.ConfigInfo.ignoredMethods, stack) || this.isElementSingleStackLine(v, stack) } isIgnoredVulnerability (vulnerability) { return this.ConfigInfo.securityRules.origArrayIncludes(vulnerability) } isAnyElementInStackTrace (container, stackTrace) { return container.some(element => stackTrace.origStringIncludes(element)) } // check if the cal stack contains only one line and that line contins one of the methods in singleLineIgnoredMethods isElementSingleStackLine (v, stackTrace) { const singleLineIgnoredMethods = this.ConfigInfo.singleLineIgnoredMethods.get(v) || [] if (singleLineIgnoredMethods.length > 0 && stackTrace !== ''){ const stackLines = stackTrace.origSplit(',') return stackLines.length === 1 && singleLineIgnoredMethods.some(element => stackLines[0].origStringIncludes(element)) } return false } loadIgnoredVulnerabilities () { const result = [] const ignoredVulObj = this.ConfigInfo[SecurityRules] for (const secRule in ignoredVulObj) { if (ignoredVulObj[secRule] === false || ignoredVulObj[secRule] === 'false') { result.push(securityRuleToVulnerability[secRule]) } } return result } loadValuesToArray (attribute, removeWildCard = false) { const obj = this.ConfigInfo[attribute] return Object.keys(obj).map(key => obj[key]).map(value => removeWildCard ? value.replace(/\*/g, '') : value) } loadConfigFileRequests () { const userCSrfRequests = [] const jsonRequests = this.ConfigInfo[CsrfRequests] if (jsonRequests == null) { return userCSrfRequests } for (const jsonRequest of jsonRequests) { const csrfRequest = this.createUserCsrfRequestFromJson(jsonRequest) if (csrfRequest != null) { userCSrfRequests.push(csrfRequest) } } return userCSrfRequests } createUserCsrfRequestFromJson (jsonCsrfRequest) { const methodName = jsonCsrfRequest.method if (methodName == null) return null const parameterNames = jsonCsrfRequest.parameterNames if (parameterNames == null) return null const url = jsonCsrfRequest.url if (url == null || !this.isValidUrl(url)) return null return new UserCsrfRequest(methodName, url, parameterNames) } isValidUrl (url) { return UserCsrfRequest.urlRegex.test(url) || UserCsrfRequest.urlPathRegex.test(url) } } module.exports.ConfigInfo = new ConfigInfo() module.exports.UserCsrfRequest = UserCsrfRequest