global-policy: 1.0.0
info:
  title: appsentinels-pre-proc-v5
  name: appsentinels-pre-proc-v5
  version: 1.0.0
  mode: after-builtin
gateways:
  - datapower-gateway
assembly: 
  execute:
    - gatewayscript:
        version: 1.0.0
        title: appsentinels IBM agent
        source: "// ***************************************************************************\r\n//  File Name: appsentinels-pre-proc-v5.js\r\n//  Description: AppSentinels IBM Datapower API pre-processing GatewayScript.\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 request\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    // Fail-open configuration\r\n    failOpenEnabled: true,\r\n\r\n    // Payload configuration\r\n    maxSupportedPayload: 131072,\r\n    supportedContentTypes: [\"json\", \"xml\", \"graphql\", \"form\"],\r\n    bypassMethods: [\"OPTIONS\", \"HEAD\"],\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// function to build URL\r\nfunction buildUrl(path) {\r\n    return `${config.scheme}://${config.controllerHost}:${config.controllerPort}${path}`;\r\n}\r\n\r\nconst REMOTE_CONTROLLER_ENDPOINT_URL = buildUrl('/auth?sensor=datapower');\r\nconst REMOTE_CONTROLLER_ENDPOINT_URL_RESPONSE = buildUrl('/nginxlog?sensor=datapower&cap=nohb');\r\nconst MAX_SUPPORTED_PAYLOAD = config.maxSupportedPayload;\r\nconst SUPPORTED_CONTENT_TYPES = config.supportedContentTypes;\r\nconst BYPASS_METHOD = config.bypassMethods;\r\nconst APPSENTINELS_PLUGIN_VERSION = config.pluginVersion;\r\n\r\n// SENSOR_HOSTNAME and SENSOR_INSTANCE_NAME are used for Sensor visibility.\r\n// These constant have been defined with default values. These values can be\r\n// modified before loading the GatewayScript. Please configure appropriate\r\n// hostname and instance name values.\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// Fail-open configuration - set it to false to disable fail-open mode\r\nconst FAIL_OPEN_ENABLED = config.failOpenEnabled;\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// DataPower V5-specific compatibility script\r\n// Import required modules using V5 style\r\nvar sm = require('service-metadata');\r\nvar hm = require('header-metadata');\r\nvar apic = require('local://isp/policy/apim.custom.js');\r\nvar urlopen = require('urlopen');\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        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.info(\"[AppSentinels] content-type not matched, returning false\");\r\n    return false;\r\n}\r\n\r\nfunction should_bypass_method(method) {\r\n    return BYPASS_METHOD.includes(method);\r\n}\r\n\r\nfunction process_request(method, headers, body64, truncated, started_at) {\r\n    try {\r\n        console.info(\"[AppSentinels] Processing request\");\r\n        apic.setvariable(\"as_request_body\", body64);\r\n        apic.setvariable(\"as_request_body_truncated\", truncated);\r\n\r\n        // If not in auth mode, return\r\n        if (!isAuthMode) return;\r\n\r\n        // Get request information\r\n        var uri = apic.getvariable(\"request.path\");\r\n        var client_ip = apic.getvariable(\"request.headers.X-Client-IP\");\r\n        var querystring = apic.getvariable(\"request.querystring\");\r\n        var request_id = apic.getContext('request.headers.x-global-transaction-id');\r\n\r\n        console.info(\"[AppSentinels] [Auth Mode] uri: \" + uri);\r\n        console.info(\"[AppSentinels] [Auth Mode] querystring: \" + querystring);\r\n\r\n        headers[\"X-Request-Id\"] = request_id;\r\n        headers[\"X-As-Plugin-Version\"] = APPSENTINELS_PLUGIN_VERSION;\r\n        headers[\"X-As-Sensor-Instance\"] = SENSOR_INSTANCE_NAME;\r\n        headers[\"X-As-Sensor-Hostname\"] = SENSOR_HOSTNAME;\r\n\r\n        if (truncated) {\r\n            headers[\"X-As-Req-Truncated-Body\"] = \"true\";\r\n        }\r\n\r\n        var req_headers = {\r\n            \"Content-Type\": \"application/json\",\r\n        };\r\n\r\n        var request_payload = {\r\n            target: REMOTE_CONTROLLER_ENDPOINT_URL,\r\n            method: \"POST\",\r\n            headers: req_headers,\r\n            contentType: 'application/json',\r\n            timeout: 10, // Increased timeout to allow more time for response\r\n            data: {\r\n                request: {\r\n                    method: method,\r\n                    headers: headers,\r\n                    uri: uri,\r\n                    querystring: querystring,\r\n                    body64: body64,\r\n                    client_ip: client_ip,\r\n                    client_port: 0,\r\n                    started_at: started_at,\r\n                    latencies: {\r\n                        request: 0,\r\n                    }\r\n                }\r\n            }\r\n        };\r\n\r\n        if (TLS_ENABLED) {\r\n            request_payload.sslClientProfile = TLS_CLIENT_PROFILE_NAME;\r\n        }\r\n\r\n        console.info(\"[AppSentinels] Sending data: \", request_payload);\r\n        try {\r\n            urlopen.open(request_payload, function (error, response) {\r\n                if (error) {\r\n                    console.error(\"[AppSentinels] [Auth Mode] Failed to send log to Controller: \" + error);\r\n                    return;\r\n                }\r\n\r\n                // To prevent \"Error attempting to read the urlopen response data\", use readAsBuffer instead of direct access\r\n                try {\r\n                        var response_status = response.statusCode;\r\n                        var block_request = false;\r\n                        \r\n                        if (FAIL_OPEN_ENABLED) {\r\n                            if (response_status == 401) {\r\n                                block_request = true;\r\n                            }\r\n                        } else {\r\n                            if (response_status != 200) {\r\n                                block_request = true;\r\n                            }\r\n                        }\r\n                        \r\n                        if (block_request) {\r\n                            console.info(\"[AppSentinels] controller response status \", response_status, \", needs to be blocked\");\r\n\r\n                            // Generate a response log for controller\r\n                            var response_body = {\r\n                                httpCode: '401',\r\n                                httpMessage:'Unauthorized',\r\n                                moreInformation:'Request blocked by Appsentinels policy'\r\n                            };\r\n                            var binary_buffer = new Buffer(JSON.stringify(response_body));\r\n                            var encode_response_body = binary_buffer.toString(\"base64\");\r\n\r\n                            var response_headers = {\r\n                                \"X-Request-Id\": request_id,\r\n                                \"Content-Type\": 'application/json',\r\n                                \"X-As-Plugin-Version\": APPSENTINELS_PLUGIN_VERSION,\r\n                                \"X-As-Sensor-Instance\": SENSOR_INSTANCE_NAME,\r\n                                \"X-As-Sensor-Hostname\": SENSOR_HOSTNAME,\r\n                            };\r\n\r\n                            var log_data = {\r\n                                target: REMOTE_CONTROLLER_ENDPOINT_URL_RESPONSE,\r\n                                method: \"POST\",\r\n                                headers: req_headers,\r\n                                contentType: 'application/json',\r\n                                timeout: 10, // Increased timeout\r\n                                data: {\r\n                                    request: {\r\n                                        method: method,\r\n                                        headers: headers,\r\n                                        uri: uri,\r\n                                        querystring: querystring,\r\n                                        body64: body64,\r\n                                    },\r\n                                    response: {\r\n                                        status: 401,\r\n                                        body64: encode_response_body,\r\n                                        headers: response_headers,\r\n                                    },\r\n                                    client_ip: client_ip,\r\n                                    client_port: 0,\r\n                                    started_at: started_at,\r\n                                    latencies: {\r\n                                        request: 0,\r\n                                    }\r\n                                }\r\n                            };\r\n\r\n                            if (TLS_ENABLED) {\r\n                                log_data.sslClientProfile = TLS_CLIENT_PROFILE_NAME;\r\n                            }\r\n\r\n                            try {\r\n                                urlopen.open(log_data, function (error, responseLog) {\r\n                                    if (error) {\r\n                                        console.error(\"[AppSentinels] [Auth Mode] Failed to send response log to Controller: \" + error);\r\n                                    } else {\r\n                                        // To prevent \"Error attempting to read the urlopen response data\"\r\n                                        responseLog.readAsBuffer(function(readError, buffer) {\r\n                                            if (readError) {\r\n                                                console.error(\"[AppSentinels] [Auth Mode] Error reading response log: \" + readError);\r\n                                            } else {\r\n                                                console.info(\"[AppSentinels] Successfully sent and read response log\");\r\n                                            }\r\n                                        });\r\n                                    }\r\n                                });\r\n                            } catch (logError) {\r\n                                console.error(\"[AppSentinels] [Auth Mode] Error sending log data: \" + logError);\r\n                            }\r\n\r\n                            // End the transaction (no further processing)\r\n                            apic.error('AppSentinelsError', 401, 'Unauthorized', 'Request blocked by AppSentinels');\r\n                        } else {\r\n                             console.info(\"[AppSentinels] response status from controller: \", response_status);\r\n                        }\r\n                } catch (processError) {\r\n                    console.error(\"[AppSentinels] [Auth Mode] Error processing response: \" + processError);\r\n                }\r\n            });\r\n        } catch (error) {\r\n            console.error(\"[AppSentinels] [Auth Mode] Error in urlopen operation: \" + error);\r\n        }\r\n    } catch (error) {\r\n        console.error(\"[AppSentinels]  Error in process_request: \" + error);\r\n    }\r\n}\r\n\r\n// Main request processing\r\ntry {\r\n    console.info(\"[AppSentinels] Request received\");\r\n    try {\r\n        var method, url;\r\n        var truncated = false;\r\n\r\n        method = apic.getContext('request.verb');\r\n\r\n        if (!should_bypass_method(method)) {\r\n            var started_at = new Date().getTime();\r\n            // Save the start time\r\n            apic.setvariable(\"as_request_started_at\", started_at);\r\n\r\n            var headers = hm.current.headers;\r\n            for (var headerName in headers) {\r\n                console.info(\"[AppSentinels] headerName\" + \": \" + headers[headerName]);\r\n            }\r\n\r\n            if (!validate_content_type_to_log(headers)) {\r\n                process_request(method, headers, \"\", true, started_at);\r\n            } else {\r\n                var RequestUri = apic.getContext('request.path');\r\n                console.info(\"[AppSentinels] Method: \" + method);\r\n                console.info(\"[AppSentinels] URI: \" + RequestUri);\r\n    \r\n                try {\r\n                    url = sm.getVar('var://service/URL-in');\r\n                    console.info(\"[AppSentinels] URL: \" + url);\r\n                } catch (e) {\r\n                    console.error(\"[AppSentinels] Error getting URL: \" + e);\r\n                    url = \"/unknown\";\r\n                }\r\n        \r\n                apic.readInputAsBuffer(function (error,buffer) {\r\n                    if (error) {\r\n                        console.error(\"[AppSentinels] Error in reading the request body: \", error.toString());\r\n                    } else {\r\n                        if (buffer != null && buffer != undefined && buffer.length > 0) {\r\n                            if (buffer.length > MAX_SUPPORTED_PAYLOAD) {\r\n                                truncated = true;\r\n                            } else {\r\n                                try {\r\n                                    var body64 = buffer.toString(\"base64\");\r\n                                    console.info(\"[AppSentinels] request body:  \" + body64);\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                            process_request(method, headers, body64, truncated, started_at);\r\n                        }\r\n                    }\r\n                });\r\n            }\r\n        } else {\r\n            apic.setvariable(\"as_request_skipped\", true);\r\n        }\r\n    } catch (apiError) {\r\n        console.error(\"[AppSentinels] API access error: \" + apiError);\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}"