Repository: Checkmarx/kics (language: python)

Issue:
Query "Update Instruction Alone" false results with Dockerfile
### Expected Behavior

Should not report when using command `RUN apk add --update --no-cache tzdata lftp`.

### Actual Behavior

![image](https://github.com/Checkmarx/kics/assets/18613935/9abcb621-d8ff-45c7-9bf8-c29dc63564d8)

Query `9bae49be-0aa3-4de5-bab2-4c3a069e40cd` is reported, see [here](https://github.com/jokay/docker-loxone-backup/commit/6472fd3067f570b737bedb583a36abac073247d5).

> Does not work as 'one-liner' either.

### Specifications

- Version: 1.7.1
- Platform: Docker
- Subsystem: Dockerfiles
- Query: 9bae49be-0aa3-4de5-bab2-4c3a069e40cd


Discussion / hints:
Hello @jokay 
I've tested your repo with v1.7.3 and with kics-ignore it gave me no errors. Could you please run with the latest version?

Also, about that query, I've created a PR about it

Best regards

Fix patch:
diff --git a/assets/queries/dockerfile/update_instruction_alone/query.rego b/assets/queries/dockerfile/update_instruction_alone/query.rego
index a342b52f1c9..9bf0c5837c4 100644
--- a/assets/queries/dockerfile/update_instruction_alone/query.rego
+++ b/assets/queries/dockerfile/update_instruction_alone/query.rego
@@ -1,70 +1,116 @@
 package Cx
 
 CxPolicy[result] {
+	# Check if there is a command that runs install before update
 	resource := input.document[i].command[name][_]
 	resource.Cmd == "run"
 	count(resource.Value) == 1
 	command := resource.Value[0]
+    commandList := split(command, " ")
+    commandRefactor := [x | x := commandList[_]; x != ""]
+    packages := [l | commandRefactor[0] == pkg[l]]
+    count(packages) > 0
+    packageManager := pkg[packages[0]]
 
-	isValidUpdate(command)
-	not phpComposerPhar(command)
-	not updateFollowedByInstall(command)
+	update := [x | x := getDetail(commandRefactor, pkg_updater[packageManager][_]); count(x) > 0]
+    count(update) > 0
 
+	install := [x | x := getDetail(commandRefactor, pkg_installer[packageManager][_]); count(x) > 0]
+    count(install) > 0
+
+	not checkFollowedBy(update, install)
+    
 	result := {
 		"documentId": input.document[i].id,
 		"searchKey": sprintf("FROM={{%s}}.RUN={{%s}}", [name, resource.Value[0]]),
 		"issueType": "IncorrectValue",
-		"keyExpectedValue": "Instruction 'RUN <package-manager> update' should be followed by 'RUN <package-manager> install' ",
-		"keyActualValue": "Instruction 'RUN <package-manager> update' isn't followed by 'RUN <package-manager> install in the same 'RUN' statement",
+		"keyExpectedValue": sprintf("Instruction 'RUN %s %s' should be followed by 'RUN %s %s' in the same 'RUN' statement", [packageManager, pkg_installer[packageManager], packageManager, pkg_updater[packageManager]]),
+		"keyActualValue": sprintf("Instruction 'RUN %s %s' isn't followed by 'RUN %s %s in the same 'RUN' statement", [packageManager, pkg_installer[packageManager], packageManager, pkg_updater[packageManager]]),
 	}
 }
 
-isValidUpdate(command) {
-	contains(command, " update ")
-}
+CxPolicy[result] {
+	#Check if there is Update Command without Install Command
+	resource := input.document[i].command[name][n]
+	commandRefactor := getRunCommand(resource)
+    packages := [l | commandRefactor[0] == pkg[l]]
+    count(packages) > 0
+    packageManager := pkg[packages[0]]
 
-isValidUpdate(command) {
-	contains(command, " --update ")
-}
+	update := [x | x := getDetail(commandRefactor, pkg_updater[packageManager][_]); count(x) > 0]
+    count(update) > 0
 
-isValidUpdate(command) {
-	array_split := split(command, " ")
+	install := [x | x := getDetail(commandRefactor, pkg_installer[packageManager][_]); count(x) > 0]
+    count(install) == 0
+	
+    #Check if any of the next commands is RUN install Command and there is not Update Command
+    nextResources := array.slice(input.document[i].command[name], n+1, count(input.document[i].command[name]))
+    nextResource := nextResources[_]
+	nextCommandRefactor := getRunCommand(nextResource)
+    nextPackages := [l | nextCommandRefactor[0] == pkg[l]]
+    count(nextPackages) > 0
+    nextPackageManager := pkg[nextPackages[0]]
+	nextPackageManager == packageManager
+    
+	nextInstall := [x | x := getDetail(nextCommandRefactor, pkg_installer[nextPackageManager][_]); count(x) > 0]
+    count(nextInstall) > 0
 
-	len = count(array_split)
+	nextUpdate := [x | x := getDetail(nextCommandRefactor, pkg_updater[nextPackageManager][_]); count(x) > 0]
+    count(nextUpdate) == 0
+    
+    result := {
+		"documentId": input.document[i].id,
+		"searchKey": sprintf("FROM={{%s}}.RUN={{%s}}", [name, nextResource.Value[0]]),
+		"issueType": "IncorrectValue",
+		"keyExpectedValue": sprintf("Instruction 'RUN %s %s' should be combined with 'RUN %s %s' in the same 'RUN' statement", [nextPackageManager, pkg_installer[nextPackageManager], nextPackageManager, pkg_updater[nextPackageManager]]),
+		"keyActualValue": sprintf("Instruction 'RUN %s %s' isn't combined with 'RUN %s %s in the same 'RUN' statement", [nextPackageManager, pkg_installer[nextPackageManager], nextPackageManager, pkg_updater[nextPackageManager]]),
+	}
+}
 
-	update := {"update", "--update"}
+pkg := [
+    "apt-get",
+    "apk",
+    "yum",
+    "dnf",
+    "zypper",
+    "pacman",
+    "apt",
+    "pkg_add"
+]
 
-	array_split[len - 1] == update[j]
+pkg_updater := {
+    "apt-get": ["update"],
+    "apk": ["update"],
+    "yum": ["update"],
+    "dnf": ["update"],
+    "zypper": ["refresh"],
+    "pacman": ["-Syu"],
+    "apt": ["update"],
 }
 
-phpComposerPhar(command) {
-	php := {x | x := indexof_n(command, "php"); count(x) != 0}
-	count(php) > 0
-	composer := {x | x := indexof_n(command, "composer"); count(x) != 0}
-	count(composer) > 0
-	checkFollowedBy(php[_],composer)
-	update := {x | x := indexof_n(command, "update"); count(x) != 0}
-	count(update) > 0
-	checkFollowedBy(composer[_],update)
+pkg_installer := {
+    "apt-get": ["install", "source-install", "reinstall"],
+    "apk": ["add"],
+    "yum": ["install"],
+    "dnf": ["install"],
+    "zypper": ["install"],
+    "pacman": ["-S"],
+    "apt": ["install"],
 }
 
-commandList := [
-	"install",
-	"source-install",
-	"reinstall",
-	"groupinstall",
-	"localinstall",
-	"add",
-]
 
-updateFollowedByInstall(command) {
-	update := {x | x := indexof_n(command, "update"); count(x) != 0}
-	count(update) > 0
-	install := {x | x := indexof_n(command, commandList[_]); count(x) != 0}
-	count(install) > 0
-	checkFollowedBy(update[_], install)
+getRunCommand(resource) = commandRefactor {
+	resource.Cmd == "run"
+	count(resource.Value) == 1
+	command := resource.Value[0]
+    commandList := split(command, " ")
+    commandRefactor := [x | x := commandList[_]; x != ""]
+}
+
+getDetail(commandRefactor, value) = list{
+	list := [u | commandRefactor[u] == value]
 }
 
 checkFollowedBy(first, after) {
-	first[_] < after[_][_]
+	first[_] < after[_]
 }


Test patch:
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative1.dockerfile
similarity index 100%
rename from assets/queries/dockerfile/update_instruction_alone/test/negative.dockerfile
rename to assets/queries/dockerfile/update_instruction_alone/test/negative1.dockerfile
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative10.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative10.dockerfile
new file mode 100644
index 00000000000..c9ae3680374
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative10.dockerfile
@@ -0,0 +1,6 @@
+FROM alpine:latest
+RUN apk update && apk add nginx
+RUN apk --update-cache add vim
+RUN apk -U add nano
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative11.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative11.dockerfile
new file mode 100644
index 00000000000..9c05aa3a011
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative11.dockerfile
@@ -0,0 +1,5 @@
+FROM alpine:latest
+RUN apk --update add nginx
+RUN apk add --update nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative4.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative4.dockerfile
new file mode 100644
index 00000000000..b01884b80b7
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative4.dockerfile
@@ -0,0 +1,4 @@
+FROM archlinux:latest
+RUN pacman -Syu && pacman -S nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative5.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative5.dockerfile
new file mode 100644
index 00000000000..968d8f12e11
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative5.dockerfile
@@ -0,0 +1,5 @@
+FROM ubuntu:18.04
+RUN apt-get update && apt-get install -y --no-install-recommends mysql-client \
+    && rm -rf /var/lib/apt/lists/*
+RUN apk update
+ENTRYPOINT ["mysql"]
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative6.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative6.dockerfile
new file mode 100644
index 00000000000..a7876ef3345
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative6.dockerfile
@@ -0,0 +1,4 @@
+FROM opensuse:latest
+RUN zypper refresh && zypper install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative7.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative7.dockerfile
new file mode 100644
index 00000000000..fb56c5d2ee0
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative7.dockerfile
@@ -0,0 +1,4 @@
+FROM debian:latest
+RUN apt update && install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative8.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative8.dockerfile
new file mode 100644
index 00000000000..35f686265ac
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative8.dockerfile
@@ -0,0 +1,4 @@
+FROM centos:latest
+RUN yum update && yum install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/negative9.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/negative9.dockerfile
new file mode 100644
index 00000000000..b50ebc9498a
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/negative9.dockerfile
@@ -0,0 +1,4 @@
+FROM fedora:latest
+RUN dnf update && dnf install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive1.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive1.dockerfile
new file mode 100644
index 00000000000..df232ec37d9
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive1.dockerfile
@@ -0,0 +1,5 @@
+FROM alpine:latest
+RUN apk update
+RUN apk add nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive2.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive2.dockerfile
new file mode 100644
index 00000000000..f5894330b8d
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive2.dockerfile
@@ -0,0 +1,5 @@
+FROM opensuse:latest
+RUN zypper refresh
+RUN zypper install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive3.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive3.dockerfile
new file mode 100644
index 00000000000..e15e652ff0c
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive3.dockerfile
@@ -0,0 +1,5 @@
+FROM debian:latest
+RUN apt update
+RUN apt install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive4.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive4.dockerfile
new file mode 100644
index 00000000000..7dfe01f99ed
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive4.dockerfile
@@ -0,0 +1,5 @@
+FROM centos:latest
+RUN yum update
+RUN yum install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive5.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive5.dockerfile
new file mode 100644
index 00000000000..799b69db2a9
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive5.dockerfile
@@ -0,0 +1,5 @@
+FROM fedora:latest
+RUN dnf update
+RUN dnf install nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive6.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive6.dockerfile
new file mode 100644
index 00000000000..97a72651cc9
--- /dev/null
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive6.dockerfile
@@ -0,0 +1,5 @@
+FROM archlinux:latest
+RUN pacman -Syu
+RUN pacman -S nginx
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive.dockerfile b/assets/queries/dockerfile/update_instruction_alone/test/positive7.dockerfile
similarity index 87%
rename from assets/queries/dockerfile/update_instruction_alone/test/positive.dockerfile
rename to assets/queries/dockerfile/update_instruction_alone/test/positive7.dockerfile
index 915a6c1d1cb..c588dda10cb 100644
--- a/assets/queries/dockerfile/update_instruction_alone/test/positive.dockerfile
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive7.dockerfile
@@ -3,4 +3,4 @@ RUN apt-get update
 RUN apt-get install -y --no-install-recommends mysql-client \
     && rm -rf /var/lib/apt/lists/*
 RUN apk update
-ENTRYPOINT ["mysql"]
+ENTRYPOINT ["mysql"]
\ No newline at end of file
diff --git a/assets/queries/dockerfile/update_instruction_alone/test/positive_expected_result.json b/assets/queries/dockerfile/update_instruction_alone/test/positive_expected_result.json
index f06c9035bfe..0b6c8f27df4 100644
--- a/assets/queries/dockerfile/update_instruction_alone/test/positive_expected_result.json
+++ b/assets/queries/dockerfile/update_instruction_alone/test/positive_expected_result.json
@@ -2,11 +2,43 @@
   {
     "queryName": "Update Instruction Alone",
     "severity": "MEDIUM",
-    "line": 2
+    "line": 3,
+    "fileName": "positive1.dockerfile"
   },
   {
     "queryName": "Update Instruction Alone",
     "severity": "MEDIUM",
-    "line": 5
+    "line": 3,
+    "fileName": "positive2.dockerfile"
+  },
+  {
+    "queryName": "Update Instruction Alone",
+    "severity": "MEDIUM",
+    "line": 3,
+    "fileName": "positive3.dockerfile"
+  },
+  {
+    "queryName": "Update Instruction Alone",
+    "severity": "MEDIUM",
+    "line": 3,
+    "fileName": "positive4.dockerfile"
+  },
+  {
+    "queryName": "Update Instruction Alone",
+    "severity": "MEDIUM",
+    "line": 3,
+    "fileName": "positive5.dockerfile"
+  },
+  {
+    "queryName": "Update Instruction Alone",
+    "severity": "MEDIUM",
+    "line": 3,
+    "fileName": "positive6.dockerfile"
+  },
+  {
+    "queryName": "Update Instruction Alone",
+    "severity": "MEDIUM",
+    "line": 3,
+    "fileName": "positive7.dockerfile"
   }
 ]