Repository: bridgecrewio/checkov
Issue category: Bug Report

Issue:
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.


Hints:
Thanks for contributing to Checkov!  We've automatically marked this issue as stale to keep our issues list tidy,  because it has not had any activity for 6 months.  It will be closed in 14 days if no further activity occurs.  Commenting on this issue will remove the stale tag. If you want to talk through the issue or help us understand the priority and context,  feel free to add a comment or join us in the Checkov slack channel at codifiedsecurity.slack.com
Thanks! 

Closing issue due to inactivity. If you feel this is in error, please re-open, or reach out to the community via slack: codifiedsecurity.slack.com Thanks!

This is still an issue as of version 3.2.164 and really should be addressed.

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