@hclsoftware/secagent
Version:
IAST agent
243 lines (213 loc) • 9.85 kB
JavaScript
//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