Repository: bridgecrewio/checkov

Issue:
Add a check for AzureInstancePassword specific to scale sets 
Currently AzureInstancePassword scans for virtual instance but not scale sets.  Either extend the existing check to also check scale sets or create a new check for scale sets only.   

https://docs.microsoft.com/en-us/azure/templates/microsoft.compute/2019-12-01/virtualmachinescalesets
https://www.terraform.io/docs/providers/azurerm/r/virtual_machine_scale_set.html
https://www.terraform.io/docs/providers/azurerm/r/linux_virtual_machine_scale_set.html



Discussion:
check CKV_AZURE_1? @jonjozwiak 

Resolving patch:
diff --git a/checkov/arm/checks/resource/AzureInstancePassword.py b/checkov/arm/checks/resource/AzureInstancePassword.py
--- a/checkov/arm/checks/resource/AzureInstancePassword.py
+++ b/checkov/arm/checks/resource/AzureInstancePassword.py
@@ -1,16 +1,18 @@
+from typing import Any, Dict
+
+from checkov.arm.base_resource_value_check import BaseResourceValueCheck
 from checkov.common.models.enums import CheckResult, CheckCategories
-from checkov.arm.base_resource_check import BaseResourceCheck
 
 
-class AzureInstancePassword(BaseResourceCheck):
-    def __init__(self):
+class AzureInstancePassword(BaseResourceValueCheck):
+    def __init__(self) -> None:
         name = "Ensure Azure Instance does not use basic authentication(Use SSH Key Instead)"
         id = "CKV_AZURE_1"
-        supported_resources = ['Microsoft.Compute/virtualMachines']
+        supported_resources = ("Microsoft.Compute/virtualMachines",)
         categories = [CheckCategories.GENERAL_SECURITY]
         super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)
 
-    def scan_resource_conf(self, conf):
+    def scan_resource_conf(self, conf: Dict[str, Any]) -> CheckResult:
         properties = conf.get("properties")
         if isinstance(properties, dict):
             storage_profile = properties.get("storageProfile")
@@ -20,13 +22,15 @@ def scan_resource_conf(self, conf):
                     publisher = image_reference.get("publisher")
                     if publisher and "windows" in publisher.lower():
                         # This check is not relevant to Windows systems
-                        return CheckResult.PASSED
-
-            os_profile = properties.get("osProfile")
-            if isinstance(os_profile, dict):
-                linux_conf = os_profile.get("linuxConfiguration")
-                if isinstance(linux_conf, dict) and linux_conf.get("disablePasswordAuthentication"):
-                    return CheckResult.PASSED
-        return CheckResult.FAILED
+                        return CheckResult.UNKNOWN
+
+        return super().scan_resource_conf(conf)
+
+    def get_inspected_key(self) -> str:
+        return "properties/osProfile/linuxConfiguration/disablePasswordAuthentication"
+
+    def get_expected_value(self) -> Any:
+        return True
+
 
 check = AzureInstancePassword()
diff --git a/checkov/arm/checks/resource/AzureScaleSetPassword.py b/checkov/arm/checks/resource/AzureScaleSetPassword.py
new file mode 100644
--- /dev/null
+++ b/checkov/arm/checks/resource/AzureScaleSetPassword.py
@@ -0,0 +1,38 @@
+from typing import Dict, Any
+
+from checkov.arm.base_resource_value_check import BaseResourceValueCheck
+from checkov.common.models.enums import CheckResult, CheckCategories
+
+
+class AzureScaleSetPassword(BaseResourceValueCheck):
+    def __init__(self) -> None:
+        name = "Ensure Azure linux scale set does not use basic authentication(Use SSH Key Instead)"
+        id = "CKV_AZURE_49"
+        supported_resources = ("Microsoft.Compute/virtualMachineScaleSets",)
+        categories = [CheckCategories.GENERAL_SECURITY]
+        super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)
+
+    def scan_resource_conf(self, conf: Dict[str, Any]) -> CheckResult:
+        properties = conf.get("properties")
+        if isinstance(properties, dict):
+            vm_profile = properties.get("virtualMachineProfile")
+            if isinstance(vm_profile, dict):
+                storage_profile = vm_profile.get("storageProfile")
+                if isinstance(storage_profile, dict):
+                    image_reference = storage_profile.get("imageReference")
+                    if isinstance(image_reference, dict):
+                        publisher = image_reference.get("publisher")
+                        if publisher and "windows" in publisher.lower():
+                            # This check is not relevant to Windows systems
+                            return CheckResult.UNKNOWN
+
+        return super().scan_resource_conf(conf)
+
+    def get_inspected_key(self) -> str:
+        return "properties/virtualMachineProfile/osProfile/linuxConfiguration/disablePasswordAuthentication"
+
+    def get_expected_value(self) -> Any:
+        return True
+
+
+check = AzureScaleSetPassword()


Test patch:
diff --git a/tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-FAILED.json b/tests/arm/checks/resource/example_AzureInstancePassword/FAILED.json
similarity index 99%
rename from tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-FAILED.json
rename to tests/arm/checks/resource/example_AzureInstancePassword/FAILED.json
--- a/tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-FAILED.json
+++ b/tests/arm/checks/resource/example_AzureInstancePassword/FAILED.json
@@ -205,7 +205,7 @@
     {
       "type": "Microsoft.Compute/virtualMachines",
       "apiVersion": "2019-03-01",
-      "name": "[parameters('vmName')]",
+      "name": "default",
       "location": "[parameters('location')]",
       "dependsOn": [
         "[resourceId('Microsoft.Network/networkInterfaces/', variables('networkInterfaceName'))]"
diff --git a/tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-PASSED2.json b/tests/arm/checks/resource/example_AzureInstancePassword/PASSED.json
similarity index 99%
rename from tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-PASSED2.json
rename to tests/arm/checks/resource/example_AzureInstancePassword/PASSED.json
--- a/tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-PASSED2.json
+++ b/tests/arm/checks/resource/example_AzureInstancePassword/PASSED.json
@@ -183,7 +183,7 @@
     {
       "type": "Microsoft.Compute/virtualMachines",
       "apiVersion": "2019-03-01",
-      "name": "[parameters('vmName')]",
+      "name": "enabled",
       "location": "[parameters('location')]",
       "dependsOn": [
         "[resourceId('Microsoft.Network/networkInterfaces/', variables('networkInterfaceName'))]"
diff --git a/tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-PASSED.json b/tests/arm/checks/resource/example_AzureInstancePassword/UNKNOWN.json
similarity index 99%
rename from tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-PASSED.json
rename to tests/arm/checks/resource/example_AzureInstancePassword/UNKNOWN.json
--- a/tests/arm/checks/resource/example_AzureInstancePassword/azureInstancePassword-PASSED.json
+++ b/tests/arm/checks/resource/example_AzureInstancePassword/UNKNOWN.json
@@ -167,7 +167,7 @@
     {
       "type": "Microsoft.Compute/virtualMachines",
       "apiVersion": "2018-10-01",
-      "name": "[variables('vmName')]",
+      "name": "windows",
       "location": "[parameters('location')]",
       "dependsOn": [
         "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
diff --git a/tests/arm/checks/resource/example_AzureScaleSetPassword/FAILED.json b/tests/arm/checks/resource/example_AzureScaleSetPassword/FAILED.json
new file mode 100644
--- /dev/null
+++ b/tests/arm/checks/resource/example_AzureScaleSetPassword/FAILED.json
@@ -0,0 +1,42 @@
+{
+  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+  "contentVersion": "1.0.0.0",
+  "resources": [
+    {
+      "type": "Microsoft.Compute/virtualMachineScaleSets",
+      "name": "default",
+      "location": "East US 2",
+      "apiVersion": "2021-07-01",
+      "zones": [
+        "1"
+      ],
+      "sku": {
+        "name": "Standard_A1",
+        "capacity": "2"
+      },
+      "properties": {
+        "virtualMachineProfile": {
+          "storageProfile": {
+            "osDisk": {
+              "caching": "ReadWrite",
+              "createOption": "FromImage"
+            },
+            "imageReference": {
+              "publisher": "Canonical",
+              "offer": "UbuntuServer",
+              "sku": "16.04-LTS",
+              "version": "latest"
+            }
+          },
+          "osProfile": {
+            "linuxConfiguration": {
+              "computerNamePrefix": "myvmss",
+              "adminUsername": "azureuser",
+              "adminPassword": "P@ssw0rd!"
+            }
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/arm/checks/resource/example_AzureScaleSetPassword/PASSED.json b/tests/arm/checks/resource/example_AzureScaleSetPassword/PASSED.json
new file mode 100644
--- /dev/null
+++ b/tests/arm/checks/resource/example_AzureScaleSetPassword/PASSED.json
@@ -0,0 +1,48 @@
+{
+  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+  "contentVersion": "1.0.0.0",
+  "resources": [
+    {
+      "type": "Microsoft.Compute/virtualMachineScaleSets",
+      "name": "enabled",
+      "location": "East US 2",
+      "apiVersion": "2021-07-01",
+      "zones": [
+        "1"
+      ],
+      "sku": {
+        "name": "Standard_A1",
+        "capacity": "2"
+      },
+      "properties": {
+        "virtualMachineProfile": {
+          "storageProfile": {
+            "osDisk": {
+              "caching": "ReadWrite",
+              "createOption": "FromImage"
+            },
+            "imageReference": {
+              "publisher": "Canonical",
+              "offer": "UbuntuServer",
+              "sku": "16.04-LTS",
+              "version": "latest"
+            }
+          },
+          "osProfile": {
+            "linuxConfiguration": {
+              "disablePasswordAuthentication": true,
+              "ssh": {
+                "publicKeys": [
+                  {
+                    "path": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]",
+                    "keyData": "[parameters('adminPasswordOrKey')]"
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/arm/checks/resource/example_AzureScaleSetPassword/UNKNOWN.json b/tests/arm/checks/resource/example_AzureScaleSetPassword/UNKNOWN.json
new file mode 100644
--- /dev/null
+++ b/tests/arm/checks/resource/example_AzureScaleSetPassword/UNKNOWN.json
@@ -0,0 +1,40 @@
+{
+  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+  "contentVersion": "1.0.0.0",
+  "resources": [
+    {
+      "type": "Microsoft.Compute/virtualMachineScaleSets",
+      "name": "windows",
+      "location": "East US 2",
+      "apiVersion": "2021-07-01",
+      "zones": [
+        "1"
+      ],
+      "sku": {
+        "name": "Standard_A1",
+        "capacity": "2"
+      },
+      "properties": {
+        "virtualMachineProfile": {
+          "storageProfile": {
+            "osDisk": {
+              "caching": "ReadWrite",
+              "createOption": "FromImage"
+            },
+            "imageReference": {
+              "publisher": "MicrosoftWindowsServer",
+              "offer": "WindowsServer",
+              "sku": "2019-Datacenter",
+              "version": "latest"
+            }
+          },
+          "osProfile": {
+            "computerNamePrefix": "myvmss",
+            "adminUsername": "azureuser",
+            "adminPassword": "P@ssw0rd!"
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/arm/checks/resource/test_AzureInstancePassword.py b/tests/arm/checks/resource/test_AzureInstancePassword.py
--- a/tests/arm/checks/resource/test_AzureInstancePassword.py
+++ b/tests/arm/checks/resource/test_AzureInstancePassword.py
@@ -1,5 +1,5 @@
-import os
 import unittest
+from pathlib import Path
 
 from checkov.arm.checks.resource.AzureInstancePassword import check
 from checkov.arm.runner import Runner
@@ -7,20 +7,34 @@
 
 
 class TestAzureInstancePassword(unittest.TestCase):
-
     def test_summary(self):
-        runner = Runner()
-        current_dir = os.path.dirname(os.path.realpath(__file__))
+        # given
+        test_files_dir = Path(__file__).parent / "example_AzureInstancePassword"
+
+        # when
+        report = Runner().run(root_folder=str(test_files_dir), runner_filter=RunnerFilter(checks=[check.id]))
 
-        test_files_dir = current_dir + "/example_AzureInstancePassword"
-        report = runner.run(root_folder=test_files_dir,runner_filter=RunnerFilter(checks=[check.id]))
+        # then
         summary = report.get_summary()
 
-        self.assertEqual(summary['passed'], 2)
-        self.assertEqual(summary['failed'], 1)
-        self.assertEqual(summary['skipped'], 0)
-        self.assertEqual(summary['parsing_errors'], 0)
+        passing_resources = {
+            "Microsoft.Compute/virtualMachines.enabled",
+        }
+        failing_resources = {
+            "Microsoft.Compute/virtualMachines.default",
+        }
+
+        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"], 1)
+        self.assertEqual(summary["failed"], 1)
+        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__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/tests/arm/checks/resource/test_AzureScaleSetPassword.py b/tests/arm/checks/resource/test_AzureScaleSetPassword.py
new file mode 100644
--- /dev/null
+++ b/tests/arm/checks/resource/test_AzureScaleSetPassword.py
@@ -0,0 +1,41 @@
+import unittest
+from pathlib import Path
+
+from checkov.arm.checks.resource.AzureScaleSetPassword import check
+from checkov.arm.runner import Runner
+from checkov.runner_filter import RunnerFilter
+
+
+class TestAzureScaleSetPassword(unittest.TestCase):
+    def test_summary(self):
+        # given
+        test_files_dir = Path(__file__).parent / "example_AzureScaleSetPassword"
+
+        # when
+        report = Runner().run(root_folder=str(test_files_dir), runner_filter=RunnerFilter(checks=[check.id]))
+
+        # then
+        summary = report.get_summary()
+
+        passing_resources = {
+            "Microsoft.Compute/virtualMachineScaleSets.enabled",
+        }
+        failing_resources = {
+            "Microsoft.Compute/virtualMachineScaleSets.default",
+        }
+
+        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"], 1)
+        self.assertEqual(summary["failed"], 1)
+        self.assertEqual(summary["skipped"], 0)
+        self.assertEqual(summary["parsing_errors"], 0)
+        self.assertEqual(summary["resource_count"], 3)  # 1 unknown
+
+        self.assertEqual(passing_resources, passed_check_resources)
+        self.assertEqual(failing_resources, failed_check_resources)
+
+
+if __name__ == "__main__":
+    unittest.main()