Skip to content

Commit f0e822d

Browse files
committed
2.0.14 Docker Compose inspections update
1 parent 148be15 commit f0e822d

6 files changed

Lines changed: 118 additions & 95 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
# Cloud (IaC) Security Changelog
44

5+
## [2.0.14] 13-10-2025
6+
7+
### Changed
8+
9+
- Docker Compose inspections were rewritten to better YAML analyze approach
10+
11+
Please support plugin by starring [GitHub repository](https://github.com/NordCoderd/cloud-security-plugin)
12+
513
## [2.0.13] 21-09-2025
614

715
Small cosmetic changes

build.gradle.kts

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,30 +55,32 @@ intellijPlatform {
5555
version = providers.gradleProperty("pluginVersion")
5656

5757
// Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest
58-
description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map {
59-
val start = "<!-- Plugin description -->"
60-
val end = "<!-- Plugin description end -->"
61-
62-
with(it.lines()) {
63-
if (!containsAll(listOf(start, end))) {
64-
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
58+
description =
59+
providers.fileContents(layout.projectDirectory.file("README.md")).asText.map {
60+
val start = "<!-- Plugin description -->"
61+
val end = "<!-- Plugin description end -->"
62+
63+
with(it.lines()) {
64+
if (!containsAll(listOf(start, end))) {
65+
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
66+
}
67+
subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML)
6568
}
66-
subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML)
6769
}
68-
}
6970

7071
val changelog = project.changelog // local variable for configuration cache compatibility
7172
// Get the latest available change notes from the changelog file
72-
changeNotes = providers.gradleProperty("pluginVersion").map { pluginVersion ->
73-
with(changelog) {
74-
renderItem(
75-
(getOrNull(pluginVersion) ?: getUnreleased())
76-
.withHeader(false)
77-
.withEmptySections(false),
78-
Changelog.OutputType.HTML,
79-
)
73+
changeNotes =
74+
providers.gradleProperty("pluginVersion").map { pluginVersion ->
75+
with(changelog) {
76+
renderItem(
77+
(getOrNull(pluginVersion) ?: getUnreleased())
78+
.withHeader(false)
79+
.withEmptySections(false),
80+
Changelog.OutputType.HTML,
81+
)
82+
}
8083
}
81-
}
8284

8385
ideaVersion {
8486
sinceBuild = providers.gradleProperty("pluginSinceBuild")
@@ -97,7 +99,8 @@ intellijPlatform {
9799
// The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3
98100
// Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more:
99101
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
100-
channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) }
102+
channels =
103+
providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) }
101104
}
102105

103106
pluginVerification {
@@ -138,14 +141,15 @@ intellijPlatformTesting {
138141
runIde {
139142
register("runIdeForUiTests") {
140143
task {
141-
jvmArgumentProviders += CommandLineArgumentProvider {
142-
listOf(
143-
"-Drobot-server.port=8082",
144-
"-Dide.mac.message.dialogs.as.sheets=false",
145-
"-Djb.privacy.policy.text=<!--999.999-->",
146-
"-Djb.consents.confirmation.enabled=false",
147-
)
148-
}
144+
jvmArgumentProviders +=
145+
CommandLineArgumentProvider {
146+
listOf(
147+
"-Drobot-server.port=8082",
148+
"-Dide.mac.message.dialogs.as.sheets=false",
149+
"-Djb.privacy.policy.text=<!--999.999-->",
150+
"-Djb.consents.confirmation.enabled=false",
151+
)
152+
}
149153
}
150154

151155
plugins {

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pluginGroup = dev.protsenko.securityLinter
22
pluginName = Cloud (IaC) Security
33
pluginRepositoryUrl = https://github.com/NordCoderd/cloud-security-plugin
4-
pluginVersion = 2.0.13
4+
pluginVersion = 2.0.14
55

66
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
77
pluginSinceBuild = 231

gradlew

100644100755
File mode changed.

src/main/kotlin/dev/protsenko/securityLinter/dockerCompose/DockerComposeInspection.kt

Lines changed: 72 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package dev.protsenko.securityLinter.dockerCompose
33
import com.intellij.codeInspection.LocalInspectionTool
44
import com.intellij.codeInspection.ProblemHighlightType
55
import com.intellij.codeInspection.ProblemsHolder
6-
import com.intellij.psi.PsiElement
76
import com.intellij.psi.PsiElementVisitor
87
import com.intellij.psi.PsiFile
98
import dev.protsenko.securityLinter.core.DockerFileConstants.PROHIBITED_PORTS
@@ -16,16 +15,15 @@ import dev.protsenko.securityLinter.dockerCompose.DockerComposeConstants.PRIVILE
1615
import dev.protsenko.securityLinter.dockerCompose.DockerComposeConstants.USER_KEY_LITERAL
1716
import dev.protsenko.securityLinter.dockerCompose.DockerComposeConstants.supportedAttributes
1817
import dev.protsenko.securityLinter.utils.PortUtils
18+
import dev.protsenko.securityLinter.utils.YamlPath
1919
import dev.protsenko.securityLinter.utils.image.ImageAnalyzer
2020
import dev.protsenko.securityLinter.utils.image.ImageDefinitionCreator
21-
import org.jetbrains.yaml.navigation.YAMLQualifiedNameProvider
2221
import org.jetbrains.yaml.psi.YAMLFile
2322
import org.jetbrains.yaml.psi.YAMLKeyValue
23+
import org.jetbrains.yaml.psi.YAMLMapping
2424
import org.jetbrains.yaml.psi.YAMLSequenceItem
2525

2626
class DockerComposeInspection : LocalInspectionTool() {
27-
val provider = YAMLQualifiedNameProvider()
28-
2927
override fun buildVisitor(
3028
holder: ProblemsHolder,
3129
isOnTheFly: Boolean,
@@ -34,81 +32,88 @@ class DockerComposeInspection : LocalInspectionTool() {
3432
override fun visitFile(file: PsiFile) {
3533
if (file !is YAMLFile) return
3634
if (!file.name.startsWith("docker", ignoreCase = true)) return
37-
super.visitFile(file)
38-
}
3935

40-
/**
41-
* For more information about service attributes: https://docs.docker.com/reference/compose-file/services
42-
*/
43-
override fun visitElement(element: PsiElement) {
44-
if (element is YAMLKeyValue) {
45-
val fqn = provider.getQualifiedName(element) ?: return
46-
if (!fqn.startsWith("services")) return
36+
val documents = file.documents
4737

48-
val attributeName = element.key?.text ?: return
49-
if (attributeName !in supportedAttributes) return
50-
val attributeValue = element.value?.text?.trim() ?: return
38+
for (document in documents) {
39+
val serviceMapping = YamlPath.findByYamlPath("services", document) as? YAMLMapping ?: return
40+
val serviceDefinitions =
41+
serviceMapping
42+
.children
43+
.filterIsInstance<YAMLKeyValue>()
44+
.map { it.value }
45+
.filterIsInstance<YAMLMapping>()
5146

52-
when (attributeName) {
53-
// Analyzing image definition
54-
IMAGE_KEY_LITERAL -> {
55-
val imageDefinition = ImageDefinitionCreator.fromString(attributeValue, emptyMap())
56-
ImageAnalyzer.analyzeAndHighlight(imageDefinition, holder, element, emptyMap())
57-
}
47+
for (serviceDefinition in serviceDefinitions) {
48+
val serviceAttributes = serviceDefinition.children.filterIsInstance<YAMLKeyValue>()
49+
for (serviceAttribute in serviceAttributes) {
50+
val attributeName = serviceAttribute.key?.text ?: continue
51+
if (attributeName !in supportedAttributes) continue
52+
val attributeValue = serviceAttribute.value?.text ?: continue
5853

59-
USER_KEY_LITERAL -> {
60-
if (PROHIBITED_USERS.contains(attributeValue.trim())) {
61-
val descriptor =
62-
HtmlProblemDescriptor(
63-
element,
64-
SecurityPluginBundle.message("dfs002.documentation"),
65-
SecurityPluginBundle.message("dfs002.root-user-is-used"),
66-
ProblemHighlightType.ERROR,
67-
)
54+
when (attributeName) {
55+
// Analyzing image definition
56+
IMAGE_KEY_LITERAL -> {
57+
val imageDefinition = ImageDefinitionCreator.fromString(attributeValue, emptyMap())
58+
ImageAnalyzer.analyzeAndHighlight(imageDefinition, holder, serviceAttribute, emptyMap())
59+
}
6860

69-
holder.registerProblem(descriptor)
70-
}
71-
}
61+
USER_KEY_LITERAL -> {
62+
if (PROHIBITED_USERS.contains(attributeValue.trim())) {
63+
val descriptor =
64+
HtmlProblemDescriptor(
65+
serviceAttribute,
66+
SecurityPluginBundle.message("dfs002.documentation"),
67+
SecurityPluginBundle.message("dfs002.root-user-is-used"),
68+
ProblemHighlightType.ERROR,
69+
)
7270

73-
PRIVILEGED_LITERAL -> {
74-
if (attributeValue == "true") {
75-
holder.registerProblem(
76-
element,
77-
SecurityPluginBundle.message("ds033.using-privileged"),
78-
ProblemHighlightType.ERROR,
79-
)
80-
}
81-
}
71+
holder.registerProblem(descriptor)
72+
}
73+
}
74+
75+
PRIVILEGED_LITERAL -> {
76+
if (attributeValue == "true") {
77+
holder.registerProblem(
78+
serviceAttribute,
79+
SecurityPluginBundle.message("ds033.using-privileged"),
80+
ProblemHighlightType.ERROR,
81+
)
82+
}
83+
}
8284

83-
PORTS_LITERAL -> {
84-
element.value?.children?.forEach {
85-
if (it is YAMLSequenceItem) {
86-
var portsDefinitions = it.value?.text ?: return@forEach
87-
// quotes at start and end should be trimmed
88-
if (portsDefinitions.length > 2) {
89-
portsDefinitions = portsDefinitions.substring(1, portsDefinitions.length - 1)
90-
val containerPorts = PortUtils.parseContainerPorts(portsDefinitions)
91-
containerPorts.forEach { containerPort ->
92-
if (PROHIBITED_PORTS.contains(containerPort)) {
93-
val descriptor =
94-
HtmlProblemDescriptor(
95-
it,
96-
SecurityPluginBundle.message("dfs011.documentation"),
97-
SecurityPluginBundle.message("dfs011.ssh-port-exposed"),
98-
ProblemHighlightType.ERROR,
99-
emptyArray(),
100-
)
85+
PORTS_LITERAL -> {
86+
serviceAttribute.value?.children?.forEach {
87+
if (it is YAMLSequenceItem) {
88+
var portsDefinitions = it.value?.text ?: return@forEach
89+
// quotes at start and end should be trimmed
90+
if (portsDefinitions.length > 2) {
91+
portsDefinitions =
92+
portsDefinitions.substring(1, portsDefinitions.length - 1)
93+
val containerPorts = PortUtils.parseContainerPorts(portsDefinitions)
94+
containerPorts.forEach { containerPort ->
95+
if (PROHIBITED_PORTS.contains(containerPort)) {
96+
val descriptor =
97+
HtmlProblemDescriptor(
98+
it,
99+
SecurityPluginBundle.message("dfs011.documentation"),
100+
SecurityPluginBundle.message("dfs011.ssh-port-exposed"),
101+
ProblemHighlightType.ERROR,
102+
emptyArray(),
103+
)
101104

102-
holder.registerProblem(descriptor)
105+
holder.registerProblem(descriptor)
106+
}
107+
}
103108
}
104109
}
105-
}
110+
return@forEach
111+
} ?: return
106112
}
107-
return@forEach
108-
} ?: return
109-
}
110113

111-
else -> return
114+
else -> return
115+
}
116+
}
112117
}
113118
}
114119
}

src/test/testData/docker_compose/DC001/docker-compose.yml.denied

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ version: '3.1'
33
services:
44
db:
55
<error descr="Missing version tag
6+
When using image you should use a specific tag to avoid uncontrolled behavior when the image is updated.">image: mysql</error>
7+
restart: always
8+
environment:
9+
MYSQL_ROOT_PASSWORD: example
10+
extended-service:
11+
<error descr="Missing version tag
612
When using image you should use a specific tag to avoid uncontrolled behavior when the image is updated.">image: mysql</error>
713
restart: always
814
environment:

0 commit comments

Comments
 (0)