policy: 1.0.0
info:
  title: appsentinels-post-proc
  name: appsentinels-post-proc
  version: 1.0.0
  description: Policy to instrument IBM datapower-api-gateway(1.0.1)
attach:
  - rest
  - soap
properties: { }
gateways:
  - datapower-api-gateway
assembly:
  execute:
    - gatewayscript:
        version: 2.0.0
        title: appsentinels IBM agent
        description: Policy to instrument IBM datapower-api-gateway
        source: "// ***************************************************************************\r\n//  File Name: appsentinels-post-proc.js\r\n//  Description: AppSentinels IBM Datapower API post-processing GatewayScript \r\n//               for auth mode.\r\n//\r\n//  Copyright (c) 2024 AppSentinels Inc\r\n//  All rights reserved.\r\n// \r\n//  This source code is the property of AppSentinels Inc and may not be used,\r\n//  copied, or distributed without prior written permission from AppSentinels.\r\n//  NOTICE: All information contained herein is, and remains the property\r\n//  of AppSentinels. The intellectual and technical concepts contained\r\n//  herein are proprietary to AppSentinels and may be covered by U.S. and\r\n//  Foreign Patents, patents in process, and are protected by trade secret\r\n//  or copyright law. Dissemination of this information or reproduction\r\n//  of this material is strictly forbidden unless prior written permission\r\n//  is obtained from AppSentinels.\r\n//\r\n//  Author: AppSentinels\r\n//  Creation Date: Jan 2024\r\n//\r\n// ***************************************************************************\r\n\r\n// Gateway script to process the response\r\n\r\n// Configuration\r\nconst config = {\r\n    // Deployment mode - 'auth' or 'transparent'\r\n    deploymentMode: 'transparent',\r\n\r\n    // Appsentinels controller configuration\r\n    controllerHost: 'AS_CONTROLLER_URL',\r\n    controllerPort: '9004',\r\n    scheme: 'http', // or 'https'\r\n\r\n    // TLS client profile name - needed for HTTPS communication\r\n    tlsClientProfileName: 'client:AS_TLS_CLIENT_PROFILE',\r\n\r\n    // Payload configuration\r\n    maxSupportedPayload: 131072,\r\n    supportedContentTypes: [\"json\", \"xml\", \"graphql\", \"form\"],\r\n\r\n    // Sensor visibility configuration\r\n    pluginVersion: '1.0.5',\r\n    sensorHostname: 'datapower-gateway',\r\n    sensorInstanceName: 'datapower-apigw',\r\n\r\n    // Health check enabled or not\r\n    healthCheckDisabled: true,\r\n    // Don't send log if health check response not received within this threshold\r\n    healthCheckThreshold: 2000, // in milliseconds\r\n\r\n    // Configuration for IBM message queue\r\n    mqPostLogToQueue: false,\r\n    mqQueueManager: '<queue-manager-name>',\r\n    mqQueueName: '<queue-name>',\r\n    mqScheme: 'idgmq'\r\n}\r\n\r\n// function to build URL\r\nfunction buildUrl(path) {\r\n    return `${config.scheme}://${config.controllerHost}:${config.controllerPort}${path}`;\r\n}\r\n\r\nfunction buildMqUrl() {\r\n    return `${config.mqScheme}://${config.mqQueueManager}?RequestQueue=${config.mqQueueName}`;\r\n}\r\n\r\nconst REMOTE_CONTROLLER_ENDPOINT_URL_AUTH_MODE = buildUrl('/nginxlog?sensor=datapower&cap=nohb');\r\nconst REMOTE_CONTROLLER_ENDPOINT_URL_TRANSPARENT_MODE = buildUrl('/mergedlog?sensor=datapower&cap=nohb');\r\nconst MAX_SUPPORTED_PAYLOAD = config.maxSupportedPayload;\r\nconst SUPPORTED_CONTENT_TYPES = config.supportedContentTypes;\r\nconst APPSENTINELS_PLUGIN_VERSION = config.pluginVersion;\r\n\r\nconst SENSOR_HOSTNAME = config.sensorHostname;\r\nconst SENSOR_INSTANCE_NAME = config.sensorInstanceName;\r\n\r\n// TLS Client profile name\r\nconst TLS_CLIENT_PROFILE_NAME = config.tlsClientProfileName;\r\n\r\n// TLS enabled or not\r\nconst TLS_ENABLED = (config.scheme == 'https');\r\nconst isAuthMode = (config.deploymentMode.toLocaleLowerCase() == 'auth');\r\n\r\n// IBM MQ configuration\r\nconst POST_LOG_TO_QUEUE = config.mqPostLogToQueue;\r\nconst mqTargetUrl = buildMqUrl();\r\n\r\nvar urlopen = require('urlopen');\r\nvar sm = require('system-metadata');\r\n\r\nfunction validate_content_type_to_log(headers) {\r\n    let contentTypeHeader = headers[\"Content-Type\"];\r\n    if (!contentTypeHeader) {\r\n        console.notice(\"[AppSentinels] content-type header not present, returning true\");\r\n        console.notice(\"[AppSentinels] response headers: \", JSON.stringify(headers));\r\n        return true;\r\n    }\r\n    for (let i in SUPPORTED_CONTENT_TYPES) {\r\n        if (contentTypeHeader.indexOf(SUPPORTED_CONTENT_TYPES[i]) > -1) {\r\n            console.info(\"[AppSentinels] content-type matched, returning true\");\r\n            return true;\r\n        }\r\n    }\r\n    console.notice(\"[AppSentinels] content-type not matched, returning false\");\r\n    return false;\r\n}\r\n\r\nfunction postLogToQueue(logPayload) {\r\n    if (!POST_LOG_TO_QUEUE) {\r\n        return;\r\n    }\r\n\r\n    console.info(\"[AppSentinels] Attempting to send to MQ. Payload:\", JSON.stringify(logPayload));\r\n    console.info(\"[AppSentinels] MQ URL:\", mqTargetUrl);\r\n\r\n    const options = {\r\n        target: mqTargetUrl,\r\n        method: 'PUT',\r\n        headers: {\r\n            'Content-Type': 'application/json',\r\n            'DataType': 'MQSTR',\r\n            'MessageType': '8',\r\n            'Format': 'MQSTR',\r\n            'CharacterSet': '1208',\r\n            'Persistence': 'MQPER_PERSISTENT'\r\n        },\r\n        timeout: 1,\r\n        data: JSON.stringify(logPayload)\r\n    };\r\n\r\n    console.info(\"[AppSentinels] MQ Headers:\", JSON.stringify(options.headers));\r\n\r\n    urlopen.open(options, function(error, response) {\r\n        if (error) {\r\n            console.error(\"[AppSentinels] Failed to send to MQ:\", error);\r\n            console.error(\"[AppSentinels] Error details:\", error.stack || error.message || \"No error details\");\r\n        } //else {\r\n          //  console.debug(\"[AppSentinels] Successfully sent to MQ\");\r\n          //  Log response details if available\r\n          //  if (response) {\r\n          //      response.readAsBuffer(function(readError, buffer) {\r\n          //         if (!readError && buffer) {\r\n          //             console.debug(\"[AppSentinels] MQ response:\", buffer.toString());\r\n          //         }\r\n          //      });\r\n          //  }\r\n        // }\r\n    });\r\n}\r\n\r\nfunction check_controller_health() {\r\n    var lastHealthCheckTime = sm.appsentinels.lastHealthCheckTime || 0;\r\n    var currentTime = new Date().getTime();\r\n    var diff = currentTime - lastHealthCheckTime;\r\n    console.info(\"[AppSentinels] lastHealthCheckTime: \", lastHealthCheckTime, \"currentTime: \", currentTime, \"diff: \", diff);\r\n    if (diff >= config.healthCheckThreshold) {\r\n        return false;\r\n    }\r\n\r\n    return true;\r\n}\r\n\r\nfunction process_response(response_headers, body64, truncated) {\r\n    try {\r\n        var request_headers = context.get(\"request.headers\");\r\n        var method = context.get(\"request.verb\");\r\n        var uri = context.get(\"request.path\");\r\n        var querystring = context.get(\"request.querystring\");\r\n        var request_id = context.get(\"session.globalTransactionID\");\r\n\r\n        var started_at = context.get(\"as_request_started_at\");\r\n        var end_time = new Date().getTime();\r\n        var latency = end_time - started_at;\r\n       // var client_ip = context.get(\"session.clientAddress\");\r\n\r\n        var req_truncated = context.get(\"as_request_body_truncated\");\r\n\r\n        var status_code = context.get(\"message.status.code\");\r\n        console.info(\"[AppSentinels] headers: \", response_headers, \"statusCode: \", status_code);\r\n\r\n        var request_body = context.get(\"as_request_body\");\r\n        console.info(\"[AppSentinels] request_body read from context: \", request_body);\r\n        if (request_body == null || request_body == undefined) {\r\n            console.info(\"[AppSentinels], request_body is null or undefined\");\r\n            request_body = \"\";\r\n        }\r\n\r\n        uri = uri + \"?\" + querystring;\r\n        request_headers[\"X-Request-Id\"] = request_id;\r\n        response_headers[\"X-Response-Id\"] = request_id;\r\n        var log_headers = {\r\n            \"Content-Type\": \"application/json\",\r\n            \"Connection\": \"keep-alive\",\r\n            \"Keep-Alive\": \"timeout=5, max=100\"\r\n        };\r\n\r\n        response_headers[\"X-As-Sensor-Instance\"] = SENSOR_INSTANCE_NAME;\r\n        response_headers[\"X-As-Sensor-Hostname\"] = SENSOR_HOSTNAME;\r\n        response_headers[\"X-As-Plugin-Version\"] =  APPSENTINELS_PLUGIN_VERSION;\r\n\r\n        if (truncated) {\r\n            response_headers[\"X-As-Resp-Truncated-Body\"] = \"true\";\r\n        }\r\n        if (req_truncated) {\r\n            request_headers[\"X-As-Req-Truncated-Body\"] = \"true\";\r\n        }\r\n\r\n        var payload = {\r\n            request: {\r\n                method: method,\r\n                headers: request_headers,\r\n                uri: uri,\r\n                body64: request_body,\r\n            },\r\n            response: {\r\n                body64: body64,\r\n                status: status_code,\r\n                headers: response_headers,\r\n            },\r\n            client_ip: context.get(\"session.clientAddress\"),\r\n            client_port: 0,\r\n            started_at: started_at,\r\n            latencies: {\r\n                request: latency,\r\n            }\r\n        }\r\n\r\n        if (POST_LOG_TO_QUEUE) {\r\n            postLogToQueue(payload);\r\n        } else {\r\n            let targetUrl;\r\n            if (isAuthMode) {\r\n                targetUrl = REMOTE_CONTROLLER_ENDPOINT_URL_AUTH_MODE;\r\n            } else {\r\n                targetUrl = REMOTE_CONTROLLER_ENDPOINT_URL_TRANSPARENT_MODE;\r\n            }\r\n            \r\n            var request_payload = {\r\n                target: targetUrl,\r\n                method: 'POST',\r\n                headers: log_headers,\r\n                contentType: 'application/json',\r\n                timeout: 1,\r\n                data: payload\r\n            };\r\n\r\n            if (TLS_ENABLED) {\r\n                request_payload.sslClientProfile = TLS_CLIENT_PROFILE_NAME;\r\n            }\r\n\r\n            if (config.healthCheckDisabled || check_controller_health()) {\r\n                console.info(\"[AppSentinels] [APIGW] Controller is healthy\");\r\n                urlopen.open(request_payload, function (error, response) {\r\n                    if (error) {\r\n                        console.error(\"[AppSentinels] [APIGW] Failed to send log to Controller: \", error.errorMessage);\r\n                    }\r\n                });\r\n            } else {\r\n                console.warn(\"[AppSentinels] [APIGW] Controller is not healthy\");\r\n            }\r\n        }\r\n    } catch (e) {\r\n        console.error(\"[AppSentinels] Exception occurred in process_response\");\r\n        console.error(\"[AppSentinels] Error occurred: \", e.message);\r\n        console.error(\"[AppSentinels] Stack trace: \", e.stack);\r\n    }\r\n}\r\n\r\n// Main response processing\r\ntry {\r\n    // Get the saved request context\r\n    console.info(\"[AppSentinels] Processing response\")\r\n    var request_skipped = context.get(\"as_request_skipped\");\r\n    if (request_skipped) {\r\n        console.warn(\"[AppSentinels] Skipping response because request was also skipped\");\r\n        return;\r\n    }\r\n\r\n    var response_headers = context.get(\"message.headers\");\r\n    var truncated = false;\r\n    var body64 = \"\";\r\n    if (!validate_content_type_to_log(response_headers)) {\r\n        process_response(response_headers, \"\", true)\r\n    } else {\r\n        context.message.body.readAsBuffer(function (error, buffer) {\r\n            if (error) {\r\n                console.error(\"[AppSentinels] Failed to read the response body: \", error);\r\n            } else {\r\n                if (buffer != null && buffer != undefined && buffer.length > 0) {\r\n                    console.info(\"[AppSentinels] response body length is \", buffer.length);\r\n                    // When there is no data to read, readAsBuffer sets buffer to 0x6e756c6c which is 'null'\r\n                    const is_buffer_null = buffer.length == 4 && (buffer[0] == 0x6e && buffer[1] == 0x75 && buffer[2] == 0x6c && buffer[3] == 0x6c);\r\n                    if (!is_buffer_null) {\r\n                        if (buffer.length > MAX_SUPPORTED_PAYLOAD) {\r\n                            truncated = true;\r\n                            console.info(\"[AppSentinels] Body truncated\");\r\n                        } else {\r\n                            try {\r\n                                body64 = buffer.toString(\"base64\");\r\n                            } catch (e) {\r\n                                console.error(\"[AppSentinels] Error occurred: \", e.message);\r\n                                body64 = \"\";\r\n                                truncated = true;\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n\r\n                process_response(response_headers, body64, truncated);\r\n            }\r\n        });\r\n    }\r\n} catch (e) {\r\n    console.error(\"[AppSentinels] Exception occurred\");\r\n    console.error(\"[AppSentinels] Error occurred: \", e.message);\r\n    console.error(\"[AppSentinels] Stack trace: \", e.stack);\r\n}"