@newrelic/security-agent
Advanced tools
Comparing version 1.1.1 to 1.2.0
@@ -0,1 +1,13 @@ | ||
### v1.2.0 (2024-04-12) | ||
#### Features | ||
* Added instrumentation for express framework's res.download() and res.sendFile() | ||
#### Bug fixes | ||
* Handling to decrypt fuzz header data for IAST scanning | ||
* Logging and snapshot file fixes | ||
#### Miscellaneous chores | ||
* Prepend vulnerability case type with apiId | ||
* Updated jsonVersion to v1.2.0 | ||
* Bumped undici from 5.28.3 to 5.28.4 | ||
### v1.1.1 (2024-03-21) | ||
@@ -2,0 +14,0 @@ #### Bug fixes |
@@ -57,2 +57,3 @@ | ||
EMPTY_STRING: '', | ||
COMMA: ',', | ||
QUESTION_MARK: '?', | ||
@@ -59,0 +60,0 @@ DOUBLE_PIPE: '||', |
@@ -12,6 +12,7 @@ /* | ||
const async_hooks = require('async_hooks'); | ||
const crypto = require('crypto'); | ||
let AGENT_DIR = path.join(__dirname, '../../'); | ||
let AGENT_DIR = path.join(__dirname, '../../'); | ||
if(process.platform == 'win32'){ | ||
if (process.platform == 'win32') { | ||
AGENT_DIR = path.join(__dirname, "..\\..\\"); | ||
@@ -40,6 +41,7 @@ } | ||
createSkipList(); | ||
const fs = require('fs'); | ||
const fs = require('fs'); | ||
const cp = require('child_process'); | ||
const requestIp = require('request-ip'); | ||
const URL = require('url') | ||
const URL = require('url'); | ||
const { EMPTY_STR } = require('./constants'); | ||
@@ -52,3 +54,3 @@ /** | ||
*/ | ||
function createSkipList () { | ||
function createSkipList() { | ||
let loadList = []; | ||
@@ -69,3 +71,3 @@ process.moduleLoadList.forEach(function (loadString) { | ||
function getTraceObject (shim) { | ||
function getTraceObject(shim) { | ||
const trace = stackTraceModule.get(); | ||
@@ -88,8 +90,8 @@ const traceLength = 10; | ||
const routeFile = routeManager.getRoute(key); | ||
if(routeFile){ | ||
if (routeFile) { | ||
stkTrace.push(routeFile); | ||
} | ||
const sourceDetails = secTrace.getSourceDetailsFromTrace(trace,__filename, stkTrace); | ||
const sourceDetails = secTrace.getSourceDetailsFromTrace(trace, __filename, stkTrace); | ||
const traceObject = { | ||
sourceDetails:sourceDetails, | ||
sourceDetails: sourceDetails, | ||
stacktrace: stkTrace | ||
@@ -100,3 +102,3 @@ } | ||
function getTraceObjectFallback (request) { | ||
function getTraceObjectFallback(request) { | ||
const trace = stackTraceModule.get(); | ||
@@ -118,8 +120,8 @@ const traceLength = 10; | ||
const routeFile = routeManager.getRoute(key); | ||
if(routeFile){ | ||
if (routeFile) { | ||
stkTrace.push(routeFile); | ||
} | ||
const sourceDetails = secTrace.getSourceDetailsFromTrace(trace,__filename, stkTrace); | ||
const sourceDetails = secTrace.getSourceDetailsFromTrace(trace, __filename, stkTrace); | ||
const traceObject = { | ||
sourceDetails:sourceDetails, | ||
sourceDetails: sourceDetails, | ||
stacktrace: stkTrace | ||
@@ -131,3 +133,3 @@ } | ||
function traceElementForRoute () { | ||
function traceElementForRoute() { | ||
const stkTrace = []; | ||
@@ -141,3 +143,3 @@ const trace = stackTraceModule.get(); | ||
const lineNumber = trace[i].getLineNumber(); | ||
if(i>0){ | ||
if (i > 0) { | ||
methodName = trace[i - 1].getMethodName(); | ||
@@ -155,7 +157,7 @@ } | ||
function getExecutionId(){ | ||
function getExecutionId() { | ||
return async_hooks.executionAsyncId(); | ||
} | ||
function createPathIfNotExist (dir) { | ||
function createPathIfNotExist(dir) { | ||
try { | ||
@@ -182,46 +184,84 @@ if (!fs.existsSync(dir)) { | ||
try { | ||
const data = Object.assign({}); | ||
const segment = shim.getActiveSegment(); | ||
if (segment && segment.transaction) { | ||
data.protocol = (request.connection && request.connection.encrypted) ? 'https' : 'http'; | ||
data.body = null; | ||
data.headers = request.headers; | ||
data.url = request.url; | ||
data.method = request.method; | ||
data.httpVersion = request.httpVersion; | ||
data.serverPort = segment.transaction.port; | ||
data.contextPath = '/'; | ||
const queryObject = URL.parse(request.url, true).query; | ||
data.parameterMap = {}; | ||
if (queryObject) { | ||
Object.keys(queryObject).forEach(function (key) { | ||
if (queryObject[key]) { | ||
if (!data.parameterMap[key]) { | ||
data.parameterMap[key] = new Array(queryObject[key].toString()); | ||
} | ||
const data = Object.assign({}); | ||
const segment = shim.getActiveSegment(); | ||
if (segment && segment.transaction) { | ||
data.protocol = (request.connection && request.connection.encrypted) ? 'https' : 'http'; | ||
data.body = null; | ||
data.headers = request.headers; | ||
data.url = request.url; | ||
data.method = request.method; | ||
data.httpVersion = request.httpVersion; | ||
data.serverPort = segment.transaction.port; | ||
data.contextPath = '/'; | ||
const queryObject = URL.parse(request.url, true).query; | ||
data.parameterMap = {}; | ||
if (queryObject) { | ||
Object.keys(queryObject).forEach(function (key) { | ||
if (queryObject[key]) { | ||
if (!data.parameterMap[key]) { | ||
data.parameterMap[key] = new Array(queryObject[key].toString()); | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
data.clientIP = requestIp.getClientIp(request); | ||
const transactionId = segment.transaction.id; | ||
const storedRequest = requestManager.getRequestFromId(transactionId); | ||
if (storedRequest && storedRequest.uri) { | ||
data.uri = storedRequest.uri; | ||
data.parameterMap = storedRequest.parameterMap; | ||
} | ||
requestManager.setRequest(transactionId, data); | ||
if (shim.agent.getLinkingMetadata()) { | ||
let linkingMetadata = shim.agent.getLinkingMetadata(); | ||
if (linkingMetadata['trace.id']) { | ||
let traceId = linkingMetadata['trace.id']; | ||
requestManager.setRequest(traceId, data) | ||
} | ||
} | ||
} | ||
data.clientIP = requestIp.getClientIp(request); | ||
const transactionId = segment.transaction.id; | ||
const storedRequest = requestManager.getRequestFromId(transactionId); | ||
if (storedRequest && storedRequest.uri) { | ||
data.uri = storedRequest.uri; | ||
data.parameterMap = storedRequest.parameterMap; | ||
} catch (error) { | ||
logger.debug("Error while preparing incoming request:", error); | ||
} | ||
} | ||
function decryptData(encryptedData) { | ||
let decryptedData = EMPTY_STR; | ||
try { | ||
const linkingMetadata = API.newrelic.getLinkingMetadata(); | ||
let entityGuid = linkingMetadata['entity.guid']; | ||
let password = entityGuid; | ||
let salt = password.slice(0, 16); | ||
//derive key | ||
const key = crypto.pbkdf2Sync(password, salt, 1024, 32, 'sha1'); | ||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.alloc(16)); | ||
let decrypted = decipher.update(encryptedData, 'hex'); | ||
decrypted += decipher.final(); | ||
decryptedData = decrypted.slice(16, decrypted.length); | ||
} catch (error) { | ||
logger.debug("Error while decrypting the data:", error); | ||
} | ||
return decryptedData; | ||
} | ||
function hashVerifier(decryptedData, hashFromSE) { | ||
let flag = false; | ||
try { | ||
const hash = crypto.createHash('sha256'); // Create a new hash object | ||
hash.update(decryptedData); // Write data to the hash object | ||
let calculatedSHA256Hash = hash.digest('hex'); | ||
if (calculatedSHA256Hash === hashFromSE) { | ||
flag = true; | ||
} | ||
requestManager.setRequest(transactionId, data); | ||
if(shim.agent.getLinkingMetadata()){ | ||
let linkingMetadata = shim.agent.getLinkingMetadata(); | ||
if(linkingMetadata['trace.id']){ | ||
let traceId = linkingMetadata['trace.id']; | ||
requestManager.setRequest(traceId, data) | ||
} | ||
} | ||
} | ||
} catch (error) { | ||
logger.debug("Error while preparing incoming request:", error); | ||
} | ||
} | ||
logger.debug("Error while calculating SHA256 of decrypted data:", error); | ||
} | ||
return flag; | ||
} | ||
module.exports = { | ||
@@ -233,3 +273,5 @@ getTraceObject, | ||
getTraceObjectFallback, | ||
addRequestData | ||
addRequestData, | ||
decryptData, | ||
hashVerifier | ||
} |
@@ -21,4 +21,7 @@ /* | ||
const logger = API.getLogger(); | ||
const { EVENT_TYPE, EVENT_CATEGORY } = require('../../core/event-constants'); | ||
const { STRING } = require('../../core/constants'); | ||
const securityMetaData = require('../../core/security-metadata'); | ||
module.exports = function initialize(shim, express, moduleName) { | ||
module.exports = function initialize(shim, express) { | ||
logger.info("Instrumenting express") | ||
@@ -32,3 +35,4 @@ if (!express || !express.Router) { | ||
} | ||
expressFileHook(shim, express && express.response, 'download') | ||
expressFileHook(shim, express && express.response, 'sendFile') | ||
} | ||
@@ -111,2 +115,38 @@ | ||
} | ||
} | ||
} | ||
/** | ||
* Wrapper for resp.download() | ||
* @param {*} shim | ||
* @param {*} mod | ||
* @param {*} fun | ||
*/ | ||
function expressFileHook(shim, mod, fun){ | ||
shim.wrap(mod, fun, function makeFAWrapper(shim, fn) { | ||
if (!shim.isFunction(fn)) { | ||
return fn | ||
} | ||
logger.debug(`Instrumenting express.response.${fun}`) | ||
return function FAWrapper() { | ||
let parameters = Array.prototype.slice.apply(arguments); | ||
const interceptedArgs = [arguments[0]]; | ||
shim.interceptedArgs = interceptedArgs; | ||
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); | ||
API.sendEvent(this.secEvent); | ||
} | ||
return fn.apply(this, arguments); | ||
}; | ||
}); | ||
} | ||
@@ -8,3 +8,3 @@ /* | ||
const requestManager = require('../../core/request-manager'); | ||
const { NR_CSEC_FUZZ_REQUEST_ID, QUESTION_MARK, EMPTY_STRING, UTF8, CONTENT_TYPE, TEXT_HTML, APPLICATION_JSON, APPLICATION_XML, APPLICATION_XHTML, TEXT_PLAIN, APPLICATION_X_FORM_URLENCODED, MULTIPART_FORM_DATA } = require('../../core/constants'); | ||
const { NR_CSEC_FUZZ_REQUEST_ID, QUESTION_MARK, EMPTY_STRING, UTF8, CONTENT_TYPE, TEXT_HTML, APPLICATION_JSON, APPLICATION_XML, APPLICATION_XHTML, TEXT_PLAIN, APPLICATION_X_FORM_URLENCODED, MULTIPART_FORM_DATA, COMMA } = require('../../core/constants'); | ||
const ARRAY_TYPE = 'Array'; | ||
@@ -172,23 +172,34 @@ const STRING_TYPE = 'string'; | ||
logger.debug('AdditionalData:', additionalData); | ||
if (additionalData.length >= 7) { | ||
for (let i = 6; i < additionalData.length; i++) { | ||
let file = additionalData[i].trim(); | ||
file = file.replace(CSEC_HOME_TMP_CONST, CSEC_HOME_TMP); | ||
file = path.resolve(file); | ||
const parentDir = path.dirname(file); | ||
if (!isInvalid(parentDir) && isPathInside(parentDir, CSEC_HOME_TMP)) { | ||
try { | ||
if (!fs.existsSync(parentDir)) { | ||
secUtils.createPathIfNotExist(parentDir); | ||
} else { | ||
logger.debug(parentDir + ' Already Exists'); | ||
if (additionalData.length >= 8) { | ||
let encryptedData = additionalData[6]; | ||
let hashVerifier = additionalData[7]; | ||
let decryptedData = secUtils.decryptData(encryptedData); | ||
let verifiedHash = secUtils.hashVerifier(decryptedData, hashVerifier); | ||
logger.debug("verifiedHash:", verifiedHash); | ||
let filesToCreate = decryptedData.split(COMMA); | ||
if (verifiedHash) { | ||
logger.debug("Encrypted Data:", encryptedData); | ||
logger.debug("Decrypted Data:", decryptedData) | ||
logger.debug("fliesTocreate:",filesToCreate); | ||
for (let i = 0; i < filesToCreate.length; i++) { | ||
let file = filesToCreate[i].trim(); | ||
file = file.replace(CSEC_HOME_TMP_CONST, CSEC_HOME_TMP); | ||
file = path.resolve(file); | ||
const parentDir = path.dirname(file); | ||
if (!isInvalid(parentDir) && isPathInside(parentDir, CSEC_HOME_TMP)) { | ||
try { | ||
if (!fs.existsSync(parentDir)) { | ||
secUtils.createPathIfNotExist(parentDir); | ||
} else { | ||
logger.debug(parentDir + ' Already Exists'); | ||
} | ||
fs.closeSync(fs.openSync(file, 'w')); | ||
requestData.tempFiles = []; | ||
requestData.tempFiles.push(file); | ||
requestManager.setRequest(transactionId, requestData); | ||
} catch (error) { | ||
logger.debug(error); | ||
} | ||
fs.closeSync(fs.openSync(file, 'w')); | ||
requestData.tempFiles = []; | ||
requestData.tempFiles.push(file); | ||
requestManager.setRequest(transactionId, requestData); | ||
} catch (error) { | ||
logger.debug(error); | ||
} | ||
@@ -397,3 +408,3 @@ } | ||
let isUnsupportedType = isUnsupportedContentType(type); | ||
if (request && (construct || dynamicScanningFlag) && !isUnsupportedType) { | ||
@@ -406,3 +417,3 @@ const args = []; | ||
const secEvent = API.generateSecEvent(secMetadata); | ||
secEvent.httpResponse = {}; | ||
secEvent.httpResponse = {}; | ||
secEvent.httpResponse.contentType = response.getHeader(CONTENT_TYPE); | ||
@@ -409,0 +420,0 @@ API.sendEvent(secEvent); |
@@ -15,3 +15,3 @@ /* | ||
const AgentStatus = require('../core/agent-status'); | ||
const { CSEC_HOME, SLASH } = require('./sec-agent-constants'); | ||
const { CSEC_HOME, SLASH, EMPTY_STR } = require('./sec-agent-constants'); | ||
@@ -449,3 +449,17 @@ const njsAgentConstants = require('./sec-agent-constants'); | ||
} | ||
/** | ||
* Utility to get framework from APM | ||
* @returns | ||
*/ | ||
function getFramework(){ | ||
let framework = EMPTY_STR; | ||
try { | ||
framework = NRAgent.environment.get('Framework')[0]; | ||
} catch (error) { | ||
logger.debug("Unable to get framework"); | ||
} | ||
return framework; | ||
} | ||
module.exports = { | ||
@@ -474,3 +488,4 @@ getUUID, | ||
addLogEventtoBuffer, | ||
getLogEvents | ||
getLogEvents, | ||
getFramework | ||
}; |
@@ -54,3 +54,2 @@ | ||
initLogger.info("[STEP-7] => Received and applied policy/configuration", JSON.stringify(policy.data)); | ||
logger.info('Applied policy is:', JSON.stringify(policy.data)); | ||
if (NRAgent && NRAgent.config.security.detection) { | ||
@@ -57,0 +56,0 @@ logger.info('Security detection flags:', JSON.stringify(NRAgent.config.security.detection)); |
@@ -114,2 +114,3 @@ /* | ||
CRITICAL: 'CRITICAL', | ||
HYPHEN: '-', | ||
@@ -169,3 +170,3 @@ LOG_MESSAGES: { | ||
SENDING_APPINFO: '[APP_INFO] Sending Application Info To Prevent-web Service: ', | ||
SENDING_APPINFO_COMPLETE: '[COMPLETE][APP_INFO] Application info sent to Prevent-Web service :', | ||
SENDING_APPINFO_COMPLETE: '[STEP-3][COMPLETE][APP_INFO] Application info sent to Prevent-Web service :', | ||
APPLY_INSTRUMENTATION: '[STEP-6][BEGIN][instrumentation] Applying Instrumentation', | ||
@@ -172,0 +173,0 @@ AVAIL_DISK: 'Available Disk Space Is: ', |
@@ -12,3 +12,3 @@ /* | ||
const shaUtil = require('./sha-size-util'); | ||
const { LOG_MESSAGES, NR_CSEC_FUZZ_REQUEST_ID, COLON, VULNERABLE, EXITEVENT, EMPTY_STR, RASP, SEVERE } = require('./sec-agent-constants'); | ||
const { LOG_MESSAGES, NR_CSEC_FUZZ_REQUEST_ID, COLON, VULNERABLE, EXITEVENT, EMPTY_STR, RASP, SEVERE, HYPHEN } = require('./sec-agent-constants'); | ||
const API = require('../../../nr-security-api'); | ||
@@ -87,2 +87,3 @@ const NRAgent = API.getNRAgent(); | ||
let apiId = shaUtil.getSHA256ForData(traceObject.stacktrace.join('|') + uri); | ||
apiId = securityMetadata.eventType + HYPHEN + apiId; | ||
const agentModule = Agent.getAgent(); | ||
@@ -89,0 +90,0 @@ const metaData = {}; |
@@ -26,6 +26,7 @@ /* | ||
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` + | ||
`SEC_HOME: ${njsAgentConstants.CSEC_HOME}\n` + | ||
`CSEC_HOME: ${njsAgentConstants.CSEC_HOME}${sep}nr-security-home${sep}\n` + | ||
`Agent location: ${AGENT_DIR}\n` + | ||
@@ -39,6 +40,6 @@ `Using CSEC Agent for Node.js, Node version:${process.version}, PID:${process.pid}\n` + | ||
'Application server: %s\n' + | ||
'Application Framework: %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' + | ||
'Instrumentation successful\n' + | ||
'Tracking loaded modules in the application\n' + | ||
'Policy applied successfully. Policy version is: %s\n' + | ||
@@ -88,3 +89,3 @@ 'Started Health Check for Agent\n' + | ||
const appLoc = deployedApplications[0].deployedPath; | ||
const formattedSnapshot = util.format(statusTemplate, new Date().toString(), appLoc, appInfo.serverInfo.name, njsAgentConstants.FRAMEWORK, commonUtils.getWSHealthStatus(), appInfo.policyVersion, getKeyValPairs(lastHC.stats), getKeyValPairs(lastHC.serviceStatus), JSON.stringify(getBufferedErrors()), JSON.stringify(getBufferedHC())); | ||
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; | ||
@@ -91,0 +92,0 @@ } |
{ | ||
"name": "@newrelic/security-agent", | ||
"version": "1.1.1", | ||
"version": "1.2.0", | ||
"description": "New Relic Security Agent for Node.js", | ||
"main": "index.js", | ||
"jsonVersion": "1.1.1", | ||
"jsonVersion": "1.2.0", | ||
"contributors": [ | ||
@@ -8,0 +8,0 @@ { |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
503965
9009