Repository: bridgecrewio/checkov
Issue category: Bug Report

Incompatibility with Dockerfile heredoc syntax
**Describe the issue**

The heredoc syntax in Dockerfiles (https://www.docker.com/blog/introduction-to-heredocs-in-dockerfiles/) is not parsed correctly by checkov. While a warning is printed, this causes some checks to pass incorrectly.

**Example Value**

The following Dockerfile

```Dockerfile
# syntax=docker/dockerfile:1.4
FROM docker.io/library/ubuntu:22.04

RUN <<EOF
echo "Hello"
echo "World"
mkdir -p /hello/world
# not detected by checkov:
apt update
EOF

# detected by checkov:
RUN apt update
```

Checkov'd via `checkov --file=test.Dockerfile` logs:

```console
2023-05-15 12:44:28,413 [MainThread  ] [WARNI]  An unsupported instruction ECHO was used in test.Dockerfile
2023-05-15 12:44:28,413 [MainThread  ] [WARNI]  An unsupported instruction ECHO was used in test.Dockerfile
2023-05-15 12:44:28,413 [MainThread  ] [WARNI]  An unsupported instruction MKDIR was used in test.Dockerfile
2023-05-15 12:44:28,413 [MainThread  ] [WARNI]  An unsupported instruction APT was used in test.Dockerfile
2023-05-15 12:44:28,413 [MainThread  ] [WARNI]  An unsupported instruction EOF was used in test.Dockerfile
[ dockerfile framework ]: 100%|████████████████████|[1/1], Current File Scanned=test.Dockerfile
[ secrets framework ]: 100%|████████████████████|[1/1], Current File Scanned=test.Dockerfile

       _               _              
   ___| |__   ___  ___| | _______   __
  / __| '_ \ / _ \/ __| |/ / _ \ \ / /
 | (__| | | |  __/ (__|   < (_) \ V / 
  \___|_| |_|\___|\___|_|\_\___/ \_/  
                                      
By bridgecrew.io | version: 2.3.240 

dockerfile scan results:

Passed checks: 34, Failed checks: 4, Skipped checks: 0

Check: CKV_DOCKER_7: "Ensure the base image uses a non latest version tag"
        PASSED for resource: test.Dockerfile.
        File: test.Dockerfile:1-13
        Guide: https://docs.bridgecrew.io/docs/ensure-the-base-image-uses-a-non-latest-version-tag
# ...
Check: CKV_DOCKER_9: "Ensure that APT isn't used"
        FAILED for resource: test.Dockerfile.RUN
        File: test.Dockerfile:13-13
        Guide: https://docs.bridgecrew.io/docs/ensure-docker-apt-is-not-used

                13 | RUN apt update

Check: CKV_DOCKER_5: "Ensure update instructions are not use alone in the Dockerfile"
        FAILED for resource: test.Dockerfile.RUN
        File: test.Dockerfile:13-13
        Guide: https://docs.bridgecrew.io/docs/ensure-update-instructions-are-not-used-alone-in-the-dockerfile

                13 | RUN apt update

Check: CKV_DOCKER_3: "Ensure that a user for the container has been created"
        FAILED for resource: test.Dockerfile.
        File: test.Dockerfile:1-13
        Guide: https://docs.bridgecrew.io/docs/ensure-that-a-user-for-the-container-has-been-created

                1  | # syntax=docker/dockerfile:1.4
                2  | FROM docker.io/library/ubuntu:22.04
                3  | 
                4  | RUN <<EOF
                5  | echo "Hello"
                6  | echo "World"
                7  | mkdir -p /hello/world
                8  | # not detected by checkov:
                9  | apt update
                10 | EOF
                11 | 
                12 | # detected by checkov:
                13 | RUN apt update

Check: CKV_DOCKER_2: "Ensure that HEALTHCHECK instructions have been added to container images"
        FAILED for resource: test.Dockerfile.
        File: test.Dockerfile:1-13
        Guide: https://docs.bridgecrew.io/docs/ensure-that-healthcheck-instructions-have-been-added-to-container-images

                1  | # syntax=docker/dockerfile:1.4
                2  | FROM docker.io/library/ubuntu:22.04
                3  | 
                4  | RUN <<EOF
                5  | echo "Hello"
                6  | echo "World"
                7  | mkdir -p /hello/world
                8  | # not detected by checkov:
                9  | apt update
                10 | EOF
                11 | 
                12 | # detected by checkov:
                13 | RUN apt update
```

Note that while the `apt update` in L13 is detected, the one inside the heredoc is not.


Functions to edit:
checkov/dockerfile/parser.py:parse
Functions added: checkov/dockerfile/parser.py:convert_multiline_commands

Patch:
diff --git a/checkov/dockerfile/parser.py b/checkov/dockerfile/parser.py
index 1af3ce6c242..bf7166be98a 100644
--- a/checkov/dockerfile/parser.py
+++ b/checkov/dockerfile/parser.py
@@ -3,6 +3,7 @@
 from collections import OrderedDict
 from pathlib import Path
 from typing import TYPE_CHECKING
+import io
 
 from dockerfile_parse import DockerfileParser
 from dockerfile_parse.constants import COMMENT_INSTRUCTION
@@ -16,7 +17,9 @@
 
 def parse(filename: str | Path) -> tuple[dict[str, list[_Instruction]], list[str]]:
     with open(filename) as dockerfile:
-        dfp = DockerfileParser(fileobj=dockerfile)
+        content = dockerfile.read()
+        converted_content = convert_multiline_commands(content)
+        dfp = DockerfileParser(fileobj=io.StringIO(converted_content))
         return dfp_group_by_instructions(dfp)
 
 
@@ -39,3 +42,25 @@ def collect_skipped_checks(parse_result: dict[str, list[_Instruction]]) -> list[
         skipped_checks = collect_suppressions_for_context(code_lines=comment_lines)
 
     return skipped_checks
+
+
+def convert_multiline_commands(dockerfile_content: str) -> str:
+    lines = dockerfile_content.splitlines()
+    converted_lines = []
+    in_multiline = False
+    multiline_command: list[str] = []
+
+    for line in lines:
+        if line.strip().startswith('RUN <<EOF'):
+            in_multiline = True
+            continue
+        elif in_multiline and line.strip() == 'EOF':
+            in_multiline = False
+            converted_lines.append(f"RUN {' && '.join(multiline_command)}")
+            multiline_command = []
+        elif in_multiline:
+            multiline_command.append(line.strip())
+        else:
+            converted_lines.append(line)
+
+    return '\n'.join(converted_lines)

Test patch: diff --git a/tests/dockerfile/resources/multiline_command/Dockerfile b/tests/dockerfile/resources/multiline_command/Dockerfile
new file mode 100644
index 00000000000..79d5de99e1c
--- /dev/null
+++ b/tests/dockerfile/resources/multiline_command/Dockerfile
@@ -0,0 +1,13 @@
+# syntax=docker/dockerfile:1.4
+FROM docker.io/library/ubuntu:22.04
+
+RUN <<EOF
+echo "Hello"
+echo "World"
+mkdir -p /hello/world
+# not detected by checkov:
+apt update
+EOF
+
+# detected by checkov:
+RUN apt update
\ No newline at end of file
diff --git a/tests/dockerfile/test_runner.py b/tests/dockerfile/test_runner.py
index 8201744772b..d6eed56bcbf 100644
--- a/tests/dockerfile/test_runner.py
+++ b/tests/dockerfile/test_runner.py
@@ -339,6 +339,17 @@ def test_runner_extra_resources(self):
         self.assertEqual(extra_resource.file_abs_path, str(test_file))
         self.assertTrue(extra_resource.file_path.endswith("Dockerfile.prod"))
 
+    def test_runner_multiline(self):
+        current_dir = os.path.dirname(os.path.realpath(__file__))
+        valid_dir_path = current_dir + "/resources/multiline_command"
+        runner = Runner(db_connector=self.db_connector())
+        report = runner.run(root_folder=valid_dir_path, external_checks_dir=None,
+                            runner_filter=RunnerFilter(framework='dockerfile', checks=['CKV_DOCKER_9']))
+        self.assertEqual(len(report.failed_checks), 1)
+        self.assertEqual(report.parsing_errors, [])
+        self.assertEqual(report.passed_checks, [])
+        self.assertEqual(report.skipped_checks, [])
+
 
     def tearDown(self) -> None:
         registry.checks = self.orig_checks