Skip to content

Commit 4afba2f

Browse files
author
Dmitry Protsenko
committed
2.0.12 Added a new inspection:
- Docker Inspection: [Using pip installation without --no-cache-dir](https://protsenko.dev/infrastructure-security/using-pip-install-without-no-cache-dir/)
1 parent 982d65e commit 4afba2f

File tree

15 files changed

+188
-6
lines changed

15 files changed

+188
-6
lines changed

CHANGELOG.md

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

33
# Cloud (IaC) Security Changelog
44

5+
## [2.0.12] 14-09-2025
6+
7+
### Added
8+
- Docker Inspection: [Using pip installation without --no-cache-dir](https://protsenko.dev/infrastructure-security/using-pip-install-without-no-cache-dir/)
9+
510
## [2.0.11] 30-08-2025
611

712
### Added
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.protsenko.securityLinter.docker.checker
2+
3+
import dev.protsenko.securityLinter.docker.checker.core.RunCommandValidator
4+
5+
object PipNoCacheDirValidator : RunCommandValidator {
6+
/**
7+
* Regular expression to detect pip install commands that lack the --no-cache-dir option.
8+
* Pattern explanation:
9+
* - ^RUN\s+ : Command must start with 'RUN' followed by one or more whitespace characters
10+
* - (?=.*pip\s+install) : Positive lookahead to ensure 'pip install' is present somewhere
11+
* - (?!.*--no-cache-dir) : Negative lookahead to ensure '--no-cache-dir' is NOT present anywhere
12+
* - .* : Match any remaining characters including newlines and special symbols
13+
* - $ : End of string anchor
14+
*/
15+
private val regex =
16+
Regex(
17+
"^RUN\\s+(?=.*pip\\s+install)(?!.*--no-cache-dir).*$",
18+
RegexOption.DOT_MATCHES_ALL,
19+
)
20+
21+
/**
22+
* Validates if a Docker RUN command follows security best practices for pip install.
23+
*
24+
* @param command The Docker command string to validate
25+
* @return false if pip install is used without --no-cache-dir (security violation), true otherwise
26+
*/
27+
override fun isValid(command: String): Boolean = !regex.matches(command)
28+
}

src/main/kotlin/dev/protsenko/securityLinter/docker/checker/core/RunCommandValidator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface RunCommandValidator {
55
* Every command must start with RUN and contain a shell script (Linux commands).
66
* Before implementing, please follow these guidelines:
77
* - The name of the implemented object should end with "Validator".
8+
* - The validator is singleton and should use Kotlin object (not class).
89
* - Your validator logic should not create additional objects (e.g., lists).
910
* - Your validator must handle new lines and special symbols (e.g., &&).
1011
* - Use only one regular expression to check the command.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// package dev.protsenko.securityLinter.docker.inspection.run
2+
//
3+
// import com.intellij.codeInspection.LocalInspectionTool
4+
// import com.intellij.codeInspection.ProblemHighlightType
5+
// import com.intellij.codeInspection.ProblemsHolder
6+
// import com.intellij.docker.dockerFile.parser.psi.DockerFileRunCommand
7+
// import com.intellij.docker.dockerFile.parser.psi.DockerFileVisitor
8+
// import com.intellij.openapi.extensions.ExtensionPointName
9+
// import com.intellij.psi.PsiElement
10+
// import com.intellij.psi.PsiElementVisitor
11+
// import dev.protsenko.securityLinter.core.SecurityPluginBundle
12+
// import dev.protsenko.securityLinter.docker.inspection.run.core.DockerfileRunAnalyzer
13+
//
14+
// const val MALICIOUS_RM_RF_COMMAND = "rm -rf /"
15+
//
16+
// interface DockerfileRunAnalyzer {
17+
// fun handle(
18+
// runCommand: String,
19+
// psiElement: PsiElement,
20+
// holder: ProblemsHolder,
21+
// )
22+
// }
23+
//
24+
// class RmRfAnalyzer : DockerfileRunAnalyzer {
25+
// override fun handle(
26+
// runCommand: String,
27+
// psiElement: PsiElement,
28+
// holder: ProblemsHolder,
29+
// ) {
30+
// if (runCommand.contains(MALICIOUS_RM_RF_COMMAND)) {
31+
// holder.registerProblem(
32+
// psiElement,
33+
// SecurityPluginBundle.message("inspection.text"),
34+
// ProblemHighlightType.ERROR,
35+
// )
36+
// }
37+
// }
38+
// }
39+
//
40+
// class RunCommandInspection : LocalInspectionTool() {
41+
// override fun buildVisitor(
42+
// holder: ProblemsHolder,
43+
// isOnTheFly: Boolean,
44+
// ): PsiElementVisitor =
45+
// object : DockerFileVisitor() {
46+
// override fun visitRunCommand(o: DockerFileRunCommand) {
47+
// val extensionPointName =
48+
// ExtensionPointName.create<DockerfileRunAnalyzer>("dev.protsenko.security-linter.dockerFileRunAnalyzer")
49+
//
50+
// val runCommand = o.text
51+
//
52+
// for (extension in extensionPointName.extensions) {
53+
// extension.handle(runCommand, o, holder)
54+
// }
55+
// }
56+
// }
57+
// }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.protsenko.securityLinter.docker.inspection.run.impl
2+
3+
import com.intellij.codeInspection.ProblemHighlightType
4+
import com.intellij.codeInspection.ProblemsHolder
5+
import com.intellij.psi.PsiElement
6+
import dev.protsenko.securityLinter.core.HtmlProblemDescriptor
7+
import dev.protsenko.securityLinter.core.SecurityPluginBundle
8+
import dev.protsenko.securityLinter.docker.checker.PipNoCacheDirValidator
9+
import dev.protsenko.securityLinter.docker.inspection.run.core.DockerfileRunAnalyzer
10+
11+
class PipNoCacheDirAnalyzer : DockerfileRunAnalyzer {
12+
override fun handle(
13+
runCommand: String,
14+
psiElement: PsiElement,
15+
holder: ProblemsHolder,
16+
) {
17+
if (!PipNoCacheDirValidator.isValid(runCommand)) {
18+
val descriptor =
19+
HtmlProblemDescriptor(
20+
psiElement,
21+
SecurityPluginBundle.message("dfs031.documentation"),
22+
SecurityPluginBundle.message("dfs031.problem-text"),
23+
ProblemHighlightType.WARNING,
24+
)
25+
26+
holder.registerProblem(descriptor)
27+
}
28+
}
29+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
<dockerFileRunAnalyzer implementation="dev.protsenko.securityLinter.docker.inspection.run.impl.AptIsUsedAnalyzer"/>
9898
<dockerFileRunAnalyzer implementation="dev.protsenko.securityLinter.docker.inspection.run.impl.UserAddAnalyzer"/>
9999
<dockerFileRunAnalyzer implementation="dev.protsenko.securityLinter.docker.inspection.run.impl.ApkNoCacheValidatorAnalyzer"/>
100+
<dockerFileRunAnalyzer implementation="dev.protsenko.securityLinter.docker.inspection.run.impl.PipNoCacheDirAnalyzer"/>
100101

101102
<dockerFileExposeAnalyzer implementation="dev.protsenko.securityLinter.docker.inspection.expose.impl.SshPortExposedAnalyzer"/>
102103
<dockerFileExposeAnalyzer implementation="dev.protsenko.securityLinter.docker.inspection.expose.impl.ExposedPortOutOfRangeAnalyzer"/>

src/main/resources/messages/SecurityPluginBundle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ dfs029.useradd-missing-l-flag-high-uid='useradd' without the '-l' flag and a hig
112112
### dfs030
113113
dfs030.documentation=simplify-your-apk-cache-clean-strategy
114114
dfs030.problem-text=Alpine package installation without --no-cache, consider using it to reduce image size.
115+
### dfs031
116+
dfs031.documentation=using-pip-install-without-no-cache-dir
117+
dfs031.problem-text=The pip package installations without --no-cache-dir, consider using it to reduce image size.
115118

116119
### dcs001 [only docker-compose]
117120
ds033.using-privileged=Using privileged: true grants full root access to the host, bypassing isolation mechanisms.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.protsenko.securityLinter.docker
2+
3+
import com.intellij.codeInspection.LocalInspectionTool
4+
import dev.protsenko.securityLinter.core.DockerHighlightingBaseTest
5+
import dev.protsenko.securityLinter.docker.inspection.run.DockerfileRunInspection
6+
7+
class DFS031PipNoCacheDirTest(
8+
override val ruleFolderName: String = "DFS031",
9+
override val customFiles: Set<String> = emptySet(),
10+
override val targetInspection: LocalInspectionTool = DockerfileRunInspection(),
11+
) : DockerHighlightingBaseTest()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dev.protsenko.securityLinter.utils
2+
3+
import dev.protsenko.securityLinter.docker.checker.PipNoCacheDirValidator
4+
import junit.framework.TestCase
5+
6+
class PipNoCacheDirValidatorTest : TestCase() {
7+
fun testValidCommands() {
8+
// Commands with --no-cache-dir (secure)
9+
assertTrue(PipNoCacheDirValidator.isValid("RUN pip install --no-cache-dir package"))
10+
assertTrue(PipNoCacheDirValidator.isValid("RUN pip install package --no-cache-dir"))
11+
assertTrue(PipNoCacheDirValidator.isValid("RUN pip install --no-cache-dir package==1.2.3"))
12+
assertTrue(PipNoCacheDirValidator.isValid("RUN pip install --upgrade --no-cache-dir package"))
13+
assertTrue(PipNoCacheDirValidator.isValid("RUN pip install --no-cache-dir package && echo 'done'"))
14+
15+
// Non-pip install commands (not applicable)
16+
assertTrue(PipNoCacheDirValidator.isValid("RUN apt-get update"))
17+
assertTrue(PipNoCacheDirValidator.isValid("RUN echo 'hello world'"))
18+
assertTrue(PipNoCacheDirValidator.isValid("RUN npm install package"))
19+
}
20+
21+
fun testInvalidCommands() {
22+
// pip install without --no-cache-dir (security violations)
23+
assertFalse(PipNoCacheDirValidator.isValid("RUN pip install package"))
24+
assertFalse(PipNoCacheDirValidator.isValid("RUN pip install package==1.2.3"))
25+
assertFalse(PipNoCacheDirValidator.isValid("RUN pip install --upgrade package"))
26+
assertFalse(PipNoCacheDirValidator.isValid("RUN pip install package && echo 'done'"))
27+
assertFalse(PipNoCacheDirValidator.isValid("RUN pip install -r requirements.txt"))
28+
29+
// Multi-line commands
30+
assertFalse(PipNoCacheDirValidator.isValid("RUN pip install \\\n package \\\n another-package"))
31+
}
32+
33+
fun testEdgeCases() {
34+
// Commands that don't start with RUN
35+
assertTrue(PipNoCacheDirValidator.isValid("pip install package"))
36+
assertTrue(PipNoCacheDirValidator.isValid("COPY . ."))
37+
38+
// Empty or malformed commands
39+
assertTrue(PipNoCacheDirValidator.isValid(""))
40+
assertTrue(PipNoCacheDirValidator.isValid("RUN"))
41+
assertTrue(PipNoCacheDirValidator.isValid("RUN "))
42+
}
43+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM alpine:3.13
2+
RUN pip install --no-cache-dir package

0 commit comments

Comments
 (0)