@hclsoftware/secagent
Version:
IAST agent
217 lines (183 loc) • 8.74 kB
JavaScript
//IASTIGNORE
/*
* ****************************************************
* Licensed Materials - Property of HCL.
* (c) Copyright HCL Technologies Ltd. 2017, 2025.
* Note to U.S. Government Users *Restricted Rights.
* ****************************************************
*/
const TimedHash = require('./TimedHash')
const IastLogger = require('../Logger/IastLogger')
const ConfigFileManager = require('../ConfigFile/ConfigFileManager')
const AsocConnector = require('./AsocConnector')
let pendingVulnerabilities = new Map() // map of issues pending to be sent, hashValue -> json
const reportedVulnerabilities = new Map() // map of the vulnerabilities already reported, per execution id: execution_id -> set of already reported hashes
let timedVulnerabilities = new Set() // set of TimedHash - hash value and the timestamp when it happened
// private final LinkedBlockingQueue<VulnerabilityInfo> vulnerabilityQueue;
let reportToAsoc = true // TODO: user-config.json
const MB_SIZE = 1048576
const MAX_REPORT_SIZE = 2 * MB_SIZE
let copyOfPendingVulnerabilities = new Map() // used to save pending vulnerabilities when snapshot taken
let copyOfTimedVulnerabilities = new Set() // used to save pending vulnerabilities when snapshot taken
const stagedVulnerabilities = new Set() // vulnerabilities sent to asoc. if asoc report succeeds, they will be added to reported set
const agentVersion = '1.0-SNAPSHOT'
let _agentId
module.exports.pushVulnerabilityInfo = (info) => {
const infoString = JSON.origStringify(info, function replacer (key, value) {
return (['timestamp', 'hashValue', 'rawStack', 'propagatorTargetAsString'].origArrayIncludes(key)) ? undefined :
((key === 'request' || key === 'stack') && value == null)? undefined: value
})
writeVulnerabilityToFile(infoString)
if (reportToAsoc) {
addToPendingVulnerabilityMap(info, infoString)
}
}
function writeVulnerabilityToFile (infoString) {
IastLogger.findingsLog.info(wrapJson(infoString))
// .replace("Cryptography.InsecureAlgorithm", "Cryptography.Mac")
// .replace("Cryptography.NonStandard", "Cryptography.Ciphers"))
}
// save a copy of current maps for reporting and clear the original maps
module.exports.takeSnapshot = () => {
// synchronized (mapLock)
// {
copyOfPendingVulnerabilities = new Map(pendingVulnerabilities)
pendingVulnerabilities.clear()
copyOfTimedVulnerabilities = new Set(timedVulnerabilities)
timedVulnerabilities.clear()
// }
}
module.exports.clearAllIssues = () => {
try {
IastLogger.eventLog.debug("Deleting remaining issues");
pendingVulnerabilities.clear();
reportedVulnerabilities.clear();
timedVulnerabilities.clear();
stagedVulnerabilities.clear();
copyOfPendingVulnerabilities.clear();
copyOfTimedVulnerabilities.clear();
} catch (Exception) {}
}
module.exports.getReport = (startTime, endTime, executionId) => {
// If we need to clear the found vulnerabilities (when the execution changes), we lock the maps and clear everything.
// Before we do that, we copy the map and then we traverse and send the remaining data at our leisure (unlocked).
let jsonOutput = ''
stagedVulnerabilities.clear()
let dataPending = false
if (!reportedVulnerabilities.has(executionId)) {
reportedVulnerabilities.set(executionId, new Set())
}
let issueCounter = 0
let issuesToSend = 0
let reportExceeded = false
// include all hash values with timestamp before start and end
for (const timedhash of copyOfTimedVulnerabilities) {
if (timedhash.isAfter(startTime) && timedhash.isBefore(endTime)) {
const hashValue = timedhash.hashValue
// send each issue only once per execution
if (!reportedVulnerabilities.get(executionId).has(hashValue) && !stagedVulnerabilities.has(hashValue)) {
issueCounter++
if (jsonOutput.length < MAX_REPORT_SIZE) {
issuesToSend++
stagedVulnerabilities.add(hashValue)
if (!(jsonOutput.length === 0)) {
jsonOutput += ','
}
jsonOutput += copyOfPendingVulnerabilities.get(hashValue)
} else { // if we passed max report size, put the issues back to maps
// this is the best place to do it, since report is done per execution_id, and this is the only place we are aware of it
if (!reportExceeded) {
reportExceeded = true
IastLogger.eventLog.debug(`Reached max report size: number of issues sent on first report: ${issuesToSend}`)
}
dataPending = true
// synchronized (mapLock) {
pendingVulnerabilities.set(hashValue, copyOfPendingVulnerabilities.get(hashValue))
timedVulnerabilities.add(timedhash)
// }
}
}
}
}
if (dataPending) {
IastLogger.eventLog.info('Exceeded max size for sending data. Waiting for next upload request.')
IastLogger.eventLog.debug(`Total number of issues: ${issueCounter}`)
IastLogger.eventLog.debug(`Sending: ${issuesToSend}, pending: ${issueCounter-issuesToSend}`)
}
// no need to send anything if no issues found
if (jsonOutput.length === 0) {
return { data: undefined, pending: false }
}
return { data: wrapJson(jsonOutput), pending: dataPending }
}
function wrapJson (jsonOutput) {
return `{"agent-version":"${agentVersion}","issue-group": [${jsonOutput}]}`
}
// report to asoc may terminate in one of three ways:
// 1. returnToMap() - if the communication to asoc failed, the issues that were last processed should return to the map
// and the 'staged'issues should be discarded. this is done only after processing all the executions in the time period.
// 2. stagedToReported(executionId) - if the communication to asoc succeeded and the execution continues, we move the staged issues
// to reported
// 3. clearReported(executionId) - if the communication to asoc succeeded and the execution ended, we can remove the
// execution id from the map of reported issues
// in case the report to ASoC failed, the reporting task will ask to return the issues to the map, to get them again next time
module.exports.returnToMap = () => {
// synchronized (mapLock) {
IastLogger.eventLog.debug(`returnToMap: pendingVulnerabilities: ${copyOfPendingVulnerabilities.size}, timedVulnerabilities: ${copyOfTimedVulnerabilities.size}`)
pendingVulnerabilities = new Map([...pendingVulnerabilities, ...copyOfPendingVulnerabilities])
timedVulnerabilities = new Set([...timedVulnerabilities, ...copyOfTimedVulnerabilities])
// }
}
// called when report to asoc succeeded, vulnerabilities can be recorded as reported
module.exports.stagedToReported = (executionId) => {
const existingData = reportedVulnerabilities.get(executionId)
if (existingData !== undefined) { reportedVulnerabilities.set(executionId, new Set([...existingData, ...stagedVulnerabilities])) } else { reportedVulnerabilities.set(executionId, new Set([...stagedVulnerabilities])) }
}
// removes the entry for the execution id
module.exports.clearReported = (executionId) => {
reportedVulnerabilities.delete(executionId)
}
module.exports.vulnerabilitiesPending = () => {
return pendingVulnerabilities.size > 0
}
function addToPendingVulnerabilityMap (info, infoString) {
// We only add to the pending map if we haven't sent it before. We use the found map to keep track
// synchronized (mapLock)
// {
pendingVulnerabilities.set(info.id, infoString)
timedVulnerabilities.add(new TimedHash.TimedHash(info.timestamp, info.id))
// }
}
module.exports.init = (agentId) => {
AsocConnector.ConfigureAsocConnector()
_agentId = agentId
ConfigFileManager.configFileEventEmitter.on(ConfigFileManager.FILE_UPLOADED, onFileUploaded)
}
module.exports.getAgentId = () => {
return _agentId
}
function onFileUploaded (configInfo) {
IastLogger.eventLog.debug('asoc config file listener called')
// update asoc polling interval
AsocConnector.setPollingInterval(configInfo.asoc.asocPollingIntervalInSec)
// set asoc testmode
AsocConnector.setTestMode(configInfo.asoc.testmode)
// report to asoc - default is true
if (configInfo.asoc.reportToAsoc === undefined || configInfo.asoc.reportToAsoc) {
reportToAsoc = true
// if (initializationComplete)
AsocConnector.start() // no effect if asocConnector is already started
} else {
if (reportToAsoc) { AsocConnector.stopAfterNext() }
reportToAsoc = false
}
}
module.exports.startAsocConnector = () => {
if (reportToAsoc) {
AsocConnector.start() // no effect if asocConnector is already started
}
}
module.exports.shutDown = () => {
AsocConnector.stopAfterNext()
}