diff --git a/agent/agent_standalone_pkg/pom.xml b/agent/agent_standalone_pkg/pom.xml
index 18d1d1e53..47e1e23b7 100755
--- a/agent/agent_standalone_pkg/pom.xml
+++ b/agent/agent_standalone_pkg/pom.xml
@@ -21,8 +21,19 @@
${project.version}
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+ runtime
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
runtime
diff --git a/agent/apiharness/pom.xml b/agent/apiharness/pom.xml
index f1676d2cd..03fe84b26 100644
--- a/agent/apiharness/pom.xml
+++ b/agent/apiharness/pom.xml
@@ -70,8 +70,18 @@
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
diff --git a/agent/apiharness/src/main/java/com/intuit/tank/runner/method/LogicRunner.java b/agent/apiharness/src/main/java/com/intuit/tank/runner/method/LogicRunner.java
index 928f43ae0..96a0d2a43 100644
--- a/agent/apiharness/src/main/java/com/intuit/tank/runner/method/LogicRunner.java
+++ b/agent/apiharness/src/main/java/com/intuit/tank/runner/method/LogicRunner.java
@@ -30,7 +30,7 @@
import com.intuit.tank.vm.common.LogicScriptUtil;
import com.intuit.tank.vm.common.TankConstants;
-import javax.script.ScriptEngineManager;
+import com.intuit.tank.tools.script.JsEngineFactory;
class LogicRunner implements Runner {
private static Logger LOG = LogManager.getLogger(LogicRunner.class);
@@ -60,7 +60,7 @@ public String execute() {
String scriptToRun = new LogicScriptUtil().buildScript(step.getScript());
try {
ScriptIOBean ioBean = new ScriptRunner().runScript(step.getName(), scriptToRun,
- new ScriptEngineManager().getEngineByExtension("js"), inputs, outputLogger);
+ JsEngineFactory.createJsEngine(), inputs, outputLogger);
String action = (String) ioBean.getOutput("action");
if (action != null) {
ret = handleAction(action);
diff --git a/agent/apiharness_pkg/pom.xml b/agent/apiharness_pkg/pom.xml
index 98edb5936..7dd6a34ea 100644
--- a/agent/apiharness_pkg/pom.xml
+++ b/agent/apiharness_pkg/pom.xml
@@ -26,8 +26,19 @@
runtime
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+ runtime
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
runtime
diff --git a/data_model/pom.xml b/data_model/pom.xml
index 9a4af8ad7..97ccdbcc8 100644
--- a/data_model/pom.xml
+++ b/data_model/pom.xml
@@ -36,5 +36,10 @@
commons-io
commons-io
+
+ ${project.groupId}
+ script-engine
+ ${project.version}
+
diff --git a/data_model/src/main/java/com/intuit/tank/project/ExternalScript.java b/data_model/src/main/java/com/intuit/tank/project/ExternalScript.java
index 8038e9696..8fd5d08e0 100644
--- a/data_model/src/main/java/com/intuit/tank/project/ExternalScript.java
+++ b/data_model/src/main/java/com/intuit/tank/project/ExternalScript.java
@@ -20,6 +20,7 @@
import jakarta.persistence.Table;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
+import com.intuit.tank.tools.script.JsEngineFactory;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -89,7 +90,11 @@ public void setProductName(String productName) {
}
public ScriptEngine getEngine() {
- return new ScriptEngineManager().getEngineByExtension(FilenameUtils.getExtension(name));
+ String ext = FilenameUtils.getExtension(name);
+ if ("js".equalsIgnoreCase(ext)) {
+ return JsEngineFactory.createJsEngine();
+ }
+ return new ScriptEngineManager().getEngineByExtension(ext);
}
/**
diff --git a/pom.xml b/pom.xml
index 7e06eea2d..62beb7256 100644
--- a/pom.xml
+++ b/pom.xml
@@ -744,9 +744,20 @@
10.0.2.0
- org.openjdk.nashorn
- nashorn-core
- 15.7
+ org.graalvm.polyglot
+ polyglot
+ 25.0.2
+
+
+ org.graalvm.polyglot
+ js-community
+ 25.0.2
+ pom
+
+
+ org.graalvm.js
+ js-scriptengine
+ 25.0.2
jakarta.servlet.jsp.jstl
diff --git a/tools/agent_debugger/src/main/java/com/intuit/tank/tools/debugger/ConfiguredLanguage.java b/tools/agent_debugger/src/main/java/com/intuit/tank/tools/debugger/ConfiguredLanguage.java
index 52c444561..04e7ea458 100644
--- a/tools/agent_debugger/src/main/java/com/intuit/tank/tools/debugger/ConfiguredLanguage.java
+++ b/tools/agent_debugger/src/main/java/com/intuit/tank/tools/debugger/ConfiguredLanguage.java
@@ -46,7 +46,7 @@ public class ConfiguredLanguage {
private static final String[][] data = {
{ "ECMAScript", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT, "Javascript",
- "com.sun.script.javascript.RhinoScriptEngineFactory", "js" },
+ "com.oracle.truffle.js.scriptengine.GraalJSScriptEngineFactory", "js" },
{ "ruby", SyntaxConstants.SYNTAX_STYLE_RUBY, "Ruby", "com.sun.script.jruby.JRubyScriptEngineFactory", "rb" },
{ "groovy", SyntaxConstants.SYNTAX_STYLE_GROOVY, "Groovy",
"org.codehaus.groovy.jsr223.GroovyScriptEngineFactory", "groovy" }
diff --git a/tools/agent_debugger_pkg/pom.xml b/tools/agent_debugger_pkg/pom.xml
index 6bb0a367f..acda49ad1 100644
--- a/tools/agent_debugger_pkg/pom.xml
+++ b/tools/agent_debugger_pkg/pom.xml
@@ -60,8 +60,19 @@
runtime
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+ runtime
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
runtime
diff --git a/tools/script_engine/pom.xml b/tools/script_engine/pom.xml
index 42f0ce514..b4ab60462 100644
--- a/tools/script_engine/pom.xml
+++ b/tools/script_engine/pom.xml
@@ -24,7 +24,37 @@
jakarta.annotation
jakarta.annotation-api
-
+
+ org.graalvm.polyglot
+ polyglot
+
+
+ org.graalvm.js
+ js-scriptengine
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ com.intuit.tank
+ api
+ ${project.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
diff --git a/tools/script_engine/src/main/java/com/intuit/tank/tools/script/JsEngineFactory.java b/tools/script_engine/src/main/java/com/intuit/tank/tools/script/JsEngineFactory.java
new file mode 100644
index 000000000..04aee8953
--- /dev/null
+++ b/tools/script_engine/src/main/java/com/intuit/tank/tools/script/JsEngineFactory.java
@@ -0,0 +1,73 @@
+package com.intuit.tank.tools.script;
+
+import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.HostAccess;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import java.util.Set;
+
+/**
+ * Factory for obtaining a JavaScript {@link ScriptEngine}.
+ *
+ *
When GraalVM JS is on the classpath a cached, thread-local engine is returned
+ * with host access restricted to Tank packages that scripts legitimately need.
+ * Falls back to whatever JSR-223 "js" engine the JVM provides.
+ */
+public final class JsEngineFactory {
+
+ /**
+ * Package prefixes that user-authored scripts are allowed to access via
+ * {@code Java.type()} / {@code importPackage()}. Anything outside this
+ * list is blocked to prevent arbitrary class access (e.g. Runtime, ProcessBuilder).
+ */
+ private static final Set ALLOWED_PACKAGE_PREFIXES = Set.of(
+ "com.intuit.tank.script.models.",
+ "java.util."
+ );
+
+ private static final boolean GRAALVM_AVAILABLE;
+
+ static {
+ boolean available;
+ try {
+ Class.forName("com.oracle.truffle.js.scriptengine.GraalJSScriptEngine");
+ available = true;
+ } catch (ClassNotFoundException e) {
+ available = false;
+ }
+ GRAALVM_AVAILABLE = available;
+ }
+
+ private JsEngineFactory() {}
+
+ static boolean isAllowedClass(String className) {
+ for (String prefix : ALLOWED_PACKAGE_PREFIXES) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return a JS {@link ScriptEngine} configured for restricted host access.
+ * A new engine is created per call because GraalVM contexts cannot
+ * be safely reused after script evaluation. Engine creation is fast
+ * once the GraalVM runtime is warmed up.
+ */
+ public static ScriptEngine createJsEngine() {
+ if (GRAALVM_AVAILABLE) {
+ return GraalJSScriptEngine.create(null,
+ Context.newBuilder("js")
+ .allowExperimentalOptions(true)
+ .allowHostAccess(HostAccess.ALL)
+ .allowHostClassLookup(JsEngineFactory::isAllowedClass)
+ .option("js.ecmascript-version", "2025")
+ .option("js.nashorn-compat", "true"));
+ }
+ // GraalVM not on classpath — fall back to whatever JSR-223 provides
+ return new ScriptEngineManager().getEngineByExtension("js");
+ }
+}
diff --git a/tools/script_engine/src/test/java/com/intuit/tank/tools/script/ScriptRunnerTest.java b/tools/script_engine/src/test/java/com/intuit/tank/tools/script/ScriptRunnerTest.java
new file mode 100644
index 000000000..2131514ca
--- /dev/null
+++ b/tools/script_engine/src/test/java/com/intuit/tank/tools/script/ScriptRunnerTest.java
@@ -0,0 +1,141 @@
+package com.intuit.tank.tools.script;
+
+import com.intuit.tank.script.models.ScriptStepTO;
+import com.intuit.tank.script.models.ScriptTO;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for ScriptRunner using GraalVM JS engine (JSR-223).
+ */
+class ScriptRunnerTest {
+
+ private ScriptEngine jsEngine() {
+ ScriptEngine engine = JsEngineFactory.createJsEngine();
+ assertNotNull(engine, "GraalVM JS ScriptEngine must be present on classpath");
+ return engine;
+ }
+
+ @Test
+ void testEngineIsGraalJS() {
+ ScriptEngine engine = jsEngine();
+ String engineName = engine.getFactory().getEngineName();
+ assertTrue(engineName.contains("Graal") || engineName.contains("GraalVM"),
+ "Expected GraalVM engine but got: " + engineName);
+ }
+
+ @Test
+ void testRunSimpleScript() throws ScriptException {
+ ScriptRunner runner = new ScriptRunner();
+ String script = "ioBean.setOutput('result', 'hello');";
+ Map inputs = new HashMap<>();
+ StringOutputLogger logger = new StringOutputLogger();
+
+ ScriptIOBean result = runner.runScript("test", script, jsEngine(), inputs, logger);
+
+ assertEquals("hello", result.getOutput("result"));
+ }
+
+ @Test
+ void testRunComplexScript() throws ScriptException, IOException {
+ ScriptRunner runner = new ScriptRunner();
+ String script = IOUtils.toString(Objects.requireNonNull(
+ getClass().getResourceAsStream("/scriptFilter.js")), StandardCharsets.UTF_8);
+ ScriptTO scriptTO = ScriptTO.builder()
+ .withName("test-script")
+ .withProductName("test-product")
+ .withSteps(List.of(ScriptStepTO.builder().build()))
+ .build();
+ Map inputs = new HashMap<>();
+ inputs.put("script", scriptTO);
+ StringOutputLogger logger = new StringOutputLogger();
+
+ runner.runScript("test", script, jsEngine(), inputs, logger);
+
+ assertTrue(logger.getOutput().contains("Number of Steps: 1"));
+ }
+
+ @Test
+ void testInputsAreAvailableInScript() throws ScriptException {
+ ScriptRunner runner = new ScriptRunner();
+ String script = "ioBean.setOutput('echo', ioBean.getInput('value'));";
+ Map inputs = new HashMap<>();
+ inputs.put("value", "tank-graalvm");
+ StringOutputLogger logger = new StringOutputLogger();
+
+ ScriptIOBean result = runner.runScript("echo-test", script, jsEngine(), inputs, logger);
+
+ assertEquals("tank-graalvm", result.getOutput("echo"));
+ }
+
+ @Test
+ void testScriptExceptionPropagated() {
+ ScriptRunner runner = new ScriptRunner();
+ String badScript = "this is not valid javascript }{";
+ Map inputs = new HashMap<>();
+
+ assertThrows(Exception.class,
+ () -> runner.runScript("bad-script", badScript, jsEngine(), inputs, new NullOutputLogger()));
+ }
+
+ @Test
+ void testRunScriptWithoutName() throws ScriptException {
+ ScriptRunner runner = new ScriptRunner();
+ String script = "ioBean.setOutput('x', 42);";
+ Map inputs = new HashMap<>();
+
+ ScriptIOBean result = runner.runScript(script, jsEngine(), inputs, new NullOutputLogger());
+
+ assertEquals(42, ((Number) result.getOutput("x")).intValue());
+ }
+
+ @Test
+ void testDebugOutputCaptured() throws ScriptException {
+ ScriptRunner runner = new ScriptRunner();
+ String script = "ioBean.setOutput('done', true);";
+ Map inputs = new HashMap<>();
+ StringOutputLogger logger = new StringOutputLogger();
+
+ runner.runScript("debug-test", script, jsEngine(), inputs, logger);
+
+ String output = logger.getOutput();
+ assertFalse(output.isEmpty(), "Output logger should have been invoked");
+ assertTrue(output.contains("Starting scriptEngine") || output.contains("Finished scriptEngine"));
+ }
+
+ @Test
+ void testAllowedClassLookupPermitsTankPackages() {
+ assertTrue(JsEngineFactory.isAllowedClass("com.intuit.tank.script.models.ScriptTO"));
+ assertTrue(JsEngineFactory.isAllowedClass("com.intuit.tank.script.models.ScriptStepTO"));
+ assertTrue(JsEngineFactory.isAllowedClass("com.intuit.tank.script.models.StepDataTO"));
+ }
+
+ @Test
+ void testAllowedClassLookupPermitsJavaUtil() {
+ assertTrue(JsEngineFactory.isAllowedClass("java.util.HashMap"));
+ assertTrue(JsEngineFactory.isAllowedClass("java.util.List"));
+ }
+
+ @Test
+ void testAllowedClassLookupBlocksDangerousClasses() {
+ assertFalse(JsEngineFactory.isAllowedClass("java.lang.Runtime"));
+ assertFalse(JsEngineFactory.isAllowedClass("java.lang.ProcessBuilder"));
+ assertFalse(JsEngineFactory.isAllowedClass("java.io.File"));
+ assertFalse(JsEngineFactory.isAllowedClass("java.net.URL"));
+ }
+
+ @Test
+ void testNewEnginePerCall() {
+ ScriptEngine first = JsEngineFactory.createJsEngine();
+ ScriptEngine second = JsEngineFactory.createJsEngine();
+ assertNotSame(first, second, "Each call should return a fresh engine instance");
+ }
+}
diff --git a/tools/script_engine/src/test/resources/scriptFilter.js b/tools/script_engine/src/test/resources/scriptFilter.js
new file mode 100644
index 000000000..60265e5c6
--- /dev/null
+++ b/tools/script_engine/src/test/resources/scriptFilter.js
@@ -0,0 +1,12 @@
+
+try{
+ load("nashorn:mozilla_compat.js");
+}catch(e){}
+
+
+importPackage(com.intuit.tank.script.models);
+
+var TankScript = ioBean.getInput("script");
+
+steps = TankScript.getSteps().toArray();
+ioBean.println("Number of Steps: " + TankScript.getSteps().size());
diff --git a/tools/script_filter/pom.xml b/tools/script_filter/pom.xml
index 0a4d99948..a487eb651 100755
--- a/tools/script_filter/pom.xml
+++ b/tools/script_filter/pom.xml
@@ -41,8 +41,19 @@
3.0.4
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+ runtime
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
runtime
diff --git a/tools/script_filter/src/main/java/com/intuit/tank/tools/script/ConfiguredLanguage.java b/tools/script_filter/src/main/java/com/intuit/tank/tools/script/ConfiguredLanguage.java
index 096924d64..187b8491c 100644
--- a/tools/script_filter/src/main/java/com/intuit/tank/tools/script/ConfiguredLanguage.java
+++ b/tools/script_filter/src/main/java/com/intuit/tank/tools/script/ConfiguredLanguage.java
@@ -47,7 +47,7 @@ public class ConfiguredLanguage {
private static final String[][] data = {
{ "ECMAScript", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT, "Javascript",
- "org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory", "js" },
+ "com.oracle.truffle.js.scriptengine.GraalJSScriptEngineFactory", "js" },
{ "ruby", SyntaxConstants.SYNTAX_STYLE_RUBY, "Ruby", "org.jruby.embed.jsr223.JRubyEngineFactory", "rb" },
{ "groovy", SyntaxConstants.SYNTAX_STYLE_GROOVY, "Groovy",
"org.codehaus.groovy.jsr223.GroovyScriptEngineFactory", "groovy" }
diff --git a/tools/script_filter_pkg/pom.xml b/tools/script_filter_pkg/pom.xml
index 4a5ce7252..f2160ea24 100644
--- a/tools/script_filter_pkg/pom.xml
+++ b/tools/script_filter_pkg/pom.xml
@@ -37,8 +37,19 @@
runtime
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+ runtime
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
runtime
diff --git a/web/web_support/pom.xml b/web/web_support/pom.xml
index 715e73f2a..d5151881d 100644
--- a/web/web_support/pom.xml
+++ b/web/web_support/pom.xml
@@ -179,8 +179,19 @@
- org.openjdk.nashorn
- nashorn-core
+ org.graalvm.polyglot
+ polyglot
+ runtime
+
+
+ org.graalvm.polyglot
+ js-community
+ pom
+ runtime
+
+
+ org.graalvm.js
+ js-scriptengine
runtime
diff --git a/web/web_support/src/main/java/com/intuit/tank/script/LogicStepEditor.java b/web/web_support/src/main/java/com/intuit/tank/script/LogicStepEditor.java
index 0e606ffda..ded3e3c45 100644
--- a/web/web_support/src/main/java/com/intuit/tank/script/LogicStepEditor.java
+++ b/web/web_support/src/main/java/com/intuit/tank/script/LogicStepEditor.java
@@ -26,7 +26,7 @@
import jakarta.enterprise.context.ConversationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
-import javax.script.ScriptEngineManager;
+import com.intuit.tank.tools.script.JsEngineFactory;
import lombok.Getter;
import lombok.Setter;
@@ -184,7 +184,7 @@ public void testScript() {
logMap("Variables", vars.getVariableValues(), outputLogger);
outputLogger.logLine(DASHES + " script " + DASHES);
ScriptIOBean ioBean = new ScriptRunner().runScript(name, scriptToRun,
- new ScriptEngineManager().getEngineByExtension("js"), inputs, outputLogger);
+ JsEngineFactory.createJsEngine(), inputs, outputLogger);
logMap("Outputs", ioBean.getOutputs(), outputLogger);
logMap("Variables", vars.getVariableValues(), outputLogger);
} catch (Exception e) {