From 862410ad275e61a3c5994b8387e3c554b77aec76 Mon Sep 17 00:00:00 2001 From: Mohammed Alaa Date: Sat, 6 Jun 2026 17:08:13 +0300 Subject: [PATCH 1/3] feat: enhance security checks and improve violation reporting in various components --- .../safegradle/NetworkActivityCheck.kt | 1 - .../safegradle/PluginInjectionCheck.kt | 2 +- .../safegradle/SafeGradleScanCache.kt | 10 ++++--- .../safegradle/SafeGradleToolWindowFactory.kt | 19 +++++++++----- .../safegradle/SafeGradleYamlConfig.kt | 8 +++--- .../safegradle/SecurityUtils.kt | 26 ++++++++++++------- .../safegradle/VulnerabilityCheck.kt | 18 ++++++++----- .../safegradle/SecurityCheckTests.kt | 2 +- 8 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/NetworkActivityCheck.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/NetworkActivityCheck.kt index 5494e0c..beab50f 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/NetworkActivityCheck.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/NetworkActivityCheck.kt @@ -90,7 +90,6 @@ class NetworkActivityCheck : SecurityCheck { // Skip legitimate dependency declarations and plugin repositories or safe blocks if (currentInsideSafeBlock || - strippedLine.startsWith("maven") || strippedLine.startsWith("google()") || strippedLine.startsWith("classpath") || strippedLine.startsWith("implementation") || diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/PluginInjectionCheck.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/PluginInjectionCheck.kt index 8d23bbf..1c7b5c4 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/PluginInjectionCheck.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/PluginInjectionCheck.kt @@ -88,7 +88,7 @@ class PluginInjectionCheck : SecurityCheck { ) private val pluginPattern = Pattern.compile( - "(id|plugin)\\s*[\\(\"']\\s*([^\"'\\)]+)\\s*[\\)\"']", + "(id|plugin)\\s*\\(?[\"']\\s*([^\"'\\)]+)\\s*[\"']\\)?", Pattern.CASE_INSENSITIVE ) private val applyPattern = Pattern.compile( diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleScanCache.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleScanCache.kt index e32e8f1..1d7830a 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleScanCache.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleScanCache.kt @@ -36,13 +36,14 @@ class SafeGradleScanCache : PersistentStateComponent val entry = myState.cacheEntries[file.path] ?: return null if (entry.hash != file.modificationCount.toString()) return null - return entry.violations.map { + return entry.violations.map { SecurityViolation( file = file, line = it.line, content = it.content, message = it.message, - riskLevel = RiskLevel.valueOf(it.riskLevel) + riskLevel = RiskLevel.valueOf(it.riskLevel), + checkId = it.checkId ) } } @@ -54,12 +55,13 @@ class SafeGradleScanCache : PersistentStateComponent fun updateCache(file: VirtualFile, violations: List) { val entry = CacheEntry( hash = file.modificationCount.toString(), - violations = violations.map { + violations = violations.map { CachedViolation( line = it.line, content = it.content, message = it.message, - riskLevel = it.riskLevel.name + riskLevel = it.riskLevel.name, + checkId = it.checkId ) } ) diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt index 4e01a67..140b571 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt @@ -59,9 +59,9 @@ class SafeGradleToolWindowFactory : ToolWindowFactory, DumbAware { // Filter controls private val searchField = JTextField(20) - private val showHighToggle = JToggleButton("🔴 HIGH", true) - private val showMediumToggle = JToggleButton("🟠 MED", true) - private val showLowToggle = JToggleButton("🔵 LOW", true) + private val showHighToggle = JToggleButton("🔴 HIGH", false) + private val showMediumToggle = JToggleButton("🟠 MED", false) + private val showLowToggle = JToggleButton("🔵 LOW", false) init { project.messageBus.connect().subscribe(SafeGradleResultService.TOPIC, this) @@ -176,7 +176,7 @@ class SafeGradleToolWindowFactory : ToolWindowFactory, DumbAware { foreground = when (value) { RiskLevel.HIGH -> Color.RED RiskLevel.MEDIUM -> Color.ORANGE - RiskLevel.LOW -> Color.BLUE + RiskLevel.LOW -> Color(130, 130, 130) } } return c @@ -245,10 +245,11 @@ class SafeGradleToolWindowFactory : ToolWindowFactory, DumbAware { val baseline = if (newOnlyToggle.isSelected) SafeGradleBaseline.load(project) else emptySet() val text = searchField.text.trim() + val noneSelected = !showHighToggle.isSelected && !showMediumToggle.isSelected && !showLowToggle.isSelected val allowedLevels = mutableSetOf() - if (showHighToggle.isSelected) allowedLevels.add("HIGH") - if (showMediumToggle.isSelected) allowedLevels.add("MEDIUM") - if (showLowToggle.isSelected) allowedLevels.add("LOW") + if (noneSelected || showHighToggle.isSelected) allowedLevels.add("HIGH") + if (noneSelected || showMediumToggle.isSelected) allowedLevels.add("MEDIUM") + if (noneSelected || showLowToggle.isSelected) allowedLevels.add("LOW") rowSorter.rowFilter = object : RowFilter() { override fun include(entry: Entry): Boolean { @@ -277,6 +278,10 @@ class SafeGradleToolWindowFactory : ToolWindowFactory, DumbAware { } private fun rebuildTable() { + tableModel.setColumnIdentifiers( + if (groupByCheckToggle.isSelected) arrayOf("Check", "Line", "Risk", "Message") + else arrayOf("File", "Line", "Risk", "Message") + ) tableModel.rowCount = 0 flatViolations.clear() val orderedViolations = if (groupByCheckToggle.isSelected) { diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleYamlConfig.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleYamlConfig.kt index c3976f9..9eb97eb 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleYamlConfig.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleYamlConfig.kt @@ -34,10 +34,10 @@ object YamlConfigParser { if (trimmed.isEmpty() || trimmed.startsWith("#")) continue when { - line.startsWith("whitelist_domains:") -> { currentSection = "whitelist"; continue } - line.startsWith("suppressions:") -> { currentSection = "suppressions"; continue } - line.startsWith("severity_overrides:") -> { currentSection = "severity_overrides"; continue } - line.startsWith("allowed_script_sources:") -> { currentSection = "allowed_script_sources"; continue } + trimmed.startsWith("whitelist_domains:") -> { currentSection = "whitelist"; continue } + trimmed.startsWith("suppressions:") -> { currentSection = "suppressions"; continue } + trimmed.startsWith("severity_overrides:") -> { currentSection = "severity_overrides"; continue } + trimmed.startsWith("allowed_script_sources:") -> { currentSection = "allowed_script_sources"; continue } } when (currentSection) { diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SecurityUtils.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SecurityUtils.kt index 627bce9..9fbf1cc 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SecurityUtils.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SecurityUtils.kt @@ -1,18 +1,24 @@ package com.mohammedalaamorsi.safegradle object SecurityUtils { - /** - * Strips single-line comments from a line. - * Note: This is a simple implementation and doesn't handle strings containing // correctly. - * In a full implementation, we'd use a lexer or more complex regex. - */ fun stripComments(line: String): String { - val commentIndex = line.indexOf("//") - return if (commentIndex >= 0) { - line.substring(0, commentIndex) - } else { - line + var inString = false + var stringChar = ' ' + var i = 0 + while (i < line.length) { + val c = line[i] + if (inString) { + if (c == '\\') { i += 2; continue } + if (c == stringChar) inString = false + } else { + if (c == '"' || c == '\'') { inString = true; stringChar = c } + else if (c == '/' && i + 1 < line.length && line[i + 1] == '/') { + return line.substring(0, i) + } + } + i++ } + return line } /** diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/VulnerabilityCheck.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/VulnerabilityCheck.kt index a282509..f8adc01 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/VulnerabilityCheck.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/VulnerabilityCheck.kt @@ -204,7 +204,8 @@ class VulnerabilityCheck : SecurityCheck { violations.add(SecurityViolation( file = file, line = lineNum, content = content, message = "Vulnerable dependency: $group:$artifact:$version is affected by ${entry.cve} (${entry.description}). Upgrade to ${entry.fixVersion} or later.", - riskLevel = RiskLevel.HIGH + riskLevel = RiskLevel.HIGH, + checkId = id )) } @@ -228,7 +229,8 @@ class VulnerabilityCheck : SecurityCheck { file = file, line = lineNum, content = lines.getOrElse(lineNum - 1) { "" }.trim(), message = "OSV advisory: ${parts[0]}:${parts[1]}:${parts[2]} has known vulnerabilities [$ids]. $summary", - riskLevel = RiskLevel.HIGH + riskLevel = RiskLevel.HIGH, + checkId = id )) } } @@ -262,7 +264,8 @@ class VulnerabilityCheck : SecurityCheck { content = stripped, message = "Vulnerable dependency: $key:$version is affected by ${entry.cve} " + "(${entry.description}). Upgrade to ${entry.fixVersion} or later.", - riskLevel = RiskLevel.HIGH + riskLevel = RiskLevel.HIGH, + checkId = id ) ) } @@ -282,7 +285,8 @@ class VulnerabilityCheck : SecurityCheck { line = index + 1, content = stripped, message = "Dynamic version '$ver' used for '$dep' — floating versions break reproducible builds and may silently pull in compromised releases. Pin to an exact version.", - riskLevel = RiskLevel.MEDIUM + riskLevel = RiskLevel.MEDIUM, + checkId = id ) ) } @@ -297,7 +301,8 @@ class VulnerabilityCheck : SecurityCheck { line = index + 1, content = stripped, message = "Version range '$gav' resolves to a different artifact on every build — an attacker who publishes a matching version can silently inject malicious code. Pin to an exact version.", - riskLevel = RiskLevel.MEDIUM + riskLevel = RiskLevel.MEDIUM, + checkId = id ) ) } @@ -320,7 +325,8 @@ class VulnerabilityCheck : SecurityCheck { "resolutionStrategy.force pins '$gav' to a version affected by ${entry!!.cve}. This silently re-introduces a known vulnerability." else "resolutionStrategy.force('$gav') overrides transitive resolution — verify this version has no known vulnerabilities and is intentional.", - riskLevel = if (isVulnerable) RiskLevel.HIGH else RiskLevel.MEDIUM + riskLevel = if (isVulnerable) RiskLevel.HIGH else RiskLevel.MEDIUM, + checkId = id ) ) } diff --git a/src/test/kotlin/com/mohammedalaamorsi/safegradle/SecurityCheckTests.kt b/src/test/kotlin/com/mohammedalaamorsi/safegradle/SecurityCheckTests.kt index a014cf2..a58f2eb 100644 --- a/src/test/kotlin/com/mohammedalaamorsi/safegradle/SecurityCheckTests.kt +++ b/src/test/kotlin/com/mohammedalaamorsi/safegradle/SecurityCheckTests.kt @@ -165,7 +165,7 @@ class SecurityCheckTests : BasePlatformTestCase() { assertEquals(RiskLevel.MEDIUM, violations[0].riskLevel) } - fun `test file exfiltration detects Files.copy`() { + fun `test file exfiltration detects Files copy`() { val check = FileExfiltrationCheck() val code = """Files.copy(src, dst)""" val file = myFixture.configureByText("build.gradle.kts", code) From 2847214d4f0724ae5bdccc4cc988013aee7426ef Mon Sep 17 00:00:00 2001 From: Mohammed Alaa Date: Sat, 6 Jun 2026 17:10:46 +0300 Subject: [PATCH 2/3] feat: update version to 0.0.36 and enhance changelog with bug fixes and improvements --- CHANGELOG.md | 14 ++++++++++++++ gradle.properties | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8015670..2ed36a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## [Unreleased] +## [0.0.36] + +### 🐛 Bug Fixes + +- **Filter buttons now work correctly**: Clicking `🔴 HIGH`, `🟠 MED`, or `🔵 LOW` in the tool window now shows only findings at that severity level. Previously the buttons started in a "selected" state and toggling them *hid* results instead — the logic was inverted. Buttons now start inactive (show all), and pressing one filters *to* that level. Multiple buttons can be pressed together to show any combination. +- **LOW severity color**: Risk-level text for `LOW` findings in the results table was rendered in blue, making it visually identical to links. It is now displayed in gray for clear distinction from `HIGH` (red) and `MEDIUM` (orange). +- **HTTP URL detection in `maven {}` blocks**: Plain `http://` repository URLs inside `maven { url = ... }` declarations were silently skipped and never flagged as MITM risks. The check now runs on all lines; legitimate HTTPS repository domains continue to be suppressed via the built-in whitelist. +- **Plugin detection for Kotlin DSL syntax**: `id("com.example.plugin")` declarations were not detected by `PluginInjectionCheck`. The regex only handled Groovy-style `id 'plugin'` and `id "plugin"` forms. The pattern now correctly parses the parenthesised Kotlin form `id("…")`. +- **`checkId` lost on disk cache round-trip**: `SafeGradleScanCache` stored violations without their `checkId` field. On the next IDE startup the field defaulted to `"unknown"`, breaking suppression entries and baseline matching that relied on the check identifier. The field is now persisted and restored correctly. +- **`VulnerabilityCheck` violations missing `checkId`**: All six `SecurityViolation` constructors inside `VulnerabilityCheck` (static CVE, dynamic version, version range, `resolutionStrategy.force`, OSV advisory) omitted `checkId`. Suppressions targeting `dependency_vulnerability` now work as expected. +- **Comment stripping inside string literals**: `SecurityUtils.stripComments()` used a naive `indexOf("//")` approach that truncated URLs in string literals — e.g. `"https://example.com"` was cut to `"https:`. Replaced with a state-machine parser that tracks open string delimiters (`"` and `'`) and only strips `//` sequences that appear outside of strings. +- **YAML config section header parsing**: Section keys in `.safegradle.yml` (`whitelist_domains:`, `suppressions:`, etc.) were matched against the raw unindented line. Files with leading whitespace on section headers were silently ignored. Detection now uses the already-trimmed line so indented YAML is parsed correctly. +- **"Group by Check" column header**: When the *Group by Check* toggle was active, the first column of the results table still showed the label **File** instead of **Check**. The header now updates dynamically when the toggle changes. + ## [0.0.35] ### 🛡️ 17 New Security Checks diff --git a/gradle.properties b/gradle.properties index aec144d..bd808f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.safegradle pluginName = SafeGradle pluginRepositoryUrl = https://github.com/MohammedAlaaMorsi/SafeGradle # SemVer format -> https://semver.org -pluginVersion = 0.0.35 +pluginVersion = 0.0.36 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 251 From 55622b9e4bd519faa0eb30955e625787f7c2fbad Mon Sep 17 00:00:00 2001 From: Mohammed Alaa Date: Sat, 6 Jun 2026 17:26:25 +0300 Subject: [PATCH 3/3] feat: enhance filtering logic in SafeGradleToolWindow for improved violation display --- .../safegradle/SafeGradleToolWindowFactory.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt index 140b571..9145486 100644 --- a/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt +++ b/src/main/kotlin/com/mohammedalaamorsi/safegradle/SafeGradleToolWindowFactory.kt @@ -243,29 +243,36 @@ class SafeGradleToolWindowFactory : ToolWindowFactory, DumbAware { private fun applyFilter() { val baseline = if (newOnlyToggle.isSelected) SafeGradleBaseline.load(project) else emptySet() - val text = searchField.text.trim() - - val noneSelected = !showHighToggle.isSelected && !showMediumToggle.isSelected && !showLowToggle.isSelected - val allowedLevels = mutableSetOf() - if (noneSelected || showHighToggle.isSelected) allowedLevels.add("HIGH") - if (noneSelected || showMediumToggle.isSelected) allowedLevels.add("MEDIUM") - if (noneSelected || showLowToggle.isSelected) allowedLevels.add("LOW") + val text = searchField.text.trim().lowercase() + val highOn = showHighToggle.isSelected + val medOn = showMediumToggle.isSelected + val lowOn = showLowToggle.isSelected + val anyOn = highOn || medOn || lowOn rowSorter.rowFilter = object : RowFilter() { override fun include(entry: Entry): Boolean { - val modelRow = entry.identifier - val violation = flatViolations.getOrNull(modelRow) ?: return false + // Risk-level filter: read directly from column 2 (the RiskLevel object), + // so this never depends on flatViolations ordering. + if (anyOn) { + val risk = entry.getValue(2) as? RiskLevel + val pass = (highOn && risk == RiskLevel.HIGH) || + (medOn && risk == RiskLevel.MEDIUM) || + (lowOn && risk == RiskLevel.LOW) + if (!pass) return false + } // Baseline filter - if (baseline.isNotEmpty() && !SafeGradleBaseline.isNew(violation, baseline)) return false - - // Risk level filter - if (violation.riskLevel.name !in allowedLevels) return false + if (baseline.isNotEmpty()) { + val violation = flatViolations.getOrNull(entry.identifier) ?: return false + if (!SafeGradleBaseline.isNew(violation, baseline)) return false + } - // Text search filter + // Text search if (text.isNotEmpty()) { - val haystack = "${violation.file.name} ${violation.message} ${violation.riskLevel}".lowercase() - if (!haystack.contains(text.lowercase())) return false + val row = (0 until entry.valueCount) + .joinToString(" ") { entry.getStringValue(it) } + .lowercase() + if (!row.contains(text)) return false } return true