diff --git a/core/src/main/java/hudson/cli/GroovyCommand.java b/core/src/main/java/hudson/cli/GroovyCommand.java index 0d7e45e6ca18..10324992d151 100644 --- a/core/src/main/java/hudson/cli/GroovyCommand.java +++ b/core/src/main/java/hudson/cli/GroovyCommand.java @@ -27,12 +27,14 @@ import groovy.lang.Binding; import groovy.lang.GroovyShell; import hudson.Extension; +import hudson.model.User; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import jenkins.model.Jenkins; +import jenkins.model.ScriptListener; import org.apache.commons.io.IOUtils; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; @@ -70,7 +72,9 @@ protected int run() throws Exception { binding.setProperty("stderr", stderr); GroovyShell groovy = new GroovyShell(Jenkins.get().getPluginManager().uberClassLoader, binding); - groovy.run(loadScript(), "RemoteClass", remaining.toArray(new String[0])); + String script = loadScript(); + ScriptListener.fireScriptEvent(script, "CLI/GroovyCommand", User.current()); + groovy.run(script, "RemoteClass", remaining.toArray(new String[0])); return 0; } diff --git a/core/src/main/java/hudson/cli/GroovyshCommand.java b/core/src/main/java/hudson/cli/GroovyshCommand.java index 4f6c741c8a47..b682022ed167 100644 --- a/core/src/main/java/hudson/cli/GroovyshCommand.java +++ b/core/src/main/java/hudson/cli/GroovyshCommand.java @@ -28,6 +28,7 @@ import groovy.lang.Binding; import groovy.lang.Closure; import hudson.Extension; +import hudson.model.User; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -39,6 +40,7 @@ import java.util.ArrayList; import java.util.List; import jenkins.model.Jenkins; +import jenkins.model.ScriptListener; import jline.TerminalFactory; import jline.UnsupportedTerminal; import org.codehaus.groovy.tools.shell.Groovysh; @@ -119,9 +121,20 @@ public Object doCall(Object[] args) { return null; } }; - Groovysh shell = new Groovysh(cl, binding, io, registrar); + Groovysh shell = new LoggingGroovySh(cl, binding, io, registrar); shell.getImports().add("hudson.model.*"); return shell; } + private static class LoggingGroovySh extends Groovysh { + LoggingGroovySh(ClassLoader cl, Binding binding, IO io, Closure registrar) { + super(cl, binding, io, registrar); + } + + @Override + protected void maybeRecordInput(String line) { + ScriptListener.fireScriptEvent(line, "CLI/GroovySh", User.current()); + super.maybeRecordInput(line); + } + } } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index d5c3d4265536..4ffdba1d3246 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -142,6 +142,7 @@ import hudson.model.listeners.SCMListener; import hudson.model.listeners.SaveableListener; import hudson.remoting.Callable; +import hudson.remoting.Channel; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; import hudson.scm.RepositoryBrowser; @@ -4730,6 +4731,14 @@ public static void _doScript(StaplerRequest req, StaplerResponse rsp, RequestDis } try { + String runner = "Script Console "; + + if (!(channel instanceof LocalChannel)) { + runner += ((Channel) channel).getName(); + } else { + runner += "Controller"; + } + ScriptListener.fireScriptEvent(text, runner, User.current()); req.setAttribute("output", RemotingDiagnostics.executeGroovy(text, channel)); } catch (InterruptedException e) { diff --git a/core/src/main/java/jenkins/model/ScriptListener.java b/core/src/main/java/jenkins/model/ScriptListener.java new file mode 100644 index 000000000000..e0aff3279e95 --- /dev/null +++ b/core/src/main/java/jenkins/model/ScriptListener.java @@ -0,0 +1,54 @@ +package jenkins.model; + +import hudson.Extension; +import hudson.ExtensionPoint; +import hudson.model.User; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.util.Listeners; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest; + +/** + * A listener to track Groovy scripts. + * + * @see Jenkins#_doScript(StaplerRequest, org.kohsuke.stapler.StaplerResponse, javax.servlet.RequestDispatcher, hudson.remoting.VirtualChannel, hudson.security.ACL) + * @see hudson.cli.GroovyCommand#run() + * @see hudson.cli.GroovyshCommand#run() + */ +public interface ScriptListener extends ExtensionPoint { + + /** + * Called when a (privileged) groovy script is executed. + * + * @see Jenkins#_doScript(StaplerRequest, org.kohsuke.stapler.StaplerResponse, javax.servlet.RequestDispatcher, hudson.remoting.VirtualChannel, hudson.security.ACL) + * @param script The script to be executed. + * @param origin Descriptive identifier of the origin where the script is executed (Controller, Agent ID, Run ID). + * @param u If available, the user that executed the script. Can be null. + */ + void onScript(String script, String origin, User u); + + /** + * Fires the {@link #onScript(String, String, User)} event to track the usage of the script console. + * + * @see Jenkins#_doScript(StaplerRequest, org.kohsuke.stapler.StaplerResponse, javax.servlet.RequestDispatcher, hudson.remoting.VirtualChannel, hudson.security.ACL) + * @param script The script to be executed. + * @param origin Descriptive identifier of the origin where the script is executed (Controller, Agent ID, Run ID). + * @param u If available, the user that executed the script. + */ + static void fireScriptEvent(String script, String origin, User u) { + Listeners.notify(ScriptListener.class, true, listener -> listener.onScript(script, origin, u)); + } + + @Extension + @Restricted(NoExternalUse.class) + class LoggingListener implements ScriptListener { + public static final Logger LOGGER = Logger.getLogger(LoggingListener.class.getName()); + + @Override + public void onScript(String script, String origin, User u) { + LOGGER.log(Level.FINE, () -> "Script: '" + script + "' from origin: '" + origin + "' by user: '" + u + "'"); + } + } +} diff --git a/test/src/test/java/jenkins/model/ScriptListenerTest.java b/test/src/test/java/jenkins/model/ScriptListenerTest.java new file mode 100644 index 000000000000..c86f73a8e4d7 --- /dev/null +++ b/test/src/test/java/jenkins/model/ScriptListenerTest.java @@ -0,0 +1,85 @@ +package jenkins.model; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import hudson.ExtensionList; +import hudson.cli.GroovyCommand; +import hudson.cli.GroovyshCommand; +import hudson.model.User; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Locale; +import javax.servlet.RequestDispatcher; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +public class ScriptListenerTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + private final String testMessage = "light of the world"; + private final String script = String.format("System.out.println('%s')", testMessage); + private PrintStream ps; + private ByteArrayOutputStream altStdout; + private String expectedOutFormat = "Script: '%s' from '%s' by '%s'"; + private DummyScriptUsageListener listener = new DummyScriptUsageListener(); + + @Before + public void setUp() { + altStdout = new ByteArrayOutputStream(); + ps = new PrintStream(altStdout); + ExtensionList.lookup(ScriptListener.class).add(listener); + } + + @Test + public void consoleUsageIsLogged() throws Exception { + RequestDispatcher view = mock(RequestDispatcher.class); + StaplerRequest req = mock(StaplerRequest.class); + StaplerResponse rsp = mock(StaplerResponse.class); + + when(req.getMethod()).thenReturn("POST"); + when(req.getParameter("script")).thenReturn(script); + when(req.getView(j.jenkins, "_scriptText.jelly")).thenReturn(view); + j.jenkins.doScriptText(req, rsp); + + assertEquals(String.format(expectedOutFormat, script, "Script Console Controller", "SYSTEM"), altStdout.toString().trim()); + } + + @Test + public void groovyCliUsageIsLogged() throws Exception { + GroovyCommand cmd = new GroovyCommand(); + cmd.script = "="; + InputStream scriptStream = new ByteArrayInputStream(script.getBytes()); + cmd.main(new ArrayList<>(), Locale.ENGLISH, scriptStream, System.out, System.err); + assertEquals(String.format(expectedOutFormat, script, "CLI/GroovyCommand", "null"), altStdout.toString().trim()); + } + + @Test + public void groovyShCliUsageIsLogged() throws Exception { + GroovyshCommand cmd = new GroovyshCommand(); + InputStream scriptStream = new ByteArrayInputStream(script.getBytes()); + + cmd.main(new ArrayList<>(), Locale.ENGLISH, scriptStream, System.out, System.err); + assertEquals(String.format(expectedOutFormat, script, "CLI/GroovySh", "null"), altStdout.toString().trim()); + } + + private class DummyScriptUsageListener implements ScriptListener { + @Override + public void onScript(String script, String origin, User u) { + String username = "null"; + if (u != null) { + username = u.getFullName(); + } + ps.println(String.format(expectedOutFormat, script, origin, username)); + } + } +}