From f2ee5f098a803a738185ad9016f9058a8b4b71e1 Mon Sep 17 00:00:00 2001 From: kevin-mcgoldrick Date: Thu, 12 Mar 2026 22:00:37 -0700 Subject: [PATCH 1/4] Nashorn to GraalVM Migration --- agent/agent_standalone_pkg/pom.xml | 15 ++- agent/apiharness/pom.xml | 14 ++- .../tank/runner/method/LogicRunner.java | 4 +- agent/apiharness_pkg/pom.xml | 15 ++- data_model/pom.xml | 14 +++ .../intuit/tank/project/ExternalScript.java | 15 ++- pom.xml | 17 ++- .../tools/debugger/ConfiguredLanguage.java | 2 +- tools/agent_debugger_pkg/pom.xml | 15 ++- tools/script_engine/pom.xml | 32 ++++- .../tank/tools/script/JsEngineFactory.java | 38 ++++++ .../tank/tools/script/ScriptRunnerTest.java | 114 ++++++++++++++++++ .../src/test/resources/scriptFilter.js | 13 ++ tools/script_filter/pom.xml | 15 ++- .../tank/tools/script/ConfiguredLanguage.java | 2 +- tools/script_filter_pkg/pom.xml | 15 ++- web/web_support/pom.xml | 15 ++- .../intuit/tank/script/LogicStepEditor.java | 4 +- 18 files changed, 334 insertions(+), 25 deletions(-) create mode 100644 tools/script_engine/src/main/java/com/intuit/tank/tools/script/JsEngineFactory.java create mode 100644 tools/script_engine/src/test/java/com/intuit/tank/tools/script/ScriptRunnerTest.java create mode 100644 tools/script_engine/src/test/resources/scriptFilter.js diff --git a/agent/agent_standalone_pkg/pom.xml b/agent/agent_standalone_pkg/pom.xml index 4e40f89c3..0ec39fac2 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 beb00a9e3..98ab0921d 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 d8a72275f..59dafd321 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 116edb782..2bf8df145 100644 --- a/data_model/pom.xml +++ b/data_model/pom.xml @@ -36,5 +36,19 @@ commons-io commons-io + + org.graalvm.polyglot + polyglot + + + org.graalvm.js + js-scriptengine + + + org.graalvm.polyglot + js-community + pom + runtime + 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..00b47b097 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,9 @@ import jakarta.persistence.Table; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; +import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -89,7 +92,17 @@ 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 GraalJSScriptEngine.create(null, + Context.newBuilder("js") + .allowExperimentalOptions(true) + .allowHostAccess(HostAccess.ALL) + .allowHostClassLookup(className -> true) + .option("js.ecmascript-version", "2023") + .option("js.nashorn-compat", "true")); + } + return new ScriptEngineManager().getEngineByExtension(ext); } /** diff --git a/pom.xml b/pom.xml index 240f53bfe..dde581433 100644 --- a/pom.xml +++ b/pom.xml @@ -749,9 +749,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 f2549f035..ad88edb43 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 b46f06fab..3d6947bc9 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..902c968d2 --- /dev/null +++ b/tools/script_engine/src/main/java/com/intuit/tank/tools/script/JsEngineFactory.java @@ -0,0 +1,38 @@ +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; + +/** + * Factory for obtaining a JavaScript {@link ScriptEngine}. + * + *

When GraalVM JS is on the classpath it is returned with {@link HostAccess#ALL} + * so that scripts can invoke public methods on Java objects (e.g. {@code ioBean.setOutput(...)}). + * Falls back to whatever JSR-223 "js" engine the JVM provides. + */ +public final class JsEngineFactory { + + private JsEngineFactory() {} + + /** + * @return a JS {@link ScriptEngine} configured for full host access. + */ + public static ScriptEngine createJsEngine() { + try { + return GraalJSScriptEngine.create(null, + Context.newBuilder("js") + .allowExperimentalOptions(true) + .allowHostAccess(HostAccess.ALL) + .allowHostClassLookup(className -> true) + .option("js.ecmascript-version", "2023") + .option("js.nashorn-compat", "true")); + } catch (NoClassDefFoundError e) { + // 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..564a6a503 --- /dev/null +++ b/tools/script_engine/src/test/java/com/intuit/tank/tools/script/ScriptRunnerTest.java @@ -0,0 +1,114 @@ +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 org.junit.platform.commons.util.ResourceUtils; + +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( + ResourceUtils.class.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(ScriptException.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(); + assertTrue(output.contains("Starting scriptEngine") || output.contains("Finished scriptEngine") + || output.length() >= 0, "Output logger should have been invoked"); + } +} 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..aa4fcbfe7 --- /dev/null +++ b/tools/script_engine/src/test/resources/scriptFilter.js @@ -0,0 +1,13 @@ +//jsCombinedFilter_20171010.js + +try{ + load("nashorn:mozilla_compat.js"); +}catch(e){} + + +importPackage(com.intuit.tank.script.models); + +var TurboScaleScript = ioBean.getInput("script"); + +steps = TurboScaleScript.getSteps().toArray(); +ioBean.println("Number of Steps: " + TurboScaleScript.getSteps().size()); \ No newline at end of file diff --git a/tools/script_filter/pom.xml b/tools/script_filter/pom.xml index 301153ee9..cf639ecd3 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 b76e16cdd..49b714e2d 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 f27ee3833..0edd04f51 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) { From c54e6f016395ed122f2efd7ec55dd82d5134edec Mon Sep 17 00:00:00 2001 From: kevin-mcgoldrick Date: Thu, 12 Mar 2026 22:13:12 -0700 Subject: [PATCH 2/4] Minor cleanup --- .../java/com/intuit/tank/tools/script/ScriptRunnerTest.java | 4 ++-- tools/script_engine/src/test/resources/scriptFilter.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 564a6a503..de4373fe6 100644 --- 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 @@ -108,7 +108,7 @@ void testDebugOutputCaptured() throws ScriptException { runner.runScript("debug-test", script, jsEngine(), inputs, logger); String output = logger.getOutput(); - assertTrue(output.contains("Starting scriptEngine") || output.contains("Finished scriptEngine") - || output.length() >= 0, "Output logger should have been invoked"); + assertFalse(output.isEmpty(), "Output logger should have been invoked"); + assertTrue(output.contains("Starting scriptEngine") || output.contains("Finished scriptEngine")); } } diff --git a/tools/script_engine/src/test/resources/scriptFilter.js b/tools/script_engine/src/test/resources/scriptFilter.js index aa4fcbfe7..0878e4c0e 100644 --- a/tools/script_engine/src/test/resources/scriptFilter.js +++ b/tools/script_engine/src/test/resources/scriptFilter.js @@ -10,4 +10,4 @@ importPackage(com.intuit.tank.script.models); var TurboScaleScript = ioBean.getInput("script"); steps = TurboScaleScript.getSteps().toArray(); -ioBean.println("Number of Steps: " + TurboScaleScript.getSteps().size()); \ No newline at end of file +ioBean.println("Number of Steps: " + TurboScaleScript.getSteps().size()); From 944a5e517cea7d57318d0d5e150737db350a140f Mon Sep 17 00:00:00 2001 From: kevin-mcgoldrick Date: Thu, 12 Mar 2026 22:14:42 -0700 Subject: [PATCH 3/4] Minor cleanup --- tools/script_engine/src/test/resources/scriptFilter.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/script_engine/src/test/resources/scriptFilter.js b/tools/script_engine/src/test/resources/scriptFilter.js index 0878e4c0e..60265e5c6 100644 --- a/tools/script_engine/src/test/resources/scriptFilter.js +++ b/tools/script_engine/src/test/resources/scriptFilter.js @@ -1,4 +1,3 @@ -//jsCombinedFilter_20171010.js try{ load("nashorn:mozilla_compat.js"); @@ -7,7 +6,7 @@ try{ importPackage(com.intuit.tank.script.models); -var TurboScaleScript = ioBean.getInput("script"); +var TankScript = ioBean.getInput("script"); -steps = TurboScaleScript.getSteps().toArray(); -ioBean.println("Number of Steps: " + TurboScaleScript.getSteps().size()); +steps = TankScript.getSteps().toArray(); +ioBean.println("Number of Steps: " + TankScript.getSteps().size()); From 2e1f699245f4a53cae7a8887afae999f061266cc Mon Sep 17 00:00:00 2001 From: kevin-mcgoldrick Date: Wed, 15 Apr 2026 16:50:13 -0700 Subject: [PATCH 4/4] Consolidate to a single ScriptEngine --- data_model/pom.xml | 15 ++---- .../intuit/tank/project/ExternalScript.java | 12 +---- .../tank/tools/script/JsEngineFactory.java | 53 +++++++++++++++---- .../tank/tools/script/ScriptRunnerTest.java | 33 ++++++++++-- 4 files changed, 79 insertions(+), 34 deletions(-) diff --git a/data_model/pom.xml b/data_model/pom.xml index 58758316d..449fd1dc1 100644 --- a/data_model/pom.xml +++ b/data_model/pom.xml @@ -37,18 +37,9 @@ commons-io - org.graalvm.polyglot - polyglot - - - org.graalvm.js - js-scriptengine - - - org.graalvm.polyglot - js-community - pom - runtime + ${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 00b47b097..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,9 +20,7 @@ import jakarta.persistence.Table; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; -import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.HostAccess; +import com.intuit.tank.tools.script.JsEngineFactory; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -94,13 +92,7 @@ public void setProductName(String productName) { public ScriptEngine getEngine() { String ext = FilenameUtils.getExtension(name); if ("js".equalsIgnoreCase(ext)) { - return GraalJSScriptEngine.create(null, - Context.newBuilder("js") - .allowExperimentalOptions(true) - .allowHostAccess(HostAccess.ALL) - .allowHostClassLookup(className -> true) - .option("js.ecmascript-version", "2023") - .option("js.nashorn-compat", "true")); + return JsEngineFactory.createJsEngine(); } return new ScriptEngineManager().getEngineByExtension(ext); } 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 index 902c968d2..04aee8953 100644 --- 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 @@ -6,33 +6,68 @@ 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 it is returned with {@link HostAccess#ALL} - * so that scripts can invoke public methods on Java objects (e.g. {@code ioBean.setOutput(...)}). + *

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 full host access. + * @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() { - try { + if (GRAALVM_AVAILABLE) { return GraalJSScriptEngine.create(null, Context.newBuilder("js") .allowExperimentalOptions(true) .allowHostAccess(HostAccess.ALL) - .allowHostClassLookup(className -> true) - .option("js.ecmascript-version", "2023") + .allowHostClassLookup(JsEngineFactory::isAllowedClass) + .option("js.ecmascript-version", "2025") .option("js.nashorn-compat", "true")); - } catch (NoClassDefFoundError e) { - // GraalVM not on classpath — fall back to whatever JSR-223 provides - return new ScriptEngineManager().getEngineByExtension("js"); } + // 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 index de4373fe6..2131514ca 100644 --- 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 @@ -4,7 +4,6 @@ import com.intuit.tank.script.models.ScriptTO; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; -import org.junit.platform.commons.util.ResourceUtils; import javax.script.ScriptEngine; import javax.script.ScriptException; @@ -49,7 +48,7 @@ void testRunSimpleScript() throws ScriptException { void testRunComplexScript() throws ScriptException, IOException { ScriptRunner runner = new ScriptRunner(); String script = IOUtils.toString(Objects.requireNonNull( - ResourceUtils.class.getResourceAsStream("/scriptFilter.js")), StandardCharsets.UTF_8); + getClass().getResourceAsStream("/scriptFilter.js")), StandardCharsets.UTF_8); ScriptTO scriptTO = ScriptTO.builder() .withName("test-script") .withProductName("test-product") @@ -83,7 +82,7 @@ void testScriptExceptionPropagated() { String badScript = "this is not valid javascript }{"; Map inputs = new HashMap<>(); - assertThrows(ScriptException.class, + assertThrows(Exception.class, () -> runner.runScript("bad-script", badScript, jsEngine(), inputs, new NullOutputLogger())); } @@ -111,4 +110,32 @@ void testDebugOutputCaptured() throws ScriptException { 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"); + } }