diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..da68d80 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +## General rules +1. Add only new files to git. Do not modify the existing staging unless explicitly required. + +## Verification development stages + +1. Implement everything in pure Kotlin, making full use of IDE-assisted language features and inspections. +2. Add inspection messages and tooltips to [SecurityPluginBundle.properties](src/main/resources/messages/SecurityPluginBundle.properties). +3. Create the inspection draft in the technology-specific folder under [securityLinter](src/main/kotlin/dev/protsenko/securityLinter). +4. Add tests covering the inspection: + - Example of a Docker inspection: [DS003SshPortExposed.kt](src/test/kotlin/dev/protsenko/securityLinter/docker/DS003SshPortExposed.kt) + - Example of test data for Docker inspections: [DS003](src/test/testData/docker/DS003) + - Example of extended test data with additional cases: [DS029](src/test/testData/docker/DS029) +5. Create an inspection description in HTML using the same filename as the inspection class in [inspectionDescriptions](src/main/resources/inspectionDescriptions). +6. Register the inspection in XML: + - For YAML-based inspections: [dev.protsenko.security-linter-yaml.xml](src/main/resources/META-INF/dev.protsenko.security-linter-yaml.xml) + - For general and Docker-based inspections: [plugin.xml](src/main/resources/META-INF/plugin.xml) +7. If the inspection validates Docker RUN commands, follow the rules defined in [RunCommandValidator.kt](src/main/kotlin/dev/protsenko/securityLinter/docker/checker/core/RunCommandValidator.kt). + - Register the required extension points in [plugin.xml](src/main/resources/META-INF/plugin.xml). +8. If the inspection is YAML-based, use the bundled helper classes such as [YamlPath.kt](src/main/kotlin/dev/protsenko/securityLinter/utils/YamlPath.kt). +9. Ensure that all tests pass. +10. If the plugin version has not been updated in [gradle.properties](gradle.properties), update it. +11. Update the changelog in [CHANGELOG.md](CHANGELOG.md). +12. Perform a full build and run the complete test suite for the plugin. + +## Summary for verification development + +1. All inspections must be registered and covered by tests. +2. All inspections must leverage Kotlin and IDE capabilities effectively. +3. All reusable helper utilities should be covered by JUnit 3 tests. +4. All inspections must cover corner cases and include extended test scenarios. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 17841b7..09f7367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # Cloud (IaC) Security Changelog +## [2.0.17] 15-03-2026 + +### Added + +- Inspection: [wget without recommended flags](https://protsenko.dev/infrastructure-security/avoid-wget-without-progress-or-quiet/) + ## [2.0.16] 07-02-2026 ### Fixed diff --git a/gradle.properties b/gradle.properties index 8852006..74ce3b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ pluginGroup = dev.protsenko.securityLinter pluginName = Cloud (IaC) Security pluginRepositoryUrl = https://github.com/NordCoderd/cloud-security-plugin -pluginVersion = 2.0.16 +pluginVersion = 2.0.17 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 231 diff --git a/src/main/kotlin/dev/protsenko/securityLinter/docker/checker/WgetProgressBarValidator.kt b/src/main/kotlin/dev/protsenko/securityLinter/docker/checker/WgetProgressBarValidator.kt new file mode 100644 index 0000000..ab0a4e5 --- /dev/null +++ b/src/main/kotlin/dev/protsenko/securityLinter/docker/checker/WgetProgressBarValidator.kt @@ -0,0 +1,30 @@ +package dev.protsenko.securityLinter.docker.checker + +import dev.protsenko.securityLinter.docker.checker.core.RunCommandValidator + +object WgetProgressBarValidator : RunCommandValidator { + private val wgetPattern = Regex("""\bwget\b""", RegexOption.IGNORE_CASE) + private val separators = Regex("""\s*(?:&&|;|\n)\s*""") + private val recommendedWgetOptionsPattern = + Regex( + pattern = """(?:^|\s)(--progress=dot:giga|--quiet|--no-verbose|-q(?:\S*)|-nv(?:\S*))(?=\s|$)""", + options = setOf(RegexOption.IGNORE_CASE), + ) + + override fun isValid(command: String): Boolean { + if (!wgetPattern.containsMatchIn(command)) return true + + val cmd = command.removePrefix("RUN").trim() + val individualCommands = separators.split(cmd) + + for (individualCommand in individualCommands) { + val cmdTrimmed = individualCommand.trim() + if (!wgetPattern.containsMatchIn(cmdTrimmed)) continue + if (!recommendedWgetOptionsPattern.containsMatchIn(cmdTrimmed)) { + return false + } + } + + return true + } +} diff --git a/src/main/kotlin/dev/protsenko/securityLinter/docker/inspection/run/impl/WgetWithoutRecommendedFlagsAnalyzer.kt b/src/main/kotlin/dev/protsenko/securityLinter/docker/inspection/run/impl/WgetWithoutRecommendedFlagsAnalyzer.kt new file mode 100644 index 0000000..a54d55e --- /dev/null +++ b/src/main/kotlin/dev/protsenko/securityLinter/docker/inspection/run/impl/WgetWithoutRecommendedFlagsAnalyzer.kt @@ -0,0 +1,29 @@ +package dev.protsenko.securityLinter.docker.inspection.run.impl + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import dev.protsenko.securityLinter.core.HtmlProblemDescriptor +import dev.protsenko.securityLinter.core.SecurityPluginBundle +import dev.protsenko.securityLinter.docker.checker.WgetProgressBarValidator +import dev.protsenko.securityLinter.docker.inspection.run.core.DockerfileRunAnalyzer + +class WgetWithoutRecommendedFlagsAnalyzer : DockerfileRunAnalyzer { + override fun handle( + runCommand: String, + psiElement: PsiElement, + holder: ProblemsHolder, + ) { + if (!WgetProgressBarValidator.isValid(runCommand)) { + val descriptor = + HtmlProblemDescriptor( + psiElement, + SecurityPluginBundle.message("dfs032.documentation"), + SecurityPluginBundle.message("dfs032.problem-text"), + ProblemHighlightType.WARNING, + ) + + holder.registerProblem(descriptor) + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 588a511..d370861 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -98,6 +98,7 @@ + @@ -116,4 +117,4 @@ interface="dev.protsenko.securityLinter.docker.inspection.copyAndAdd.core.DockerfileCopyOrAddAnalyzer"/> - \ No newline at end of file + diff --git a/src/main/resources/messages/SecurityPluginBundle.properties b/src/main/resources/messages/SecurityPluginBundle.properties index 81a3ac8..2619db6 100644 --- a/src/main/resources/messages/SecurityPluginBundle.properties +++ b/src/main/resources/messages/SecurityPluginBundle.properties @@ -115,6 +115,9 @@ dfs030.problem-text=Alpine package installation without --no-cache, consider usi ### dfs031 dfs031.documentation=using-pip-install-without-no-cache-dir dfs031.problem-text=The pip package installations without --no-cache-dir, consider using it to reduce image size. +### dfs032 +dfs032.documentation=avoid-wget-without-progress-or-quiet +dfs032.problem-text=Avoid using wget without a progress/quiet option. Use --progress=dot:giga or consider -q/-nv (--quiet/--no-verbose). ### dcs001 [only docker-compose] ds033.using-privileged=Using privileged: true grants full root access to the host, bypassing isolation mechanisms. @@ -178,4 +181,4 @@ kube012.problem-text=This volume type may expose the node or bypass pod isolatio # Not implemented ds029.missing-healthcheck=Missing HEALTHCHECK instruction ds023.multiple-exposed-port=Port {0} exposed more than one time. -ds024.wrong-port-definition=Port {0} is incorrect. \ No newline at end of file +ds024.wrong-port-definition=Port {0} is incorrect. diff --git a/src/test/kotlin/dev/protsenko/securityLinter/docker/DS036WgetWithoutProgressOrQuietInspectionTest.kt b/src/test/kotlin/dev/protsenko/securityLinter/docker/DS036WgetWithoutProgressOrQuietInspectionTest.kt new file mode 100644 index 0000000..df8a86b --- /dev/null +++ b/src/test/kotlin/dev/protsenko/securityLinter/docker/DS036WgetWithoutProgressOrQuietInspectionTest.kt @@ -0,0 +1,11 @@ +package dev.protsenko.securityLinter.docker + +import com.intellij.codeInspection.LocalInspectionTool +import dev.protsenko.securityLinter.core.DockerHighlightingBaseTest +import dev.protsenko.securityLinter.docker.inspection.run.DockerfileRunInspection + +class DS036WgetWithoutProgressOrQuietInspectionTest( + override val ruleFolderName: String = "DS036", + override val customFiles: Set = emptySet(), + override val targetInspection: LocalInspectionTool = DockerfileRunInspection() +) : DockerHighlightingBaseTest() diff --git a/src/test/testData/docker/DS013/Dockerfile.denied b/src/test/testData/docker/DS013/Dockerfile.denied index 2a894a8..dc1ea0d 100644 --- a/src/test/testData/docker/DS013/Dockerfile.denied +++ b/src/test/testData/docker/DS013/Dockerfile.denied @@ -1,6 +1,6 @@ FROM debian:stable-20210621 RUN wget http://bing.com +Avoid using both 'wget' and 'curl' since these tools have the same effect.">RUN wget -q http://bing.com FROM baseimage:1.0 USER mike diff --git a/src/test/testData/docker/DS036/Dockerfile.allowed b/src/test/testData/docker/DS036/Dockerfile.allowed new file mode 100644 index 0000000..9e2f459 --- /dev/null +++ b/src/test/testData/docker/DS036/Dockerfile.allowed @@ -0,0 +1,6 @@ +FROM ubuntu:24.04 +RUN wget --progress=dot:giga https://example.com/archive.tar.gz && \ + wget --quiet https://example.com/archive2.tar.gz && \ + wget --no-verbose https://example.com/archive3.tar.gz && \ + wget -qO /tmp/archive4.tar.gz https://example.com/archive4.tar.gz && \ + wget -nv https://example.com/archive5.tar.gz diff --git a/src/test/testData/docker/DS036/Dockerfile.denied b/src/test/testData/docker/DS036/Dockerfile.denied new file mode 100644 index 0000000..f046774 --- /dev/null +++ b/src/test/testData/docker/DS036/Dockerfile.denied @@ -0,0 +1,2 @@ +FROM ubuntu:24.04 +RUN wget https://example.com/archive.tar.gz