diff --git a/CHANGELOG.md b/CHANGELOG.md index 9900c6e..88ba0df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,19 @@ # checkov-jetbrains-idea Changelog -## [1.0.22] - 2024-09-10 +## [1.0.22] - 2024-09-15 ### Added - Added a new 'show logs' button in the plugin panel that opens the plugin log file +- Added validations to user input in the configuration screen +- Added a `test connection` button in the configuration screen ### Misc - Upgraded gradle version to 8.10.1 - Migrated the IntelliJ platform plugin to version 2.0.1 +- Refactored `ApiClient` (now `PrismaApiClient`) and `AnalyticsService` (*Work in progress*) ## [1.0.21] - 2024-08-29 diff --git a/build.gradle.kts b/build.gradle.kts index ebb733b..519782a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,8 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun properties(key: String): String = project.findProperty(key).toString() @@ -10,11 +12,11 @@ fun properties(key: String): String = project.findProperty(key).toString() plugins { id("java") // Java support alias(libs.plugins.kotlin) // Kotlin support +// alias(libs.plugins.kotlinSerialization) // Kotlin serialization support alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin alias(libs.plugins.kover) // Gradle Kover Plugin - kotlin("plugin.serialization") version "1.9.25" } group = properties("pluginGroup") @@ -38,9 +40,13 @@ dependencies { implementation("org.json:json:20231013") implementation("commons-io:commons-io:2.11.0") implementation("io.github.java-diff-utils:java-diff-utils:4.12") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") implementation("org.slf4j:slf4j-api:2.0.16") implementation("ch.qos.logback:logback-classic:1.5.6") + implementation(libs.springWeb) +// implementation(libs.kotlinxSerializationJson) + implementation(libs.jackson) + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) testImplementation(libs.junit) testImplementation(libs.jupiterApi) testRuntimeOnly("org.junit.jupiter:junit-jupiter:5.8.1") @@ -118,7 +124,10 @@ tasks { targetCompatibility = it } withType { - kotlinOptions.jvmTarget = it + compilerOptions { + jvmTarget = JvmTarget.fromTarget(it) + apiVersion = KotlinVersion.KOTLIN_2_0 + } } } diff --git a/gradle.properties b/gradle.properties index 93c0f83..36d7433 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,8 +11,8 @@ pluginVerifierIdeVersions=2020.3.4, 2021.1.3, 2021.2.4, 2024.1.6 platformType = IC # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild=203 -pluginUntilBuild=242 -platformVersion=2024.1.6 +pluginUntilBuild=242.* +platformVersion=2024.2.1 platformDownloadSources = true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77c1d6d..c84d6ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,32 @@ [versions] + # libraries +lombok = "1.18.34" junit = "4.13.2" jupiterApi = "5.8.1" +springWeb = "6.1.12" +jackson = "2.17.2" +# kotlinxSerializationJson = "1.7.2" # plugins changelog = "2.2.1" intelliJPlatform = "2.0.1" -kotlin = "1.9.25" +kotlin = "2.0.20" kover = "0.8.3" qodana = "0.1.13" [libraries] +jackson = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson"} +# kotlinxSerializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson"} +lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok" } junit = { group = "junit", name = "junit", version.ref = "junit" } jupiterApi = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "jupiterApi" } +springWeb = { group = "org.springframework", name = "spring-web", version.ref = "springWeb" } [plugins] changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +# kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } -qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } +qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } \ No newline at end of file diff --git a/src/main/java/com/bridgecrew/util/ApplicationServiceUtil.java b/src/main/java/com/bridgecrew/util/ApplicationServiceUtil.java index 245b658..4a43cec 100644 --- a/src/main/java/com/bridgecrew/util/ApplicationServiceUtil.java +++ b/src/main/java/com/bridgecrew/util/ApplicationServiceUtil.java @@ -1,12 +1,14 @@ package com.bridgecrew.util; import com.intellij.openapi.application.ApplicationManager; +import lombok.experimental.UtilityClass; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@UtilityClass public class ApplicationServiceUtil { - private final static Logger logger = LoggerFactory.getLogger(ApplicationServiceUtil.class); + private static final Logger logger = LoggerFactory.getLogger(ApplicationServiceUtil.class); public static T getService(Class clazz) { T service = ApplicationManager.getApplication().getService(clazz); diff --git a/src/main/kotlin/com/bridgecrew/activities/PostStartupActivity.kt b/src/main/kotlin/com/bridgecrew/activities/PostStartupActivity.kt index 09872c9..17c6a01 100644 --- a/src/main/kotlin/com/bridgecrew/activities/PostStartupActivity.kt +++ b/src/main/kotlin/com/bridgecrew/activities/PostStartupActivity.kt @@ -26,7 +26,7 @@ class PostStartupActivity : ProjectActivity { override suspend fun execute(project: Project) { val version = PluginManagerCore.getPlugin(PluginId.getId("com.github.bridgecrewio.prismacloud"))?.version logger.info("Starting Prisma Cloud JetBrains plugin version $version") - project.messageBus.connect(project).subscribe(INITIALIZATION_TOPIC, object : InitializationListener { + project.messageBus.connect().subscribe(INITIALIZATION_TOPIC, object : InitializationListener { override fun initializationCompleted() { project.service().subscribeToInternalEvents(project) project.service().subscribeToProjectEventChange() diff --git a/src/main/kotlin/com/bridgecrew/analytics/AnalyticsDataEvents.kt b/src/main/kotlin/com/bridgecrew/analytics/AnalyticsDataEvents.kt index 722e4d5..c9cba92 100644 --- a/src/main/kotlin/com/bridgecrew/analytics/AnalyticsDataEvents.kt +++ b/src/main/kotlin/com/bridgecrew/analytics/AnalyticsDataEvents.kt @@ -3,85 +3,67 @@ package com.bridgecrew.analytics import com.bridgecrew.cache.InMemCache import com.bridgecrew.settings.PLUGIN_NAME import com.bridgecrew.settings.PrismaSettingsState -import com.google.gson.annotations.Expose +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonIgnore import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.extensions.PluginId -import kotlinx.serialization.EncodeDefault -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import kotlinx.serialization.json.JsonObject import java.util.* +open class AnalyticsData { + + val pluginName: String = PLUGIN_NAME -@OptIn(ExperimentalSerializationApi::class) -@Serializable -sealed class AnalyticsData(@EncodeDefault val pluginName: String = PLUGIN_NAME) { - - @EncodeDefault val installationId: String = PrismaSettingsState().getInstance()!!.installationId - @EncodeDefault - var pluginVersion: String? = - PluginManagerCore.getPlugin(PluginId.getId("com.github.bridgecrewio.prismacloud"))?.version + val pluginVersion: String? = PluginManagerCore.getPlugin(PluginId.getId("com.github.bridgecrewio.prismacloud"))?.version - @EncodeDefault - var ideVersion: String? = - ApplicationInfo.getInstance().fullApplicationName + " / " + ApplicationInfo.getInstance().build + val ideVersion: String = ApplicationInfo.getInstance().fullApplicationName + " / " + ApplicationInfo.getInstance().build - @EncodeDefault - var operatingSystem: String? = System.getProperty("os.name") + " " + System.getProperty("os.version") + val operatingSystem: String = System.getProperty("os.name") + " " + System.getProperty("os.version") - @EncodeDefault - var checkovVersion: String? = InMemCache.get("checkovVersion") + val checkovVersion: String? = InMemCache.get("checkovVersion") - @Serializable lateinit var eventType: String - @Serializable(with = DateSerializer::class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSSZ", locale = "en") lateinit var eventTime: Date - abstract val eventData: Any + open var eventData: MutableMap = mutableMapOf() } -@Serializable -data class FullScanAnalyticsData(@Transient val scanNumber: Int = 0): AnalyticsData() { - @Transient +data class FullScanAnalyticsData(@JsonIgnore val scanNumber: Int = 0): AnalyticsData() { + + @JsonIgnore lateinit var buttonPressedTime: Date - @Transient + @JsonIgnore lateinit var scanStartedTime: Date - @Transient + @JsonIgnore val frameworksScanTime: MutableMap = mutableMapOf() - @Expose - override lateinit var eventData: MutableMap - - @Transient + @JsonIgnore lateinit var scanFinishedTime: Date - @Transient + @JsonIgnore lateinit var resultsWereFullyDisplayedTime: Date + @JsonIgnore fun isFullScanFinished() = ::scanFinishedTime.isInitialized + + @JsonIgnore fun isFullScanStarted() = ::scanStartedTime.isInitialized } -@OptIn(ExperimentalSerializationApi::class) -@Serializable -data class PluginInstallAnalyticsData( - @EncodeDefault override val eventData: JsonObject = JsonObject(mapOf()) -) : AnalyticsData() - -@Serializable -data class FullScanFrameworkScanTimeData( - @Serializable(with = DateSerializer::class) - val startTime: Date -) { - @Serializable(with = DateSerializer::class) +class FullScanFrameworkScanTimeData { + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSSZ", locale = "en") + val startTime: Date = Date() + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSSZ", locale = "en") var endTime: Date = Date() + var totalTimeSeconds = 0L } diff --git a/src/main/kotlin/com/bridgecrew/analytics/AnalyticsService.kt b/src/main/kotlin/com/bridgecrew/analytics/AnalyticsService.kt index f3235a0..bb2f20a 100644 --- a/src/main/kotlin/com/bridgecrew/analytics/AnalyticsService.kt +++ b/src/main/kotlin/com/bridgecrew/analytics/AnalyticsService.kt @@ -1,31 +1,32 @@ package com.bridgecrew.analytics -import com.bridgecrew.api.ApiClient +import com.bridgecrew.api.PrismaApiClient import com.bridgecrew.cache.CacheDataAnalytics import com.bridgecrew.scheduler.IntervalRunner import com.bridgecrew.services.scan.FullScanStateService import com.bridgecrew.services.scan.ScanTaskResult -import com.bridgecrew.settings.PrismaSettingsState +import com.bridgecrew.util.ApplicationServiceUtil import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import org.slf4j.LoggerFactory import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit -@Service +// TODO: +// This service should be an application level service, but it requires a major refactor with how it handles +// FullScanStateService and CacheDataAnalytics on a project level +@Service(Service.Level.PROJECT) class AnalyticsService(val project: Project) { private val logger = LoggerFactory.getLogger(javaClass) - private var apiClient: ApiClient? = null + private val analyticsReleaseTask = IntervalRunner("Analytics") private var fullScanData: FullScanAnalyticsData? = null private var fullScanNumber = 0 - private var analyticsEventData: MutableList = arrayListOf() + private var analyticsEventData: MutableList = mutableListOf() var wereFullScanResultsDisplayed = false var wereSingleFileScanResultsDisplayed = false @@ -47,7 +48,7 @@ class AnalyticsService(val project: Project) { fun fullScanByFrameworkStarted(framework: String) { logger.info("Prisma Cloud Plugin Analytics - scan #${fullScanNumber} - full scan started for framework $framework") - fullScanData!!.frameworksScanTime[framework] = FullScanFrameworkScanTimeData(Date()) + fullScanData!!.frameworksScanTime[framework] = FullScanFrameworkScanTimeData() } fun fullScanByFrameworkFinished(framework: String) { @@ -153,14 +154,18 @@ class AnalyticsService(val project: Project) { return "${minutes}:${secondsString}" } - fun releaseAnalytics() { - val apiClient = getApiClient() ?: return + fun stopAnalyticsService() { + logger.info("Analytics service for project ${project.name} is stopping, releasing all analytics") + analyticsReleaseTask.stop() + releaseAnalytics() + } + + private fun releaseAnalytics() { + val apiClient = ApplicationServiceUtil.getService(PrismaApiClient::class.java) ?: return if (analyticsEventData.isEmpty()) { return } - - val data = analyticsEventData.joinToString(prefix = "[", postfix = "]") - val isReleased = apiClient.putDataAnalytics(data) + val isReleased = apiClient.putDataAnalytics(analyticsEventData) if (isReleased) { analyticsEventData.clear() } @@ -169,45 +174,30 @@ class AnalyticsService(val project: Project) { } fun startSchedulerReleasingAnalytics(){ - val apiClient = getApiClient() ?: return + val apiClient = ApplicationServiceUtil.getService(PrismaApiClient::class.java) ?: return val config = apiClient.getConfig() CacheDataAnalytics(project).load(analyticsEventData) - project.service() - .scheduleWithTimer({ releaseAnalytics() }, config.reportingInterval) - } - - private fun getApiClient(): ApiClient? { - if (this.apiClient != null) { - return this.apiClient - } - - val settings = PrismaSettingsState().getInstance() - if (settings != null && settings.isConfigured()) { - this.apiClient = ApiClient(settings.accessKey, settings.secretKey, settings.prismaURL) - return this.apiClient - } - - return null + analyticsReleaseTask.scheduleWithTimer({ releaseAnalytics() }, config?.reportingInterval ?: 300) } private fun buildFullScanAnalyticsData(){ fullScanData!!.eventData = fullScanData!!.frameworksScanTime fullScanData!!.eventTime = fullScanData!!.buttonPressedTime fullScanData!!.eventType = EventTypeEnum.ON_FULL_SCAN - analyticsEventData.add(Json.encodeToString(fullScanData)) + analyticsEventData.add(fullScanData!!) } private fun buildPluginInstalledAnalyticsData(){ - val analyticsData = PluginInstallAnalyticsData() + val analyticsData = AnalyticsData() analyticsData.eventTime = Date() analyticsData.eventType = EventTypeEnum.ON_PLUGIN_INSTALL - analyticsEventData.add(Json.encodeToString(analyticsData)) + analyticsEventData.add(analyticsData) } private fun buildPluginUninstalledAnalyticsData(){ - val analyticsData = PluginInstallAnalyticsData() + val analyticsData = AnalyticsData() analyticsData.eventTime = Date() analyticsData.eventType = EventTypeEnum.ON_PLUGIN_UNINSTALL - analyticsEventData.add(Json.encodeToString(analyticsData)) + analyticsEventData.add(analyticsData) } } diff --git a/src/main/kotlin/com/bridgecrew/analytics/DateSerializer.kt b/src/main/kotlin/com/bridgecrew/analytics/DateSerializer.kt deleted file mode 100644 index fc6ecbf..0000000 --- a/src/main/kotlin/com/bridgecrew/analytics/DateSerializer.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.bridgecrew.analytics - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializer -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - - -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = Date::class) -object DateSerializer { - private val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", Locale.ENGLISH) - - override fun deserialize(decoder: Decoder): Date { - val dateString = decoder.decodeString() - return format.parse(dateString) - } - - override fun serialize(encoder: Encoder, value: Date) { - val dateString = format.format(value) - encoder.encodeString(dateString) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/analytics/EventType.kt b/src/main/kotlin/com/bridgecrew/analytics/EventType.kt index 104c3ce..43b2434 100644 --- a/src/main/kotlin/com/bridgecrew/analytics/EventType.kt +++ b/src/main/kotlin/com/bridgecrew/analytics/EventType.kt @@ -1,6 +1,7 @@ package com.bridgecrew.analytics class EventTypeEnum { + companion object { const val ON_PLUGIN_INSTALL = "onPluginInstall" const val ON_PLUGIN_UNINSTALL = "onPluginUninstall" diff --git a/src/main/kotlin/com/bridgecrew/api/ApiClient.kt b/src/main/kotlin/com/bridgecrew/api/ApiClient.kt deleted file mode 100644 index bf4b6fe..0000000 --- a/src/main/kotlin/com/bridgecrew/api/ApiClient.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.bridgecrew.api - -import com.bridgecrew.settings.DEFAULT_REPORTING_INTERVAL -import com.bridgecrew.settings.PLUGIN_NAME -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.slf4j.LoggerFactory -import java.io.IOException -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpRequest.BodyPublishers -import java.net.http.HttpResponse - -@Serializable -data class LoginRequest(val username: String, val password: String) - -@Serializable -data class LoginResponse(val token: String) - -@Serializable -data class ConfigResponse(val reportingInterval: Int = DEFAULT_REPORTING_INTERVAL) - -@OptIn(ExperimentalSerializationApi::class) -class ApiClient(private val username: String, private val password: String, private val prismaURL: String) { - - private val logger = LoggerFactory.getLogger(javaClass) - private val client = HttpClient.newBuilder().build() - private val prismaURI = URI.create(prismaURL) - - fun putDataAnalytics(data: String): Boolean { - try { - logger.debug("PutDataAnalytics method call") - val authToken = this.login().token - //todo maybe add jwt parser to get know exp timestamp? - if (authToken.isEmpty()) { - //todo do we need show IDE popup here? - logger.warn("Could not authorize for username: $username") - return false - } - - val request = HttpRequest.newBuilder() - .uri(prismaURI.resolve("/bridgecrew/api/v1/plugins-analytics")) - .header("Content-Type", "application/json") - .header("Authorization", authToken) - .PUT(BodyPublishers.ofString(data)) - .build() - - val response = client.send(request, HttpResponse.BodyHandlers.ofString()) - - logger.debug("PutDataAnalytics method called response body: ${response.body()}") - if(response.statusCode() == 403 || response.statusCode() == 401 || response.statusCode() == 404){ - logger.warn("Could not authorize for token: $authToken") - return false - } - return response.statusCode() == 200 - - } catch (e: IOException) { - //todo do we need show IDE popup here? - logger.warn("IOException: ${e.message}") - return false - } - } - - fun getConfig(): ConfigResponse { - try { - logger.debug("getConfig method call") - val authToken = this.login().token - if (authToken.isEmpty()) { - logger.warn("Could not authorize for username: $username") - return ConfigResponse() - } - - val request = HttpRequest.newBuilder() - .uri(prismaURI.resolve("/bridgecrew/api/v1/plugins-analytics/get-config/$PLUGIN_NAME")) - .header("Content-Type", "application/json") - .header("Authorization", authToken) - .GET() - .build() - - val response = client.send(request, HttpResponse.BodyHandlers.ofString()) - - logger.debug("getConfig method called response body: ${response.body()}") - if(response.statusCode() == 403 || response.statusCode() == 401){ - logger.warn("Could not authorize for token: $authToken") - return ConfigResponse() - } - - val json = Json { ignoreUnknownKeys = true } - return json.decodeFromString(response.body()) - - } catch (e: IOException) { - //todo do we need show IDE popup here? - logger.warn("IOException: ${e.message}") - return ConfigResponse() - } - } - - fun getToken(){ - //todo implement: compare token exp time with current time to avoid ddosing - } - - private fun login(): LoginResponse { - try { - logger.debug("Login to $prismaURL with username: $username") - val loginRequest = LoginRequest(username, password) - val jsonBody = Json.encodeToString(loginRequest) - - val request = HttpRequest.newBuilder() - .uri(prismaURI.resolve("/login")) - .header("Content-Type", "application/json") - .POST(BodyPublishers.ofString(jsonBody)) - .build() - - val response = client.send(request, HttpResponse.BodyHandlers.ofString()) - if (response.statusCode() == 401) { - //todo do we need show IDE popup here? - logger.warn("Incorrect username or password") - return LoginResponse("") - } - - val json = Json { ignoreUnknownKeys = true } - return json.decodeFromString(response.body()) - } catch (e: IOException) { - //todo do we need show IDE popup here? - logger.warn("Method login called with IOException: ${e.message}") - return LoginResponse("") - } catch (e: SerializationException) { - logger.warn("Method login called with SerializationException: ${e.message}") - return LoginResponse("") - } - } -} diff --git a/src/main/kotlin/com/bridgecrew/api/PrismaApiClient.kt b/src/main/kotlin/com/bridgecrew/api/PrismaApiClient.kt new file mode 100644 index 0000000..06438e9 --- /dev/null +++ b/src/main/kotlin/com/bridgecrew/api/PrismaApiClient.kt @@ -0,0 +1,98 @@ +package com.bridgecrew.api + +import com.bridgecrew.analytics.AnalyticsData +import com.bridgecrew.listeners.CheckovSettingsListener +import com.bridgecrew.settings.DEFAULT_REPORTING_INTERVAL +import com.bridgecrew.settings.PLUGIN_NAME +import com.bridgecrew.settings.PrismaSettingsState +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import org.slf4j.LoggerFactory +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.web.client.RestTemplate + +data class LoginRequest(val username: String?, val password: String?) + +data class LoginResponse(val token: String) + +data class ConfigResponse(val reportingInterval: Int = DEFAULT_REPORTING_INTERVAL) + +data class PrismaConnectionDetails( + val url: String, + val accessKey: String, + val secretKey: String +) + +val mapper = ObjectMapper().apply { + registerModule(KotlinModule.Builder().build()) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) +} + +@Service +class PrismaApiClient { + + private val logger = LoggerFactory.getLogger(javaClass) + private var connection: PrismaConnectionDetails? = null + private val client = RestTemplate(listOf( + MappingJackson2HttpMessageConverter(mapper) + )) + + init { + updateConnectionDetails() + ApplicationManager.getApplication().messageBus.connect() + .subscribe(CheckovSettingsListener.SETTINGS_TOPIC, object : CheckovSettingsListener { + override fun settingsUpdated() { + updateConnectionDetails() + } + }) + } + + private fun updateConnectionDetails() { + PrismaSettingsState().getInstance()?.let { settings -> + if (settings.isConfigured()) { + connection = PrismaConnectionDetails(settings.prismaURL, settings.accessKey, settings.secretKey) + } + } + } + + fun putDataAnalytics(data: MutableList): Boolean { + return sendRequest("/bridgecrew/api/v1/plugins-analytics", HttpMethod.PUT, data, true) != null + } + + fun getConfig(): ConfigResponse? { + return sendRequest("/bridgecrew/api/v1/plugins-analytics/get-config/$PLUGIN_NAME", HttpMethod.GET, null, true) + } + + fun login(): LoginResponse? { + return sendRequest("/login", HttpMethod.POST, LoginRequest(connection?.accessKey, connection?.secretKey)) + } + + private inline fun sendRequest(endpoint: String, method: HttpMethod, payload: Any?, login: Boolean = false): T? { + try { + if (connection == null) { + logger.warn("API call aborted because Prisma Cloud settings were not configured in the plugin settings") + return null + } + logger.info("Sending {} request '{}' to {}", method, endpoint, connection!!.url) + val entity = HttpEntity(payload, HttpHeaders().apply { + contentType = MediaType.APPLICATION_JSON + // TODO: Save token and renew on exp property of the JWT + if (login) login()?.token?.let { setBearerAuth(it) } + }) + val response = client.exchange(connection!!.url + endpoint, method, entity, T::class.java) + logger.info("Successfully sent {} request '{}' to {}", method, endpoint, connection!!.url) + return response.body + } catch (t: Throwable) { + logger.error("Call to '$endpoint' ended with an error: ${t.message}", t) + } + // TODO: Return a parent object containing error details + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/cache/CacheDataAnalytics.kt b/src/main/kotlin/com/bridgecrew/cache/CacheDataAnalytics.kt index e4b52c1..1512c4c 100644 --- a/src/main/kotlin/com/bridgecrew/cache/CacheDataAnalytics.kt +++ b/src/main/kotlin/com/bridgecrew/cache/CacheDataAnalytics.kt @@ -1,23 +1,26 @@ package com.bridgecrew.cache + +import com.bridgecrew.analytics.AnalyticsData +import com.bridgecrew.api.mapper import com.intellij.openapi.project.Project class CacheDataAnalytics(private val project: Project) { - fun load(analyticsEventData: MutableList) { + fun load(analyticsEventData: MutableList) { val data = CacheDataAnalyticsStorage(project).readDataFromFile() if(data.isNullOrEmpty()){ return } - val analyticsEventDataCached = data.split("\n").toMutableList() + val analyticsEventDataCached = data.split("\n").toMutableList().map { mapper.readValue(it, AnalyticsData::class.java) } analyticsEventData.addAll(analyticsEventDataCached) CacheDataAnalyticsStorage(project).clear() } - fun stash(analyticsEventData: MutableList) { - val data = analyticsEventData.joinToString("\n") + fun stash(analyticsEventData: MutableList) { + val data = analyticsEventData.joinToString("\n") { mapper.writeValueAsString(it) } CacheDataAnalyticsStorage(project).writeDataToFile(data) } } diff --git a/src/main/kotlin/com/bridgecrew/listeners/InitializationListener.kt b/src/main/kotlin/com/bridgecrew/listeners/InitializationListener.kt index f4397c1..88dcea2 100644 --- a/src/main/kotlin/com/bridgecrew/listeners/InitializationListener.kt +++ b/src/main/kotlin/com/bridgecrew/listeners/InitializationListener.kt @@ -3,9 +3,9 @@ package com.bridgecrew.listeners import com.intellij.util.messages.Topic interface InitializationListener { + companion object { - val INITIALIZATION_TOPIC = - Topic.create("Checkov initializator", InitializationListener::class.java) + val INITIALIZATION_TOPIC = Topic.create("Checkov initializator", InitializationListener::class.java) } fun initializationCompleted() diff --git a/src/main/kotlin/com/bridgecrew/listeners/ProjectListener.kt b/src/main/kotlin/com/bridgecrew/listeners/ProjectListener.kt index 7296560..b52def9 100644 --- a/src/main/kotlin/com/bridgecrew/listeners/ProjectListener.kt +++ b/src/main/kotlin/com/bridgecrew/listeners/ProjectListener.kt @@ -1,15 +1,14 @@ package com.bridgecrew.listeners import com.bridgecrew.analytics.AnalyticsService -import com.bridgecrew.scheduler.IntervalRunner import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManagerListener class ProjectListener : ProjectManagerListener { + override fun projectClosing(project: Project) { - project.service().releaseAnalytics() - project.service().stop() + project.service().stopAnalyticsService() super.projectClosing(project) } } \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/scheduler/IntervalRunner.kt b/src/main/kotlin/com/bridgecrew/scheduler/IntervalRunner.kt index 08599bb..eca935c 100644 --- a/src/main/kotlin/com/bridgecrew/scheduler/IntervalRunner.kt +++ b/src/main/kotlin/com/bridgecrew/scheduler/IntervalRunner.kt @@ -1,17 +1,15 @@ package com.bridgecrew.scheduler -import com.intellij.openapi.components.Service -import com.intellij.openapi.project.Project import org.apache.commons.lang3.time.StopWatch import org.slf4j.LoggerFactory import java.util.* import java.util.concurrent.TimeUnit -@Service -class IntervalRunner(val project: Project) { +class IntervalRunner(private val name: String) { private val logger = LoggerFactory.getLogger(javaClass) private val timer = Timer() + fun scheduleWithTimer(intervalFunction: () -> Unit, period: Int) { val stopWatch = StopWatch.createStarted() @@ -21,7 +19,7 @@ class IntervalRunner(val project: Project) { stopWatch.time intervalFunction() logger.info( - "Function in scheduleWithTimer for ${project.name} executed with delay " + TimeUnit.MILLISECONDS.toSeconds( + "Interval function for $name executed with delay " + TimeUnit.MILLISECONDS.toSeconds( stopWatch.time ) ) @@ -36,6 +34,6 @@ class IntervalRunner(val project: Project) { fun stop(){ timer.cancel() - logger.info("Timer stopped for ${project.name}") + logger.info("Timer stopped for IntervalRunner for $name") } } diff --git a/src/main/kotlin/com/bridgecrew/services/scan/FullScanState.kt b/src/main/kotlin/com/bridgecrew/services/scan/FullScanState.kt index 28b27e4..dc7db01 100644 --- a/src/main/kotlin/com/bridgecrew/services/scan/FullScanState.kt +++ b/src/main/kotlin/com/bridgecrew/services/scan/FullScanState.kt @@ -20,7 +20,7 @@ import org.json.JSONArray import org.slf4j.LoggerFactory import java.io.File -@Service +@Service(Service.Level.PROJECT) class FullScanStateService(val project: Project) { private var fullScanFinishedFrameworksNumber: Int = 0 set(value) { diff --git a/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsConfigurable.kt b/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsConfigurable.kt index f71c980..ee2a377 100644 --- a/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsConfigurable.kt +++ b/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsConfigurable.kt @@ -2,13 +2,13 @@ package com.bridgecrew.settings import com.bridgecrew.listeners.CheckovSettingsListener import com.bridgecrew.ui.PrismaSettingsComponent +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.options.Configurable -import com.intellij.openapi.project.Project import javax.swing.JComponent -class PrismaSettingsConfigurable(val project: Project) : Configurable { +class PrismaSettingsConfigurable : Configurable { - private val prismaSettingsComponent = PrismaSettingsComponent() + private val prismaSettingsComponent = PrismaSettingsComponent(this) override fun getDisplayName(): String = "Checkov" @@ -26,6 +26,10 @@ class PrismaSettingsConfigurable(val project: Project) : Configurable { } override fun apply() { + + if (!prismaSettingsComponent.isValid()) { + return + } val settings = PrismaSettingsState().getInstance() val secretKeyModified = !prismaSettingsComponent.accessKeyField.text.equals(settings?.accessKey) @@ -39,8 +43,9 @@ class PrismaSettingsConfigurable(val project: Project) : Configurable { settings?.prismaURL = prismaSettingsComponent.prismaURLField.text.trim() settings?.fullScanRepoLimit = prismaSettingsComponent.fullScanRepoLimitField.text.toInt() - if (accessKeyModified || secretKeyModified || prismaURLModified || fullScanRepoLimitModified){ - project.messageBus.syncPublisher(CheckovSettingsListener.SETTINGS_TOPIC).settingsUpdated() + if (accessKeyModified || secretKeyModified || prismaURLModified || fullScanRepoLimitModified) { + ApplicationManager.getApplication().messageBus.syncPublisher(CheckovSettingsListener.SETTINGS_TOPIC) + .settingsUpdated() } } diff --git a/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsState.kt b/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsState.kt index 0d7739d..a6c1070 100644 --- a/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsState.kt +++ b/src/main/kotlin/com/bridgecrew/settings/PrismaSettingsState.kt @@ -44,7 +44,7 @@ class PrismaSettingsState : PersistentStateComponent { return ApplicationManager.getApplication().getService(PrismaSettingsState::class.java) } - fun isConfigured(): Boolean{ + fun isConfigured(): Boolean { return accessKey.isNotEmpty() && secretKey.isNotEmpty() && prismaURL.isNotEmpty() } diff --git a/src/main/kotlin/com/bridgecrew/ui/CheckovSettingsPanel.kt b/src/main/kotlin/com/bridgecrew/ui/CheckovSettingsPanel.kt index 5e6cd4d..bfcd580 100644 --- a/src/main/kotlin/com/bridgecrew/ui/CheckovSettingsPanel.kt +++ b/src/main/kotlin/com/bridgecrew/ui/CheckovSettingsPanel.kt @@ -3,12 +3,14 @@ package com.bridgecrew.ui import com.bridgecrew.settings.PrismaSettingsConfigurable import com.bridgecrew.utils.createGridRowCol import com.intellij.openapi.application.ApplicationManager -import com.intellij.uiDesigner.core.GridLayoutManager -import com.intellij.uiDesigner.core.GridConstraints -import javax.swing.* import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project +import com.intellij.uiDesigner.core.GridConstraints +import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.util.ui.JBUI +import javax.swing.JButton +import javax.swing.JLabel +import javax.swing.JPanel class CheckovSettingsPanel(project: Project): JPanel() { diff --git a/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowFactory.kt b/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowFactory.kt index a90671c..2e6314a 100644 --- a/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowFactory.kt +++ b/src/main/kotlin/com/bridgecrew/ui/CheckovToolWindowFactory.kt @@ -52,7 +52,7 @@ class CheckovToolWindowFactory : ToolWindowFactory { CheckovActionToolbar.setComponent(checkovToolWindowPanel) buildTabs(project, toolWindow, checkovToolWindowPanel) - Disposer.register(project, checkovToolWindowPanel) + Disposer.register(toolWindow.disposable, checkovToolWindowPanel) val connection: MessageBusConnection = project.messageBus.connect() connection.subscribe(InitializationListener.INITIALIZATION_TOPIC, object : InitializationListener { diff --git a/src/main/kotlin/com/bridgecrew/ui/PrismaSettingsComponent.kt b/src/main/kotlin/com/bridgecrew/ui/PrismaSettingsComponent.kt index ab64fa5..6aadfe3 100644 --- a/src/main/kotlin/com/bridgecrew/ui/PrismaSettingsComponent.kt +++ b/src/main/kotlin/com/bridgecrew/ui/PrismaSettingsComponent.kt @@ -1,22 +1,30 @@ package com.bridgecrew.ui +import com.bridgecrew.api.PrismaApiClient +import com.bridgecrew.ui.components.ActionLink import com.bridgecrew.utils.FULL_SCAN_RERO_LIMIT +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.options.Configurable +import com.intellij.ui.JBColor import com.intellij.util.ui.JBUI -import java.awt.* +import java.awt.Dimension +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.net.URL import java.text.NumberFormat -import javax.swing.JFormattedTextField -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField +import java.util.* +import javax.swing.* import javax.swing.text.NumberFormatter -class PrismaSettingsComponent { +class PrismaSettingsComponent(private val configurable: Configurable) { + private var rootPanel: JPanel = JPanel() val secretKeyField: JTextField = JTextField() val accessKeyField: JTextField = JTextField() val certificateField: JTextField = JTextField() val prismaURLField: JTextField = JTextField() var fullScanRepoLimitField: JFormattedTextField + private val validationResults: JLabel = JLabel() init { rootPanel.layout = GridBagLayout() @@ -31,8 +39,14 @@ class PrismaSettingsComponent { createSettingsRow(settingsPanel, constraints, "Access Key (Required):", accessKeyField, 0) createSettingsRow(settingsPanel, constraints, "Secret Key (Required):", secretKeyField, 1) createSettingsRow(settingsPanel, constraints, "Prisma URL (Required):", prismaURLField, 2) - createSettingsRow(settingsPanel, constraints, "CA-Certificate:", certificateField, 3) - createSettingsRow(settingsPanel, constraints, "SAST full scan size limit (MB):", fullScanRepoLimitField, 4) + createConnectionTestRow(settingsPanel, constraints, 3) + createSettingsRow(settingsPanel, constraints, "CA-Certificate:", certificateField, 4) + createSettingsRow(settingsPanel, constraints, "SAST full scan size limit (MB):", fullScanRepoLimitField, 5) + + constraints.gridy = 6 + constraints.gridx = 1 + validationResults.foreground = JBColor.RED + settingsPanel.add(validationResults, constraints) constraints.gridx = 0 constraints.gridy = 0 @@ -46,7 +60,13 @@ class PrismaSettingsComponent { rootPanel.add(JPanel(), constraints) } - private fun createSettingsRow(settingsPanel: JPanel, constraints: GridBagConstraints, keyText: String, inputField: JTextField, gridY: Int) { + private fun createSettingsRow( + settingsPanel: JPanel, + constraints: GridBagConstraints, + keyText: String, + inputField: JComponent, + gridY: Int + ) { constraints.gridx = 0 constraints.gridy = gridY constraints.ipady = 10 @@ -59,6 +79,41 @@ class PrismaSettingsComponent { settingsPanel.add(inputField, constraints) } + private fun createConnectionTestRow( + settingsPanel: JPanel, + constraints: GridBagConstraints, + gridY: Int + ) { + val resultText = JLabel() + val testConnectionLink = ActionLink( + "Test Connection", + { + val loginResponse = ApplicationManager.getApplication().getService(PrismaApiClient::class.java).login() + constraints.gridx = 1 + constraints.gridy = gridY + if (loginResponse?.token != null) { + resultText.text = "Connection successful" + resultText.foreground = JBColor.GREEN + } else { + resultText.text = "Connection failed. Please check the log file." + resultText.foreground = JBColor.RED + } + settingsPanel.add(resultText, constraints) + }, + preValidation@{ + resultText.text = "" + if (!isValid()) { + return@preValidation false + } + configurable.apply() + return@preValidation true + } + ) + constraints.gridx = 0 + constraints.gridy = gridY + settingsPanel.add(testConnectionLink, constraints) + } + fun getPanel(): JPanel { return rootPanel } @@ -78,4 +133,24 @@ class PrismaSettingsComponent { return field } + + fun isValid(): Boolean { + try { + val url = URL(prismaURLField.text) + if (url.protocol.equals("http", ignoreCase = true)) { + validationResults.text = "Prisma URL protocol must be https" + return false + } + if (!url.host.startsWith("api", ignoreCase = true)) { + validationResults.text = "Prisma URL hostname must begin in 'api'" + return false + } + UUID.fromString(accessKeyField.text) + validationResults.text = "" + return true + } catch (e: Exception) { + validationResults.text = e.message + return false + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/ui/actions/CheckovScanAction.kt b/src/main/kotlin/com/bridgecrew/ui/actions/CheckovScanAction.kt index d2e8429..6283f32 100644 --- a/src/main/kotlin/com/bridgecrew/ui/actions/CheckovScanAction.kt +++ b/src/main/kotlin/com/bridgecrew/ui/actions/CheckovScanAction.kt @@ -3,13 +3,14 @@ package com.bridgecrew.ui.actions import com.bridgecrew.analytics.AnalyticsService import com.bridgecrew.services.scan.CheckovScanService import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAware -object CheckovScanAction : AnAction(AllIcons.Actions.Execute), DumbAware { +object CheckovScanAction : AnAction(), DumbAware { private val presentation = Presentation() private var isExecuteState = true @@ -19,6 +20,10 @@ object CheckovScanAction : AnAction(AllIcons.Actions.Execute), DumbAware { presentation.isEnabled = false } + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + override fun actionPerformed(actionEvent: AnActionEvent) { val project = actionEvent.project if (actionEvent.presentation.icon == AllIcons.Actions.Execute) { @@ -38,6 +43,7 @@ object CheckovScanAction : AnAction(AllIcons.Actions.Execute), DumbAware { override fun update(e: AnActionEvent) { super.update(e) e.presentation.copyFrom(presentation) + updateIcon() } private fun updateIcon() { diff --git a/src/main/kotlin/com/bridgecrew/ui/components/ActionLink.kt b/src/main/kotlin/com/bridgecrew/ui/components/ActionLink.kt new file mode 100644 index 0000000..9370520 --- /dev/null +++ b/src/main/kotlin/com/bridgecrew/ui/components/ActionLink.kt @@ -0,0 +1,52 @@ +package com.bridgecrew.ui.components + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.ui.AnimatedIcon +import kotlinx.coroutines.Runnable +import java.awt.Cursor +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.util.function.Supplier +import javax.swing.JLabel +import javax.swing.SwingConstants + +const val LINK_WRAPPER = "%s" + +class ActionLink( + private val label: String, + private val onClick: Runnable, + private val preClickValidate: Supplier? = null +) : JLabel() { + + init { + setDefaultState() + this.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + setLoadingState() + val isValid: Boolean = preClickValidate?.get() == true + if (!isValid) { + setDefaultState() + return + } + ApplicationManager.getApplication().invokeLater { + onClick.run() + setDefaultState() + } + } + }) + } + + private fun setDefaultState() { + this.text = LINK_WRAPPER.format(label) + this.icon = null + this.horizontalAlignment = SwingConstants.LEFT + this.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + } + + private fun setLoadingState() { + this.text = "" + this.icon = AnimatedIcon.Default() + this.horizontalAlignment = SwingConstants.CENTER + this.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/bridgecrew/utils/fileUtils.kt b/src/main/kotlin/com/bridgecrew/utils/fileUtils.kt index b1afef2..57639be 100644 --- a/src/main/kotlin/com/bridgecrew/utils/fileUtils.kt +++ b/src/main/kotlin/com/bridgecrew/utils/fileUtils.kt @@ -14,7 +14,7 @@ import java.net.URL import java.nio.file.Files import java.nio.file.Path -var logger: Logger = LoggerFactory.getLogger("FileUtils") +val logger: Logger = LoggerFactory.getLogger("FileUtils") var checkovTempDirPath: Path = Files.createTempDirectory("checkov") fun navigateToFile(project: Project, virtualFile: VirtualFile, startLine: Int = 0, startColumn: Int = 0) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f39087f..50e4f6f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -18,7 +18,7 @@ factoryClass="com.bridgecrew.ui.CheckovToolWindowFactory" icon="/icons/plugin_small_icon.svg"/> -