Skip to content
Open

V36 #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ class SafeGradleScanCache : PersistentStateComponent<SafeGradleScanCache.State>
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
)
}
}
Expand All @@ -54,12 +55,13 @@ class SafeGradleScanCache : PersistentStateComponent<SafeGradleScanCache.State>
fun updateCache(file: VirtualFile, violations: List<SecurityViolation>) {
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
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -243,28 +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 allowedLevels = mutableSetOf<String>()
if (showHighToggle.isSelected) allowedLevels.add("HIGH")
if (showMediumToggle.isSelected) allowedLevels.add("MEDIUM")
if (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<DefaultTableModel, Int>() {
override fun include(entry: Entry<out DefaultTableModel, out Int>): 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
Expand All @@ -277,6 +285,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
26 changes: 16 additions & 10 deletions src/main/kotlin/com/mohammedalaamorsi/safegradle/SecurityUtils.kt
Original file line number Diff line number Diff line change
@@ -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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
))
}

Expand All @@ -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
))
}
}
Expand Down Expand Up @@ -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
)
)
}
Expand All @@ -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
)
)
}
Expand All @@ -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
)
)
}
Expand All @@ -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
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading