Repository: bridgecrewio/checkov

Issue:
Suppression is failing for docker graph checks
On an M1 mac:

Cant suppress any of the CKV2_DOCKER issues:

In the following dockerfile we have a violation of CKV2_DOCKER_2 and a suppression of it:
```
FROM arm64v8/alpine:3.17.1
# checkov:skip=CKV_DOCKER_3: Not a service
# checkov:skip=CKV_DOCKER_2: Not a service
# checkov:skip=CKV2_DOCKER_2: Not a service

RUN apk --no-cache add build-base git curl jq bash
RUN curl -s -k https://api.github.com/repos/bridgecrewio/yor/releases/latest | jq '.assets[] | select(.name | contains("linux_arm64")) | select(.content_type | contains("gzip")) | .browser_download_url' -r | awk '{print "curl -L -k " $0 " -o /usr/bin/yor.tar.gz"}' | sh
RUN tar -xf /usr/bin/yor.tar.gz -C /usr/bin/ && rm /usr/bin/yor.tar.gz && chmod +x /usr/bin/yor && echo 'alias yor="/usr/bin/yor"' >> ~/.bashrc
COPY entrypoint.sh /entrypoint.sh

# Code file to execute when the docker container starts up (`entrypoint.sh`)
ENTRYPOINT ["/entrypoint.sh"]
```

The latest checkov- -2.2.278
gives us:
`
check: CKV2_DOCKER_2: "Ensure that certificate validation isn't disabled with curl"
        FAILED for resource: /Dockerfile.RUN
        File: /Dockerfile:7-7

                7 | RUN curl -s -k https://api.github.com/repos/bridgecrewio/yor/releases/latest | jq '.assets[] | select(.name | contains("linux_386")) | select(.content_type | contains("gzip")) | .browser_download_url' -r | awk '{print "curl -L -k " $0 " -o /usr/bin/yor.tar.gz"}' | sh


`


Discussion:
I also stumbled upon this issue. 

After a bit of code review it looks like `checkov\dockerfile\runner.py` contains code for skipping checks implemented in python (see `checkov\dockerfile\runner.py:142`) using comments, but similar code for graph checks seems to be missing.

`checkov\dockerfile\runner.py:211` calls `BaseRunner.run_graph_checks_results(...)` which relies on the `runner_filter` to decide which result to include in the return value. A quick scan of the `RunnerFilter` class didn't come up with anything related to checking suppression comments in the scanned file. So it looks like the functionality to skip yaml based policies in dockerfiles is missing.

Maybe these findings help you to get to the core of the issue faster.

Resolving patch:
diff --git a/checkov/dockerfile/runner.py b/checkov/dockerfile/runner.py
--- a/checkov/dockerfile/runner.py
+++ b/checkov/dockerfile/runner.py
@@ -3,9 +3,10 @@
 import logging
 import os
 from collections.abc import Iterable
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 from checkov.common.checks_infra.registry import get_graph_checks_registry
+from checkov.common.models.enums import CheckResult
 from checkov.common.typing import LibraryGraphConnector
 from checkov.common.graph.graph_builder import CustomAttributes
 from checkov.common.graph.graph_builder.consts import GraphSource
@@ -29,6 +30,7 @@
     get_files_definitions,
     get_scannable_file_paths,
     get_abs_path,
+    build_definitions_context,
 )
 from checkov.runner_filter import RunnerFilter
 
@@ -59,6 +61,7 @@ def __init__(
         )
         self.graph_registry = get_graph_checks_registry(self.check_type)
 
+        self.context: dict[str, dict[str, Any]] = {}
         self.definitions: "dict[str, dict[str, list[_Instruction]]]" = {}  # type:ignore[assignment]  # need to check, how to support subclass differences
         self.definitions_raw: "dict[str, list[str]]" = {}       # type:ignore[assignment]
         self.root_folder: str | None = None
@@ -79,32 +82,36 @@ def run(
             self.pbar.turn_off_progress_bar()
 
         report = Report(self.check_type)
-        files_list: "Iterable[str]" = []
-        filepath_fn = None
-        if external_checks_dir:
-            for directory in external_checks_dir:
-                registry.load_external_checks(directory)
 
-                if CHECKOV_CREATE_GRAPH and self.graph_registry:
-                    self.graph_registry.load_external_checks(directory)
+        if not self.context or not self.definitions:
+            files_list: "Iterable[str]" = []
+            filepath_fn = None
+            if external_checks_dir:
+                for directory in external_checks_dir:
+                    registry.load_external_checks(directory)
 
-        if files:
-            files_list = [file for file in files if is_docker_file(os.path.basename(file))]
+                    if CHECKOV_CREATE_GRAPH and self.graph_registry:
+                        self.graph_registry.load_external_checks(directory)
 
-        if root_folder:
-            filepath_fn = lambda f: f"/{os.path.relpath(f, os.path.commonprefix((root_folder, f)))}"
-            self.root_folder = root_folder
+            if files:
+                files_list = [file for file in files if is_docker_file(os.path.basename(file))]
+
+            if root_folder:
+                filepath_fn = lambda f: f"/{os.path.relpath(f, os.path.commonprefix((root_folder, f)))}"
+                self.root_folder = root_folder
 
-            files_list = get_scannable_file_paths(root_folder=root_folder, excluded_paths=runner_filter.excluded_paths)
+                files_list = get_scannable_file_paths(root_folder=root_folder, excluded_paths=runner_filter.excluded_paths)
 
-        self.definitions, self.definitions_raw = get_files_definitions(files_list, filepath_fn)
+            self.definitions, self.definitions_raw = get_files_definitions(files_list, filepath_fn)
 
-        if CHECKOV_CREATE_GRAPH and self.graph_registry and self.graph_manager:
-            logging.info("Creating Dockerfile graph")
-            local_graph = self.graph_manager.build_graph_from_definitions(definitions=self.definitions)
-            logging.info("Successfully created Dockerfile graph")
+            self.context = build_definitions_context(definitions=self.definitions, definitions_raw=self.definitions_raw)
 
-            self.graph_manager.save_graph(local_graph)
+            if CHECKOV_CREATE_GRAPH and self.graph_registry and self.graph_manager:
+                logging.info("Creating Dockerfile graph")
+                local_graph = self.graph_manager.build_graph_from_definitions(definitions=self.definitions)
+                logging.info("Successfully created Dockerfile graph")
+
+                self.graph_manager.save_graph(local_graph)
 
         self.pbar.initiate(len(self.definitions))
 
@@ -256,7 +263,34 @@ def build_record(
         result_instruction: str,
     ) -> None:
         codeblock: list[tuple[int, str]] = []
-        self.calc_record_codeblock(codeblock, definitions_raw, docker_file_path, endline, startline)
+
+        if result_instruction:
+            entity_context = next(
+                (
+                    resource
+                    for resource in self.context[docker_file_path][result_instruction]
+                    if resource["start_line"] == startline + 1
+                ),
+                None,
+            )
+            if entity_context:
+                codeblock = entity_context["code_lines"]
+                skipped_check = next(
+                    (
+                        skipped_check
+                        for skipped_check in entity_context.get("skipped_checks", [])
+                        if skipped_check["id"] in (check.id, check.bc_id)
+                    ),
+                    None,
+                )
+                if skipped_check:
+                    check_result["result"] = CheckResult.SKIPPED
+                    check_result["suppress_comment"] = skipped_check.get("suppress_comment", "")
+            else:
+                logging.info(f"Could not find context for resource with start line {startline + 1} in {self.context[docker_file_path][result_instruction]}")
+        else:
+            self.calc_record_codeblock(codeblock, definitions_raw, docker_file_path, endline, startline)
+
         record = Record(
             check_id=check.id,
             bc_check_id=check.bc_id,
diff --git a/checkov/dockerfile/utils.py b/checkov/dockerfile/utils.py
--- a/checkov/dockerfile/utils.py
+++ b/checkov/dockerfile/utils.py
@@ -4,10 +4,13 @@
 import os
 from collections.abc import Iterable
 from pathlib import Path
-from typing import TYPE_CHECKING, Callable
+from typing import TYPE_CHECKING, Callable, Any
+
+from dockerfile_parse.constants import COMMENT_INSTRUCTION
 
 from checkov.common.runners.base_runner import filter_ignored_paths
 from checkov.common.util.dockerfile import is_docker_file
+from checkov.common.util.suppression import collect_suppressions_for_context
 from checkov.dockerfile.parser import parse
 
 if TYPE_CHECKING:
@@ -16,6 +19,7 @@
 
 DOCKERFILE_STARTLINE: Literal["startline"] = "startline"
 DOCKERFILE_ENDLINE: Literal["endline"] = "endline"
+DOCKERFILE_VALUE: Literal["value"] = "value"
 
 
 def get_scannable_file_paths(
@@ -76,3 +80,42 @@ def get_abs_path(root_folder: str | None, file_path: str) -> str:
     path_to_convert = os.path.join(root_folder, file_path) if root_folder else file_path
 
     return os.path.abspath(path_to_convert)
+
+
+def build_definitions_context(
+    definitions: dict[str, dict[str, list[_Instruction]]],
+    definitions_raw: dict[str, list[str]]
+) -> dict[str, dict[str, Any]]:
+    definitions_context: dict[str, dict[str, Any]] = {}
+
+    for file_path, definition in definitions.items():
+        file_path = str(file_path)
+        definitions_context[file_path] = {}
+        skipped_checks = []
+        if COMMENT_INSTRUCTION in definition:
+            # collect skipped check comments
+            comments = definition[COMMENT_INSTRUCTION]
+            comment_lines = [(comment[DOCKERFILE_STARTLINE], comment[DOCKERFILE_VALUE]) for comment in comments]
+            skipped_checks = collect_suppressions_for_context(code_lines=comment_lines)
+
+        for instruction_name, instructions in definition.items():
+            if instruction_name == COMMENT_INSTRUCTION:
+                continue
+
+            definitions_context[file_path][instruction_name] = []
+            for instruction in instructions:
+                start_line = instruction[DOCKERFILE_STARTLINE]
+                end_line = instruction[DOCKERFILE_ENDLINE]
+                code_lines = [
+                    (line + 1, definitions_raw[file_path][line])
+                    for line in range(start_line, end_line + 1)
+                ]
+                definition_resource = {
+                    "start_line": start_line + 1,  # lines start with index 0
+                    "end_line": end_line + 1,
+                    "code_lines": code_lines,
+                    "skipped_checks": skipped_checks,
+                }
+                definitions_context[file_path][instruction_name].append(definition_resource)
+
+    return definitions_context


Test patch:
diff --git a/tests/dockerfile/resources/expose_port/skip/Dockerfile b/tests/dockerfile/resources/expose_port/skip/Dockerfile
--- a/tests/dockerfile/resources/expose_port/skip/Dockerfile
+++ b/tests/dockerfile/resources/expose_port/skip/Dockerfile
@@ -1,4 +1,5 @@
 #checkov:skip=CKV_DOCKER_5: no need to skip
+#checkov:skip=CKV2_DOCKER_7: no need to skip graph check
 FROM gliderlabs/alpine:3.3
 RUN apk --no-cache add nginx
 EXPOSE 3000 80 443 22
diff --git a/tests/dockerfile/test_runner.py b/tests/dockerfile/test_runner.py
--- a/tests/dockerfile/test_runner.py
+++ b/tests/dockerfile/test_runner.py
@@ -120,7 +120,7 @@ def test_runner_skip_check(self):
             external_checks_dir=None,
             runner_filter=RunnerFilter(
                 framework=["dockerfile"],
-                checks=["CKV_DOCKER_1", "CKV_DOCKER_5", "CKV_DOCKER_9"],
+                checks=["CKV_DOCKER_1", "CKV_DOCKER_5", "CKV_DOCKER_9", "CKV2_DOCKER_7"],
             ),
         )
 
@@ -129,11 +129,11 @@ def test_runner_skip_check(self):
 
         self.assertEqual(summary["passed"], 1)
         self.assertEqual(summary["failed"], 0)
-        self.assertEqual(summary["skipped"], 2)
+        self.assertEqual(summary["skipped"], 3)
         self.assertEqual(summary["parsing_errors"], 0)
 
         expected_skipped_cehcks = [record.check_id for record in report.skipped_checks]
-        self.assertCountEqual(["CKV_DOCKER_1", "CKV_DOCKER_5"], expected_skipped_cehcks)
+        self.assertCountEqual(["CKV_DOCKER_1", "CKV_DOCKER_5", "CKV2_DOCKER_7"], expected_skipped_cehcks)
 
     def test_record_has_severity(self):
         custom_check_id = "MY_CUSTOM_CHECK"
diff --git a/tests/dockerfile/test_utils.py b/tests/dockerfile/test_utils.py
--- a/tests/dockerfile/test_utils.py
+++ b/tests/dockerfile/test_utils.py
@@ -1,11 +1,63 @@
+from operator import itemgetter
+from pathlib import Path
+
 from checkov.common.util.dockerfile import is_docker_file
+from checkov.dockerfile.graph_builder.graph_components.resource_types import ResourceType
+from checkov.dockerfile.utils import get_files_definitions, build_definitions_context
 
-VALID_DOCKER_FILE_NAMES = ["Dockerfile", "dockerfile", "Dockerfile.prod", "Dockerfile.Product1", "dev.Dockerfile",
-                           "team1.product.dockerfile"]
-INVALID_DOCKER_FILE_NAMES = ["package.json", "dockerfil", "dockerfilee", ".dockerfile", "ddockerfile",
-                             "ockerfile", "docker-file", "dockerfile1"]
+VALID_DOCKER_FILE_NAMES = [
+    "Dockerfile",
+    "dockerfile",
+    "Dockerfile.prod",
+    "Dockerfile.Product1",
+    "dev.Dockerfile",
+    "team1.product.dockerfile",
+]
+INVALID_DOCKER_FILE_NAMES = [
+    "package.json",
+    "dockerfil",
+    "dockerfilee",
+    ".dockerfile",
+    "ddockerfile",
+    "ockerfile",
+    "docker-file",
+    "dockerfile1",
+]
 
 
 def test_is_docker_file():
     assert all(is_docker_file(curr_name) for curr_name in VALID_DOCKER_FILE_NAMES)
     assert all(not is_docker_file(curr_name) for curr_name in INVALID_DOCKER_FILE_NAMES)
+
+
+def test_build_definitions_context():
+    # given
+    file_path = Path(__file__).parent / "resources/expose_port/skip/Dockerfile"
+    definitions, definitions_raw = get_files_definitions(files=[str(file_path)])
+
+    # when
+    context = build_definitions_context(definitions=definitions, definitions_raw=definitions_raw)
+
+    assert len(context) == 1
+
+    definition_context = next(iter(context.values()))
+    assert len(definition_context) == 4
+
+    run_instructions = definition_context[ResourceType.RUN]
+    assert len(run_instructions) == 1
+    run_instruction = run_instructions[0]
+    assert run_instruction["start_line"] == 4
+    assert run_instruction["end_line"] == 4
+    assert run_instruction["code_lines"] == [(4, "RUN apk --no-cache add nginx\n")]
+
+    for skip in run_instruction["skipped_checks"]:
+        skip.pop("bc_id", None)  # depending on the test order they are set or not
+
+    assert sorted(run_instruction["skipped_checks"], key=itemgetter("id")) == sorted(
+        [
+            {"id": "CKV_DOCKER_1", "line_number": 5, "suppress_comment": " required"},
+            {"id": "CKV_DOCKER_5", "line_number": 0, "suppress_comment": " no need to skip"},
+            {"id": "CKV2_DOCKER_7", "line_number": 1, "suppress_comment": " no need to skip graph check"},
+        ],
+        key=itemgetter("id"),
+    )