diff --git a/index.js b/index.js index 53d7b2f..1e9c98b 100644 --- a/index.js +++ b/index.js @@ -29,4 +29,4 @@ module.exports = { PxCdEnforcer: require('./lib/pxcdenforcer'), PxCdFirstParty: require('./lib/pxcdfirstparty'), addNonce: require('./lib/nonce') -}; \ No newline at end of file +}; diff --git a/lib/enums/CIVersion.js b/lib/enums/CIVersion.js index 324696c..9df466a 100644 --- a/lib/enums/CIVersion.js +++ b/lib/enums/CIVersion.js @@ -6,4 +6,4 @@ const CIVersion = { module.exports = { CIVersion -}; \ No newline at end of file +}; diff --git a/lib/enums/ErrorType.js b/lib/enums/ErrorType.js new file mode 100644 index 0000000..3e583f2 --- /dev/null +++ b/lib/enums/ErrorType.js @@ -0,0 +1,4 @@ +const ErrorType = { + WRITE_REMOTE_CONFIG: 'write_remote_config', +}; +module.exports = { ErrorType }; diff --git a/lib/pxapi.js b/lib/pxapi.js index ea6bfb1..ede2ce7 100644 --- a/lib/pxapi.js +++ b/lib/pxapi.js @@ -1,7 +1,7 @@ 'use strict'; const pxUtil = require('./pxutil'); const pxHttpc = require('./pxhttpc'); - +const os = require('os'); const S2SErrorInfo = require('./models/S2SErrorInfo'); const { ModuleMode } = require('./enums/ModuleMode'); const PassReason = require('./enums/PassReason'); @@ -66,9 +66,13 @@ function buildRequestData(ctx, config) { cookie_origin: ctx.cookieOrigin, request_cookie_names: ctx.requestCookieNames, request_id: ctx.requestId, + hostname: os.hostname() }, }; + if (config.REMOTE_CONFIG_ENABLED && config.REMOTE_CONFIG_ID) { + data.additional.px_remote_config_id = config.REMOTE_CONFIG_ID; + } if (ctx.graphqlData) { data.additional[GQL_OPERATIONS_FIELD] = ctx.graphqlData; } @@ -190,6 +194,10 @@ function evalByServerCall(ctx, config, callback) { return callback(ScoreEvaluateAction.UNEXPECTED_RESULT); } ctx.pxhdServer = res.pxhd; + if (config.REMOTE_CONFIG_ENABLED && res.remote_config && res.remote_config.id === config.REMOTE_CONFIG_ID) { + ctx.remoteConfigLatestVersion = res.remote_config.version; + } + if (res.data_enrichment) { ctx.pxde = res.data_enrichment; ctx.pxdeVerified = true; diff --git a/lib/pxclient.js b/lib/pxclient.js index 784cfb7..23445f4 100644 --- a/lib/pxclient.js +++ b/lib/pxclient.js @@ -4,6 +4,11 @@ const pxUtil = require('./pxutil'); const pxHttpc = require('./pxhttpc'); const { ActivityType } = require('./enums/ActivityType'); const { CIVersion } = require('./enums/CIVersion'); +const { makeAsyncRequest } = require('./request'); +const { LoggerSeverity } = require('./enums/LoggerSeverity'); +const { PxExternalLogsParser } = require('./pxexternallogparser'); +const PxLogger = require('./pxlogger'); +const PxConfig = require('./pxconfig'); const { CI_VERSION_FIELD, CI_SSO_STEP_FIELD, @@ -12,18 +17,26 @@ const { GQL_OPERATIONS_FIELD, APP_USER_ID_FIELD_NAME, JWT_ADDITIONAL_FIELDS_FIELD_NAME, - CROSS_TAB_SESSION, + CROSS_TAB_SESSION, HOST_NAME, EXTERNAL_LOGGER_SERVICE_PATH, INVALID_VERSION_NUMBER, } = require('./utils/constants'); +const { ErrorType } = require('./enums/ErrorType'); class PxClient { constructor() { this.activitiesBuffer = []; + this._remoteConfigLatestVersion = INVALID_VERSION_NUMBER; } init() { //stub for overriding } + get remoteConfigLatestVersion() { + return this._remoteConfigLatestVersion; + } + set remoteConfigLatestVersion(value) { + this._remoteConfigLatestVersion = value; + } /** * generateActivity - returns a JSON representing the activity. * @param {string} activityType - name of the activity @@ -43,7 +56,7 @@ class PxClient { }; details['request_id'] = ctx.requestId; - this.addAdditionalFieldsToActivity(details, ctx); + this.addAdditionalFieldsToActivity(details, ctx, config); if (activityType !== ActivityType.ADDITIONAL_S2S) { activity.headers = pxUtil.formatHeaders(ctx.headers, config.SENSITIVE_HEADERS); activity.pxhd = (ctx.pxhdServer ? ctx.pxhdServer : ctx.pxhdClient) || undefined; @@ -58,7 +71,47 @@ class PxClient { return activity; } - addAdditionalFieldsToActivity(details, ctx) { + async fetchRemoteConfig(config) { + const maxRetries = 5; + for (let i = 0; i < maxRetries; i++) { + try { + + const remoteConfigObject = await this.getRemoteConfigObject(config); + return { + px_remote_config_id: remoteConfigObject.id, + px_remote_config_version: remoteConfigObject.version, + ...remoteConfigObject.configValue + }; + } catch (e) { + const message = `Error fetching remote configurations: ${e.message}`; + this.sendRemoteLog(message, LoggerSeverity.DEBUG, ErrorType.WRITE_REMOTE_CONFIG, config); + if (i < maxRetries - 1) { // if it's not the last retry + await new Promise(resolve => setTimeout(resolve, 1000)); // wait for 1 second before retrying + } else { + config.logger.error('Failed to fetch remote configuration after 5 attempts'); + } + } + } + } + + async getRemoteConfigObject(config) { + const callData = { + url: `https://sapi-${config.px_app_id}.perimeterx.net/config/`, + headers: { 'Authorization': `Bearer ${config.px_remote_config_secret}`, 'Accept-Encoding': '' }, + timeout: 20000, + }; + const res = await makeAsyncRequest({ url: callData.url, headers: callData.headers, timeout: callData.timeout, method: 'GET' }, config); + const remoteConfigObject = JSON.parse(res.body); + if (remoteConfigObject.id !== config.px_remote_config_id) { + throw new Error(`Remote configuration id mismatch. Expected: ${config.px_remote_config_id}, Actual: ${remoteConfigObject.id}`); + } + if (this._remoteConfigLatestVersion !== INVALID_VERSION_NUMBER && remoteConfigObject.version !== this._remoteConfigLatestVersion) { + throw new Error(`Remote configuration version mismatch. Expected: ${this._remoteConfigLatestVersion}, Actual: ${remoteConfigObject.version}`); + } + return remoteConfigObject; + } + + addAdditionalFieldsToActivity(details, ctx, config) { if (ctx.additionalFields && ctx.additionalFields.loginCredentials) { const { loginCredentials } = ctx.additionalFields; details[CI_VERSION_FIELD] = loginCredentials.version; @@ -84,6 +137,12 @@ class PxClient { } } + if (config.remoteConfigVersion !== INVALID_VERSION_NUMBER) { + details['px_remote_config_version'] = config.remoteConfigVersion; + } + + details[HOST_NAME] = os.hostname(); + if (ctx.cts) { details[CROSS_TAB_SESSION] = ctx.cts; } @@ -182,5 +241,26 @@ class PxClient { cb(); } } + + sendRemoteLog(message, severity, errorType, config) { + const pxLogger = config.logger ? config.logger : new PxLogger(config); + const enforcerConfig = new PxConfig(config, pxLogger); + const reqHeaders = { + 'Authorization': 'Bearer ' + enforcerConfig.config.LOGGER_AUTH_TOKEN, + 'Content-Type': 'application/json', + }; + const logParser = new PxExternalLogsParser( { appId: config.PX_APP_ID, remoteConfigId: config.REMOTE_CONFIG_ID, remoteConfigVersion: config.REMOTE_CONFIG_VERSION }); + const logs = [{ message, severity, errorType }]; + const enrichedLogs = logParser.enrichLogs(logs); + pxHttpc.callServer( + enrichedLogs, + reqHeaders, + EXTERNAL_LOGGER_SERVICE_PATH, + 'remote-log', + enforcerConfig.conf, + null, + false, + ); + } } module.exports = PxClient; diff --git a/lib/pxconfig.js b/lib/pxconfig.js index 695c8c8..c3d1fc5 100644 --- a/lib/pxconfig.js +++ b/lib/pxconfig.js @@ -7,7 +7,7 @@ const { LoggerSeverity } = require('./enums/LoggerSeverity'); const { DEFAULT_COMPROMISED_CREDENTIALS_HEADER_NAME } = require('./utils/constants'); const { CIVersion } = require('./enums/CIVersion'); const { LoginSuccessfulReportingMethod } = require('./enums/LoginSuccessfulReportingMethod'); - +const { INVALID_VERSION_NUMBER } = require('./utils/constants'); class PxConfig { constructor(params, logger) { this.PX_INTERNAL = pxInternalConfig(); @@ -16,7 +16,6 @@ class PxConfig { this.config = this.mergeParams(params); this.config.FILTER_BY_METHOD = this.config.FILTER_BY_METHOD.map((v) => v.toUpperCase()); this.config.logger = this.logger; - this.config.WHITELIST_EXT = [...this.PX_INTERNAL.STATIC_FILES_EXT, ...this.PX_DEFAULT.WHITELIST_EXT]; if (this.PX_DEFAULT.TESTING_MODE) { @@ -104,7 +103,12 @@ class PxConfig { ['JWT_HEADER_ADDITIONAL_FIELD_NAMES', 'px_jwt_header_additional_field_names'], ['CUSTOM_IS_SENSITIVE_REQUEST', 'px_custom_is_sensitive_request'], ['FIRST_PARTY_TIMEOUT_MS', 'px_first_party_timeout_ms'], - ['URL_DECODE_RESERVED_CHARACTERS', 'px_url_decode_reserved_characters'] + ['URL_DECODE_RESERVED_CHARACTERS', 'px_url_decode_reserved_characters'], + ['REMOTE_CONFIG_ENABLED', 'px_remote_config_enabled'], + ['REMOTE_CONFIG_AUTH_TOKEN', 'px_remote_config_auth_token'], + ['REMOTE_CONFIG_ID', 'px_remote_config_id'], + ['REMOTE_CONFIG_VERSION', 'px_remote_config_version'], + ['LOGGER_AUTH_TOKEN', 'px_logger_auth_token'] ]; configKeyMapping.forEach(([targetKey, sourceKey]) => { @@ -365,7 +369,12 @@ function pxDefaultConfig() { JWT_HEADER_ADDITIONAL_FIELD_NAMES: [], CUSTOM_IS_SENSITIVE_REQUEST: '', FIRST_PARTY_TIMEOUT_MS: 4000, - URL_DECODE_RESERVED_CHARACTERS: false + URL_DECODE_RESERVED_CHARACTERS: false, + REMOTE_CONFIG_ENABLED: false, + REMOTE_CONFIG_AUTH_TOKEN: '', + REMOTE_CONFIG_ID: '', + REMOTE_CONFIG_VERSION: INVALID_VERSION_NUMBER, + LOGGER_AUTH_TOKEN: '' }; } diff --git a/lib/pxenforcer.js b/lib/pxenforcer.js index c15a902..a6d7e2e 100644 --- a/lib/pxenforcer.js +++ b/lib/pxenforcer.js @@ -261,6 +261,10 @@ class PxEnforcer { pxApi.evalByServerCall(ctx, this._config, (action) => { ctx.riskRtt = Date.now() - startRiskRtt; + if (this.config.config.REMOTE_CONFIG_ENABLED) { + this.pxClient.remoteConfigLatestVersion = ctx.remoteConfigLatestVersion; + } + if (action === ScoreEvaluateAction.UNEXPECTED_RESULT) { this.logger.debug('perimeterx score evaluation failed. unexpected error. passing traffic'); return callback(ScoreEvaluateAction.S2S_PASS_TRAFFIC); @@ -308,7 +312,6 @@ class PxEnforcer { handleVerification(ctx, req, res, cb) { const verified = ctx.score < this._config.BLOCKING_SCORE; - if (res) { const setCookie = res.getHeader('Set-Cookie') ? res.getHeader('Set-Cookie') : ''; const secure = this._config.PXHD_SECURE ? '; Secure' : ''; @@ -639,7 +642,7 @@ class PxEnforcer { cb(htmlTemplate); }); } - + sendHeaderBasedLogs(pxCtx, config, req) { // eslint-disable-line // Feature has been removed, function definition for backwards compatibility. } diff --git a/lib/pxexternallogparser.js b/lib/pxexternallogparser.js new file mode 100644 index 0000000..a3715eb --- /dev/null +++ b/lib/pxexternallogparser.js @@ -0,0 +1,25 @@ +class PxExternalLogsParser { + constructor(appId, remoteConfigId, remoteConfigVersion) { + this.appId = appId; + this.remoteConfigId = remoteConfigId; + this.remoteConfigVersion = remoteConfigVersion; + } + enrichLogs(logs) { + const enrichedLogs = logs.map((log) => { + return this.enrichLogRecord(log); + }); + return enrichedLogs; + } + + enrichLogRecord(log) { + return {...log, ...{ + messageTimestamp: new Date().toISOString(), + appID: this.appId, + container: 'enforcer', + configID: this.remoteConfigId, + configVersion: this.remoteConfigVersion + }}; + } +} + +module.exports = { PxExternalLogsParser }; diff --git a/lib/pxhttpc.js b/lib/pxhttpc.js index 0b4e47f..9517590 100644 --- a/lib/pxhttpc.js +++ b/lib/pxhttpc.js @@ -33,6 +33,9 @@ function callServer(data, headers, uri, callType, config, callback, failOnEmptyB try { request.post(callData, config, function (err, response) { + if (callType === 'remote-log') { + return; + } if (err) { if (err.toString().toLowerCase().includes('timeout')) { return callback('timeout'); diff --git a/lib/request.js b/lib/request.js index 5b9e34c..c537e2a 100644 --- a/lib/request.js +++ b/lib/request.js @@ -16,6 +16,16 @@ exports.post = (options, config, cb) => { return makeRequest(options, config, cb); }; +exports.makeAsyncRequest = (options, config) => { + return new Promise((resolve, reject) => { + makeRequest(options, config, (err, res) => { + if (err) { + return reject(err); + } + return resolve(res); + }); + }); +}; function makeRequest(options, config, cb) { if (options.url && options.url.startsWith('https://')) { options.agent = config.agent || httpsKeepAliveAgent; @@ -23,4 +33,4 @@ function makeRequest(options, config, cb) { options.agent = new http.Agent(); } p(options, cb); -} \ No newline at end of file +} diff --git a/lib/utils/constants.js b/lib/utils/constants.js index aa8de74..5ddb231 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -36,6 +36,10 @@ const JWT_ADDITIONAL_FIELDS_FIELD_NAME = 'jwt_additional_fields'; const CROSS_TAB_SESSION = 'cross_tab_session'; const COOKIE_SEPARATOR = ';'; +const EXTERNAL_LOGGER_SERVICE_PATH = '/enforcer-logs/'; +const INVALID_VERSION_NUMBER = -1; +const HOST_NAME = 'hostname'; + module.exports = { MILLISECONDS_IN_SECOND, SECONDS_IN_MINUTE, @@ -66,4 +70,7 @@ module.exports = { JWT_ADDITIONAL_FIELDS_FIELD_NAME, CROSS_TAB_SESSION, COOKIE_SEPARATOR, + EXTERNAL_LOGGER_SERVICE_PATH, + INVALID_VERSION_NUMBER, + HOST_NAME };