Is SAP CPI Helper safe?
SAP CPI Helper is low risk. SAP CPI Helper's GroovyDebugX action confirms, then opens a groovyide.com URL: a raw-DEFLATE, base64url JSON package built from trace data, which can include message body, runtime headers, exchange properties, and Groovy script source.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
SAP CPI trace data is packed into a groovyide.com share URL
SAP CPI Helper's GroovyDebugX action confirms, then opens a groovyide.com URL: a raw-DEFLATE, base64url JSON package built from trace data, which can include message body, runtime headers, exchange properties, and Groovy script source.
You click the GroovyDebugX option to debug a traced SAP CPI Groovy step in an external IDE.
The confirmation dialog lets you choose message body, script, properties, and headers before continuing.
The extension creates a compressed share link and opens groovyide.com in a new tab.
The share link carries the selected trace fields inside the URL path rather than a separate request body.
| Field | Value | Why it matters | |
|---|---|---|---|
Message body | input.body = payload from TraceMessages(traceId)/$value | Can include the business payload from the SAP CPI trace you are debugging. | |
Runtime headers | input.headers = {"Content-Type":"application/json"} | Can reveal routing, integration, and message-handling context for the traced exchange. | |
Exchange properties | input.properties = {"CamelSplitIndex":"0"} | Can include configuration and runtime values attached to the SAP CPI exchange. | |
Groovy script source | script.code = "import com.sap.gateway.ip.core.customdev.util.Message\nMessage processData(Message message) { return message }" | Can disclose the integration logic you are trying to debug. | |
Function name | script.function = "processData" | Identifies which Groovy entry point the external IDE should run for the debug session. |
The confirmation dialog and sender that build the groovyide.com URL
window.groovyDebugSendToIDE = async function () {
const settings = window.groovyDebuggerData?.settings || {};
const ideUrl = settings["groovyDebugger---externalIdeUrl"] || "https://groovyide.com/cpi/share/v1/";
const domain = new URL(ideUrl).hostname;
// Load saved user preferences for checkbox states using plugin settings
const loadTransferPreferences = async () => {
const body = (await getStorageValue("groovyDebugger", "transferBody", "browser")) ?? true;
const script = (await getStorageValue("groovyDebugger", "transferScript", "browser")) ?? true;
const properties = (await getStorageValue("groovyDebugger", "transferProperties", "browser")) ?? false;
const headers = (await getStorageValue("groovyDebugger", "transferHeaders", "browser")) ?? false;
return {
body: body,
script: script,
properties: properties,
headers: headers,
};
};
// Load saved user preferences for checkbox states
const userPreferences = await loadTransferPreferences();
// Create custom confirmation popup
const popupContent = `
<div class="ui warning message">
<div class="header">
<i class="exclamation triangle icon"></i>
Data Transfer Confirmation
</div>
<div class="ui info message">
<i class="info circle icon"></i>
<strong>Privacy Notice:</strong> The selected data may contain sensitive business information. Proceed with caution.
</div>
<p><strong>Destination:</strong> ${domain}</p>
<p>Select which data you want to transfer to the external Groovy WebIDE:</p>
<div class="ui form">
<div class="grouped fields">
<div class="field">
<div class="ui checkbox" id="transfer-body">
<input type="checkbox" name="transfer-body" ${userPreferences.body ? "checked" : ""}>
<label>Message Body <em>(may contain sensitive data)</em></label>
</div>
</div>
<div class="field">
<div class="ui checkbox" id="transfer-script">
<input type="checkbox" name="transfer-script" ${userPreferences.script ? "checked" : ""}>
<label>Groovy Script <em>(source code)</em></label>
</div>
</div>
<div class="field">
<div class="ui checkbox" id="transfer-properties">
<input type="checkbox" name="transfer-properties" ${userPreferences.properties ? "checked" : ""}>
<label>Properties <em>(may contain configuration data)</em></label>
</div>
</div>
<div class="field">
<div class="ui checkbox" id="transfer-headers">
<input type="checkbox" name="transfer-headers" ${userPreferences.headers ? "checked" : ""}>
<label>Headers <em>(may contain security & metadata)</em></label>
</div>
</div>
</div>
</div>
</div>
`;
showBigPopup(popupContent, "Confirm Data Transfer", {
fullscreen: false,
large: false,
callback: () => {
// Add custom buttons to the actions div
let actionsDiv = $("#cpiHelper_semanticui_modal .actions");
actionsDiv.empty(); // Remove default close button
// Cancel button
let cancelBtn = $('<div class="ui button">Cancel</div>');
cancelBtn.on("click", () => {
$("#cpiHelper_semanticui_modal").modal("hide");
});
actionsDiv.append(cancelBtn);
// Continue button
let continueBtn = $('<div class="ui positive button"><i class="rocket icon"></i>Continue</div>');
// Function to check if any checkbox is selected and enable/disable continue button
const updateContinueButton = () => {
const anyChecked = $("#transfer-body input").is(":checked") || $("#transfer-properties input").is(":checked") || $("#transfer-headers input").is(":checked") || $("#transfer-script input").is(":checked");
if (anyChecked) {
continueBtn.removeClass("disabled");
continueBtn.prop("disabled", false);
} else {
continueBtn.addClass("disabled");
continueBtn.prop("disabled", true);
}
};
// Add event listeners to all checkboxes to update continue button state
$("#transfer-body input, #transfer-properties input, #transfer-headers input, #transfer-script input").on("change", updateContinueButton);
// Initial check
updateContinueButton();
continueBtn.on("click", async () => {
// Get selected data types
const transferOptions = {
body: $("#transfer-body input").is(":checked"),
properties: $("#transfer-properties input").is(":checked"),
headers: $("#transfer-headers input").is(":checked"),
script: $("#transfer-script input").is(":checked"),
};
// Save user preferences for next time using plugin settings
await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferBody", "browser"), transferOptions.body);
await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferScript", "browser"), transferOptions.script);
await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferProperties", "browser"), transferOptions.properties);
await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferHeaders", "browser"), transferOptions.headers);
$("#cpiHelper_semanticui_modal").modal("hide");
const debugData = window.currentGroovyDebugData;
await sendToExternalIDE(settings, debugData, transferOptions);
showToast(`Debug data sent to IDE`, "Success");
// Close the main debug popup after sending
$("#cpiHelper_semanticui_modal").modal("hide");
});
actionsDiv.append(continueBtn);
},
});
}async function sendToExternalIDE(settings, debugData, transferOptions = { body: true, properties: true, headers: true, script: true }) {
var ideUrl = settings["groovyDebugger---externalIdeUrl"] || "https://groovyide.com/cpi/share/v1/";
// Use actual debug data based on transfer options
let groovyScript = "";
if (transferOptions.script) {
groovyScript = debugData.groovyScript;
// If script not fetched yet (lazy loading), fetch it now
if (groovyScript === "// Script content not available" && debugData.scriptInfo) {
try {
if (debugData.scriptInfo.scriptPath) {
let scriptPath = debugData.scriptInfo.scriptPath;
if (scriptPath.startsWith("/script/")) {
scriptPath = scriptPath.replace("/script/", "//");
}
const scriptUrl = "https://" + debugData.scriptInfo.tenant + "/api/1.0/iflows/" + debugData.scriptInfo.artifactId + "/script/" + scriptPath;
const scriptResponse = await fetch(scriptUrl);
const scriptData = await scriptResponse.json();
groovyScript = scriptData.content || "// Script content not available";
}
} catch (error) {
log.error("Error fetching script for IDE:", error);
groovyScript = "// Script content not available";
}
}
}
let payload = "";
if (transferOptions.body) {
payload = debugData.payload;
// If payload not fetched yet (lazy loading), fetch it now
if (!payload && debugData.traceId) {
try {
payload = await makeCallPromise("GET", "/" + cpiData.urlExtension + "odata/api/v1/TraceMessages(" + debugData.traceId + ")/$value", true);
} catch (error) {
log.error("Error fetching payload for IDE:", error);
payload = "";
}
}
}
let headers = {};
if (transferOptions.headers) {
headers = debugData.headers || {};
// If headers not fetched yet (lazy loading), fetch them now
if ((!headers || Object.keys(headers).length === 0) && debugData.traceId) {
try {
let headersData = JSON.parse(await makeCallPromise("GET", "/" + cpiData.urlExtension + "odata/api/v1/TraceMessages(" + debugData.traceId + ")/Properties?$format=json", true)).d.results;
headers = {};
headersData.forEach((header) => {
headers[header.Name] = header.Value;
});
} catch (error) {
log.error("Error fetching headers for IDE:", error);
headers = {};
}
}
}
let properties = {};
if (transferOptions.properties) {
properties = debugData.properties || {};
}
// Build the JSON structure from actual debug data
let dataObject = {
input: {
body: payload,
headers: headers,
properties: properties,
},
script: {
code: groovyScript,
function: debugData.scriptFunction || "processData",
},
};
let dataString = JSON.stringify(dataObject);
// Compress and encode
let encoded;
encoded = await compressToBase64(dataString);
// Make URL-safe and remove padding
encoded = encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
var fullUrl = ideUrl + encoded;
// Open in new tab/window
window.open(fullUrl, "_blank");
}
function compressToBase64(dataString) {
// Step A: Convert the JSON string into a Uint8Array (binary data)
const dataBytes = new TextEncoder().encode(dataString);
// Step B: Compress using pako.deflateRaw()
// This creates the raw Deflate stream without Zlib/Gzip headers.
const compressedBytes = pako.deflateRaw(dataBytes, { level: 9 }); // level 9 is max compression
// Step C: Base64URL Encode the compressed binary data
const encodedString = uint8ArrayToBase64Url(compressedBytes);
return encodedString;
}
function uint8ArrayToBase64Url(bytes) {
// Convert Uint8Array to a binary string
let binaryString = "";
bytes.forEach((byte) => {
binaryString += String.fromCharCode(byte);
});
// Standard Base64 encoding using the built-in browser function
let base64 = btoa(binaryString);
// Convert to URL-safe format and remove padding
let base64Url = base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return base64Url;
}- groovyide.com
Default Groovy WebIDE share destination that receives the encoded SAP CPI trace package in the URL path.