Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
====
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/hudson/plugins/audit_trail/AuditTrailPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuditLogger> loggers = new ArrayList<>();

private transient String log;
Expand Down Expand Up @@ -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<AuditLogger> getLoggers() { return loggers; }

public AuditTrailPlugin() {
Expand Down Expand Up @@ -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);
Expand Down
120 changes: 120 additions & 0 deletions src/main/java/hudson/plugins/audit_trail/CredentialUsageListener.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<f:entry title="${%Log how each build is triggered}">
<f:checkbox name="logBuildCause" checked="${descriptor.logBuildCause}"/>
</f:entry>
<f:entry title="${%Log credentials usage}">
<f:checkbox name="logCredentialsUsage" checked="${descriptor.logCredentialsUsage}"/>
</f:entry>
</f:advanced>
</f:section>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions src/test/resources/hudson/plugins/audit_trail/expected.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
logBuildCause: true
logCredentialsUsage: true
loggers:
- console:
dateFormat: "yyyy-MM-dd HH:mm:ss:SSS"
Expand Down