diff --git a/src/main/kotlin/no/digipost/github/monitoring/GithubGraphql.kt b/src/main/kotlin/no/digipost/github/monitoring/GithubGraphql.kt index 48a8703..c114fbe 100644 --- a/src/main/kotlin/no/digipost/github/monitoring/GithubGraphql.kt +++ b/src/main/kotlin/no/digipost/github/monitoring/GithubGraphql.kt @@ -13,7 +13,11 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.IOException -data class Repos(val all: List) +data class Repos(val all: List) { + fun getUniqueCVEs(): Map { + return all.flatMap{ it.vulnerabilities }.associateBy{ it.CVE } + } +} val logger: Logger = LoggerFactory.getLogger("no.digipost.github.monitoring.GithubGraphql") diff --git a/src/main/kotlin/no/digipost/github/monitoring/Main.kt b/src/main/kotlin/no/digipost/github/monitoring/Main.kt index a9b7c74..dba35c4 100644 --- a/src/main/kotlin/no/digipost/github/monitoring/Main.kt +++ b/src/main/kotlin/no/digipost/github/monitoring/Main.kt @@ -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? = 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") @@ -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) } @@ -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() 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 { diff --git a/src/main/kotlin/no/digipost/github/monitoring/SlackClient.kt b/src/main/kotlin/no/digipost/github/monitoring/SlackClient.kt new file mode 100644 index 0000000..c39940d --- /dev/null +++ b/src/main/kotlin/no/digipost/github/monitoring/SlackClient.kt @@ -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})* " + + ", " + + "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() + } +} \ No newline at end of file