diff --git a/README.adoc b/README.adoc index b8a69ba..17e68f9 100644 --- a/README.adoc +++ b/README.adoc @@ -70,6 +70,10 @@ Will log the cause of a build. Defaults to true. Will log usage of credentials as long as they are consumed through the https://plugins.jenkins.io/credentials/[Credentials plugin]. Defaults to true. +=== Log Groovy script usage + +Will log potentially dangerous groovy scripts, for example from the script console. Defaults to true. + === About the client IP-address appearing in the logs ==== The plugin uses a method that cannot guarantee that the actual IP of the client is captured. diff --git a/pom.xml b/pom.xml index f45007f..9082bca 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ 999999-SNAPSHOT jenkinsci/audit-trail-plugin - 2.346.1 + 2.364-SNAPSHOT 8 1.7.30 diff --git a/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java b/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java index 1cdaab1..583df11 100644 --- a/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java +++ b/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java @@ -71,6 +71,8 @@ public class AuditTrailPlugin extends GlobalConfiguration { private static final Logger LOGGER = Logger.getLogger(AuditTrailPlugin.class.getName()); private boolean logBuildCause = true; private boolean logCredentialsUsage = true; + private boolean logScriptUsage = true; + private List loggers = new ArrayList<>(); private transient String log; @@ -117,6 +119,8 @@ public boolean shouldLogBuildCause() { public boolean shouldLogCredentialsUsage() { return logCredentialsUsage; } + public boolean getLogScriptUsage() { return logScriptUsage; } + public List getLoggers() { return loggers; } public AuditTrailPlugin() { @@ -165,6 +169,11 @@ public void setLogCredentialsUsage(boolean logCredentialsUsage) { save(); } + @DataBoundSetter + public void setLogScriptUsage(boolean logScriptUsage) { + this.logScriptUsage = logScriptUsage; + save(); + } private void updateFilterPattern() { try { AuditTrailFilter.setPattern(pattern); diff --git a/src/main/java/hudson/plugins/audit_trail/ScriptUsageListener.java b/src/main/java/hudson/plugins/audit_trail/ScriptUsageListener.java new file mode 100644 index 0000000..a1eec96 --- /dev/null +++ b/src/main/java/hudson/plugins/audit_trail/ScriptUsageListener.java @@ -0,0 +1,62 @@ +package hudson.plugins.audit_trail; + +import hudson.Extension; +import hudson.model.User; +import jenkins.model.Jenkins; +import jenkins.model.ScriptListener; +import org.kohsuke.stapler.StaplerRequest; + +import javax.inject.Inject; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Log when a (privileged) Groovy script is executed. + * + * @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() + * @see org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval#using(String, org.jenkinsci.plugins.scriptsecurity.scripts.Language, String) + * + * @author Jan Meiswinkel + */ +@Extension +public class ScriptUsageListener implements ScriptListener { + private static final Logger LOGGER = Logger.getLogger(ScriptUsageListener.class.getName()); + + @Inject + AuditTrailPlugin configuration; + + /** + * 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. + */ + + @Override + public void onScript(String script, String origin, User u) { + if (!configuration.getLogScriptUsage()) { + return; + } + StringBuilder builder = new StringBuilder(); + + if (u != null) { + builder.append(String.format("A groovy script was executed by user '%s'. Origin: %s. ", u.getId(), origin)); + } else { + builder.append(String.format("A groovy script was executed. Origin: %s.", origin)); + } + + builder.append("\nThe executed script: \n"); + builder.append(script); + String log = builder.toString(); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Detected groovy script usage, details: {0}", new Object[]{log}); + } + for (AuditLogger logger : configuration.getLoggers()) { + logger.log(log); + } + } +} diff --git a/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config.jelly b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config.jelly index f61bf35..e08da29 100644 --- a/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config.jelly +++ b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config.jelly @@ -18,6 +18,9 @@ + + + diff --git a/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config_de.properties b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config_de.properties index 6392094..995c034 100644 --- a/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config_de.properties +++ b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config_de.properties @@ -22,3 +22,4 @@ Log\ how\ each\ build\ is\ triggered=Aufzeichnen, wodurch die jeweiligen Builds angesto\u00DFen wurden. Log\ credentials\ usage=Aufzeichnen, welche Objekte auf Credentials zugreifen. +Log\ Groovy\ scripts=Groovy Skripte aufzeichnen. \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/help-logScriptUsage.html b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/help-logScriptUsage.html new file mode 100644 index 0000000..4e94ef7 --- /dev/null +++ b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/help-logScriptUsage.html @@ -0,0 +1,4 @@ +
+ When this option is enabled, Groovy scripts that run with privileged rights are logged. This includes the script console, CLI and runs using scripts outside the sandbox. +
+ diff --git a/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java b/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java index 7cf1c3d..955bdc2 100644 --- a/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java +++ b/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java @@ -32,6 +32,7 @@ public void should_support_configuration_as_code() { assertEquals(".*/(?:configSubmit|doUninstall|doDelete|postBuildResult|enable|disable|cancelQueue|stop|toggleLogKeep|doWipeOutWorkspace|createItem|createView|toggleOffline|cancelQuietDown|quietDown|restart|exit|safeExit)", plugin.getPattern()); assertTrue(plugin.getLogBuildCause()); assertTrue(plugin.shouldLogCredentialsUsage()); + assertTrue(plugin.shouldLogBuildCause()); assertEquals(3, plugin.getLoggers().size()); //first logger diff --git a/src/test/java/hudson/plugins/audit_trail/ScriptUsageListenerTest.java b/src/test/java/hudson/plugins/audit_trail/ScriptUsageListenerTest.java new file mode 100644 index 0000000..e6dae98 --- /dev/null +++ b/src/test/java/hudson/plugins/audit_trail/ScriptUsageListenerTest.java @@ -0,0 +1,106 @@ +package hudson.plugins.audit_trail; + +import hudson.Util; +import hudson.cli.GroovyCommand; +import hudson.cli.GroovyshCommand; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; +import org.kohsuke.stapler.StaplerRequest; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Locale; +import java.util.regex.Pattern; + +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.RequestDispatcher; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ScriptUsageListenerTest { + @Rule + public JenkinsRule r = new JenkinsRule(); + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + private final String script = "println('light of the world')"; + + @Test + public void consoleUsageIsLogged() throws Exception { + String logFileName = "consoleUsageIsProperlyLogged.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).sendConfiguration(r, wc); + + 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(r.jenkins, "_scriptText.jelly")).thenReturn(view); + r.jenkins.doScriptText(req, rsp); + + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue("logged actions: " + log, Pattern.compile( + ".*A groovy script was executed by user 'SYSTEM'\\. Origin: Script Console Controller\\..*The " + + "executed script:.*" + Pattern.quote(script) + ".*", Pattern.DOTALL).matcher(log).matches()); + } + + @Test + public void groovyCliUsageIsLogged() throws Exception { + String logFileName = "cliUsageIsProperlyLogged.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).sendConfiguration(r, wc); + + GroovyCommand cmd = new GroovyCommand(); + cmd.script = "="; + InputStream scriptStream = new ByteArrayInputStream(script.getBytes(StandardCharsets.UTF_8)); + cmd.main(new ArrayList<>(), Locale.ENGLISH, scriptStream, System.out, System.err); + + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue("logged actions: " + log, Pattern.compile( + ".*A groovy script was executed\\. Origin: CLI/GroovyCommand.*The executed script:.*" + + Pattern.quote(script) + ".*", Pattern.DOTALL).matcher(log).matches()); + } + + @Test + public void groovyShCliUsageIsLogged() throws Exception { + String logFileName = "cliUsageIsProperlyLogged2.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).sendConfiguration(r, wc); + + GroovyshCommand cmd = new GroovyshCommand(); + InputStream scriptStream = new ByteArrayInputStream(script.getBytes(StandardCharsets.UTF_8)); + cmd.main(new ArrayList<>(), Locale.ENGLISH, scriptStream, System.out, System.err); + + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue("logged actions: " + log, Pattern.compile( + ".*A groovy script was executed\\. Origin: CLI/GroovySh.*The executed script:.*" + + Pattern.quote(script) + ".*", Pattern.DOTALL).matcher(log).matches()); + } + @Test + public void disabledLoggingOptionIsRespected() throws Exception { + String logFileName = "disabledCredentialUsageIsRespected.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).withLogScriptUsage(false).sendConfiguration(r, wc); + + GroovyCommand cmd = new GroovyCommand(); + cmd.script = "="; + InputStream scriptStream = new ByteArrayInputStream(script.getBytes(StandardCharsets.UTF_8)); + cmd.main(new ArrayList<>(), Locale.ENGLISH, scriptStream, System.out, System.err); + + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue(log.isEmpty()); + } +} diff --git a/src/test/java/hudson/plugins/audit_trail/SimpleAuditTrailPluginConfiguratorHelper.java b/src/test/java/hudson/plugins/audit_trail/SimpleAuditTrailPluginConfiguratorHelper.java index 6320e33..88baf77 100644 --- a/src/test/java/hudson/plugins/audit_trail/SimpleAuditTrailPluginConfiguratorHelper.java +++ b/src/test/java/hudson/plugins/audit_trail/SimpleAuditTrailPluginConfiguratorHelper.java @@ -20,6 +20,7 @@ public class SimpleAuditTrailPluginConfiguratorHelper { private static final String PATTERN_INPUT_NAME= "pattern"; private static final String LOG_BUILD_CAUSE_INPUT_NAME="logBuildCause"; private static final String LOG_CREDENTIALS_USAGE_INPUT_NAME="logCredentialsUsage"; + private static final String LOG_SCRIPT_USAGE_INPUT_NAME="logScriptUsage"; private static final String ADD_LOGGER_BUTTON_TEXT = "Add Logger"; private static final String LOG_FILE_COMBO_TEXT = new LogFileAuditLogger.DescriptorImpl().getDisplayName(); @@ -27,6 +28,7 @@ public class SimpleAuditTrailPluginConfiguratorHelper { private boolean logBuildCause = true; private boolean logCredentialsUsage = true; + private boolean logScriptUsage = true; private String pattern = ".*/(?:enable|cancelItem|quietDown|createItem)/?.*"; public SimpleAuditTrailPluginConfiguratorHelper(File logFile) { @@ -37,11 +39,17 @@ public SimpleAuditTrailPluginConfiguratorHelper withLogBuildCause(boolean logBui this.logBuildCause = logBuildCause; return this; } + public SimpleAuditTrailPluginConfiguratorHelper withLogCredentialsUsage(boolean logCredentialsUsage) { this.logCredentialsUsage = logCredentialsUsage; return this; } + public SimpleAuditTrailPluginConfiguratorHelper withLogScriptUsage(boolean logScriptUsage) { + this.logScriptUsage = logScriptUsage; + return this; + } + public SimpleAuditTrailPluginConfiguratorHelper withPattern(String pattern) { this.pattern = pattern; return this; @@ -60,6 +68,7 @@ public void sendConfiguration(JenkinsRule j, JenkinsRule.WebClient wc) throws Ex form.getInputByName(PATTERN_INPUT_NAME).setValueAttribute(pattern); form.getInputByName(LOG_BUILD_CAUSE_INPUT_NAME).setChecked(logBuildCause); form.getInputByName(LOG_CREDENTIALS_USAGE_INPUT_NAME).setChecked(logCredentialsUsage); + form.getInputByName(LOG_SCRIPT_USAGE_INPUT_NAME).setChecked(logScriptUsage); j.submit(form); } } diff --git a/src/test/resources/hudson/plugins/audit_trail/expected.yml b/src/test/resources/hudson/plugins/audit_trail/expected.yml index e777a75..ca692e4 100644 --- a/src/test/resources/hudson/plugins/audit_trail/expected.yml +++ b/src/test/resources/hudson/plugins/audit_trail/expected.yml @@ -1,5 +1,6 @@ logBuildCause: true logCredentialsUsage: true +logScriptUsage: true loggers: - console: dateFormat: "yyyy-MM-dd HH:mm:ss:SSS"