Skip to content

Commit

Permalink
Merge pull request #16 from digipost/slack-notifications
Browse files Browse the repository at this point in the history
Add slack notifications for new vulnerabilities
  • Loading branch information
Kristianrosland authored Feb 22, 2024
2 parents 7bf69c2 + c2aa1cd commit d143168
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException

data class Repos(val all: List<Repository>)
data class Repos(val all: List<Repository>) {
fun getUniqueCVEs(): Map<String, Vulnerability> {
return all.flatMap{ it.vulnerabilities }.associateBy{ it.CVE }
}
}

val logger: Logger = LoggerFactory.getLogger("no.digipost.github.monitoring.GithubGraphql")

Expand Down
46 changes: 36 additions & 10 deletions src/main/kotlin/no/digipost/github/monitoring/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,27 @@ import kotlin.system.measureTimeMillis

val LANGUAGES = setOf("JavaScript", "Java", "TypeScript", "C#", "Kotlin", "Go")
val POSSIBLE_CONTAINER_SCAN = setOf("JavaScript", "Java", "TypeScript", "Kotlin", "Shell", "Dockerfile")
const val GITHUB_OWNER = "digipost";
val GITHUB_SECRET_PATH = Path.of("/secrets/githubtoken.txt")
val SLACK_WEBHOOK_URL_PATH = Path.of("/secrets/slack-webhook-url.txt")
const val GITHUB_OWNER = "digipost"
const val TIMOUT_PUBLISH_VULNS = 1000L * 60 * 2
const val DELAY_BETWEEN_PUBLISH_VULNS = 1000L * 60 * 5

var existingVulnerabilities: Map<String, Vulnerability>? = null

suspend fun main(): Unit = coroutineScope {
val env = System.getenv("env")
val token = if (env == "local") System.getenv("token") else withContext(Dispatchers.IO) {
Files.readString(Path.of("/secrets/githubtoken.txt")).trim()
val isLocal = System.getenv("env") == "local"

val githubToken = if (isLocal) System.getenv("token") else withContext(Dispatchers.IO) {
Files.readString(GITHUB_SECRET_PATH).trim()
}

val slackWebhookUrl: String? = if (isLocal && System.getenv().containsKey("SLACK_WEBHOOK_URL")) System.getenv("SLACK_WEBHOOK_URL") else withContext(Dispatchers.IO) {
if (Files.exists(SLACK_WEBHOOK_URL_PATH)) {
Files.readString(SLACK_WEBHOOK_URL_PATH).trim()
} else {
null
}
}

val logger = LoggerFactory.getLogger("no.digipost.github.monitoring.Main")
Expand All @@ -52,20 +65,21 @@ suspend fun main(): Unit = coroutineScope {
.tags("owner", GITHUB_OWNER)
.register(prometheusMeterRegistry)

val apolloClientFactory = cachedApolloClientFactory(token)
val githubApiClient = GithubApiClient(token)
val apolloClientFactory = cachedApolloClientFactory(githubToken)
val githubApiClient = GithubApiClient(githubToken)
val slackClient = slackWebhookUrl?.let{ SlackClient(it) }

launch {
while (isActive) {
try {
withTimeout(TIMOUT_PUBLISH_VULNS) {
val timeMillis = measureTimeMillis {
publish(apolloClientFactory.invoke(), githubApiClient, multiGaugeRepoVulnCount, multiGaugeContainerScan, multiGaugeInfoScore)
publish(apolloClientFactory.invoke(), githubApiClient, slackClient, multiGaugeRepoVulnCount, multiGaugeContainerScan, multiGaugeInfoScore)
}
logger.info("Henting av repos med sårbarheter tok ${timeMillis}ms")
}
} catch (e: TimeoutCancellationException) {
logger.warn("Henting av repos med sårbarheter brukte for lang tid (timeout)")
logger.warn("Henting av repos med sårbarheter brukte for lang tid (timeout) $e")
}
delay(DELAY_BETWEEN_PUBLISH_VULNS)
}
Expand Down Expand Up @@ -101,12 +115,24 @@ fun cachedApolloClientFactory(token: String): () -> ApolloClient {
}
}

suspend fun publish(apolloClient: ApolloClient, githubApiClient: GithubApiClient, registerRepos: MultiGauge, registerContainerScanStats: MultiGauge, registerVulnerabilites: MultiGauge): Unit = coroutineScope {
suspend fun publish(apolloClient: ApolloClient, githubApiClient: GithubApiClient, slackClient: SlackClient?, registerRepos: MultiGauge, registerContainerScanStats: MultiGauge, registerVulnerabilites: MultiGauge): Unit = coroutineScope {

val channel = Channel<Repos>()
launch {
fetchAllReposWithVulnerabilities(apolloClient, githubApiClient)
.let { channel.send(it) }
.let { repos ->
if (existingVulnerabilities != null) {
repos.getUniqueCVEs()
.filter { (cve, _) -> !existingVulnerabilities!!.containsKey(cve) }
.forEach { (_, vulnerability) ->
println("Ny sårbarhet: $vulnerability")
slackClient?.sendToSlack(vulnerability)
}
}

existingVulnerabilities = repos.getUniqueCVEs()
channel.send(repos)
}
}

launch {
Expand Down
38 changes: 38 additions & 0 deletions src/main/kotlin/no/digipost/github/monitoring/SlackClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package no.digipost.github.monitoring

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

class SlackClient(private val webhookUrl: String) {

private val logger: Logger = LoggerFactory.getLogger("no.digipost.github.monitoring.SlackClient")
private val client: HttpClient = HttpClient.newBuilder().build()

fun sendToSlack(vulnerability: Vulnerability) {
val request = slackRequest("Ny sårbarhet: ${toSlackInformation(vulnerability)}")
val response = client.send(request, HttpResponse.BodyHandlers.ofString())

if (response.statusCode() != 200) {
logger.warn("Failed to report new vulnerability to slack. Status code ${response.statusCode()}, body: ${response.body()}")
}
}

private fun toSlackInformation(vulnerability: Vulnerability): String {
return "*${vulnerability.severity} (${vulnerability.score})* " +
"<https://nvd.nist.gov/vuln/detail/${vulnerability.CVE}|${vulnerability.CVE}>, " +
"package name: ${vulnerability.packageName}"
}

private fun slackRequest(message: String): HttpRequest {
return HttpRequest
.newBuilder()
.uri(URI.create(webhookUrl))
.POST(HttpRequest.BodyPublishers.ofString("{ \"text\": \"$message\"}"))
.header("Content-Type", "application/json")
.build()
}
}

0 comments on commit d143168

Please sign in to comment.