UNPKG

@hclsoftware/secagent

Version:

IAST agent

217 lines (183 loc) 8.74 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 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() }