Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for microservice security #211

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/instrumentation-security/core/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@ module.exports = {
UNDEFINED: 'undefined',
SELF_FD_PATH: '/proc/self/fd/',
NR_CSEC_FUZZ_REQUEST_ID: 'nr-csec-fuzz-request-id',
NR_CSEC_TRACING_DATA: 'nr-csec-tracing-data',
NR_CSEC_PARENT_ID: 'nr-csec-parent-id'

};
16 changes: 15 additions & 1 deletion lib/instrumentation-security/hooks/http/nr-http.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

module.exports = initialize
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, COMMA } = 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, NR_CSEC_PARENT_ID, NR_CSEC_TRACING_DATA } = require('../../core/constants');
const ARRAY_TYPE = 'Array';
const STRING_TYPE = 'string';
const BUFFER_TYPE = 'Buffer';
Expand Down Expand Up @@ -39,6 +39,8 @@ 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}`

const ms = require('../../../nr-security-agent/lib/core/micro-service');

/**
* Entry point of http module hook
* @param {*} shim
Expand Down Expand Up @@ -148,6 +150,9 @@ function outboundHook(shim, mod, method, moduleName) {
if (request && request.headers[NR_CSEC_FUZZ_REQUEST_ID] && arguments[0].headers) {
arguments[0].headers[NR_CSEC_FUZZ_REQUEST_ID] = request.headers[NR_CSEC_FUZZ_REQUEST_ID];
}
if (request && request.headers[NR_CSEC_PARENT_ID] && arguments[0].headers) {
arguments[0].headers[NR_CSEC_PARENT_ID] = request.headers[NR_CSEC_PARENT_ID];
}
}
}
return fn.apply(this, arguments);
Expand All @@ -161,6 +166,9 @@ function outboundHook(shim, mod, method, moduleName) {
* @param {*} fuzzheaders
*/
function parseFuzzheaders(requestData, transactionId) {
if(!requestData.headers){
return;
}
let fuzzheaders = requestData.headers;
const policy = API.getPolicy();
const dynamicScanningFlag = policy.data ? policy.data.vulnerabilityScan.iastScan.enabled : false;
Expand Down Expand Up @@ -208,6 +216,12 @@ function parseFuzzheaders(requestData, transactionId) {
logger.debug(error);
}
}
let csecTracingHeader = requestData.headers[NR_CSEC_TRACING_DATA] ? requestData.headers[NR_CSEC_TRACING_DATA] : EMPTY_STRING;;
let csecParentId = requestData.headers[NR_CSEC_PARENT_ID] ? requestData.headers[NR_CSEC_PARENT_ID] : EMPTY_STRING;
let csecFuzzHeader = requestData.headers[NR_CSEC_FUZZ_REQUEST_ID] ? requestData.headers[NR_CSEC_FUZZ_REQUEST_ID] : EMPTY_STRING

ms.prepareGeneratedEventforIncomingReq(csecFuzzHeader, csecParentId, csecTracingHeader);

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,56 @@
* SPDX-License-Identifier: New Relic Software License v1.0
*/
const { Agent } = require('../../../agent');
let pendingRequestIds = new Set();
let completedRequestsMap = new Map();
const PolicyManager = require('../../../Policy');
const microServiceUtils = require('../../../micro-service');
let batchSize;
const batchthreshold = 160;
function getPendingRequestIds() {
return [...pendingRequestIds];
}

function addPendingRequestId(id) {
pendingRequestIds.add(id);
}
// micro service changes
let completedReplay = new Set();
let errorInReplay = new Set();
let clearFromPending = new Set();


function removeRequestId(id) {
completedRequestsMap.delete(id);
if (completedReplay.has(id)) {
completedReplay.delete(id);
}
else if (clearFromPending.has(id)) {
clearFromPending.delete(id);
}
}

function removePendingRequestId(id) {
pendingRequestIds.delete(id);

function getClearFromPending() {
return [...clearFromPending];
}

// for last leg
function completedRequestsMapInit(id) {
completedRequestsMap.set(id, []);
function IASTCleanup() {
completedReplay.clear();
errorInReplay.clear();
clearFromPending.clear();
}

function addCompletedRequests(id, eventId) {
let data = completedRequestsMap.get(id);
if(!data){
return;
}
data.push(eventId);
completedRequestsMap.set(id, data);
function addCompletedReplay(fuzzId) {
completedReplay.add(fuzzId);
}

function getCompletedRequestsMap() {
return completedRequestsMap;
function addErrorInReplay(fuzzId) {
errorInReplay.add(fuzzId);
}

function clearCompletedRequestMap(){
completedRequestsMap.clear();
function addClearFromPending(fuzzId) {
clearFromPending.add(fuzzId);
}

function clearPendingRequestIdSet(){
pendingRequestIds.clear();
function removeClearFromPending(fuzzId) {
clearFromPending.delete(fuzzId);
}

function IASTCleanup(){
clearCompletedRequestMap();
clearPendingRequestIdSet();

function getCompletedReplay() {
return completedReplay;
}

function generateIASTDataRequest() {
Expand All @@ -63,31 +63,39 @@ function generateIASTDataRequest() {
batchSize = policyInstance.data.vulnerabilityScan.iastScan.probing.batchSize;
}
}
if (getCompletedRequestsMap().size >= batchSize || batchSize < batchthreshold || getPendingRequestIds().length < batchSize) {
if (getCompletedReplay().size >= batchSize || batchSize < batchthreshold || getClearFromPending().length < batchSize) {
batchSize = (2 * batchSize);
}
let object = {};
object['jsonName'] = 'iast-data-request';
object['applicationUUID'] = Agent.getAgent().applicationInfo.applicationUUID;
object["batchSize"] = batchSize ? batchSize : 300;
object['pendingRequestIds'] = getPendingRequestIds();
object['completedRequests'] = Object.fromEntries(getCompletedRequestsMap());
object['completedReplay'] = [...completedReplay];
object['errorInReplay'] = [...errorInReplay];
object['clearFromPending'] = [...clearFromPending];
object['generatedEvent'] = microServiceUtils.getGeneratedEventsMap();

return object;
}

function addGeneratedEvents(applicationUUID, parentId, eventId) {
microServiceUtils.addGeneratedEvents(applicationUUID, parentId, eventId)
}


module.exports = {
getPendingRequestIds,
addPendingRequestId,
removePendingRequestId,
generateIASTDataRequest,

// for last leg
addCompletedRequests,
completedRequestsMapInit,
getCompletedRequestsMap,
removeRequestId,
IASTCleanup
IASTCleanup,
addGeneratedEvents,
addCompletedReplay,
addErrorInReplay,
addClearFromPending,
removeClearFromPending,
getCompletedReplay,
getClearFromPending



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ function startIASTSchedular() {
iastIntervalConst = setInterval(() => {
let data = IASTUtil.generateIASTDataRequest();
let currentTime = Date.now();
let completedListSize = IASTUtil.getCompletedRequestsMap().size;
let pendingListSize = IASTUtil.getPendingRequestIds().length;
let completedListSize = IASTUtil.getCompletedReplay().size;
let pendingListSize = IASTUtil.getClearFromPending().length;
let timeDiffInSeconds = ((currentTime - lastFuzzEventTime) / 1000);
logger.trace("Time difference since last fuzz request:", timeDiffInSeconds);
logger.trace("Completed requests so far:", completedListSize);
Expand Down Expand Up @@ -94,9 +94,9 @@ function handler(json) {
try {
fuzzRequest = JSON.parse(rawFuzzRequest);
fuzzRequest['id'] = json.id;
IASTUtil.addPendingRequestId(json.id);
} catch (error) {
logger.error('Parsing exeception in fuzz request: ', error);
IASTUtil.addErrorInReplay(fuzzRequest.id);

const logMessage = new LogMessage.logMessage(SEVERE, 'Parsing exeception in fuzz request', __filename, error);
require('../../../commonUtils').addLogEventtoBuffer(logMessage);
Expand Down Expand Up @@ -146,7 +146,7 @@ function handleFuzzRequest(fuzzDetails) {

config.headers['nr-csec-parent-id'] = fuzzRequest.id;

IASTUtil.completedRequestsMapInit(fuzzRequest.id);
IASTUtil.addClearFromPending(fuzzRequest.id);

if (fuzzRequest.headers && fuzzRequest.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
logScannedApiId(fuzzRequest.headers[NR_CSEC_FUZZ_REQUEST_ID], fuzzRequest.requestURI)
Expand Down Expand Up @@ -178,7 +178,8 @@ function handleFuzzRequest(fuzzDetails) {
function handleFuzzResponse(response, fuzzDetails) {
const { rawFuzzRequest, fuzzRequest } = fuzzDetails;
if (response) {
IASTUtil.removePendingRequestId(fuzzRequest.id);
IASTUtil.addCompletedReplay(fuzzRequest.id);
IASTUtil.removeClearFromPending(fuzzRequest.id);
response.then(() => {
logger.info('Fuzz success: ' + rawFuzzRequest);
}).catch(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const { Agent } = require('../../agent');
const logs = require('../../logging');
const hc = require('../../health-check');
const fs = require('fs');
const { promisify } = require('../../sec-util');
const { promisify, getAPIID } = 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');
Expand Down Expand Up @@ -227,9 +227,9 @@ SecWebSocket.prototype.dispatch = async function dispatch(event) {
}
if (event.parentId && event.httpRequest.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
try {
let apiId = event.httpRequest.headers[NR_CSEC_FUZZ_REQUEST_ID].split(CSEC_SEP)[0]
let apiId = getAPIID( event.httpRequest.headers[NR_CSEC_FUZZ_REQUEST_ID].split(CSEC_SEP)[0]).apiId
if (apiId == event.apiId) {
IASTUtil.addCompletedRequests(event.parentId, event.id);
IASTUtil.addGeneratedEvents(event.applicationUUID, event.parentId, event.id)
}
} catch (error) {
logger.debug("Error while mapping completedRequests:", error);
Expand Down
90 changes: 90 additions & 0 deletions lib/nr-security-agent/lib/core/micro-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: New Relic Software License v1.0
*/

const { EMPTY_STR, SEMICOLON, CSEC_SEP, FORWARDSLASH } = require("./sec-agent-constants");
const logs = require('./logging');
const logger = logs.getLogger();
let generatedEventsMap = new Map();
const LinkingMetaData = require('./LinkingMetadata');
const crypto = require('crypto');
const secUtils = require('./sec-util');
let hashedEntityGuid = EMPTY_STR;

function addGeneratedEvents(applicationUUID, parentId, eventId) {
let generatedEventobject = generatedEventsMap.get(applicationUUID);
if (generatedEventobject && parentId !== EMPTY_STR && eventId !== EMPTY_STR) {
generatedEventobject.set(parentId, eventId)
}
generatedEventsMap.set(applicationUUID, generatedEventobject);
}

function generatedEventsInit(applicationUUID) {
generatedEventsMap.set(applicationUUID, new Map());
}

function getGeneratedEventsMap(){
return generatedEventsMap;
}

function applicationUUIDFromTraceHeader(traceHeader) {
let firstServiceHeaderUUID = EMPTY_STR;
try {
let firstServiceHeader = traceHeader.split(SEMICOLON)[0];
firstServiceHeaderUUID = firstServiceHeader.split(FORWARDSLASH)[0];
} catch (error) {
logger.debug("Unable to get applicationUUID from trace header", error);
}
return firstServiceHeaderUUID;
}

function prepareGeneratedEventforIncomingReq(csecFuzzHeader, csecParentId, csecTracingHeader) {
if (csecFuzzHeader) {
let guidSHA256 = secUtils.getAPIID(csecFuzzHeader.split(CSEC_SEP)[0]).guid;
let linkingMetadata = LinkingMetaData.getLinkingMetadata();
let entityGuid = linkingMetadata['entity.guid'];
let verified = verfiyGUID(entityGuid, guidSHA256);
if(!verified){
return;
}
}
let firstServiceHeaderUUID = applicationUUIDFromTraceHeader(csecTracingHeader);
generatedEventsInit(firstServiceHeaderUUID);
}


function verfiyGUID(entityGuid, guidSHA256) {
let verifyFlag = false;
try {
const hash = getHashedEntityGUID(entityGuid);
if (hash == guidSHA256) {
verifyFlag = true;
}
} catch (error) {
logger.debug("Error while verifying entityGuid SHA", error);
}

return verifyFlag;
}

function getHashedEntityGUID(entityGuid){
if(hashedEntityGuid!=EMPTY_STR){
return hashedEntityGuid;
}
try {
const sha256Hash = crypto.createHash('sha256');
sha256Hash.update(entityGuid);
hashedEntityGuid = sha256Hash.digest('hex');
} catch (error) {
logger.debug("Error while calculated sha256 of entity Guid", error);
}
return hashedEntityGuid;

}

module.exports = {
prepareGeneratedEventforIncomingReq,
addGeneratedEvents,
getGeneratedEventsMap
}
2 changes: 2 additions & 0 deletions lib/nr-security-agent/lib/core/sec-agent-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ module.exports = {
HIGH: 'HIGH',
CRITICAL: 'CRITICAL',
HYPHEN: '-',
SEMICOLON: ';',
FORWARDSLASH: '/',

LOG_MESSAGES: {
LOADED_CSEC_ENVS_MSG: '[STEP-1][COMPLETE][env] Environment Information Gathering Done.',
Expand Down
19 changes: 18 additions & 1 deletion lib/nr-security-agent/lib/core/sec-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ function sendEvent(secEvent) {
}

}
/**
* utility to get apiId and guid Identifier object
* @param {*} identifier
* @returns
*/
function getAPIID(identifier) {
let identifierObject = {};
identifierObject.apiId = EMPTY_STR;
identifierObject.guid = EMPTY_STR;
const data = identifier.split(COLON);
if (data.length > 1) {
identifierObject.guid = data[0];
identifierObject.apiId = data[1];
}
return identifierObject;
}

module.exports = {
setLogger,
Expand All @@ -176,5 +192,6 @@ module.exports = {
sleep,
generateSecEvent,
sendEvent,
generateExitEvent
generateExitEvent,
getAPIID
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.3.0",
"description": "New Relic Security Agent for Node.js",
"main": "index.js",
"jsonVersion": "1.2.0",
"jsonVersion": "2.0.0",
"contributors": [
{
"name": "Sumit Suthar",
Expand Down
Loading