diff --git a/README.adoc b/README.adoc index 7612785..b8a69ba 100644 --- a/README.adoc +++ b/README.adoc @@ -22,7 +22,7 @@ Also see the https://wiki.jenkins.io/display/JENKINS/JobConfigHistory+Plugin[JobConfigHistory Plugin] for recording actual changes made to job configurations. -== Configuration +== Logger Configuration === File logger @@ -59,6 +59,16 @@ Send audit logs to an Elastic Search server image:docs/images/jenkins-audit-trail-elastic-search-logger.png[image,width=400] +== Other configuration + +=== Log build triggers + +Will log the cause of a build. Defaults to true. + +=== Log credential users + +Will log usage of credentials as long as they are consumed through the https://plugins.jenkins.io/credentials/[Credentials plugin]. +Defaults to true. === About the client IP-address appearing in the logs ==== diff --git a/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java b/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java index a350c1c..1cdaab1 100644 --- a/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java +++ b/src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java @@ -70,7 +70,7 @@ public class AuditTrailPlugin extends GlobalConfiguration { private static final Logger LOGGER = Logger.getLogger(AuditTrailPlugin.class.getName()); private boolean logBuildCause = true; - + private boolean logCredentialsUsage = true; private List loggers = new ArrayList<>(); private transient String log; @@ -108,11 +108,15 @@ public class AuditTrailPlugin extends GlobalConfiguration { public boolean getLogBuildCause() { return shouldLogBuildCause(); } - + public boolean shouldLogBuildCause() { return logBuildCause; } + public boolean getLogCredentialsUsage() { return shouldLogCredentialsUsage(); } + + public boolean shouldLogCredentialsUsage() { return logCredentialsUsage; } + public List getLoggers() { return loggers; } public AuditTrailPlugin() { @@ -155,6 +159,12 @@ public void setLogBuildCause(boolean logBuildCause) { save(); } + @DataBoundSetter + public void setLogCredentialsUsage(boolean logCredentialsUsage) { + this.logCredentialsUsage = logCredentialsUsage; + save(); + } + private void updateFilterPattern() { try { AuditTrailFilter.setPattern(pattern); diff --git a/src/main/java/hudson/plugins/audit_trail/CredentialUsageListener.java b/src/main/java/hudson/plugins/audit_trail/CredentialUsageListener.java new file mode 100644 index 0000000..1976ffc --- /dev/null +++ b/src/main/java/hudson/plugins/audit_trail/CredentialUsageListener.java @@ -0,0 +1,120 @@ +package hudson.plugins.audit_trail; + +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsUseListener; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; +import hudson.Extension; +import hudson.model.Item; +import hudson.model.Node; +import hudson.model.Run; + +import javax.inject.Inject; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Log when credentials are used. Only works if the job decides to access the credentials via the + * {@link com.cloudbees.plugins.credentials.CredentialsProvider}. Credential-types that do not extend + * {@link com.cloudbees.plugins.credentials.Credentials} + * + * @author Jan Meiswinkel + */ +@Extension +public class CredentialUsageListener implements CredentialsUseListener { + private static final Logger LOGGER = Logger.getLogger(CredentialUsageListener.class.getName()); + + @Inject + AuditTrailPlugin configuration; + + /** + * Triggered when the {@link com.cloudbees.plugins.credentials.CredentialsProvider} accesses + * {@link com.cloudbees.plugins.credentials.Credentials}. + * + * @param c The used Credentials. + * @param run The object using the credentials. + * @see CredentialsProvider#trackAll(Run, java.util.List) + */ + @Override + public void onUse(Credentials c, Run run) { + if (!configuration.shouldLogCredentialsUsage()) + return; + + StringBuilder builder = new StringBuilder(100); + + String runName = run.getExternalizableId(); + String runType = run.getClass().toString(); + builder.append(String.format("'%s' (%s) ", runName, runType)); + auditLog(c, builder); + } + + /** + * Triggered when the {@link com.cloudbees.plugins.credentials.CredentialsProvider} accesses + * {@link com.cloudbees.plugins.credentials.Credentials}. + * + * @param c The used Credentials. + * @param node The object using the credentials. + * @see CredentialsProvider#trackAll(Node, java.util.List) + */ + @Override + public void onUse(Credentials c, Node node) { + if (!configuration.shouldLogCredentialsUsage()) + return; + + StringBuilder builder = new StringBuilder(100); + + String nodeName = node.getNodeName(); + String nodeType = node.getClass().toString(); + builder.append(String.format("'%s' (%s) ", nodeName, nodeType)); + auditLog(c, builder); + } + + /** + * Triggered when the {@link com.cloudbees.plugins.credentials.CredentialsProvider} accesses + * {@link com.cloudbees.plugins.credentials.Credentials}. + * + * @param c The used Credentials. + * @param item The object using the credentials. + * @see CredentialsProvider#trackAll(Item, java.util.List) + */ + @Override + public void onUse(Credentials c, Item item) { + if (!configuration.shouldLogCredentialsUsage()) + return; + + StringBuilder builder = new StringBuilder(100); + + String runName = item.getFullName(); + String itemType = item.getClass().toString(); + builder.append(String.format("'%s' (%s) ", runName, itemType)); + auditLog(c, builder); + } + + private void auditLog(Credentials c, StringBuilder builder) { + String credsType = c.getClass().toString(); + if (c instanceof BaseStandardCredentials) { + String credsId = ((BaseStandardCredentials) c).getId(); + builder.append(String.format("used credentials '%s' (%s).", credsId, credsType)); + } else if (c instanceof IdCredentials) { + String credsId = ((IdCredentials) c).getId(); + builder.append(String.format("used credentials '%s' (%s).", credsId, credsType)); + } else { + String noIdAvailableWarning = builder + ("used an unsupported credentials type (" + credsType + + ") whose ID cannot be audit-logged. Consider opening an issue."); + Logger.getLogger(CredentialUsageListener.class.getName()).log(Level.WARNING, null, noIdAvailableWarning); + + builder.append("used credentials of type " + credsType + " (Note: Used fallback method for log as " + + "credentials type is not supported. See INFO log for more information)."); + } + + String log = builder.toString(); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Detected credential 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 e7bbff0..f61bf35 100644 --- a/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config.jelly +++ b/src/main/resources/hudson/plugins/audit_trail/AuditTrailPlugin/config.jelly @@ -15,6 +15,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 76e7095..6392094 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 @@ -20,4 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Log\ how\ each\ build\ is\ triggered=Zeichne auf wodurch die jeweiligen Builds angesto\u00DFen worden sind. +Log\ how\ each\ build\ is\ triggered=Aufzeichnen, wodurch die jeweiligen Builds angesto\u00DFen wurden. +Log\ credentials\ usage=Aufzeichnen, welche Objekte auf Credentials zugreifen. diff --git a/src/test/java/hudson/plugins/audit_trail/AuditTrailTest.java b/src/test/java/hudson/plugins/audit_trail/AuditTrailTest.java index 1477803..13e87e3 100644 --- a/src/test/java/hudson/plugins/audit_trail/AuditTrailTest.java +++ b/src/test/java/hudson/plugins/audit_trail/AuditTrailTest.java @@ -84,7 +84,7 @@ public void shouldGenerateTwoAuditLogs() throws Exception { assertEquals("log size", 1, logger.getLimit()); assertEquals("log count", 2, logger.getCount()); assertTrue("log build cause", plugin.getLogBuildCause()); - + assertTrue("log credentials usage", plugin.shouldLogCredentialsUsage()); // When createJobAndPush(); diff --git a/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java b/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java index 16d554d..7cf1c3d 100644 --- a/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java +++ b/src/test/java/hudson/plugins/audit_trail/ConfigurationAsCodeTest.java @@ -31,6 +31,7 @@ public void should_support_configuration_as_code() { AuditTrailPlugin plugin = extensionList.get(0); 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()); assertEquals(3, plugin.getLoggers().size()); //first logger diff --git a/src/test/java/hudson/plugins/audit_trail/CredentialUsageListenerTest.java b/src/test/java/hudson/plugins/audit_trail/CredentialUsageListenerTest.java new file mode 100644 index 0000000..a79c1e6 --- /dev/null +++ b/src/test/java/hudson/plugins/audit_trail/CredentialUsageListenerTest.java @@ -0,0 +1,96 @@ +package hudson.plugins.audit_trail; + +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import hudson.Util; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.slaves.DumbSlave; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertTrue; + +public class CredentialUsageListenerTest { + @Rule + public JenkinsRule r = new JenkinsRule(); + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + + @Test + public void jobCredentialUsageIsLogged() throws Exception { + String logFileName = "jobCredentialUsageIsProperlyLogged.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).sendConfiguration(r, wc); + + FreeStyleProject job = r.createFreeStyleProject("test-job"); + String id = "id"; + Credentials creds = new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, id, "description", "username", "password"); + CredentialsProvider.track(job, creds); + + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue("logged actions: " + log, Pattern.compile(".*test-job.*used credentials '" + id + "'.*", Pattern.DOTALL).matcher(log).matches()); + } + + @Test + public void nodeCredentialUsageIsLogged() throws Exception { + String logFileName = "nodeCredentialUsageIsProperlyLogged.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).sendConfiguration(r, wc); + + DumbSlave dummyAgent = r.createSlave(); + dummyAgent.setNodeName("test-agent"); + String id = "id"; + Credentials creds = new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, id, "description", "username", "password"); + CredentialsProvider.track(dummyAgent, creds); + + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue("logged actions: " + log, Pattern.compile(".*test-agent.*used credentials '" + id + "'.*", Pattern.DOTALL).matcher(log).matches()); + } + + @Test + public void itemCredentialUsageIsLogged() throws Exception { + String logFileName = "itemCredentialUsageIsProperlyLogged.log"; + File logFile = new File(tmpDir.getRoot(), logFileName); + JenkinsRule.WebClient wc = r.createWebClient(); + new SimpleAuditTrailPluginConfiguratorHelper(logFile).sendConfiguration(r, wc); + // 'Folder' because it is a non-traditional item to access credentials. + Item item = r.createFolder("test-item"); + + String id = "id"; + Credentials creds = new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, id, "description", "username", "password"); + CredentialsProvider.track(item, creds); + String log = Util.loadFile(new File(tmpDir.getRoot(), logFileName + ".0"), StandardCharsets.UTF_8); + assertTrue("logged actions: " + log, Pattern.compile(".*test-item.*used credentials '" + id + "'.*", 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).withLogCredentialsUsage(false).sendConfiguration(r, wc); + + FreeStyleProject job = r.createFreeStyleProject("test-job"); + String id = "id"; + Credentials creds = new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, id, "description", "username", "password"); + CredentialsProvider.track(job, creds); + + 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 55cd1ec..6320e33 100644 --- a/src/test/java/hudson/plugins/audit_trail/SimpleAuditTrailPluginConfiguratorHelper.java +++ b/src/test/java/hudson/plugins/audit_trail/SimpleAuditTrailPluginConfiguratorHelper.java @@ -19,12 +19,14 @@ public class SimpleAuditTrailPluginConfiguratorHelper { private static final String LOG_FILE_LOG_SEPARATOR_INPUT_NAME = "_.logSeparator"; 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 ADD_LOGGER_BUTTON_TEXT = "Add Logger"; private static final String LOG_FILE_COMBO_TEXT = new LogFileAuditLogger.DescriptorImpl().getDisplayName(); private final File logFile; - private boolean logBuildCause =true; + private boolean logBuildCause = true; + private boolean logCredentialsUsage = true; private String pattern = ".*/(?:enable|cancelItem|quietDown|createItem)/?.*"; public SimpleAuditTrailPluginConfiguratorHelper(File logFile) { @@ -35,6 +37,10 @@ public SimpleAuditTrailPluginConfiguratorHelper withLogBuildCause(boolean logBui this.logBuildCause = logBuildCause; return this; } + public SimpleAuditTrailPluginConfiguratorHelper withLogCredentialsUsage(boolean logCredentialsUsage) { + this.logCredentialsUsage = logCredentialsUsage; + return this; + } public SimpleAuditTrailPluginConfiguratorHelper withPattern(String pattern) { this.pattern = pattern; @@ -53,6 +59,7 @@ public void sendConfiguration(JenkinsRule j, JenkinsRule.WebClient wc) throws Ex form.getInputByName(LOG_FILE_LOG_SEPARATOR_INPUT_NAME).setValueAttribute(DEFAULT_LOG_SEPARATOR); 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); 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 740dc47..e777a75 100644 --- a/src/test/resources/hudson/plugins/audit_trail/expected.yml +++ b/src/test/resources/hudson/plugins/audit_trail/expected.yml @@ -1,4 +1,5 @@ logBuildCause: true +logCredentialsUsage: true loggers: - console: dateFormat: "yyyy-MM-dd HH:mm:ss:SSS"