mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
feat(kubernetes): include cluster name in compliance reports (#12506)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
`Cluster` column in Kubernetes CIS, ISO27001, Prowler ThreatScore, and universal compliance outputs, populated with the resolved cluster name so multi-cluster scans can be told apart in the output
|
||||
@@ -55,6 +55,7 @@ class KubernetesCIS(ComplianceOutput):
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
Context=finding.account_name,
|
||||
Cluster=finding.account_uid,
|
||||
Namespace=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
@@ -89,6 +90,7 @@ class KubernetesCIS(ComplianceOutput):
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
Context="",
|
||||
Cluster="",
|
||||
Namespace="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
|
||||
@@ -148,6 +148,7 @@ class KubernetesCISModel(BaseModel):
|
||||
Provider: str
|
||||
Description: str
|
||||
Context: str
|
||||
Cluster: str
|
||||
Namespace: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||
@@ -55,6 +55,7 @@ class KubernetesISO27001(ComplianceOutput):
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
Context=finding.account_name,
|
||||
Cluster=finding.account_uid,
|
||||
Namespace=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
@@ -82,6 +83,7 @@ class KubernetesISO27001(ComplianceOutput):
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
Context="",
|
||||
Cluster="",
|
||||
Namespace="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
|
||||
@@ -90,6 +90,7 @@ class KubernetesISO27001Model(BaseModel):
|
||||
Provider: str
|
||||
Description: str
|
||||
Context: str
|
||||
Cluster: str
|
||||
Namespace: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||
@@ -127,6 +127,7 @@ class ProwlerThreatScoreKubernetesModel(BaseModel):
|
||||
Provider: str
|
||||
Description: str
|
||||
Context: str
|
||||
Cluster: str
|
||||
Namespace: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||
@@ -58,6 +58,7 @@ class ProwlerThreatScoreKubernetes(ComplianceOutput):
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
Context=finding.account_name,
|
||||
Cluster=finding.account_uid,
|
||||
Namespace=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
@@ -87,6 +88,7 @@ class ProwlerThreatScoreKubernetes(ComplianceOutput):
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
Context="",
|
||||
Cluster="",
|
||||
Namespace="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
|
||||
@@ -30,6 +30,9 @@ PROVIDER_HEADER_MAP = {
|
||||
"e2enetworks": ("ProjectId", "account_uid", "Location", "region"),
|
||||
}
|
||||
_DEFAULT_HEADERS = ("AccountId", "account_uid", "Region", "region")
|
||||
PROVIDER_EXTRA_HEADER_MAP = {
|
||||
"kubernetes": (("Cluster", "account_uid"),),
|
||||
}
|
||||
|
||||
|
||||
class UniversalComplianceOutput:
|
||||
@@ -88,11 +91,19 @@ class UniversalComplianceOutput:
|
||||
"Provider": (str, ...),
|
||||
"Description": (str, ...),
|
||||
acct_header: (str, ...),
|
||||
loc_header: (str, ...),
|
||||
"AssessmentDate": (str, ...),
|
||||
"Requirements_Id": (str, ...),
|
||||
"Requirements_Description": (str, ...),
|
||||
}
|
||||
for header, _ in PROVIDER_EXTRA_HEADER_MAP.get(
|
||||
(self._provider or "").lower(), ()
|
||||
):
|
||||
fields[header] = (str, ...)
|
||||
fields.update(
|
||||
{
|
||||
loc_header: (str, ...),
|
||||
"AssessmentDate": (str, ...),
|
||||
"Requirements_Id": (str, ...),
|
||||
"Requirements_Description": (str, ...),
|
||||
}
|
||||
)
|
||||
|
||||
# Dynamic attribute columns from metadata
|
||||
if framework.attributes_metadata:
|
||||
@@ -154,13 +165,17 @@ class UniversalComplianceOutput:
|
||||
self._acct_header: (
|
||||
getattr(finding, self._acct_field, "") if not is_manual else ""
|
||||
),
|
||||
self._loc_header: (
|
||||
getattr(finding, self._loc_field, "") if not is_manual else ""
|
||||
),
|
||||
"AssessmentDate": str(timestamp),
|
||||
"Requirements_Id": requirement.id,
|
||||
"Requirements_Description": requirement.description,
|
||||
}
|
||||
for header, field in PROVIDER_EXTRA_HEADER_MAP.get(
|
||||
(self._provider or "").lower(), ()
|
||||
):
|
||||
row[header] = getattr(finding, field, "") if not is_manual else ""
|
||||
row[self._loc_header] = (
|
||||
getattr(finding, self._loc_field, "") if not is_manual else ""
|
||||
)
|
||||
|
||||
# Add dynamic attribute columns
|
||||
if framework.attributes_metadata:
|
||||
|
||||
@@ -34,6 +34,7 @@ class TestKubernetesCIS:
|
||||
assert output_data.Framework == CIS_1_8_KUBERNETES.Framework
|
||||
assert output_data.Name == CIS_1_8_KUBERNETES.Name
|
||||
assert output_data.Context == KUBERNETES_CLUSTER_NAME
|
||||
assert output_data.Cluster == KUBERNETES_CLUSTER_NAME
|
||||
assert output_data.Namespace == KUBERNETES_NAMESPACE
|
||||
assert output_data.Description == CIS_1_8_KUBERNETES.Description
|
||||
assert output_data.Requirements_Id == CIS_1_8_KUBERNETES.Requirements[0].Id
|
||||
@@ -101,6 +102,7 @@ class TestKubernetesCIS:
|
||||
assert output_data_manual.Framework == CIS_1_8_KUBERNETES.Framework
|
||||
assert output_data_manual.Name == CIS_1_8_KUBERNETES.Name
|
||||
assert output_data_manual.Context == ""
|
||||
assert output_data_manual.Cluster == ""
|
||||
assert output_data_manual.Namespace == ""
|
||||
assert output_data_manual.Description == CIS_1_8_KUBERNETES.Description
|
||||
assert (
|
||||
@@ -190,5 +192,5 @@ class TestKubernetesCIS:
|
||||
|
||||
mock_file.seek(0)
|
||||
content = mock_file.read()
|
||||
expected_csv = f"PROVIDER;DESCRIPTION;CONTEXT;NAMESPACE;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_REFERENCES;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nkubernetes;This CIS Kubernetes Benchmark provides prescriptive guidance for establishing a secure configuration posture for Kubernetes v1.27.;test-cluster;test-namespace;{datetime.now()};1.1.3;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1. Control Plane;1.1 Control Plane Node Configuration Files;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;https://kubernetes.io/docs/admin/kube-apiserver/;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;PASS;;;;service_test_check_id;False;CIS;CIS Kubernetes Benchmark v1.8.0\r\nkubernetes;This CIS Kubernetes Benchmark provides prescriptive guidance for establishing a secure configuration posture for Kubernetes v1.27.;;;{datetime.now()};1.1.4;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1.1 Control Plane Node Configuration Files;;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;https://kubernetes.io/docs/admin/kube-apiserver/;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;MANUAL;Manual check;manual_check;Manual check;manual;False;CIS;CIS Kubernetes Benchmark v1.8.0\r\n"
|
||||
expected_csv = f"PROVIDER;DESCRIPTION;CONTEXT;CLUSTER;NAMESPACE;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_REFERENCES;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nkubernetes;This CIS Kubernetes Benchmark provides prescriptive guidance for establishing a secure configuration posture for Kubernetes v1.27.;test-cluster;test-cluster;test-namespace;{datetime.now()};1.1.3;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1. Control Plane;1.1 Control Plane Node Configuration Files;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;https://kubernetes.io/docs/admin/kube-apiserver/;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;PASS;;;;service_test_check_id;False;CIS;CIS Kubernetes Benchmark v1.8.0\r\nkubernetes;This CIS Kubernetes Benchmark provides prescriptive guidance for establishing a secure configuration posture for Kubernetes v1.27.;;;;{datetime.now()};1.1.4;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1.1 Control Plane Node Configuration Files;;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;https://kubernetes.io/docs/admin/kube-apiserver/;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;MANUAL;Manual check;manual_check;Manual check;manual;False;CIS;CIS Kubernetes Benchmark v1.8.0\r\n"
|
||||
assert content == expected_csv
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from prowler.lib.check.compliance_models import (
|
||||
Compliance,
|
||||
Compliance_Requirement,
|
||||
ISO27001_2013_Requirement_Attribute,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.iso27001.iso27001_kubernetes import (
|
||||
KubernetesISO27001,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.iso27001.models import KubernetesISO27001Model
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.kubernetes.kubernetes_fixtures import (
|
||||
KUBERNETES_CLUSTER_NAME,
|
||||
KUBERNETES_NAMESPACE,
|
||||
)
|
||||
|
||||
ISO27001_2013_KUBERNETES = Compliance(
|
||||
Framework="ISO27001",
|
||||
Name="ISO/IEC 27001 Information Security Management Standard 2013",
|
||||
Provider="Kubernetes",
|
||||
Version="2013",
|
||||
Description="ISO 27001 controls mapped to Kubernetes findings.",
|
||||
Requirements=[
|
||||
Compliance_Requirement(
|
||||
Id="A.10.1",
|
||||
Description="Protect Kubernetes workload configuration",
|
||||
Name="Cryptographic Controls",
|
||||
Attributes=[
|
||||
ISO27001_2013_Requirement_Attribute(
|
||||
Category="A.10 Cryptography",
|
||||
Objetive_ID="A.10.1",
|
||||
Objetive_Name="Cryptographic Controls",
|
||||
Check_Summary="Protect Kubernetes workload configuration",
|
||||
)
|
||||
],
|
||||
Checks=["service_test_check_id"],
|
||||
),
|
||||
Compliance_Requirement(
|
||||
Id="A.10.2",
|
||||
Description="Manual Kubernetes control",
|
||||
Name="Cryptographic Controls",
|
||||
Attributes=[
|
||||
ISO27001_2013_Requirement_Attribute(
|
||||
Category="A.10 Cryptography",
|
||||
Objetive_ID="A.10.2",
|
||||
Objetive_Name="Cryptographic Controls",
|
||||
Check_Summary="Manual Kubernetes control",
|
||||
)
|
||||
],
|
||||
Checks=[],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestKubernetesISO27001:
|
||||
def test_output_transform_includes_cluster(self):
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
provider="kubernetes",
|
||||
compliance={"ISO27001-2013": "A.10.1"},
|
||||
account_name="context: kind-dev",
|
||||
account_uid=KUBERNETES_CLUSTER_NAME,
|
||||
region=KUBERNETES_NAMESPACE,
|
||||
)
|
||||
]
|
||||
|
||||
output = KubernetesISO27001(findings, ISO27001_2013_KUBERNETES)
|
||||
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, KubernetesISO27001Model)
|
||||
assert output_data.Context == "context: kind-dev"
|
||||
assert output_data.Cluster == KUBERNETES_CLUSTER_NAME
|
||||
assert output_data.Namespace == KUBERNETES_NAMESPACE
|
||||
|
||||
manual = output.data[1]
|
||||
assert manual.Context == ""
|
||||
assert manual.Cluster == ""
|
||||
assert manual.Namespace == ""
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
from prowler.lib.check.compliance_models import (
|
||||
Compliance,
|
||||
Compliance_Requirement,
|
||||
Prowler_ThreatScore_Requirement_Attribute,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
ProwlerThreatScoreKubernetesModel,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_kubernetes import (
|
||||
ProwlerThreatScoreKubernetes,
|
||||
)
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.kubernetes.kubernetes_fixtures import (
|
||||
KUBERNETES_CLUSTER_NAME,
|
||||
KUBERNETES_NAMESPACE,
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_KUBERNETES = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Name="Prowler ThreatScore Compliance Framework for Kubernetes",
|
||||
Version="1.0",
|
||||
Provider="Kubernetes",
|
||||
Description="Prowler ThreatScore controls mapped to Kubernetes findings.",
|
||||
Requirements=[
|
||||
Compliance_Requirement(
|
||||
Id="1.1.1",
|
||||
Description="Kubernetes workload hardening",
|
||||
Attributes=[
|
||||
Prowler_ThreatScore_Requirement_Attribute(
|
||||
Title="Workload hardening",
|
||||
Section="1. Kubernetes",
|
||||
SubSection="1.1 Workloads",
|
||||
AttributeDescription="Kubernetes workload hardening control.",
|
||||
AdditionalInformation="",
|
||||
LevelOfRisk=5,
|
||||
Weight=1000,
|
||||
)
|
||||
],
|
||||
Checks=["service_test_check_id"],
|
||||
),
|
||||
Compliance_Requirement(
|
||||
Id="1.1.2",
|
||||
Description="Manual Kubernetes review",
|
||||
Attributes=[
|
||||
Prowler_ThreatScore_Requirement_Attribute(
|
||||
Title="Manual review",
|
||||
Section="1. Kubernetes",
|
||||
SubSection="1.1 Workloads",
|
||||
AttributeDescription="Manual Kubernetes review control.",
|
||||
AdditionalInformation="",
|
||||
LevelOfRisk=3,
|
||||
Weight=10,
|
||||
)
|
||||
],
|
||||
Checks=[],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestProwlerThreatScoreKubernetes:
|
||||
def test_output_transform_includes_cluster(self):
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
provider="kubernetes",
|
||||
compliance={"ProwlerThreatScore-1.0": "1.1.1"},
|
||||
account_name="context: kind-dev",
|
||||
account_uid=KUBERNETES_CLUSTER_NAME,
|
||||
region=KUBERNETES_NAMESPACE,
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreKubernetes(findings, PROWLER_THREATSCORE_KUBERNETES)
|
||||
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreKubernetesModel)
|
||||
assert output_data.Context == "context: kind-dev"
|
||||
assert output_data.Cluster == KUBERNETES_CLUSTER_NAME
|
||||
assert output_data.Namespace == KUBERNETES_NAMESPACE
|
||||
|
||||
manual = output.data[1]
|
||||
assert manual.Context == ""
|
||||
assert manual.Cluster == ""
|
||||
assert manual.Namespace == ""
|
||||
@@ -493,9 +493,11 @@ class TestProviderHeaders:
|
||||
)
|
||||
row_dict = output.data[0].dict()
|
||||
assert "Context" in row_dict
|
||||
assert "Cluster" in row_dict
|
||||
assert "Namespace" in row_dict
|
||||
# Kubernetes Context maps to account_name
|
||||
assert row_dict["Context"] == "test-account"
|
||||
assert row_dict["Cluster"] == "123456789012"
|
||||
assert row_dict["Namespace"] == "us-east-1"
|
||||
|
||||
def test_github_headers(self, tmp_path):
|
||||
@@ -562,17 +564,17 @@ class TestProviderHeaders:
|
||||
assert "ACCOUNTID" not in content
|
||||
|
||||
def test_column_order_matches_legacy(self, tmp_path):
|
||||
"""Verify that the base column order matches the legacy per-provider models.
|
||||
"""Verify that the base column order matches per-provider models.
|
||||
|
||||
Legacy models all define: Provider, Description, <col3>, <col4>, AssessmentDate, ...
|
||||
The universal output must preserve this exact order for backward compatibility.
|
||||
Most models define: Provider, Description, <account>, <location>, AssessmentDate.
|
||||
Kubernetes includes the dedicated Cluster column between Context and Namespace.
|
||||
"""
|
||||
# Expected column order per provider (positions 3 and 4 after Provider, Description)
|
||||
legacy_order = {
|
||||
"aws": ("AccountId", "Region"),
|
||||
"azure": ("SubscriptionId", "Location"),
|
||||
"gcp": ("ProjectId", "Location"),
|
||||
"kubernetes": ("Context", "Namespace"),
|
||||
"kubernetes": ("Context", "Cluster", "Namespace"),
|
||||
"m365": ("TenantId", "Location"),
|
||||
"github": ("Account_Name", "Account_Id"),
|
||||
"oraclecloud": ("TenancyId", "Region"),
|
||||
@@ -580,7 +582,7 @@ class TestProviderHeaders:
|
||||
"nhn": ("AccountId", "Region"),
|
||||
}
|
||||
|
||||
for provider_name, (expected_col3, expected_col4) in legacy_order.items():
|
||||
for provider_name, expected_columns in legacy_order.items():
|
||||
fw = _simple_framework()
|
||||
findings = [_make_provider_finding(provider_name)]
|
||||
output = UniversalComplianceOutput(
|
||||
@@ -594,12 +596,12 @@ class TestProviderHeaders:
|
||||
assert (
|
||||
keys[1] == "Description"
|
||||
), f"{provider_name}: col 2 should be Description"
|
||||
assert (
|
||||
keys[2] == expected_col3
|
||||
), f"{provider_name}: col 3 should be {expected_col3}, got {keys[2]}"
|
||||
assert (
|
||||
keys[3] == expected_col4
|
||||
), f"{provider_name}: col 4 should be {expected_col4}, got {keys[3]}"
|
||||
assert (
|
||||
keys[4] == "AssessmentDate"
|
||||
), f"{provider_name}: col 5 should be AssessmentDate"
|
||||
for index, expected_column in enumerate(expected_columns, start=2):
|
||||
assert keys[index] == expected_column, (
|
||||
f"{provider_name}: col {index + 1} should be "
|
||||
f"{expected_column}, got {keys[index]}"
|
||||
)
|
||||
assert keys[2 + len(expected_columns)] == "AssessmentDate", (
|
||||
f"{provider_name}: col {3 + len(expected_columns)} should be "
|
||||
"AssessmentDate"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user