No more information leaks in Confluence

Do you know how to create security alerts if your Confluence has different types of access levels? For example, internal company employees and external employees, so that if an admin of a Confluence space gives too many permissions, the system could detect it and alert us.

In this exercise, we are going to create a small script that will help us safeguard the security of the information in the spaces of our Confluence Server or Datacenter. To do this, we will create a ‘Job’ in Scriptrunner within our Jira Server or Datacenter. Adaptavist Scriptrunner is required in order to complete this exercise.

Confluence Server and Datacenter can be configured to have different levels of access. For example, we can give more open access to the internal people of our company and on the other hand, we can have more limited access for external people or outsourcers.

To maintain that security we use Confluence groups.

What is the problem?

The Confluence spaces are managed by their own Space Administrators, the Confluence admins defer responsibility to the space managers. This can lead to information leakage problems and for this we need to create mechanisms that inform us of possible leaks, in such a way that the Security Office of our company can take action.

What do we need to start?

To start with, we are going to create a scheduled script (a ‘Job’) that will check for any leaks in Confluence every day. If there is a leak, the script will send an email to the Security Office and also will send a message to a Slack channel. In this exercise you will see 2 ways to make REST calls abroad and you will also see how to send an email from code.

You will see two REST calls. In one of them we will use CURL, that means that your Jira System Administrator must install CURL in the system. CURL exists and is used since 1998. 

In the other REST call, we’ll send a message through Slack and use the RESTClient class to invoke a WebHook from our Slack channel.

Let us begin

For the recipe we need:

  • A limited access level user (belonging to the Confluence group of external or outsourcer) for when we call CURL to see what the user can see in Confluence: ‘user:password’ encoded in base64will be necessary.
  • Know the access URL to the REST access point of our Confluence.
  • An email where we will send the Security alert when it occurs.
  • Slack WebHook.

And now the Job script code:

Here the script:

import groovy.json.JsonSlurper;
import com.atlassian.jira.issue.comments.CommentManager
import com.atlassian.jira.user.util.UserManager
import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

import com.atlassian.jira.config.properties.APKeys
import groovyx.net.http.RESTClient
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.URIBuilder

import com.atlassian.mail.Email
import com.atlassian.mail.server.MailServerManager
import com.atlassian.mail.server.SMTPMailServer

MailServerManager mailServerManager = ComponentAccessor.getMailServerManager()
SMTPMailServer mailServer = mailServerManager.getDefaultSMTPMailServer()

def sout = new StringBuilder(), serr = new StringBuilder()
def proc = ""
proc = ("curl -H 'Authorization: Basic BASE64-ENCODED-USER:PASS' -H 'Accept: application/json' -H 'Content-Type: application/json' https://jira.example.com/wiki/rest/spacedirectory/1/search?type=global")
def proc2 = [ 'bash', '-c', proc].execute()
proc2.consumeProcessOutput(sout, serr)
proc2.waitFor();
def result = sout.toString()
def error = serr.toString();
def jsonSlurper = new JsonSlurper()
def seleccion = jsonSlurper.parseText(result)
//TotalSize must be 4
def size = seleccion.totalSize
def spaces = seleccion.spaces.name
log.debug("Size=" + size + " :: " + spaces)
def today = new Date().format('yyyy-MM-dd');
if (size != 2) {
    try {
        log.debug("Sending email to Security Office")
        Email email = new Email("no-reply@example.com")
        email.setMimeType("text/html")
        email.setTo("mraddon@example.com, security-office@example.com")
        email.setSubject("ALERT POSSIBLE CONFLUENCE SPACE LEAK! " + today);
        email.setBody(spaces.toString())
        mailServer.send(email)
    } catch (Exception e) {

    }
    final String webhookURL = "https://hooks.slack.com/services/ZZZ/YYY/XXXX"
    final String channelOrUserId = "#event-alerts"
    def baseUrl = ComponentAccessor.applicationProperties.getString(APKeys.JIRA_BASEURL)
    def message = "ALERT POSSIBLE CONFLUENCE SPACE LEAK! " + today + " " + spaces.toString()
    def client = new RESTClient("https://hooks.slack.com")
    def data = [:]
    
    data.put("channel", channelOrUserId)
    data.put("text", message)
    data.put("iron_emoji", ":ghost:")
    log.debug("Sent to Slack")
    def response = client.post(
        path: new URIBuilder(webhookURL).path,
        contentType: ContentType.HTML,
        body: data,
        requestContentType: ContentType.JSON) as HttpResponseDecorator
    assert response.status == 200 : "Request failed with status $response.status. $response.entity.content.text"
}

Raúl Peláez February 1th, 2023