Repository: bridgecrewio/checkov

Issue:
OneOf conditional checking based on the configuration of multiple like-resources
**Describe the issue**
I have more of a question and am looking for guidance, potentially on contributing back to checkov.  My team and I are trying to implement a graph-based check that fails if "one of" a specifically configured resource does not exist.   The current concrete example is:  

> If a VPC is present, then at least one VPC Flow Log must be present that is configured with a LogDestination of s3, and the destination path must be a specific centralized bucket. 

As far as I can tell there isn't a way I can write this sort of check.  We have explored a yml graph-based approach as well as pondered how we could write this in python proper.  It seems to me that the yml logic diverges from the available to-be-subclassed python classes for writing checks.  In the yaml case we have access to the graph and can write checks based on connected resources, but (again afaict) don't have the ability to say "one of _n_ like resource types must be configured a certain way".  On the python side we could accomplish the latter, but only have access to one resource at a time, and don't know how many we might encounter.

Is my interpretation of current state accurate?

I can think of two possible ways checkov might support this, but would greatly appreciate some input. 

1. implement a solver (e.g. `OneOfSolver` that operates like https://github.com/bridgecrewio/checkov/blob/master/checkov/common/checks_infra/solvers/complex_solvers/and_solver.py but expects `any()` to pass.

2. somehow offer another implementation or variation on `scan_resource_conf` that provides greater context than it does today.  for example maybe all instances of configured `resource_types` for a check could be passed into the scan function.  or the rest of the template could be provided as context.  something like that. 

Absolutely welcome alternate thoughts and approaches.  If there is a potential desired path here details would be great as my team and I would consider PRing the functionality.

Thanks!

**Examples**
An example template that illustrates the situation I described above.  1 vpc, 2 flow logs, one of which meet a "check passed" criteria.  However, the `team-flow-log` would cause a failure without the ability to define `OneOf` conditional criteria.

```
{
    "Resources": {
      "VPCB9E5F0B4": {
        "Type": "AWS::EC2::VPC"
      },
      "team-flow-log": {
        "Type": "AWS::EC2::FlowLog",
        "Properties": {
          "ResourceId": {
            "Ref": "VPCB9E5F0B4"
          },
          "ResourceType": "VPC",
          "TrafficType": "ALL",
          "LogDestination": "arn:aws:s3:::my-teams-bucket",
          "LogDestinationType": "s3"
        }
      },
      "company-required-flow-log": {
        "Type": "AWS::EC2::FlowLog",
        "Properties": {
          "ResourceId": {
            "Ref": "VPCB9E5F0B4"
          },
          "ResourceType": "VPC",
          "TrafficType": "ALL",
          "LogDestination": "arn:aws:s3:::company-required-bucket",
          "LogDestinationType": "s3"
        }
      }
    }
  }
```


**Version (please complete the following information):**
 - Checkov Version - latest

**Additional context**
N/A



Discussion:

Resolving patch:
diff --git a/checkov/common/checks_infra/checks_parser.py b/checkov/common/checks_infra/checks_parser.py
--- a/checkov/common/checks_infra/checks_parser.py
+++ b/checkov/common/checks_infra/checks_parser.py
@@ -31,6 +31,8 @@
 )
 from checkov.common.checks_infra.solvers.attribute_solvers.not_subset_attribute_solver import NotSubsetAttributeSolver
 from checkov.common.checks_infra.solvers.attribute_solvers.subset_attribute_solver import SubsetAttributeSolver
+from checkov.common.checks_infra.solvers.connections_solvers.connection_one_exists_solver import \
+    ConnectionOneExistsSolver
 from checkov.common.graph.checks_infra.base_check import BaseGraphCheck
 from checkov.common.graph.checks_infra.base_parser import BaseGraphCheckParser
 from checkov.common.graph.checks_infra.enums import SolverType
@@ -67,7 +69,11 @@
     "or": OrSolver,
 }
 
-operator_to_connection_solver_classes = {"exists": ConnectionExistsSolver, "not_exists": ConnectionNotExistsSolver}
+operator_to_connection_solver_classes = {
+    "exists": ConnectionExistsSolver,
+    "one_exists": ConnectionOneExistsSolver,
+    "not_exists": ConnectionNotExistsSolver
+}
 
 operator_to_complex_connection_solver_classes = {"and": AndConnectionSolver, "or": OrConnectionSolver}
 
@@ -88,6 +94,7 @@ def parse_raw_check(self, raw_check: Dict[str, Dict[str, Any]], **kwargs: Any) -
         policy_definition = raw_check.get("definition", {})
         check = self._parse_raw_check(policy_definition, kwargs.get("resources_types"))
         check.id = raw_check.get("metadata", {}).get("id", "")
+        check.bc_id = raw_check.get("metadata", {}).get("id", "")
         check.name = raw_check.get("metadata", {}).get("name", "")
         check.category = raw_check.get("metadata", {}).get("category", "")
         solver = self.get_check_solver(check)
diff --git a/checkov/common/checks_infra/solvers/connections_solvers/connection_one_exists_solver.py b/checkov/common/checks_infra/solvers/connections_solvers/connection_one_exists_solver.py
new file mode 100644
--- /dev/null
+++ b/checkov/common/checks_infra/solvers/connections_solvers/connection_one_exists_solver.py
@@ -0,0 +1,27 @@
+from typing import List, Optional, Dict, Any, Tuple
+from checkov.common.checks_infra.solvers import ConnectionExistsSolver
+from checkov.common.graph.checks_infra.enums import Operators
+from networkx import edge_dfs, DiGraph
+
+
+class ConnectionOneExistsSolver(ConnectionExistsSolver):
+    operator = Operators.ONE_EXISTS
+
+    def __init__(
+        self,
+        resource_types: List[str],
+        connected_resources_types: List[str],
+        vertices_under_resource_types: Optional[List[Dict[str, Any]]] = None,
+        vertices_under_connected_resources_types: Optional[List[Dict[str, Any]]] = None,
+    ) -> None:
+        super().__init__(
+            resource_types,
+            connected_resources_types,
+            vertices_under_resource_types,
+            vertices_under_connected_resources_types,
+        )
+
+    def get_operation(self, graph_connector: DiGraph) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
+        passed, failed = super().get_operation(graph_connector)
+        failed = [f for f in failed if f not in passed]
+        return passed, failed
diff --git a/checkov/common/graph/checks_infra/enums.py b/checkov/common/graph/checks_infra/enums.py
--- a/checkov/common/graph/checks_infra/enums.py
+++ b/checkov/common/graph/checks_infra/enums.py
@@ -24,6 +24,7 @@ class SolverType(str, Enum):
 class Operators:
     ANY = 'any'
     EXISTS = 'exists'
+    ONE_EXISTS = 'one_exists'
     NOT_EXISTS = 'not_exists'
     CONTAINS = 'contains'
     NOT_CONTAINS = 'not_contains'


Test patch:
diff --git a/tests/terraform/graph/checks/resources/VPCHasFlowLog/main.tf b/tests/terraform/graph/checks/resources/VPCHasFlowLog/main.tf
--- a/tests/terraform/graph/checks/resources/VPCHasFlowLog/main.tf
+++ b/tests/terraform/graph/checks/resources/VPCHasFlowLog/main.tf
@@ -11,4 +11,4 @@ resource "aws_vpc" "not_ok_vpc" {
 
 resource "aws_vpc" "ok_vpc" {
   cidr_block = "10.0.0.0/16"
-}
\ No newline at end of file
+}
diff --git a/tests/terraform/graph/checks/resources/VPCHasOneOfWantedFlowLogs/main.tf b/tests/terraform/graph/checks/resources/VPCHasOneOfWantedFlowLogs/main.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/graph/checks/resources/VPCHasOneOfWantedFlowLogs/main.tf
@@ -0,0 +1,72 @@
+resource "aws_flow_log" "example" {
+  iam_role_arn    = "arn"
+  log_destination      = "arn:aws:s3:::test-bucket"
+  log_destination_type = "s3"
+  traffic_type         = "ALL"
+  vpc_id               = aws_vpc.ok_vpc.id
+}
+
+resource "aws_flow_log" "example1" {
+  iam_role_arn    = "arn"
+  log_destination = "log"
+  traffic_type    = "ALL"
+  log_destination_type = "s3"
+  vpc_id          = aws_vpc.ok_vpc.id
+}
+
+resource "aws_flow_log" "example2" {
+  iam_role_arn    = "arn"
+  log_destination = "log"
+  traffic_type    = "ALL"
+  log_destination_type = "s3"
+  vpc_id          = aws_vpc.not_ok_vpc2.id
+}
+
+resource "aws_flow_log" "example3" {
+  iam_role_arn    = "arn"
+  log_destination      = "arn:aws:s3:::test-bucket"
+  log_destination_type = "s3"
+  traffic_type         = "ALL"
+  vpc_id               = aws_vpc.ok_vpc1.id
+}
+
+resource "aws_flow_log" "example4" {
+  iam_role_arn    = "arn"
+  log_destination      = "arn:aws:s3:::test-bucket"
+  log_destination_type = "log"
+  traffic_type         = "ALL"
+  vpc_id               = aws_vpc.ok_vpc1.id
+}
+
+resource "aws_flow_log" "example5" {
+  iam_role_arn    = "arn"
+  log_destination      = "name"
+  log_destination_type = "log"
+  traffic_type         = "ALL"
+  vpc_id               = aws_vpc.ok_vpc1.id
+}
+
+resource "aws_flow_log" "example6" {
+  iam_role_arn    = "arn"
+  log_destination      = "name1"
+  log_destination_type = "s3"
+  traffic_type         = "ALL"
+  vpc_id               = aws_vpc.ok_vpc1.id
+}
+
+resource "aws_vpc" "not_ok_vpc" {
+  cidr_block = "10.0.0.0/16"
+}
+
+resource "aws_vpc" "ok_vpc" {
+  cidr_block = "10.0.0.0/16"
+}
+
+resource "aws_vpc" "not_ok_vpc2" {
+  cidr_block = "10.0.0.0/16"
+}
+
+resource "aws_vpc" "ok_vpc1" {
+  cidr_block = "10.0.0.0/16"
+}
+
diff --git a/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/VPCHasOneOfWantedFlowLogs.yaml b/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/VPCHasOneOfWantedFlowLogs.yaml
new file mode 100644
--- /dev/null
+++ b/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/VPCHasOneOfWantedFlowLogs.yaml
@@ -0,0 +1,36 @@
+metadata:
+  name: "Ensure a specific VPC flow log exists in VPC's flow logs"
+  category: "LOGGING"
+  id: "VPCHasOneOfWantedFlowLogs"
+definition:
+   and:
+      - resource_types:
+          - aws_vpc
+        connected_resource_types:
+          - aws_flow_log
+        operator: one_exists
+        attribute: networking
+        cond_type: connection
+
+      - resource_types:
+          - aws_flow_log
+        cond_type: attribute
+        operator: equals
+        attribute: log_destination_type
+        value: "s3"
+
+      - resource_types:
+          - aws_flow_log
+        cond_type: attribute
+        operator: equals
+        attribute: log_destination
+        value: "arn:aws:s3:::test-bucket"
+
+      - cond_type: filter
+        attribute: resource_type
+        value:
+           - aws_vpc
+        operator: within
+
+
+
diff --git a/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/__init__.py b/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/__init__.py
new file mode 100644
diff --git a/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/test_solver.py b/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/test_solver.py
new file mode 100644
--- /dev/null
+++ b/tests/terraform/graph/checks_infra/connection_solvers/connection_one_exists/test_solver.py
@@ -0,0 +1,19 @@
+import os
+from tests.terraform.graph.checks_infra.test_base import TestBaseSolver
+
+TEST_DIRNAME = os.path.dirname(os.path.realpath(__file__))
+
+
+class ConnectionSolver(TestBaseSolver):
+    def setUp(self):
+        self.checks_dir = TEST_DIRNAME
+        super(ConnectionSolver, self).setUp()
+
+    def test_connection_not_found(self):
+        root_folder = '../../../checks/resources/VPCHasOneOfWantedFlowLogs'
+        check_id = "VPCHasOneOfWantedFlowLogs"
+        should_pass = ['aws_vpc.ok_vpc', 'aws_vpc.ok_vpc1']
+        should_fail = ['aws_vpc.not_ok_vpc', 'aws_vpc.not_ok_vpc2']
+        expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}}
+
+        self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id)
\ No newline at end of file