From 70e5baa30feb0a9c04d72e46993347d67685428c Mon Sep 17 00:00:00 2001 From: Tj <52830708+tjwald@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:38:40 +0200 Subject: [PATCH] feat(terraform): check cognitive services restrict outbound network (#6919) * added check for cognitive services restrict outbound network * changed to None * Update checkov/terraform/checks/resource/azure/CognitiveServicesRestrictOutboundNetwork.py Co-authored-by: Taylor <28880387+tsmithv11@users.noreply.github.com> * update name to reflect openai specific rule * remove unused import * python3.8 compat --------- Co-authored-by: Taylor <28880387+tsmithv11@users.noreply.github.com> --- ...ognitiveServicesRestrictOutboundNetwork.py | 32 ++++ .../main.tf | 143 ++++++++++++++++++ ...nitiveServicesRestrictedOutboundNetwork.py | 42 +++++ 3 files changed, 217 insertions(+) create mode 100644 checkov/terraform/checks/resource/azure/OpenAICognitiveServicesRestrictOutboundNetwork.py create mode 100644 tests/terraform/checks/resource/azure/example_OpenAICognitiveServicesRestrictOutboundNetwork/main.tf create mode 100644 tests/terraform/checks/resource/azure/test_OpenAICognitiveServicesRestrictedOutboundNetwork.py diff --git a/checkov/terraform/checks/resource/azure/OpenAICognitiveServicesRestrictOutboundNetwork.py b/checkov/terraform/checks/resource/azure/OpenAICognitiveServicesRestrictOutboundNetwork.py new file mode 100644 index 00000000000..ab286b51962 --- /dev/null +++ b/checkov/terraform/checks/resource/azure/OpenAICognitiveServicesRestrictOutboundNetwork.py @@ -0,0 +1,32 @@ +from typing import Any, List, Dict + +from checkov.common.models.enums import CheckCategories, CheckResult +from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck + + +class OpenAICognitiveServicesRestrictOutboundNetwork(BaseResourceCheck): + def __init__(self): + name = "Ensure that Azure Cognitive Services account hosted with OpenAI is configured with data loss prevention" + id = "CKV_AZURE_247" + supported_resources = ('azurerm_cognitive_account', ) + categories = (CheckCategories.NETWORKING, ) + super().__init__( + name=name, + id=id, + categories=categories, + supported_resources=supported_resources, + ) + + def scan_resource_conf(self, conf: Dict[str, List[Any]]) -> CheckResult: + if conf.get("kind", [""])[0].lower() != 'openai': + return CheckResult.PASSED + + outbound_network_access_restricted = conf.get('outbound_network_access_restricted', [None])[0] + fqdns = conf.get('fqdns', [[]])[0] + if not outbound_network_access_restricted or not fqdns: + return CheckResult.FAILED + + return CheckResult.PASSED + + +check = OpenAICognitiveServicesRestrictOutboundNetwork() diff --git a/tests/terraform/checks/resource/azure/example_OpenAICognitiveServicesRestrictOutboundNetwork/main.tf b/tests/terraform/checks/resource/azure/example_OpenAICognitiveServicesRestrictOutboundNetwork/main.tf new file mode 100644 index 00000000000..abff79504a4 --- /dev/null +++ b/tests/terraform/checks/resource/azure/example_OpenAICognitiveServicesRestrictOutboundNetwork/main.tf @@ -0,0 +1,143 @@ +resource "azurerm_cognitive_account" "pass_openai" { + name = "openai-account" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + outbound_network_access_restricted = true + fqdns = ["openai.example.com"] # Valid FQDN should pass the check + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "fail_openai_missing_fqdns" { + name = "openai-account-missing-fqdns" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + outbound_network_access_restricted = true + fqdns = [] # Empty list of FQDNs should trigger failure + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "fail_openai_missing_outbound_network_access" { + name = "openai-account-missing-outbound-network-access" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + # Missing outbound_network_access_restricted field should trigger failure + fqdns = ["openai.example.com"] + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "pass_non_openai" { + name = "non-openai-account" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "TextAnalytics" # Non-OpenAI kind should automatically pass the check + identity { + type = "a" + } + sku_name = "S0" + + outbound_network_access_restricted = false + fqdns = [] # Doesn't matter since kind is not OpenAI + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "fail_openai_missing_fqdns_and_outbound_network_access" { + name = "openai-account-missing-both" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + # Missing outbound access should trigger failure + # Empty FQDNs list should trigger failure + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "pass_openai_multiple_fqdns" { + name = "openai-account-multiple-fqdns" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + outbound_network_access_restricted = true + fqdns = ["openai1.example.com", "openai2.example.com", "openai3.example.com"] # Multiple FQDNs should pass + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "fail_openai_missing_fqdns_but_present_outbound_network_access" { + name = "openai-account-failed-missing-fqdns" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + outbound_network_access_restricted = true # Present outbound access but missing FQDNs + fqdns = [] # Empty list of FQDNs should trigger failure + + tags = { + Acceptance = "Test" + } +} + +resource "azurerm_cognitive_account" "fail_openai_no_outbound_access_and_multiple_fqdns" { + name = "openai-account-failed-no-outbound-access-multiple-fqdns" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + kind = "OpenAI" + identity { + type = "a" + } + sku_name = "S0" + + # Missing outbound access but multiple FQDNs present + fqdns = ["openai1.example.com", "openai2.example.com", "openai3.example.com"] # Multiple FQDNs should trigger failure due to missing outbound access + + tags = { + Acceptance = "Test" + } +} diff --git a/tests/terraform/checks/resource/azure/test_OpenAICognitiveServicesRestrictedOutboundNetwork.py b/tests/terraform/checks/resource/azure/test_OpenAICognitiveServicesRestrictedOutboundNetwork.py new file mode 100644 index 00000000000..041d25cc5c7 --- /dev/null +++ b/tests/terraform/checks/resource/azure/test_OpenAICognitiveServicesRestrictedOutboundNetwork.py @@ -0,0 +1,42 @@ +import unittest +from pathlib import Path + +from checkov.runner_filter import RunnerFilter +from checkov.terraform.checks.resource.azure.OpenAICognitiveServicesRestrictOutboundNetwork import check +from checkov.terraform.runner import Runner + + +class TestOpenAICognitiveServicesRestrictedOutboundNetwork(unittest.TestCase): + def test(self): + test_files_dir = Path(__file__).parent / "example_OpenAICognitiveServicesRestrictOutboundNetwork" + + report = Runner().run(root_folder=str(test_files_dir), runner_filter=RunnerFilter(checks=[check.id])) + summary = report.get_summary() + + passing_resources = { + "azurerm_cognitive_account.pass_openai", + "azurerm_cognitive_account.pass_non_openai", + "azurerm_cognitive_account.pass_openai_multiple_fqdns", + } + failing_resources = { + "azurerm_cognitive_account.fail_openai_missing_fqdns", + "azurerm_cognitive_account.fail_openai_missing_outbound_network_access", + "azurerm_cognitive_account.fail_openai_missing_fqdns_and_outbound_network_access", + "azurerm_cognitive_account.fail_openai_missing_fqdns_but_present_outbound_network_access", + "azurerm_cognitive_account.fail_openai_no_outbound_access_and_multiple_fqdns", + } + + passed_check_resources = {c.resource for c in report.passed_checks} + failed_check_resources = {c.resource for c in report.failed_checks} + + self.assertEqual(summary["passed"], len(passing_resources)) + self.assertEqual(summary["failed"], len(failing_resources)) + self.assertEqual(summary["skipped"], 0) + self.assertEqual(summary["parsing_errors"], 0) + + self.assertEqual(passing_resources, passed_check_resources) + self.assertEqual(failing_resources, failed_check_resources) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file