diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index a6a0ba81..70dc66a9 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1603b79b..fea7c8ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +### v2.0.0 (2024-09-20) +### ⚠ BREAKING CHANGES +* Dropped support for Node.js v16 +* Dropped functionality to generate snapshot file +#### Features +* Support to honour proxy settings via config +* Support for secure cookie security event generation +* Report error to Error Inbox upon connection failure to Security Engine +* Support to detect application and server path +* Functionality to truncate Incoming HTTP request upto default limit +* Dropped support for Node.js v16 +* Dropped functionality to generate snapshot file +#### Bug fixes +* Handling for empty data in IAST fuzzing header +* Added identifiers in events +* Fix for file integrity security event generation +* Fix for missing identifiers in iast-data-request JSON + ### v1.5.0 (2024-08-14) #### Features * Support for Node.js v22.x diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 3f6b7de9..4fc58c99 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -21,6 +21,7 @@ code, the source code can be found at [https://github.com/newrelic/csec-node-age * [find-package-json](#find-package-json) * [hash.js](#hashjs) * [html-entities](#html-entities) +* [https-proxy-agent](#https-proxy-agent) * [is-invalid-path](#is-invalid-path) * [js-yaml](#js-yaml) * [jsonschema](#jsonschema) @@ -239,6 +240,22 @@ THE SOFTWARE. ``` +### https-proxy-agent + +This product includes source derived from [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) ([v5.0.1](https://github.com/TooTallNate/node-https-proxy-agent/tree/v5.0.1)), distributed under the [MIT License](https://github.com/TooTallNate/node-https-proxy-agent/blob/v5.0.1/README.md): + +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + ### is-invalid-path This product includes source derived from [is-invalid-path](https://github.com/jonschlinkert/is-invalid-path) ([v1.0.2](https://github.com/jonschlinkert/is-invalid-path/tree/v1.0.2)), distributed under the [MIT License](https://github.com/jonschlinkert/is-invalid-path/blob/v1.0.2/LICENSE): @@ -588,7 +605,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ### ws -This product includes source derived from [ws](https://github.com/websockets/ws) ([v8.17.1](https://github.com/websockets/ws/tree/v8.17.1)), distributed under the [MIT License](https://github.com/websockets/ws/blob/v8.17.1/LICENSE): +This product includes source derived from [ws](https://github.com/websockets/ws) ([v8.18.0](https://github.com/websockets/ws/tree/v8.18.0)), distributed under the [MIT License](https://github.com/websockets/ws/blob/v8.18.0/LICENSE): ``` Copyright (c) 2011 Einar Otto Stangvik diff --git a/lib/instrumentation-security/core/request-manager.js b/lib/instrumentation-security/core/request-manager.js index 40584c19..1b23a60f 100644 --- a/lib/instrumentation-security/core/request-manager.js +++ b/lib/instrumentation-security/core/request-manager.js @@ -6,6 +6,7 @@ const requestMap = new Map(); const API = require("../../nr-security-api"); const NRAgent = API.getNRAgent(); +const logger = API.getLogger(); const regexPatterns = NRAgent && NRAgent.config.security.exclude_from_iast_scan.api /** @@ -23,12 +24,33 @@ function getRequestFromId(id) { * @param {*} requestData */ function setRequest(id, requestData) { + requestMap.set(id, requestData); - const stringToMatch = requestData.uri ? requestData.uri : requestData.url; - const filteredString = stringToMatch.split('?')[0]; - if (regexPatterns.some(regex => filteredString.match(regex))) { - requestMap.delete(id); + try { + const stringToMatch = requestData.url; + // const filteredString = stringToMatch.split('?')[0]; + const filteredString = stringToMatch; + let isRegexMatchestoURL = false; + for (let index = 0; index < regexPatterns.length; index++) { + const regex = new RegExp(regexPatterns[index], 'gm'); + const matchResult = filteredString.match(regex); + if (matchResult && matchResult.length > 0) { + const data = matchResult[0]; + if (filteredString === data) { + isRegexMatchestoURL = true; + } + } + } + if (isRegexMatchestoURL) { + requestMap.delete(id); + if (API.getSecAgent().status.getStatus() !== 'disabled') { + logger.debug("Excluding URL %s from IAST processing due to ignore API setting", filteredString); + } + } + } catch (error) { + logger.debug("Error while processing API regex for restriction", error); } + } /** @@ -56,13 +78,14 @@ function getRequest(shim) { * @param {*} shim * @param {*} data */ -function updateRequestBody(shim, data) { +function updateRequestBody(shim, data, isTruncated) { const segment = shim.getActiveSegment(); if (segment) { const transactionId = segment.transaction.id; const requestData = getRequestFromId(transactionId); if (requestData) { requestData.body = data; + requestData.dataTruncated = isTruncated; setRequest(transactionId, requestData); } } @@ -73,6 +96,7 @@ function updateRequestBody(shim, data) { const requestData = getRequestFromId(traceId); if (requestData) { requestData.body = data; + requestData.dataTruncated = isTruncated; setRequest(traceId, requestData) } } diff --git a/lib/instrumentation-security/core/sec-utils.js b/lib/instrumentation-security/core/sec-utils.js index 30b8b592..df19c506 100644 --- a/lib/instrumentation-security/core/sec-utils.js +++ b/lib/instrumentation-security/core/sec-utils.js @@ -194,6 +194,7 @@ function addRequestData(shim, request) { }); } data.clientIP = requestIp.getClientIp(request); + data.dataTruncated = false; const transactionId = segment.transaction.id; const storedRequest = requestManager.getRequestFromId(transactionId); if (storedRequest && storedRequest.uri) { diff --git a/lib/instrumentation-security/hooks/fs/nr-fs.js b/lib/instrumentation-security/hooks/fs/nr-fs.js index 67abfcde..eee80e46 100644 --- a/lib/instrumentation-security/hooks/fs/nr-fs.js +++ b/lib/instrumentation-security/hooks/fs/nr-fs.js @@ -25,6 +25,7 @@ const RENAME = 'rename'; const FSReqCallback = 'FSReqCallback'; const FSReqWrap = 'FSReqWrap'; const OBJECT = 'object'; +const semver = require('semver'); const functionsProbableToFA = [ "fstat", @@ -37,7 +38,7 @@ const functionsProbableToFA = [ "realpath", ]; -const functionProbableToFI = [ +let functionProbableToFI = [ "rename", "ftruncate", "rmdir", @@ -143,9 +144,8 @@ function openHook(shim, mod, moduleName) { parameters[0] = API.getSecAgent().applicationInfo.serverInfo.deployedApplications[0].deployedPath + SLASH + parameters[0]; } else if (parameters[0].startsWith(SLASHDOTDOT)) { parameters[0] = API.getSecAgent().applicationInfo.serverInfo.deployedApplications[0].deployedPath + parameters[0]; - } else { - parameters[0] = path.resolve(parameters[0]); - } + } + } catch (error) { } @@ -189,11 +189,6 @@ function probableToFAHooks(shim, mod, moduleName) { const request = requestManager.getRequest(shim); if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) { const traceObject = secUtils.getTraceObject(shim); - try { - parameters[0] = path.resolve(parameters[0]); - } catch (error) { - - } let absoluteParameters = [parameters[0]]; const secMetadata = securityMetaData.getSecurityMetaData(request, absoluteParameters, traceObject, secUtils.getExecutionId(), EVENT_TYPE.FILE_OPERATION, EVENT_CATEGORY.FILE) this.secEvent = API.generateSecEvent(secMetadata); @@ -216,6 +211,9 @@ function probableToFAHooks(shim, mod, moduleName) { * @param {*} moduleName */ function probableToFIHooks(shim, mod, moduleName) { + if (semver.satisfies(process.version, '>19.0.0')) { + functionProbableToFI.push('writeFileSync'); + } functionProbableToFI.forEach(function (fun) { shim.wrap(mod, fun, function makeFIWrapper(shim, fn) { logger.debug(`Instrumenting ${moduleName}.${fun}`); @@ -226,12 +224,6 @@ function probableToFIHooks(shim, mod, moduleName) { const request = requestManager.getRequest(shim); if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) { const traceObject = secUtils.getTraceObject(shim); - try { - parameters[0] = path.resolve(parameters[0]); - parameters[1] = path.resolve(parameters[1]); - } catch (error) { - - } let absoluteParameters = [parameters[0]]; if (fun === COPY_FILE || fun === RENAME) { const secMetadata = securityMetaData.getSecurityMetaData(request, parameters[1], traceObject, secUtils.getExecutionId(), getCase(arguments[1]), EVENT_CATEGORY.FILE) @@ -269,11 +261,6 @@ function probablePromiseToFAHooks(shim, mod, moduleName) { const request = requestManager.getRequest(shim); if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) { const traceObject = secUtils.getTraceObject(shim); - try { - parameters[0] = path.resolve(parameters[0]); - } catch (error) { - - } let absoluteParameters = [parameters[0]]; const secMetadata = securityMetaData.getSecurityMetaData(request, absoluteParameters, traceObject, secUtils.getExecutionId(), EVENT_TYPE.FILE_OPERATION, EVENT_CATEGORY.FILE) this.secEvent = API.generateSecEvent(secMetadata); @@ -302,12 +289,6 @@ function probablePromiseToFIHooks(shim, mod, moduleName) { const request = requestManager.getRequest(shim); if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) { const traceObject = secUtils.getTraceObject(shim); - try { - parameters[0] = path.resolve(parameters[0]); - parameters[1] = path.resolve(parameters[1]); - } catch (error) { - - } let absoluteParameters = [parameters[0]]; if (fun === COPY_FILE || fun === RENAME) { const secMetadata = securityMetaData.getSecurityMetaData(request, parameters[1], traceObject, secUtils.getExecutionId(), getCase(arguments[1]), EVENT_CATEGORY.FILE) diff --git a/lib/instrumentation-security/hooks/http/nr-http.js b/lib/instrumentation-security/hooks/http/nr-http.js index 78c9f3e2..dc899e5b 100755 --- a/lib/instrumentation-security/hooks/http/nr-http.js +++ b/lib/instrumentation-security/hooks/http/nr-http.js @@ -34,12 +34,14 @@ const semver = require('semver'); const ExceptionReporting = require('../../../nr-security-agent/lib/core/ExceptionReporting'); const CSEC_SEP = ':IAST:'; -const find = '{{NR_CSEC_VALIDATOR_HOME_TMP}}'; +const sep = require('path').sep; +const find = `${sep}{{NR_CSEC_VALIDATOR_HOME_TMP}}`; const CSEC_HOME_TMP_CONST = new RegExp(find, 'g'); let CSEC_HOME = NRAgent && NRAgent.config.newrelic_home ? NRAgent.config.newrelic_home : NRAgent && NRAgent.config.logging.filepath ? path.dirname(NRAgent.config.logging.filepath) : require('path').join(process.cwd()); const CSEC_HOME_TMP = `${CSEC_HOME}/nr-security-home/tmp/language-agent/${process.env.applicationUUID}` let lastTransactionId = EMPTY_STRING; let uncaughtExceptionReportFlag = false; +const requestBodyLimit = 500; /** @@ -178,6 +180,7 @@ function parseFuzzheaders(requestData, transactionId) { if (additionalData.length >= 8) { let encryptedData = additionalData[6].trim(); let hashVerifier = additionalData[7].trim(); + if (lodash.isEmpty(encryptedData) || lodash.isEmpty(hashVerifier)) { return; } @@ -191,6 +194,7 @@ function parseFuzzheaders(requestData, transactionId) { logger.debug("fliesTocreate:", filesToCreate); for (let i = 0; i < filesToCreate.length; i++) { let file = filesToCreate[i].trim(); + file = decodeURIComponent(file); file = file.replace(CSEC_HOME_TMP_CONST, CSEC_HOME_TMP); file = path.resolve(file); const parentDir = path.dirname(file); @@ -251,6 +255,7 @@ function addRequestData(shim, request) { }); } data.clientIP = requestIp.getClientIp(request); + data.dataTruncated = false; const transactionId = segment.transaction.id; const storedRequest = requestManager.getRequestFromId(transactionId); lastTransactionId = transactionId; @@ -291,7 +296,7 @@ function emitHook(shim, mod, moduleName) { const resp = arguments[2]; if (arguments[0] == 'request') { - if (NRAgent && NRAgent.config.security.detection.rxss.enabled && !NRAgent.config.security.exclude_from_iast_scan.iast_detection_category.rxss) { + if (NRAgent && !NRAgent.config.security.exclude_from_iast_scan.iast_detection_category.rxss && NRAgent.config.security.detection.rxss.enabled) { responseHook(resp, req, shim); } @@ -370,7 +375,21 @@ function onDataHook(shim, mod) { const requestData = requestManager.getRequestFromId(transactionId); if (requestData) { data = requestData.body ? requestData.body.concat(chunk.toString()) : chunk.toString(); - requestManager.updateRequestBody(shim, data); + requestManager.updateRequestBody(shim, data, false); + try { + const contentLength = Buffer.byteLength(data, 'utf8'); + let bodyLimit = requestBodyLimit; + if (!isNaN(bodyLimit)) { + bodyLimit = bodyLimit * 1000; + if (contentLength && contentLength > bodyLimit) { + data = truncateStringToBytes(data, bodyLimit); + requestManager.updateRequestBody(shim, data, true); + } + } + + } catch (error) { + logger.error("Error while truncating request body", error); + } } } @@ -425,7 +444,12 @@ function responseHook(resp, req, shim) { } catch (error) { logger.debug("Error while reporting error", error); } - secureCookieCheck(response, shim); + try { + secureCookieCheck(response, shim); + } catch (error) { + logger.debug("Error while generating secure cookie event"); + } + responseBodyCompute(response, arguments); const construct = API.checkForReflectedXSS(request, response.res.body, response.getHeaders()); @@ -613,20 +637,48 @@ process.on('uncaughtException', (err, origin) => { uncaughtExceptionReportFlag = true; }); +/** + * utility to parse response headers for cookie check + * @param {*} headersString + * @returns + */ +function responseRawHeaderParsing(headersString) { + let setCookieValue; + try { + const headersArray = headersString.split('\r\n'); + for (const header of headersArray) { + if (header.toLowerCase().startsWith('set-cookie:')) { + setCookieValue = header.trim().replace('set-cookie:', ''); + break; + } + } + } catch (error) { + + } + return setCookieValue; +} + /** * Utility to check insecure cookie settings * @param {*} response * @param {*} shim */ function secureCookieCheck(response, shim) { - const cookieHeader = response.getHeader('set-cookie'); - let csec_request = requestManager.getRequest(shim); + let cookieHeader = response.getHeader('set-cookie') ? response.getHeader('set-cookie') : responseRawHeaderParsing(response._header); + let cookieHeaderList = []; if (!cookieHeader) { return; } + if (typeof cookieHeader === 'string') { + cookieHeaderList.push(cookieHeader) + } + else { + cookieHeaderList = cookieHeaderList.concat(cookieHeader); + } + let csec_request = requestManager.getRequest(shim); try { let parameters = []; - for (const cookieString of cookieHeader) { + for (const cookieString of cookieHeaderList) { if (cookieString) { const keyValuePairs = cookieString.split(SEMI_COLON); let params = { @@ -663,4 +715,18 @@ function secureCookieCheck(response, shim) { logger.debug("Error while generating secure cookie event:", error); } -} \ No newline at end of file +} + +function truncateStringToBytes(inputString, maxBytes) { + if (!inputString || maxBytes <= 0) { + return EMPTY_STRING; // Return an empty string if input is invalid + } + const buffer = Buffer.from(inputString, UTF8); + if (buffer.length <= maxBytes) { + return inputString; + } + const truncatedBuffer = buffer.slice(0, maxBytes); + const truncatedString = truncatedBuffer.toString(UTF8); + return truncatedString; +} + diff --git a/lib/nr-security-agent/index.js b/lib/nr-security-agent/index.js index 61758b29..ff60af74 100644 --- a/lib/nr-security-agent/index.js +++ b/lib/nr-security-agent/index.js @@ -118,30 +118,43 @@ function initialize() { NRAgent.on('started', () => { NRLogger.info("Started New Relic Node.js Security Agent"); logger.info('NR Agent started with agent_run_id:', NRAgent.config.run_id, NRAgent.config.account_id); - + if (Agent.getAgent().status.getStatus() != 'connecting') { + Agent.getAgent().delayed = false; + wsClient.obeyReconnect(); + return; + } if (NRAgent.config.security.enabled || !NRAgent.config.high_security) { - const delay = NRAgent.config.security.scan_schedule.delay; - const allowSampling = NRAgent.config.security.scan_schedule.always_sample_traces - logger.info("IAST scan delay is set to %s minute", delay); - if (delay > 0) { + let delayFromConfig = parseInt(NRAgent.config.security.scan_schedule.delay); + let delay = delayFromConfig; + if (isNaN(delay) || delay < 0) { + delay = 0; + } + NRAgent.config.security.scan_schedule.delay = delay; + const allowSampling = NRAgent.config.security.scan_schedule.always_sample_traces; + + if (delay >= 0) { + logger.debug("IAST delay is set to:", delay); + logger.info("Security Agent delay scan time is set to:", commonUtils.getScheduledScanTime(delay)) Agent.getAgent().delayed = true; setTimeout(() => { Agent.getAgent().delayed = false; - logger.info("IAST scanning delay over"); + logger.info("IAST scanning delay of %s minutes over", delay); + commonUtils.honourDurationConfiguration(); + commonUtils.executeCronSchedule(); + if (!allowSampling) { + wsClient.initialize(); + Agent.getAgent().status.setStatus('connecting'); + Agent.setNRAgent(NRAgent); + } }, delay * 60000); } - let tempDelay = delay; + logger.info("Security Agent is running with config:", JSON.stringify(NRAgent.config.security)); if (allowSampling) { - tempDelay = 0; - } - setTimeout(() => { wsClient.initialize(); Agent.getAgent().status.setStatus('connecting'); Agent.setNRAgent(NRAgent); - commonUtils.executeCronSchedule(); - commonUtils.honourDurationConfiguration(); - }, tempDelay * 60000); - + } + } else { logger.warn("security.enabled flag is set to false"); diff --git a/lib/nr-security-agent/lib/core/Auth-headers.js b/lib/nr-security-agent/lib/core/Auth-headers.js index 67b9ef7d..29212234 100644 --- a/lib/nr-security-agent/lib/core/Auth-headers.js +++ b/lib/nr-security-agent/lib/core/Auth-headers.js @@ -53,6 +53,7 @@ function AuthHeaders() { commonUtils.addLogEventtoBuffer(logMessage); } + return authHeaders; } AuthHeaders.prototype.constructor = AuthHeaders; diff --git a/lib/nr-security-agent/lib/core/Policy.js b/lib/nr-security-agent/lib/core/Policy.js index a45cf543..615de3bd 100644 --- a/lib/nr-security-agent/lib/core/Policy.js +++ b/lib/nr-security-agent/lib/core/Policy.js @@ -17,7 +17,6 @@ const logger = logs.getLogger(); const initLogger = logs.getInitLogger(); const jsonValidator = require('./jsonValidator'); const LOG_MESSAGES = require('./sec-agent-constants').LOG_MESSAGES; -const statusUtils = require('./statusUtils'); function Policy() { this.data = {}; @@ -72,7 +71,6 @@ function setDefaultPolicy() { logger.info(LOG_MESSAGES.DEFAULT_POL_SET, JSON.stringify(policy.data)); } catch (e) { logger.error(LOG_MESSAGES.UNABLE_SET_DEFAULT_POL, e); - statusUtils.addErrortoBuffer(e); }; } @@ -90,16 +88,6 @@ function appInfoVersionUpdate(policy, data) { } } -/** - * Function to send updated policy to validator - * @param {*} data - */ -function sendUpdatedPolicy(data) { - const wsClient = Agent.getAgent().client; - let updatedPolicy = Object.assign({}, data); - updatedPolicy.jsonName = 'lc-policy'; - wsClient.dispatcher(updatedPolicy); -} module.exports = { getInstance, setPolicyData, diff --git a/lib/nr-security-agent/lib/core/commonUtils.js b/lib/nr-security-agent/lib/core/commonUtils.js index 21c4487d..dcb11f10 100644 --- a/lib/nr-security-agent/lib/core/commonUtils.js +++ b/lib/nr-security-agent/lib/core/commonUtils.js @@ -26,6 +26,7 @@ const bufferedLogEvents = new ringBuffer(10); const { CronJob } = require('cron'); let scanStartTime = 0; let trafficStartedTime = 0; +const cron = require('cron'); const skip_detection_category_map = { insecure_settings: ['CRYPTO', 'HASH', 'RANDOM', 'SECURE_COOKIE', 'TRUSTBOUNDARY'], @@ -149,7 +150,6 @@ function getOS() { function createDirectories() { createPathIfNotExist(`${CSEC_HOME}${SLASH}nr-security-home`); createPathIfNotExist(`${CSEC_HOME}${SLASH}nr-security-home${SLASH}logs`); - createPathIfNotExist(`${CSEC_HOME}${SLASH}nr-security-home${SLASH}logs${SLASH}snapshots`); createPathIfNotExist(`${CSEC_HOME}${SLASH}nr-security-home${SLASH}tmp`); createPathIfNotExist(`${CSEC_HOME}${SLASH}nr-security-home${SLASH}tmp${SLASH}language-agent`); createPathIfNotExist(`${CSEC_HOME}${SLASH}nr-security-home${SLASH}tmp${SLASH}language-agent${SLASH}${getUUID()}`); @@ -175,14 +175,11 @@ async function initialSetup() { */ function isLogAccessible(logFileName) { return new Promise((resolve) => { - if (logFileName === njsAgentConstants.STATUS_LOG_FILE && require('./statusUtils').getBufferedHC().length === 0) { - resolve('OK'); - } fs.access(logFileName, fs.constants.F_OK | fs.constants.W_OK, (err) => { if (err) { resolve('Error'); } - else if ((!logs.isLoggerOk || logger.level.levelStr == 'OFF') && logFileName != njsAgentConstants.STATUS_LOG_FILE) { + else if ((!logs.isLoggerOk || logger.level.levelStr == 'OFF')) { resolve('Error'); } else { @@ -272,42 +269,6 @@ function isAgentActiveState() { function iastRestClientStatus() { return 'OK'; } -/** - * Utiltity to remove older snapshots - */ -function removeOlderSnapshots() { - try { - const basePath = njsAgentConstants.LOG_DIR + 'snapshots' + SLASH; - const directoryContent = fs.readdirSync(basePath); - const files = directoryContent.filter((filename) => { - return fs.statSync(`${basePath}${SLASH}${filename}`).isFile(); - }); - const sorted = files.sort((a, b) => { - const firstStat = fs.statSync(`${basePath}${SLASH}${a}`); - const secondStat = fs.statSync(`${basePath}${SLASH}${b}`); - return new Date(secondStat.mtime).getTime() - new Date(firstStat.mtime).getTime(); - }); - for (let i = 99; i < sorted.length; i++) { - const fileToDelete = basePath + sorted[i]; - try { - fs.unlink(fileToDelete, (err) => { - if (err) { - logger.error(err); - } else { - logger.debug('Snapshot deleted:', fileToDelete); - } - }); - } catch (error) { - logger.error(error); - } - } - } catch (error) { - logger.debug(error); - const LogMessage = require('./LogMessage'); - const logMessage = new LogMessage.logMessage("DEBUG", 'Error in processing snapshot files', __filename, error); - addLogEventtoBuffer(logMessage); - } -} /** * Utilty to remove older log files. */ @@ -492,40 +453,101 @@ function listSkipDetectionCategory() { function refreshAgent() { const { Agent } = require('./agent'); - const wsClient = Agent.getAgent().client; - Agent.getAgent().status.setStatus('connecting'); - wsClient.obeyReconnect(); + if (Agent.getAgent().status.getStatus() == 'disabled' || Agent.getAgent().delayed) { + Agent.getAgent().delayed = false; + const wsClient = Agent.getAgent().client; + Agent.getAgent().status.setStatus('connecting'); + wsClient.obeyReconnect(); + honourDurationConfiguration(); + } } function executeCronSchedule() { - const schedule = NRAgent.config.security.scan_schedule.schedule; - if(lodash.isEmpty(schedule)){ - return; + try { + let schedule = NRAgent.config.security.scan_schedule.schedule; + if (lodash.isEmpty(schedule)) { + return; + } + logger.debug("Schedule config is set to:", schedule); + schedule = schedule.replace(/\?/g, '*'); + logger.info("Security Agent scheduled time is set to:", getScheduledScanTime(Math.ceil(cron.timeout(schedule) / 60000))) + const job = new cron.CronJob( + schedule, // cronTime + function () { + logger.debug('Cron schedule invoked'); + logger.info("Security Agent scheduled time is set to:", getScheduledScanTime(Math.ceil(cron.timeout(schedule) / 60000))) + refreshAgent() + }, // onTick + null, // onComplete + true, // start + ); + } catch (error) { + logger.error("Error while processing schedule. Please check schedule cron expression", error); + shutDownAgent(); } - const job = new CronJob( - schedule, // cronTime - function () { - logger.debug('Cron schedule invoked'); - refreshAgent() - }, // onTick - null, // onComplete - true, // start - ); + } function honourDurationConfiguration() { - const duration = NRAgent.config.security.scan_schedule.duration; + let durationFromConfig = parseInt(NRAgent.config.security.scan_schedule.duration); + let duration = durationFromConfig; + if (isNaN(duration) || duration < 0) { + duration = 0; + } + logger.debug("IAST duration is set to:", duration); + NRAgent.config.security.scan_schedule.duration = duration; if (duration < 1) { return; } - const { Agent } = require('./agent'); + logger.info("Security Agent shutdown is set to:", getScheduledScanTime(duration)) setTimeout(() => { - Agent.getAgent().status.setStatus('disabled'); - logger.warn("Security Agent status is disabled"); - const wsClient = Agent.getAgent().client; - wsClient.closeWS(); + shutDownAgent(); }, duration * 60000); } +/** + * Utility to send error to error inbox of APM + * @param {*} error + */ +function sendErrorToErrorInbox(error) { + API.newrelic.noticeError(error); +} + +const getScheduledScanTime = (delayInMinutes) => { + // Get the current time + const currentTime = new Date(); + + // Add the given minutes to the current time + currentTime.setMinutes(currentTime.getMinutes() + delayInMinutes); + + const year = currentTime.getFullYear(); + const month = currentTime.toLocaleString("default", { month: "long" }); + const day = currentTime.toLocaleString("default", { weekday: "long" }); + const date = currentTime.getDate(); + const hours = currentTime.getHours(); + const minutes = currentTime.getMinutes(); + const seconds = currentTime.getSeconds(); + const futureUpdatedTime = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + const futureDateTime = `${day}, ${month} ${date} ${year} ${futureUpdatedTime}`; + return futureDateTime +}; + +function shutDownAgent() { + const { Agent } = require('./agent'); + if (NRAgent.config.security.scan_schedule.always_sample_traces) { + Agent.getAgent().delayed = true; + logger.warn('Scan duration completed, IAST going under hibernate mode') + return; + } + if (Agent.getAgent().status.getStatus() === 'disabled') { + return; + } + Agent.getAgent().status.setStatus('disabled'); + const wsClient = Agent.getAgent().client; + wsClient.closeWS(); + logger.warn("Security Agent status is disabled"); +} + + module.exports = { @@ -543,7 +565,6 @@ module.exports = { getHCStats, isAgentActiveState, iastRestClientStatus, - removeOlderSnapshots, logRollOver, getValidatorServiceEndpointURL, getCSECmode, @@ -557,5 +578,7 @@ module.exports = { executeCronSchedule, honourDurationConfiguration, scanStartTime, - trafficStartedTime + trafficStartedTime, + sendErrorToErrorInbox, + getScheduledScanTime, }; diff --git a/lib/nr-security-agent/lib/core/connections/websocket/proxy-util.js b/lib/nr-security-agent/lib/core/connections/websocket/proxy-util.js new file mode 100644 index 00000000..c5ebd915 --- /dev/null +++ b/lib/nr-security-agent/lib/core/connections/websocket/proxy-util.js @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: New Relic Software License v1.0 + */ + +'use strict' + +const { HttpsProxyAgent } = require('https-proxy-agent') +const logs = require('../../logging'); +let logger = logs.getLogger(); +const commonUtils = require('../../commonUtils'); +const fs = require('fs'); + +let agentProxyWithKeepAlive = null + +exports.proxyAgent = function proxyAgent(config) { + if (null !== agentProxyWithKeepAlive) { + return agentProxyWithKeepAlive + } + const proxyUrl = proxyOptions(config) + let proxyOpts = { + secureEndpoint: config.ssl, + auth: proxyUrl.auth, + ca: config?.certificates?.length ? config.certificates : [], + keepAlive: true + } + let cert = ''; + try { + const certPath = commonUtils.getPathOfCACert(); + cert = fs.readFileSync(certPath, 'utf8'); + } catch (error) { + logger.error("Error in reading certificate:", error); + + } + proxyOpts.ca.push(cert); + logger.info(`Using proxy: ${proxyUrl}`) + agentProxyWithKeepAlive = new HttpsProxyAgent(proxyUrl, proxyOpts) + return agentProxyWithKeepAlive +} + +/** + * Utility to create proxy URL + * @param config + * @returns + */ +function proxyOptions(config) { + let proxyUrl + if (config.proxy) { + proxyUrl = config.proxy + } else { + proxyUrl = 'https://' + let proxyAuth = config.proxy_user + if (config.proxy_pass !== '') { + proxyAuth += ':' + config.proxy_pass + proxyUrl += `${proxyAuth}@` + } + + proxyUrl += `${config.proxy_host || 'localhost'}:${config.proxy_port || 80}` + } + return proxyUrl +} diff --git a/lib/nr-security-agent/lib/core/connections/websocket/response/IASTUtils.js b/lib/nr-security-agent/lib/core/connections/websocket/response/IASTUtils.js index 75de5c4f..071d9ded 100644 --- a/lib/nr-security-agent/lib/core/connections/websocket/response/IASTUtils.js +++ b/lib/nr-security-agent/lib/core/connections/websocket/response/IASTUtils.js @@ -88,6 +88,8 @@ function generateIASTDataRequest() { let object = {}; object['jsonName'] = 'iast-data-request'; object['applicationUUID'] = Agent.getAgent().applicationInfo.applicationUUID; + object['appEntityGuid'] = Agent.getAgent().applicationInfo.appEntityGuid; + object['appAccountId'] = Agent.getAgent().applicationInfo.appAccountId; object["batchSize"] = batchSize ? batchSize : 300; object['pendingRequestIds'] = getPendingRequestIds(); object['completedRequests'] = Object.fromEntries(getCompletedRequestsMap()); diff --git a/lib/nr-security-agent/lib/core/connections/websocket/response/fuzz-request-handler.js b/lib/nr-security-agent/lib/core/connections/websocket/response/fuzz-request-handler.js index dcfb5ba8..e671ecea 100644 --- a/lib/nr-security-agent/lib/core/connections/websocket/response/fuzz-request-handler.js +++ b/lib/nr-security-agent/lib/core/connections/websocket/response/fuzz-request-handler.js @@ -21,25 +21,39 @@ const LogMessage = require('../../../LogMessage'); require('dns').setDefaultResultOrder('ipv4first') const PolicyManager = require('../../../Policy'); -const statusUtils = require('../../../statusUtils'); const find = `${SLASH}{{NR_CSEC_VALIDATOR_HOME_TMP}}`; const CSEC_HOME_TMP_CONST = new RegExp(find, 'g'); const commonUtils = require('../../../commonUtils'); +const CSEC_HOME_TMP_CONST_ENCODED = new RegExp('%7B%7BNR_CSEC_VALIDATOR_HOME_TMP%7D%7D', 'g'); + let logger; let lastFuzzEventTime = 0; let iastIntervalConst; let coolDownIntervalConst; let additionalCoolDownTime = 0; let fuzzedApiIDSet = new Set(); +const API = require('../../../../../../nr-security-api'); +const NRAgent = API.getNRAgent(); /** * utility to start IAST Schedular */ function startIASTSchedular() { - Agent.getAgent().client.dispatcher(IASTUtil.generateIASTDataRequest()); + if (Agent.getAgent().delayed) { + let delayFromConfig = parseInt(NRAgent.config.security.scan_schedule.delay); + let delay = delayFromConfig; + if (isNaN(delay) || delay < 0) { + delay = 0; + } + // logger.debug("IAST data pull request is scheduled at %s", require('../../../commonUtils').getScheduledScanTime(delay)) + } + if (!Agent.getAgent().delayed) { + Agent.getAgent().client.dispatcher(IASTUtil.generateIASTDataRequest()); + } + if (iastIntervalConst) { clearInterval(iastIntervalConst); } @@ -66,7 +80,7 @@ function startIASTSchedular() { logger.trace("Completed requests so far:", completedListSize); logger.trace("Pending requests so far:", pendingListSize); - if (timeDiffInSeconds > 5 && additionalCoolDownTime == 0) { + if (timeDiffInSeconds > 1 && additionalCoolDownTime == 0) { Agent.getAgent().client.dispatcher(data); } }, probingInterval * 1000); @@ -96,7 +110,14 @@ function handler(json) { } setLastFuzzEventTime(); let rawFuzzRequest = json.arguments[0]; - rawFuzzRequest = rawFuzzRequest.replace(CSEC_HOME_TMP_CONST, CSEC_HOME_TMP); + + try { + rawFuzzRequest = rawFuzzRequest.replace(CSEC_HOME_TMP_CONST, CSEC_HOME_TMP); + rawFuzzRequest = rawFuzzRequest.replace(CSEC_HOME_TMP_CONST_ENCODED, encodeURI(CSEC_HOME_TMP)); + } catch (error) { + logger.debug("Error while replacing place holder", error); + } + let fuzzRequest; try { fuzzRequest = JSON.parse(rawFuzzRequest); @@ -165,7 +186,6 @@ function handleFuzzRequest(fuzzDetails) { handleFuzzResponse(response, fuzzDetails); } } catch (err) { - statusUtils.addErrortoBuffer(err); const fuzzFailEvent = new FuzzFailEvent(fuzzRequest.headers[NR_CSEC_FUZZ_REQUEST_ID]); logger.error(stringify(fuzzFailEvent)); try { diff --git a/lib/nr-security-agent/lib/core/connections/websocket/websocket.js b/lib/nr-security-agent/lib/core/connections/websocket/websocket.js index 6366a77f..9ab5023f 100644 --- a/lib/nr-security-agent/lib/core/connections/websocket/websocket.js +++ b/lib/nr-security-agent/lib/core/connections/websocket/websocket.js @@ -17,11 +17,11 @@ const hc = require('../../health-check'); const fs = require('fs'); const { promisify } = require('../../sec-util'); const ResponseHandler = require('./response'); -const { EMPTY_APPLICATION_UUID, LOG_MESSAGES, CSEC_SEP, NR_CSEC_FUZZ_REQUEST_ID, EXITEVENT, JSON_NAME, RASP, SEVERE } = require('../../sec-agent-constants'); -const statusUtils = require('../../statusUtils'); +const { EMPTY_APPLICATION_UUID, LOG_MESSAGES, CSEC_SEP, NR_CSEC_FUZZ_REQUEST_ID, EXITEVENT, JSON_NAME, SEVERE } = require('../../sec-agent-constants'); const commonUtils = require('../../commonUtils'); const IASTUtil = require('../websocket/response/IASTUtils'); const LogMessage = require('../../LogMessage'); +const proxyUtil = require('./proxy-util'); const BUSY = 'busy'; const IDLE = 'idle'; @@ -105,18 +105,25 @@ SecWebSocket.prototype.init = function init() { const authHeaders = require('../../Auth-headers').getInstance(); initLogger.info(`Connecting to Validator at ${validatorService}`); IASTUtil.IASTCleanup(); - let cert = '' + let cert = ''; try { const certPath = commonUtils.getPathOfCACert(); cert = fs.readFileSync(certPath, 'utf8'); } catch (error) { logger.error("Error in reading certificate:", error); + commonUtils.sendErrorToErrorInbox(error) const logMessage = new LogMessage.logMessage(SEVERE, 'Error in reading certificate for websocket', __filename, error); commonUtils.addLogEventtoBuffer(logMessage); } - - webSocket = new WebSocket(validatorService, { headers: authHeaders, cert: cert, handshakeTimeout: 10000 }); + const isProxy = !!(NRAgent.config.proxy || NRAgent.config.proxy_port || NRAgent.config.proxy_host) + if(isProxy){ + const agentWithProxy = proxyUtil.proxyAgent(NRAgent.config); + webSocket = new WebSocket(validatorService, { headers: authHeaders, cert: cert, handshakeTimeout: 10000, agent:agentWithProxy }); + } + else{ + webSocket = new WebSocket(validatorService, { headers: authHeaders, cert: cert, handshakeTimeout: 10000 }); + } webSocket.on(WS_ON_OPEN, this.openCB); webSocket.on(WS_ON_MSG, this.msgCB); @@ -162,6 +169,7 @@ SecWebSocket.prototype.reconnect = function reconnect() { return; } commonUtils.setWSHealthStatus('Error'); + commonUtils.sendErrorToErrorInbox(new Error("Unable to connect to Security Engine")) logger.debug(LOG_MESSAGES.DETECTED_BROKEN_CONN); this.obeyReconnect(); @@ -219,13 +227,11 @@ SecWebSocket.prototype.pingWS = async function pingWS() { if (err) { logger.debug("Error while pinging:", err); commonUtils.setWSHealthStatus('Error'); - statusUtils.addErrortoBuffer(err); - + } }); } catch (err) { logger.debug("error in ping:", err); - statusUtils.addErrortoBuffer(err); commonUtils.setWSHealthStatus('Error'); return; } @@ -281,7 +287,6 @@ SecWebSocket.prototype.dispatch = async function dispatch(event) { }) } catch (error) { logger.debug(LOG_MESSAGES.ERROR_WHILE_SEND_EVENT, eventStr, error); - statusUtils.addErrortoBuffer(error); handleDispatchFailure(this, event, eventStr); } }; @@ -301,7 +306,6 @@ SecWebSocket.prototype.dispatchApplicationInfo = async function dispatchApplicat try { await promisify(this.instance, this.instance.send)(eventStr, { mask: true }); } catch (error) { - statusUtils.addErrortoBuffer(error); logger.warn('Error while sending applicationInfo:', error); this.obeyReconnect(); } @@ -340,7 +344,6 @@ SecWebSocket.prototype.flushEventQueue = async function flushEventQueue() { const results = await Promise.all(sentEvents); hc.getInstance().registerEventsSent(results.length); } catch (err) { - statusUtils.addErrortoBuffer(err); logger.debug('Flush failed, reception of all events on IC couldn\'t be ensured.'); } }; @@ -413,7 +416,7 @@ const defaultOnOpenCB = () => async function onOpen() { initLogger.info(LOG_MESSAGES.SENDING_APPINFO_COMPLETE, JSON.stringify(applicationInfo)); } catch (err) { logger.error(`Connection broken: ${err.message}`); - statusUtils.addErrortoBuffer(err); + commonUtils.sendErrorToErrorInbox(err); commonUtils.setWSHealthStatus('Error') setWebSocketConn(); self.setValidatorNotReadyForEvents(); @@ -446,10 +449,10 @@ const defaultOnMsgCB = () => function onMessage(data) { */ const defaultOnErrCB = () => function onClose(error) { commonUtils.setWSHealthStatus('Error'); - statusUtils.addErrortoBuffer(error); const self = this; logger.error("Error while connecting to validator:", error.message) const logMessage = new LogMessage.logMessage(SEVERE, 'Error while connecting to validator', __filename, error); + commonUtils.sendErrorToErrorInbox(error); commonUtils.addLogEventtoBuffer(logMessage); logger.warn("Security Agent INACTIVE!!!") if (error && error.code && (error.code === 'ECONNREFUSED' || error.code === 'EPIPE' || error.code === 'ECONNRESET')) { diff --git a/lib/nr-security-agent/lib/core/sec-agent-constants.js b/lib/nr-security-agent/lib/core/sec-agent-constants.js index 3ce1ba70..af270068 100644 --- a/lib/nr-security-agent/lib/core/sec-agent-constants.js +++ b/lib/nr-security-agent/lib/core/sec-agent-constants.js @@ -24,15 +24,14 @@ module.exports = { CSEC_HOME_TMP: `${CSEC_HOME}${sep}nr-security-home${sep}tmp${sep}language-agent${sep}${process.env.applicationUUID}`, AGENT_START_TIME: agentStartTime, FRAMEWORK: '', - STATUS_LOG_FILE: `${CSEC_HOME}${sep}nr-security-home${sep}logs${sep}snapshots${sep}node-security-collector-status-${process.env.applicationUUID}.log`, - + DB_LIST: { MYSQL: 'MYSQL', MYSQL2: 'MYSQL', POSTGRES: 'POSTGRES', MONGODB: 'MONGODB', ORACLEDB: 'ORACLEDB', - MSSQL: 'MSSQL', + MSSQL: 'MSSQL', SQLITE3: 'SQLITE3' }, SERVER_COMMAND: { diff --git a/lib/nr-security-agent/lib/core/sec-util.js b/lib/nr-security-agent/lib/core/sec-util.js index 8c6331d5..7c6159d3 100644 --- a/lib/nr-security-agent/lib/core/sec-util.js +++ b/lib/nr-security-agent/lib/core/sec-util.js @@ -9,16 +9,17 @@ const { SecEvent } = require('./event'); const logs = require('./logging'); const { Agent } = require('./agent'); const shaUtil = require('./sha-size-util'); -const { LOG_MESSAGES, NR_CSEC_FUZZ_REQUEST_ID, COLON, VULNERABLE, EXITEVENT, EMPTY_STR, RASP, SEVERE, HYPHEN } = require('./sec-agent-constants'); +const { NR_CSEC_FUZZ_REQUEST_ID, COLON, VULNERABLE, EXITEVENT, EMPTY_STR, SEVERE, HYPHEN } = require('./sec-agent-constants'); const API = require('../../../nr-security-api'); const NRAgent = API.getNRAgent(); const HC = require('./health-check'); const LogMessage = require('./LogMessage'); const commonUtils = require('./commonUtils'); -const statusUtils = require('./statusUtils'); +const appInfo = require('./applicationinfo').getInstance(); let firstEventSent = false; +let appDir = EMPTY_STR; const events = require('events'); const eventEmitter = undefined; @@ -27,6 +28,20 @@ let logger = logs.getLogger(); const initLogger = logs.getInitLogger(); const NODE_SER = 'node-serialize/lib/serialize.js'; +const SECURE_COOKIE = 'SECURE_COOKIE'; + +const insecureSettingApiIdSet = new Set(); + +/** + * Utility to remove insecureSettingAPIIds after 30 mins from the set. + * @param {*} entry + */ +function addInsecureSettingApiId(entry) { + insecureSettingApiIdSet.add(entry); + setTimeout(() => { + insecureSettingApiIdSet.delete(entry); + }, 30 * 60 * 1000); +} function getSecEventEmitter() { if (eventEmitter) { @@ -68,6 +83,7 @@ const sleep = (timeout) => new Promise((resolve) => { function generateSecEvent(securityMetadata) { const traceObject = securityMetadata.traceObject; const request = securityMetadata.request; + let appServerInfo = {}; if (request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) { HC.getInstance().iastEventStats.processed++; @@ -75,6 +91,16 @@ function generateSecEvent(securityMetadata) { else { HC.getInstance().raspEventStats.processed++; } + + try { + appDir = appInfo.serverInfo.deployedApplications[0].deployedPath; + appServerInfo.applicationDirectory = appDir; + appServerInfo.serverBaseDirectory = appDir; + appServerInfo.connectionConfiguration = { [request.serverPort]: request.protocol }; + + } catch (error) { + } + try { let stakTrace = traceObject.stacktrace; let uri = EMPTY_STR; @@ -84,8 +110,15 @@ function generateSecEvent(securityMetadata) { let apiId = shaUtil.getSHA256ForData(traceObject.stacktrace.join('|') + uri); apiId = securityMetadata.eventType + HYPHEN + apiId; + if (securityMetadata.eventType == SECURE_COOKIE) { + if (insecureSettingApiIdSet.has(apiId)) { + return; + } + addInsecureSettingApiId(apiId); + } const agentModule = Agent.getAgent(); const metaData = {}; + metaData.appServerInfo = appServerInfo; if (traceObject.sourceDetails && traceObject.sourceDetails.evalObj) { metaData.triggerViaRCI = true; metaData.rciMethodsCalls = traceObject.sourceDetails.evalObj.invokedCalls; @@ -105,7 +138,6 @@ function generateSecEvent(securityMetadata) { event.httpRequest.route = uri; return event; } catch (e) { - statusUtils.addErrortoBuffer(e); logger.error("Error in generating event:", e); const logMessage = new LogMessage.logMessage(SEVERE, 'Error while generating event', __filename, e); diff --git a/lib/nr-security-agent/lib/core/statusUtils.js b/lib/nr-security-agent/lib/core/statusUtils.js deleted file mode 100644 index 5edf5091..00000000 --- a/lib/nr-security-agent/lib/core/statusUtils.js +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2023 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: New Relic Software License v1.0 - */ - -/* eslint-disable new-cap */ - -const njsAgentConstants = require('./sec-agent-constants'); -const agentConfig = require('../../resources/config'); -const { AGENT_DIR } = agentConfig; -const lodash = require('lodash'); - -let errors = []; - -const ringBuffer = require('ringbufferjs'); -const bufferedHC = new ringBuffer(5); -const bufferedErrors = new ringBuffer(5); -const fs = require('fs'); -const util = require('util'); -let HC = []; -let lastHC = {}; -const logs = require('./logging'); -const commonUtils = require('./commonUtils'); -const logger = logs.getLogger(); -const statusFile = njsAgentConstants.STATUS_LOG_FILE; -const sep = require('path').sep; - -const statusTemplate = 'Snapshot timestamp: : %s\n' + - `CSEC Agent start timestamp: ${njsAgentConstants.AGENT_START_TIME} with application uuid:${commonUtils.getUUID()}\n` + - `CSEC_HOME: ${njsAgentConstants.CSEC_HOME}${sep}nr-security-home${sep}\n` + - `Agent location: ${AGENT_DIR}\n` + - `Using CSEC Agent for Node.js, Node version:${process.version}, PID:${process.pid}\n` + - `Process title: ${process.title}\n` + - `Process binary: ${process.execPath}\n` + - 'Application location: %s\n' + - `Current working directory: ${process.cwd()}\n` + - `Agent mode: ${commonUtils.getCSECmode()}\n` + - 'Application server: %s\n' + - `Application Framework: %s\n` + - `Websocket connection to Prevent Web: ${commonUtils.getValidatorServiceEndpointURL()}, Status: %s\n` + - 'Instrumentation successful\n' + - 'Tracking loaded modules in the application\n' + - 'Policy applied successfully. Policy version is: %s\n' + - 'Started Health Check for Agent\n' + - 'Started Inbound and Outbound monitoring \n' + - '\nProcess stats:\n%s\n' + - '\nService stats:\n%s\n' + - '\nLast 5 errors: \n%s\n' + - '\nLast 5 Health Checks are:\n %s \n'; - -/** - * Utility to add hc in hc buffer - * @param {*} healthCheck - */ -function addHCtoBuffer(healthCheck) { - try { - bufferedHC.enq(healthCheck); - lastHC = healthCheck; - } catch (error) { - logger.debug(error); - } -} - -/** - * Utility to add error object in error buffer - * @param {*} error - */ -function addErrortoBuffer(error) { - try { - let errorObj = { - 'error': error.stack - } - bufferedErrors.enq(errorObj); - } catch (error) { - logger.debug(error); - } -} - -/** - * utility to format status template with dynamic values - */ -function getFormattedData() { - const Agent = require('./agent').Agent.getAgent(); - const appInfo = Agent.applicationInfo; - const deployedApplications = appInfo && appInfo.serverInfo && appInfo.serverInfo.deployedApplications; - const appLoc = deployedApplications[0].deployedPath; - const formattedSnapshot = util.format(statusTemplate, new Date().toString(), appLoc, appInfo.serverInfo.name, commonUtils.getFramework(), commonUtils.getWSHealthStatus(), appInfo.policyVersion, getKeyValPairs(lastHC.stats), getKeyValPairs(lastHC.serviceStatus), JSON.stringify(getBufferedErrors()), JSON.stringify(getBufferedHC())); - return formattedSnapshot; -} - -/** - * utility to write snapshot in snapshot file - */ -function writeSnapshot() { - const snapshot = getFormattedData(); - commonUtils.removeOlderSnapshots(); - fs.writeFile(statusFile, snapshot, { mode: 0o660 }, function (err) { - if (err) { - logger.debug(err.message); - const LogMessage = require('./LogMessage'); - const logMessage = new LogMessage.logMessage("DEBUG", 'Error in creating snapshot file', __filename, err); - commonUtils.addLogEventtoBuffer(logMessage); - } else { - logger.info('Snapshot updated to file: %s', statusFile); - fs.chmod(statusFile, 0o660, (err) => { - if (err) { - addErrortoBuffer(err); - } - }); - } - }); -} -/** - * return buffered error instance. - */ -function getRingBufferedErrors() { - return bufferedErrors; -} - -/** - * return buffered HC list without null, undefined - */ -function getBufferedHC() { - try { - HC = lodash.compact(bufferedHC._elements); - } catch (error) { - logger.debug(error); - } - - return HC; -} -/** - * returns buffered error list without null, undefined - */ -function getBufferedErrors() { - try { - errors = lodash.compact(bufferedErrors._elements); - } catch (error) { - logger.debug(error); - } - return errors; -} - -/** - * Utility to get key val pairs from json object - * @param {*} jsonObject - */ -function getKeyValPairs(jsonObject) { - let statStr = njsAgentConstants.EMPTY_STR; - for (const key in jsonObject) { - statStr = statStr + key + ': ' + jsonObject[key] + '\n'; - } - return statStr; -} - -module.exports = { - statusTemplate, - addHCtoBuffer, - getFormattedData, - writeSnapshot, - addErrortoBuffer, - getRingBufferedErrors, - getBufferedHC, - getBufferedErrors -}; diff --git a/lib/nr-security-agent/lib/core/websocket-client.js b/lib/nr-security-agent/lib/core/websocket-client.js index 7f531de3..6317040c 100644 --- a/lib/nr-security-agent/lib/core/websocket-client.js +++ b/lib/nr-security-agent/lib/core/websocket-client.js @@ -25,7 +25,6 @@ const { } = require('./connections/websocket'); const { HC_INTERVAL_MS } = require('../../resources/config'); const { LOG_DIR, EXITEVENT, JSON_NAME } = require('./sec-agent-constants'); -const statusUtils = require('./statusUtils'); const njsAgentConstants = require('./sec-agent-constants'); let wsInstance; @@ -148,7 +147,6 @@ const sendHC = async function () { json.serviceStatus.websocket = commonUtils.getWSHealthStatus(); json.serviceStatus.logWriter = await commonUtils.isLogAccessible(`${LOG_DIR}node-security-collector.log`); json.serviceStatus.initLogWriter = await commonUtils.isLogAccessible(`${LOG_DIR}node-security-collector-init.log`); - json.serviceStatus.statusLogWriter = await commonUtils.isLogAccessible(njsAgentConstants.STATUS_LOG_FILE); json.serviceStatus.agentActiveStat = commonUtils.isAgentActiveState(); json.serviceStatus.iastRestClient = commonUtils.iastRestClientStatus(); @@ -170,8 +168,6 @@ const sendHC = async function () { logger.info('Health Check Status:', JSON.stringify(json)); - statusUtils.addHCtoBuffer(json); - statusUtils.writeSnapshot(); getDispatcherAndSendEvent(json); } }; diff --git a/package-lock.json b/package-lock.json index 060f874f..7438717f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@newrelic/security-agent", - "version": "1.5.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@newrelic/security-agent", - "version": "1.5.0", + "version": "2.0.0", "license": "New Relic Software License v1.0", "dependencies": { "axios": "^1.7.4", @@ -17,6 +17,7 @@ "find-package-json": "^1.2.0", "hash.js": "^1.1.7", "html-entities": "^2.3.6", + "https-proxy-agent": "^7.0.4", "is-invalid-path": "^1.0.2", "js-yaml": "^4.1.0", "jsonschema": "^1.4.1", @@ -66,6 +67,10 @@ "xmldom": "^0.6.0", "xpath": "^0.0.32", "xpath.js": "^1.1.0" + }, + "engines": { + "node": ">=18", + "npm": ">=6.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2442,6 +2447,33 @@ "npm": ">=6" } }, + "node_modules/@newrelic/native-metrics/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@newrelic/native-metrics/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@newrelic/newrelic-oss-cli": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@newrelic/newrelic-oss-cli/-/newrelic-oss-cli-0.1.2.tgz", @@ -5734,15 +5766,14 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/aggregate-error": { @@ -8933,16 +8964,15 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/hyperlinker": { @@ -10385,6 +10415,31 @@ "node": ">=12.22.0" } }, + "node_modules/mongodb-memory-server-core/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/mongodb-memory-server-core/node_modules/tslib": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", @@ -10554,6 +10609,18 @@ "@newrelic/native-metrics": "^9.0.1" } }, + "node_modules/newrelic/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/newrelic/node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -10569,6 +10636,19 @@ "typedarray": "^0.0.6" } }, + "node_modules/newrelic/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/newrelic/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -17948,6 +18028,29 @@ "https-proxy-agent": "^5.0.1", "nan": "^2.17.0", "semver": "^7.5.2" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + } } }, "@newrelic/newrelic-oss-cli": { @@ -20523,12 +20626,11 @@ "requires": {} }, "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "requires": { - "debug": "4" + "debug": "^4.3.4" } }, "aggregate-error": { @@ -22900,12 +23002,11 @@ } }, "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "requires": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" } }, @@ -24032,6 +24133,25 @@ "yauzl": "^2.10.0" }, "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "tslib": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", @@ -24176,6 +24296,15 @@ "winston-transport": "^4.5.0" }, "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, "concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -24188,6 +24317,16 @@ "typedarray": "^0.0.6" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index 5d109368..e9937407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@newrelic/security-agent", - "version": "1.5.0", + "version": "2.0.0", "description": "New Relic Security Agent for Node.js", "main": "index.js", "jsonVersion": "1.2.9", @@ -8,12 +8,12 @@ { "name": "Sumit Suthar", "email": "ssuthar@newrelic.com", - "web": "http://newrelic.com" + "web": "https://newrelic.com" }, { "name": "Pratik Gunjetiya", "email": "pgunjetiya@newrelic.com", - "web": "http://newrelic.com" + "web": "https://newrelic.com" } ], "scripts": { @@ -28,6 +28,10 @@ "New Relic Security Agent", "Node.js Application Security" ], + "engines": { + "node": ">=18", + "npm": ">=6.0.0" + }, "author": "newrelic", "license": "New Relic Software License v1.0", "repository": { @@ -43,6 +47,7 @@ "find-package-json": "^1.2.0", "hash.js": "^1.1.7", "html-entities": "^2.3.6", + "https-proxy-agent": "^7.0.4", "is-invalid-path": "^1.0.2", "js-yaml": "^4.1.0", "jsonschema": "^1.4.1", diff --git a/third_party_manifest.json b/third_party_manifest.json index b596340c..acab06b0 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -1,5 +1,5 @@ { - "lastUpdated": "Wed Aug 14 2024 10:38:44 GMT+0530 (India Standard Time)", + "lastUpdated": "Fri Sep 20 2024 15:51:01 GMT+0530 (India Standard Time)", "projectName": "@newrelic/security-agent", "projectUrl": "https://github.com/newrelic/csec-node-agent.git", "includeOptDeps": false, @@ -94,6 +94,20 @@ "publisher": "Marat Dulin", "email": "mdevils@yandex.ru" }, + "https-proxy-agent@5.0.1": { + "name": "https-proxy-agent", + "version": "5.0.1", + "range": "^7.0.4", + "licenses": "MIT", + "repoUrl": "https://github.com/TooTallNate/node-https-proxy-agent", + "versionedRepoUrl": "https://github.com/TooTallNate/node-https-proxy-agent/tree/v5.0.1", + "licenseFile": "node_modules/https-proxy-agent/README.md", + "licenseUrl": "https://github.com/TooTallNate/node-https-proxy-agent/blob/v5.0.1/README.md", + "licenseTextSource": "spdx", + "publisher": "Nathan Rajlich", + "email": "nathan@tootallnate.net", + "url": "http://n8.io/" + }, "is-invalid-path@1.0.2": { "name": "is-invalid-path", "version": "1.0.2", @@ -260,15 +274,15 @@ "licenseUrl": "https://github.com/uuidjs/uuid/blob/v9.0.1/LICENSE.md", "licenseTextSource": "file" }, - "ws@8.17.1": { + "ws@8.18.0": { "name": "ws", - "version": "8.17.1", + "version": "8.18.0", "range": "^8.17.1", "licenses": "MIT", "repoUrl": "https://github.com/websockets/ws", - "versionedRepoUrl": "https://github.com/websockets/ws/tree/v8.17.1", + "versionedRepoUrl": "https://github.com/websockets/ws/tree/v8.18.0", "licenseFile": "node_modules/ws/LICENSE", - "licenseUrl": "https://github.com/websockets/ws/blob/v8.17.1/LICENSE", + "licenseUrl": "https://github.com/websockets/ws/blob/v8.18.0/LICENSE", "licenseTextSource": "file", "publisher": "Einar Otto Stangvik", "email": "einaros@gmail.com",