diff --git a/prowler/lib/outputs/asff/asff.py b/prowler/lib/outputs/asff/asff.py
index 18a0d37a10..b3ee6f3b2f 100644
--- a/prowler/lib/outputs/asff/asff.py
+++ b/prowler/lib/outputs/asff/asff.py
@@ -25,7 +25,6 @@ class ASFF(Output):
- transform(findings: list[Finding]) -> None: Transforms a list of findings into ASFF format.
- batch_write_data_to_file() -> None: Writes the findings data to a file in JSON ASFF format.
- generate_status(status: str, muted: bool = False) -> str: Generates the ASFF status based on the provided status and muted flag.
- - format_resource_tags(tags: str) -> dict: Transforms a string of tags into a dictionary format.
References:
- AWS Security Hub API Reference: https://docs.aws.amazon.com/securityhub/1.0/APIReference/API_Compliance.html
@@ -62,7 +61,6 @@ class ASFF(Output):
if finding.status == "MANUAL":
continue
timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
- resource_tags = ASFF.format_resource_tags(finding.resource_tags)
associated_standards, compliance_summary = ASFF.format_compliance(
finding.compliance
@@ -70,7 +68,6 @@ class ASFF(Output):
# Ensures finding_status matches allowed values in ASFF
finding_status = ASFF.generate_status(finding.status, finding.muted)
-
self._data.append(
AWSSecurityFindingFormat(
# The following line cannot be changed because it is the format we use to generate unique findings for AWS Security Hub
@@ -99,7 +96,7 @@ class ASFF(Output):
Type=finding.resource_type,
Partition=finding.partition,
Region=finding.region,
- Tags=resource_tags,
+ Tags=finding.resource_tags,
)
],
Compliance=Compliance(
@@ -195,42 +192,6 @@ class ASFF(Output):
return json_asff_status
- @staticmethod
- def format_resource_tags(tags: str) -> dict:
- """
- Transforms a string of tags into a dictionary format.
-
- Parameters:
- - tags (str): A string containing tags separated by ' | ' and key-value pairs separated by '='.
-
- Returns:
- - dict: A dictionary where keys are tag names and values are tag values.
-
- Notes:
- - If the input string is empty or None, it returns None.
- - Each tag in the input string should be in the format 'key=value'.
- - If the input string is not formatted correctly, it logs an error and returns None.
- """
- try:
- tags_dict = None
- if tags:
- tags = tags.split(" | ")
- tags_dict = {}
- for tag in tags:
- value = tag.split("=")
- tags_dict[value[0]] = value[1]
- return tags_dict
- except IndexError as error:
- logger.error(
- f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
- )
- return None
- except AttributeError as error:
- logger.error(
- f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
- )
- return None
-
@staticmethod
def format_compliance(compliance: dict) -> tuple[list[dict], list[str]]:
"""
@@ -316,6 +277,12 @@ class Resource(BaseModel):
Region: str
Tags: Optional[dict]
+ @validator("Tags", pre=True, always=True)
+ def tags_cannot_be_empty_dict(tags):
+ if not tags:
+ return None
+ return tags
+
class Compliance(BaseModel):
"""
diff --git a/prowler/lib/outputs/csv/csv.py b/prowler/lib/outputs/csv/csv.py
index aac2acdd08..06c74e2135 100644
--- a/prowler/lib/outputs/csv/csv.py
+++ b/prowler/lib/outputs/csv/csv.py
@@ -3,7 +3,7 @@ from csv import DictWriter
from prowler.lib.logger import logger
from prowler.lib.outputs.finding import Finding
from prowler.lib.outputs.output import Output
-from prowler.lib.outputs.utils import unroll_dict, unroll_list
+from prowler.lib.outputs.utils import unroll_dict
class CSV(Output):
@@ -17,8 +17,13 @@ class CSV(Output):
try:
for finding in findings:
finding_dict = {k.upper(): v for k, v in finding.dict().items()}
- finding_dict["COMPLIANCE"] = unroll_dict(finding.compliance)
- finding_dict["ACCOUNT_TAGS"] = unroll_list(finding.account_tags)
+ finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
+ finding_dict["COMPLIANCE"] = unroll_dict(
+ finding.compliance, separator=": "
+ )
+ finding_dict["ACCOUNT_TAGS"] = unroll_dict(
+ finding.account_tags, separator=":"
+ )
finding_dict["STATUS"] = finding.status.value
finding_dict["SEVERITY"] = finding.severity.value
self._data.append(finding_dict)
diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py
index a30f4facb9..f7695221c2 100644
--- a/prowler/lib/outputs/finding.py
+++ b/prowler/lib/outputs/finding.py
@@ -50,7 +50,7 @@ class Finding(BaseModel):
# Optional since it depends on permissions
account_organization_name: Optional[str]
# Optional since it depends on permissions
- account_tags: Optional[list[str]]
+ account_tags: dict = {}
finding_uid: str
provider: str
check_id: str
@@ -66,7 +66,7 @@ class Finding(BaseModel):
resource_uid: str
resource_name: str
resource_details: str
- resource_tags: str
+ resource_tags: dict = {}
# Only present for AWS and Azure
partition: Optional[str]
region: str
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index e8dfd5f6af..9d75af8a13 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -45,11 +45,11 @@ class HTML(Output):
{finding.check_id.replace("_", "_")} |
{finding.check_title} |
{finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} |
- {parse_html_string(finding.resource_tags)} |
+ {parse_html_string(unroll_dict(finding.resource_tags))} |
{finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "_")} |
{html.escape(finding.risk)} |
{html.escape(finding.remediation_recommendation_text)} |
- {parse_html_string(unroll_dict(finding.compliance))} |
+ {parse_html_string(unroll_dict(finding.compliance, separator=": "))} |
"""
)
diff --git a/prowler/lib/outputs/ocsf/ocsf.py b/prowler/lib/outputs/ocsf/ocsf.py
index 14bd869f87..b948185b8c 100644
--- a/prowler/lib/outputs/ocsf/ocsf.py
+++ b/prowler/lib/outputs/ocsf/ocsf.py
@@ -20,6 +20,7 @@ from py_ocsf_models.objects.resource_details import ResourceDetails
from prowler.lib.logger import logger
from prowler.lib.outputs.finding import Finding
from prowler.lib.outputs.output import Output
+from prowler.lib.outputs.utils import unroll_dict_to_list
class OCSF(Output):
@@ -97,12 +98,7 @@ class OCSF(Output):
risk_details=finding.risk,
resources=[
ResourceDetails(
- # TODO: Check labels for other providers
- labels=(
- finding.resource_tags.split(",")
- if finding.resource_tags
- else []
- ),
+ labels=unroll_dict_to_list(finding.resource_tags),
name=finding.resource_name,
uid=finding.resource_uid,
group=Group(name=finding.service_name),
@@ -148,7 +144,7 @@ class OCSF(Output):
type_id=cloud_account_type.value,
type=cloud_account_type.name,
uid=finding.account_uid,
- labels=finding.account_tags,
+ labels=unroll_dict_to_list(finding.account_tags),
),
org=Organization(
uid=finding.account_organization_uid,
diff --git a/prowler/lib/outputs/utils.py b/prowler/lib/outputs/utils.py
index 914b7177af..636f4003e5 100644
--- a/prowler/lib/outputs/utils.py
+++ b/prowler/lib/outputs/utils.py
@@ -1,4 +1,24 @@
-def unroll_list(listed_items: list, separator: str = "|"):
+def unroll_list(listed_items: list, separator: str = "|") -> str:
+ """
+ Unrolls a list of items into a single string, separated by a specified separator.
+
+ Args:
+ listed_items (list): The list of items to be unrolled.
+ separator (str, optional): The separator to be used between the items. Defaults to "|".
+
+ Returns:
+ str: The unrolled string.
+
+ Examples:
+ >>> unroll_list(['apple', 'banana', 'orange'])
+ 'apple | banana | orange'
+
+ >>> unroll_list(['apple', 'banana', 'orange'], separator=',')
+ 'apple, banana, orange'
+
+ >>> unroll_list([])
+ ''
+ """
unrolled_items = ""
if listed_items:
for item in listed_items:
@@ -13,70 +33,118 @@ def unroll_list(listed_items: list, separator: str = "|"):
return unrolled_items
-def unroll_tags(tags: list):
- unrolled_items = ""
- separator = "|"
+def unroll_tags(tags: list) -> dict:
+ """
+ Unrolls a list of tags into a dictionary.
+
+ Args:
+ tags (list): A list of tags.
+
+ Returns:
+ dict: A dictionary containing the unrolled tags.
+
+ Examples:
+ >>> tags = [{"key": "name", "value": "John"}, {"key": "age", "value": "30"}]
+ >>> unroll_tags(tags)
+ {'name': 'John', 'age': '30'}
+
+ >>> tags = [{"Key": "name", "Value": "John"}, {"Key": "age", "Value": "30"}]
+ >>> unroll_tags(tags)
+ {'name': 'John', 'age': '30'}
+
+ >>> tags = [{"name": "John", "age": "30"}]
+ >>> unroll_tags(tags)
+ {'name': 'John', 'age': '30'}
+
+ >>> tags = []
+ >>> unroll_tags(tags)
+ {}
+ """
if tags and tags != [{}] and tags != [None]:
- for item in tags:
- # Check if there are tags in list
- if isinstance(item, dict):
- for key, value in item.items():
- if not unrolled_items:
- # Check the pattern of tags (Key:Value or Key:key/Value:value)
- if "Key" != key and "Value" != key:
- unrolled_items = f"{key}={value}"
- else:
- if "Key" == key:
- unrolled_items = f"{value}="
- else:
- unrolled_items = f"{value}"
- else:
- if "Key" != key and "Value" != key:
- unrolled_items = (
- f"{unrolled_items} {separator} {key}={value}"
- )
- else:
- if "Key" == key:
- unrolled_items = (
- f"{unrolled_items} {separator} {value}="
- )
- else:
- unrolled_items = f"{unrolled_items}{value}"
- elif not unrolled_items:
- unrolled_items = f"{item}"
- else:
- unrolled_items = f"{unrolled_items} {separator} {item}"
-
- return unrolled_items
+ if "key" in tags[0]:
+ return {item["key"]: item["value"] for item in tags}
+ elif "Key" in tags[0]:
+ return {item["Key"]: item["Value"] for item in tags}
+ else:
+ return {key: value for d in tags for key, value in d.items()}
+ return {}
-def unroll_dict(dict: dict):
+def unroll_dict(dict: dict, separator: str = "=") -> str:
+ """
+ Unrolls a dictionary into a string representation.
+
+ Args:
+ dict (dict): The dictionary to be unrolled.
+
+ Returns:
+ str: The unrolled string representation of the dictionary.
+
+ Examples:
+ >>> my_dict = {'name': 'John', 'age': 30, 'hobbies': ['reading', 'coding']}
+ >>> unroll_dict(my_dict)
+ 'name: John | age: 30 | hobbies: reading, coding'
+ """
+
unrolled_items = ""
- separator = "|"
for key, value in dict.items():
if isinstance(value, list):
value = ", ".join(value)
if not unrolled_items:
- unrolled_items = f"{key}: {value}"
+ unrolled_items = f"{key}{separator}{value}"
else:
- unrolled_items = f"{unrolled_items} {separator} {key}: {value}"
+ unrolled_items = f"{unrolled_items} | {key}{separator}{value}"
return unrolled_items
-def unroll_dict_to_list(dict: dict):
+def unroll_dict_to_list(dict: dict) -> list:
+ """
+ Unrolls a dictionary into a list of key-value pairs.
+
+ Args:
+ dict (dict): The dictionary to be unrolled.
+
+ Returns:
+ list: A list of key-value pairs, where each pair is represented as a string.
+
+ Examples:
+ >>> my_dict = {'name': 'John', 'age': 30, 'hobbies': ['reading', 'coding']}
+ >>> unroll_dict_to_list(my_dict)
+ ['name: John', 'age: 30', 'hobbies: reading, coding']
+ """
+
dict_list = []
for key, value in dict.items():
if isinstance(value, list):
value = ", ".join(value)
- dict_list.append(f"{key}: {value}")
+ dict_list.append(f"{key}:{value}")
else:
- dict_list.append(f"{key}: {value}")
+ dict_list.append(f"{key}:{value}")
return dict_list
-def parse_json_tags(tags: list):
+def parse_json_tags(tags: list) -> dict[str, str]:
+ """
+ Parses a list of JSON tags and returns a dictionary of key-value pairs.
+
+ Args:
+ tags (list): A list of JSON tags.
+
+ Returns:
+ dict: A dictionary containing the parsed key-value pairs from the tags.
+
+ Examples:
+ >>> tags = [
+ ... {"Key": "Name", "Value": "John"},
+ ... {"Key": "Age", "Value": "30"},
+ ... {"Key": "City", "Value": "New York"}
+ ... ]
+ >>> parse_json_tags(tags)
+ {'Name': 'John', 'Age': '30', 'City': 'New York'}
+ """
+
dict_tags = {}
if tags and tags != [{}] and tags != [None]:
for tag in tags:
@@ -88,7 +156,23 @@ def parse_json_tags(tags: list):
return dict_tags
-def parse_html_string(str: str):
+def parse_html_string(str: str) -> str:
+ """
+ Parses a string and returns a formatted HTML string.
+
+ This function takes an input string and splits it using the delimiter " | ".
+ It then formats each element of the split string as a bullet point in HTML format.
+
+ Args:
+ str (str): The input string to be parsed.
+
+ Returns:
+ str: The formatted HTML string.
+
+ Example:
+ >>> parse_html_string("item1 | item2 | item3")
+ '\n•item1\n\n•item2\n\n•item3\n'
+ """
string = ""
for elem in str.split(" | "):
if elem:
diff --git a/prowler/providers/aws/lib/mutelist/mutelist.py b/prowler/providers/aws/lib/mutelist/mutelist.py
index d86e374bcd..3302a13f5d 100644
--- a/prowler/providers/aws/lib/mutelist/mutelist.py
+++ b/prowler/providers/aws/lib/mutelist/mutelist.py
@@ -1,13 +1,13 @@
import re
-from typing import Any
import yaml
from boto3 import Session
from boto3.dynamodb.conditions import Attr
+from prowler.lib.check.models import Check_Report_AWS
from prowler.lib.logger import logger
from prowler.lib.mutelist.mutelist import Mutelist
-from prowler.lib.outputs.utils import unroll_tags
+from prowler.lib.outputs.utils import unroll_dict, unroll_tags
class AWSMutelist(Mutelist):
@@ -45,7 +45,7 @@ class AWSMutelist(Mutelist):
def is_finding_muted(
self,
- finding: Any,
+ finding: Check_Report_AWS,
aws_account_id: str,
) -> bool:
return self.is_muted(
@@ -53,7 +53,7 @@ class AWSMutelist(Mutelist):
finding.check_metadata.CheckID,
finding.region,
finding.resource_id,
- unroll_tags(finding.resource_tags),
+ unroll_dict(unroll_tags(finding.resource_tags)),
)
def get_mutelist_file_from_s3(self, aws_session: Session = None):
diff --git a/prowler/providers/aws/lib/organizations/organizations.py b/prowler/providers/aws/lib/organizations/organizations.py
index 8aee2ea333..9e4eefb42a 100644
--- a/prowler/providers/aws/lib/organizations/organizations.py
+++ b/prowler/providers/aws/lib/organizations/organizations.py
@@ -30,9 +30,9 @@ def get_organizations_metadata(
def parse_organizations_metadata(metadata: dict, tags: dict) -> AWSOrganizationsInfo:
try:
# Convert Tags dictionary to String
- account_details_tags = []
+ account_details_tags = {}
for tag in tags.get("Tags", {}):
- account_details_tags.append(f"{tag['Key']}:{tag['Value']}")
+ account_details_tags[tag["Key"]] = tag["Value"]
account_details = metadata.get("Account", {})
diff --git a/prowler/providers/azure/lib/mutelist/mutelist.py b/prowler/providers/azure/lib/mutelist/mutelist.py
index bac563cc8b..159a3cbb77 100644
--- a/prowler/providers/azure/lib/mutelist/mutelist.py
+++ b/prowler/providers/azure/lib/mutelist/mutelist.py
@@ -1,18 +1,17 @@
-from typing import Any
-
+from prowler.lib.check.models import Check_Report_Azure
from prowler.lib.mutelist.mutelist import Mutelist
-from prowler.lib.outputs.utils import unroll_tags
+from prowler.lib.outputs.utils import unroll_dict, unroll_tags
class AzureMutelist(Mutelist):
def is_finding_muted(
self,
- finding: Any,
+ finding: Check_Report_Azure,
) -> bool:
return self.is_muted(
finding.subscription,
finding.check_metadata.CheckID,
finding.location,
finding.resource_name,
- unroll_tags(finding.resource_tags),
+ unroll_dict(unroll_tags(finding.resource_tags)),
)
diff --git a/prowler/providers/gcp/gcp_provider.py b/prowler/providers/gcp/gcp_provider.py
index e7071c48d7..caba007fc7 100644
--- a/prowler/providers/gcp/gcp_provider.py
+++ b/prowler/providers/gcp/gcp_provider.py
@@ -279,9 +279,9 @@ class GcpProvider(Provider):
response = request.execute()
for project in response.get("projects", []):
- labels = []
+ labels = {}
for key, value in project.get("labels", {}).items():
- labels.append(f"{key}:{value}")
+ labels[key] = value
project_id = project["projectId"]
gcp_project = GCPProject(
diff --git a/prowler/providers/gcp/lib/mutelist/mutelist.py b/prowler/providers/gcp/lib/mutelist/mutelist.py
index 513e33c297..c39500bf81 100644
--- a/prowler/providers/gcp/lib/mutelist/mutelist.py
+++ b/prowler/providers/gcp/lib/mutelist/mutelist.py
@@ -1,18 +1,17 @@
-from typing import Any
-
+from prowler.lib.check.models import Check_Report_GCP
from prowler.lib.mutelist.mutelist import Mutelist
-from prowler.lib.outputs.utils import unroll_tags
+from prowler.lib.outputs.utils import unroll_dict, unroll_tags
class GCPMutelist(Mutelist):
def is_finding_muted(
self,
- finding: Any,
+ finding: Check_Report_GCP,
) -> bool:
return self.is_muted(
finding.project_id,
finding.check_metadata.CheckID,
finding.location,
finding.resource_name,
- unroll_tags(finding.resource_tags),
+ unroll_dict(unroll_tags(finding.resource_tags)),
)
diff --git a/prowler/providers/gcp/models.py b/prowler/providers/gcp/models.py
index 31b5bd3b90..3505293fd8 100644
--- a/prowler/providers/gcp/models.py
+++ b/prowler/providers/gcp/models.py
@@ -22,7 +22,7 @@ class GCPProject(BaseModel):
id: str
name: str
organization: Optional[GCPOrganization]
- labels: list[str]
+ labels: dict
lifecycle_state: str
diff --git a/prowler/providers/kubernetes/lib/mutelist/mutelist.py b/prowler/providers/kubernetes/lib/mutelist/mutelist.py
index 0fc50f7332..6ca168f5c2 100644
--- a/prowler/providers/kubernetes/lib/mutelist/mutelist.py
+++ b/prowler/providers/kubernetes/lib/mutelist/mutelist.py
@@ -1,13 +1,12 @@
-from typing import Any
-
+from prowler.lib.check.models import Check_Report_Kubernetes
from prowler.lib.mutelist.mutelist import Mutelist
-from prowler.lib.outputs.utils import unroll_tags
+from prowler.lib.outputs.utils import unroll_dict, unroll_tags
class KubernetesMutelist(Mutelist):
def is_finding_muted(
self,
- finding: Any,
+ finding: Check_Report_Kubernetes,
cluster: str,
) -> bool:
return self.is_muted(
@@ -15,5 +14,5 @@ class KubernetesMutelist(Mutelist):
finding.check_metadata.CheckID,
finding.namespace,
finding.resource_name,
- unroll_tags(finding.resource_tags),
+ unroll_dict(unroll_tags(finding.resource_tags)),
)
diff --git a/tests/lib/outputs/asff/asff_test.py b/tests/lib/outputs/asff/asff_test.py
index a9fc26e227..a60f06e769 100644
--- a/tests/lib/outputs/asff/asff_test.py
+++ b/tests/lib/outputs/asff/asff_test.py
@@ -40,7 +40,7 @@ class TestASFF:
resource_details="Test resource details",
resource_name="test-resource",
resource_uid="test-arn",
- resource_tags="key1=value1",
+ resource_tags={"key1": "value1"},
)
timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -70,7 +70,7 @@ class TestASFF:
Type=finding.resource_type,
Partition=AWS_COMMERCIAL_PARTITION,
Region=AWS_REGION_EU_WEST_1,
- Tags=ASFF.format_resource_tags(finding.resource_tags),
+ Tags={"key1": "value1"},
)
],
Compliance=Compliance(
@@ -103,7 +103,7 @@ class TestASFF:
resource_details="Test resource details",
resource_name="test-resource",
resource_uid="test-arn",
- resource_tags="key1=value1",
+ resource_tags={"key1": "value1"},
)
finding.remediation_recommendation_url = ""
@@ -136,7 +136,72 @@ class TestASFF:
Type=finding.resource_type,
Partition=AWS_COMMERCIAL_PARTITION,
Region=AWS_REGION_EU_WEST_1,
- Tags=ASFF.format_resource_tags(finding.resource_tags),
+ Tags={"key1": "value1"},
+ )
+ ],
+ Compliance=Compliance(
+ Status=ASFF.generate_status(status),
+ RelatedRequirements=compliance_summary,
+ AssociatedStandards=associated_standards,
+ ),
+ Remediation=Remediation(
+ Recommendation=Recommendation(
+ Text=finding.remediation_recommendation_text,
+ Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
+ )
+ ),
+ Description=finding.description,
+ )
+
+ asff = ASFF(findings=[finding])
+
+ assert len(asff.data) == 1
+ asff_finding = asff.data[0]
+
+ assert asff_finding == expected
+
+ def test_asff_without_resource_tags(self):
+ status = "PASS"
+ finding = generate_finding_output(
+ status=status,
+ status_extended="This is a test",
+ region=AWS_REGION_EU_WEST_1,
+ resource_details="Test resource details",
+ resource_name="test-resource",
+ resource_uid="test-arn",
+ )
+ finding.remediation_recommendation_url = ""
+
+ timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ associated_standards, compliance_summary = ASFF.format_compliance(
+ finding.compliance
+ )
+
+ timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ expected = AWSSecurityFindingFormat(
+ Id=f"prowler-{finding.check_id}-{AWS_ACCOUNT_NUMBER}-{AWS_REGION_EU_WEST_1}-{hash_sha512(finding.resource_uid)}",
+ ProductArn=f"arn:{AWS_COMMERCIAL_PARTITION}:securityhub:{AWS_REGION_EU_WEST_1}::product/prowler/prowler",
+ ProductFields=ProductFields(
+ ProviderVersion=prowler_version,
+ ProwlerResourceName=finding.resource_uid,
+ ),
+ GeneratorId="prowler-" + finding.check_id,
+ AwsAccountId=AWS_ACCOUNT_NUMBER,
+ Types=finding.check_type.split(","),
+ FirstObservedAt=timestamp,
+ UpdatedAt=timestamp,
+ CreatedAt=timestamp,
+ Severity=Severity(Label=finding.severity),
+ Title=finding.check_title,
+ Resources=[
+ Resource(
+ Id=finding.resource_uid,
+ Type=finding.resource_type,
+ Partition=AWS_COMMERCIAL_PARTITION,
+ Region=AWS_REGION_EU_WEST_1,
+ Tags=None,
)
],
Compliance=Compliance(
@@ -171,7 +236,7 @@ class TestASFF:
resource_details="Test resource details",
resource_name="test-resource",
resource_uid="test-arn",
- resource_tags="key1=value1",
+ resource_tags={"key1": "value1"},
)
finding.remediation_recommendation_url = ""
finding.remediation_recommendation_text = "x" * 513
@@ -205,7 +270,7 @@ class TestASFF:
Type=finding.resource_type,
Partition=AWS_COMMERCIAL_PARTITION,
Region=AWS_REGION_EU_WEST_1,
- Tags=ASFF.format_resource_tags(finding.resource_tags),
+ Tags={"key1": "value1"},
)
],
Compliance=Compliance(
@@ -239,7 +304,7 @@ class TestASFF:
resource_details="Test resource details",
resource_name="test-resource",
resource_uid="test-arn",
- resource_tags="key1=value1",
+ resource_tags={"key1": "value1"},
compliance={
"CISA": ["your-systems-3", "your-data-2"],
"SOC2": ["cc_2_1", "cc_7_2", "cc_a_1_2"],
@@ -412,7 +477,7 @@ class TestASFF:
Type=finding.resource_type,
Partition=AWS_COMMERCIAL_PARTITION,
Region=AWS_REGION_EU_WEST_1,
- Tags=ASFF.format_resource_tags(finding.resource_tags),
+ Tags={"key1": "value1"},
)
],
Compliance=Compliance(
@@ -448,7 +513,7 @@ class TestASFF:
resource_details="Test resource details",
resource_name="test-resource",
resource_uid="test-arn",
- resource_tags="key1=value1",
+ resource_tags={"key1": "value1"},
)
finding.remediation_recommendation_url = ""
@@ -517,14 +582,3 @@ class TestASFF:
assert ASFF.generate_status("FAIL") == "FAILED"
assert ASFF.generate_status("FAIL", True) == "WARNING"
assert ASFF.generate_status("SOMETHING ELSE") == "NOT_AVAILABLE"
-
- def test_asff_format_resource_tags(self):
- assert ASFF.format_resource_tags(None) is None
- assert ASFF.format_resource_tags("") is None
- assert ASFF.format_resource_tags([]) is None
- assert ASFF.format_resource_tags([{}]) is None
- assert ASFF.format_resource_tags("key1=value1") == {"key1": "value1"}
- assert ASFF.format_resource_tags("key1=value1 | key2=value2") == {
- "key1": "value1",
- "key2": "value2",
- }
diff --git a/tests/lib/outputs/csv/csv_test.py b/tests/lib/outputs/csv/csv_test.py
index 23fd18d99c..a466fcec27 100644
--- a/tests/lib/outputs/csv/csv_test.py
+++ b/tests/lib/outputs/csv/csv_test.py
@@ -25,7 +25,7 @@ class TestCSV:
resource_uid="resource-123",
resource_name="Example Resource",
resource_details="Detailed information about the resource",
- resource_tags="tag1,tag2",
+ resource_tags={"tag1": "value1", "tag2": "value2"},
partition="aws",
description="Description of the finding",
risk="High",
@@ -78,7 +78,7 @@ class TestCSV:
assert (
output_data["RESOURCE_DETAILS"] == "Detailed information about the resource"
)
- assert output_data["RESOURCE_TAGS"] == "tag1,tag2"
+ assert output_data["RESOURCE_TAGS"] == "tag1=value1 | tag2=value2"
assert output_data["PARTITION"] == "aws"
assert output_data["REGION"] == AWS_REGION_EU_WEST_1
assert output_data["DESCRIPTION"] == "Description of the finding"
diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py
index 990bbb31d8..ecd0a368d7 100644
--- a/tests/lib/outputs/finding_test.py
+++ b/tests/lib/outputs/finding_test.py
@@ -12,7 +12,7 @@ def mock_get_provider_data_mapping_aws(_):
"account_email": "mock_account_email",
"account_organization_uid": "mock_account_org_uid",
"account_organization_name": "mock_account_org_name",
- "account_tags": ["tag1", "tag2"],
+ "account_tags": {"tag1": "value1"},
"finding_uid": "mock_finding_uid",
"provider": "aws",
"check_id": "mock_check_id",
@@ -28,7 +28,7 @@ def mock_get_provider_data_mapping_aws(_):
"resource_uid": "mock_resource_uid",
"resource_name": "mock_resource_name",
"resource_details": "mock_resource_details",
- "resource_tags": "mock_resource_tags",
+ "resource_tags": {"tag1": "value1"},
"partition": None,
"region": "mock_region",
"description": "mock_description",
@@ -58,7 +58,7 @@ def mock_get_provider_data_mapping_azure(_):
"account_email": "mock_account_email",
"account_organization_uid": "mock_account_org_uid",
"account_organization_name": "mock_account_org_name",
- "account_tags": ["tag1", "tag2"],
+ "account_tags": {"tag1": "value1"},
"finding_uid": "mock_finding_uid",
"provider": "azure",
"check_id": "mock_check_id",
@@ -74,7 +74,7 @@ def mock_get_provider_data_mapping_azure(_):
"resource_uid": "mock_resource_uid",
"resource_name": "mock_resource_name",
"resource_details": "mock_resource_details",
- "resource_tags": "mock_resource_tags",
+ "resource_tags": {"tag1": "value1"},
"partition": None,
"description": "mock_description",
"risk": "mock_risk",
@@ -103,7 +103,7 @@ def mock_get_provider_data_mapping_gcp(_):
"account_email": "mock_account_email",
"account_organization_uid": "mock_account_org_uid",
"account_organization_name": "mock_account_org_name",
- "account_tags": ["tag1", "tag2"],
+ "account_tags": {"tag1": "value1"},
"finding_uid": "mock_finding_uid",
"provider": "gcp",
"check_id": "mock_check_id",
@@ -119,7 +119,7 @@ def mock_get_provider_data_mapping_gcp(_):
"resource_uid": "mock_resource_uid",
"resource_name": "mock_resource_name",
"resource_details": "mock_resource_details",
- "resource_tags": "mock_resource_tags",
+ "resource_tags": {"tag1": "value1"},
"partition": None,
"description": "mock_description",
"risk": "mock_risk",
@@ -148,7 +148,7 @@ def mock_get_provider_data_mapping_kubernetes(_):
"account_email": "mock_account_email",
"account_organization_uid": "mock_account_org_uid",
"account_organization_name": "mock_account_org_name",
- "account_tags": ["tag1", "tag2"],
+ "account_tags": {"tag1": "value1"},
"finding_uid": "mock_finding_uid",
"provider": "kubernetes",
"check_id": "mock_check_id",
@@ -164,7 +164,7 @@ def mock_get_provider_data_mapping_kubernetes(_):
"resource_uid": "mock_resource_uid",
"resource_name": "mock_resource_name",
"resource_details": "mock_resource_details",
- "resource_tags": "mock_resource_tags",
+ "resource_tags": {"tag1": "value1"},
"partition": None,
"description": "mock_description",
"risk": "mock_risk",
@@ -240,7 +240,7 @@ class TestFinding:
assert finding_output.subservice_name == "mock_subservice_name"
assert finding_output.severity == Severity.high
assert finding_output.resource_type == "mock_resource_type"
- assert finding_output.resource_tags == "mock_resource_tags"
+ assert finding_output.resource_tags == {"tag1": "value1"}
assert finding_output.partition is None
assert finding_output.description == "mock_description"
assert finding_output.risk == "mock_risk"
@@ -260,7 +260,7 @@ class TestFinding:
assert finding_output.account_email == "mock_account_email"
assert finding_output.account_organization_uid == "mock_account_org_uid"
assert finding_output.account_organization_name == "mock_account_org_name"
- assert finding_output.account_tags == ["tag1", "tag2"]
+ assert finding_output.account_tags == {"tag1": "value1"}
assert finding_output.prowler_version == "1.0.0"
@patch(
@@ -318,7 +318,7 @@ class TestFinding:
assert finding_output.subservice_name == "mock_subservice_name"
assert finding_output.severity == Severity.high
assert finding_output.resource_type == "mock_resource_type"
- assert finding_output.resource_tags == "mock_resource_tags"
+ assert finding_output.resource_tags == {"tag1": "value1"}
assert finding_output.partition is None
assert finding_output.description == "mock_description"
assert finding_output.risk == "mock_risk"
@@ -353,7 +353,7 @@ class TestFinding:
organization.display_name = "mock_organization_name"
project.id = "mock_project_id"
project.name = "mock_project_name"
- project.labels = ["label1", "label2"]
+ project.labels = {"tag1": "value1"}
project.organization = organization
provider.projects = {"mock_project_id": project}
@@ -388,7 +388,7 @@ class TestFinding:
assert finding_output.subservice_name == "mock_subservice_name"
assert finding_output.severity == Severity.high
assert finding_output.resource_type == "mock_resource_type"
- assert finding_output.resource_tags == "mock_resource_tags"
+ assert finding_output.resource_tags == {"tag1": "value1"}
assert finding_output.partition is None
assert finding_output.description == "mock_description"
assert finding_output.risk == "mock_risk"
@@ -408,7 +408,7 @@ class TestFinding:
assert finding_output.account_email == "mock_account_email"
assert finding_output.account_organization_uid == "mock_organization_id"
assert finding_output.account_organization_name == "mock_account_org_name"
- assert finding_output.account_tags == ["label1", "label2"]
+ assert finding_output.account_tags == {"tag1": "value1"}
assert finding_output.prowler_version == "1.0.0"
assert finding_output.timestamp == 1622520000
@@ -459,7 +459,7 @@ class TestFinding:
assert finding_output.subservice_name == "mock_subservice_name"
assert finding_output.severity == Severity.high
assert finding_output.resource_type == "mock_resource_type"
- assert finding_output.resource_tags == "mock_resource_tags"
+ assert finding_output.resource_tags == {"tag1": "value1"}
assert finding_output.partition is None
assert finding_output.description == "mock_description"
assert finding_output.risk == "mock_risk"
@@ -479,6 +479,6 @@ class TestFinding:
assert finding_output.account_email == "mock_account_email"
assert finding_output.account_organization_uid == "mock_account_org_uid"
assert finding_output.account_organization_name == "mock_account_org_name"
- assert finding_output.account_tags == ["tag1", "tag2"]
+ assert finding_output.account_tags == {"tag1": "value1"}
assert finding_output.prowler_version == "1.0.0"
assert finding_output.timestamp == 1622520000
diff --git a/tests/lib/outputs/fixtures/fixtures.py b/tests/lib/outputs/fixtures/fixtures.py
index eae49d82df..479dcc188b 100644
--- a/tests/lib/outputs/fixtures/fixtures.py
+++ b/tests/lib/outputs/fixtures/fixtures.py
@@ -16,7 +16,7 @@ def generate_finding_output(
resource_details: str = "",
resource_uid: str = "",
resource_name: str = "",
- resource_tags: str = "",
+ resource_tags: dict = {},
compliance: dict = {"test-compliance": "test-compliance"},
timestamp: datetime = None,
provider: str = "aws",
@@ -34,6 +34,10 @@ def generate_finding_output(
depends_on: str = "test-dependency",
related_to: str = "test-related-to",
notes: str = "test-notes",
+ service_name: str = "test-service",
+ check_id: str = "test-check-id",
+ check_title: str = "test-check-id",
+ check_type: str = "test-type",
) -> Finding:
return Finding(
auth_method="profile: default",
@@ -43,16 +47,16 @@ def generate_finding_output(
account_email="",
account_organization_uid="test-organization-id",
account_organization_name="test-organization",
- account_tags=["test-tag:test-value"],
+ account_tags={"test-tag": "test-value"},
finding_uid="test-unique-finding",
provider=provider,
- check_id="test-check-id",
- check_title="test-check-id",
- check_type="test-type",
+ check_id=check_id,
+ check_title=check_title,
+ check_type=check_type,
status=status,
status_extended=status_extended,
muted=muted,
- service_name="test-service",
+ service_name=service_name,
subservice_name="",
severity=severity,
resource_type="test-resource",
diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py
index caeff04963..9e978a9f80 100644
--- a/tests/lib/outputs/html/html_test.py
+++ b/tests/lib/outputs/html/html_test.py
@@ -45,11 +45,15 @@ fail_html_finding = """
eu-west-1 |
test-check-id |
test-check-id |
- |
- |
- |
+ test-resource-uid |
+
+•key1=value1
+
+•key2=value2
+ |
+ test-status-extended |
test-risk |
- |
+ test-remediation-recommendation-text |
•test-compliance: test-compliance
|
@@ -421,7 +425,23 @@ html_footer = """
class TestHTML:
def test_transform_fail_finding(self):
- findings = [generate_finding_output(status="FAIL")]
+ findings = [
+ generate_finding_output(
+ status="FAIL",
+ resource_tags={"key1": "value1", "key2": "value2"},
+ severity="high",
+ service_name="test-service",
+ region=AWS_REGION_EU_WEST_1,
+ check_id="test-check-id",
+ check_title="test-check-id",
+ resource_uid="test-resource-uid",
+ status_extended="test-status-extended",
+ risk="test-risk",
+ remediation_recommendation_text="test-remediation-recommendation-text",
+ compliance={"test-compliance": "test-compliance"},
+ )
+ ]
+
html = HTML(findings)
output_data = html.data[0]
assert isinstance(output_data, str)
diff --git a/tests/lib/outputs/ocsf/ocsf_test.py b/tests/lib/outputs/ocsf/ocsf_test.py
index 48ccdb9a34..4fa889dcac 100644
--- a/tests/lib/outputs/ocsf/ocsf_test.py
+++ b/tests/lib/outputs/ocsf/ocsf_test.py
@@ -30,7 +30,11 @@ class TestOCSF:
def test_transform(self):
findings = [
generate_finding_output(
- status="FAIL", severity="low", muted=False, region=AWS_REGION_EU_WEST_1
+ status="FAIL",
+ severity="low",
+ muted=False,
+ region=AWS_REGION_EU_WEST_1,
+ resource_tags={"Name": "test", "Environment": "dev"},
)
]
@@ -58,7 +62,7 @@ class TestOCSF:
assert output_data.status_code == findings[0].status
assert output_data.status_detail == findings[0].status_extended
assert output_data.risk_details == findings[0].risk
- assert output_data.resources[0].labels == []
+ assert output_data.resources[0].labels == ["Name:test", "Environment:dev"]
assert output_data.resources[0].name == findings[0].resource_name
assert output_data.resources[0].uid == findings[0].resource_uid
assert output_data.resources[0].type == findings[0].resource_type
@@ -190,7 +194,11 @@ class TestOCSF:
def test_finding_output_cloud_pass_low_muted(self):
finding_output = generate_finding_output(
- status="PASS", severity="low", muted=True, region=AWS_REGION_EU_WEST_1
+ status="PASS",
+ severity="low",
+ muted=True,
+ region=AWS_REGION_EU_WEST_1,
+ resource_tags={"Name": "test", "Environment": "dev"},
)
finding_ocsf = OCSF([finding_output])
@@ -248,7 +256,7 @@ class TestOCSF:
assert len(resource_details) == 1
assert isinstance(resource_details, list)
assert isinstance(resource_details[0], ResourceDetails)
- assert resource_details[0].labels == []
+ assert resource_details[0].labels == ["Name:test", "Environment:dev"]
assert resource_details[0].name == finding_output.resource_name
assert resource_details[0].uid == finding_output.resource_uid
assert resource_details[0].type == finding_output.resource_type
@@ -287,7 +295,7 @@ class TestOCSF:
assert cloud_account.type_id == TypeID.AWS_Account
assert cloud_account.type == TypeID.AWS_Account.name
assert cloud_account.uid == finding_output.account_uid
- assert cloud_account.labels == finding_output.account_tags
+ assert cloud_account.labels == ["test-tag:test-value"]
cloud_organization = cloud.org
assert isinstance(cloud_organization, Organization)
diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py
index 9a4c98f21e..43b16026c7 100644
--- a/tests/lib/outputs/outputs_test.py
+++ b/tests/lib/outputs/outputs_test.py
@@ -98,6 +98,15 @@ class TestOutputs:
{"Key": "environment", "Value": "dev"},
{"Key": "terraform", "Value": "true"},
]
+
+ assert unroll_tags(dict_list) == {
+ "environment": "dev",
+ "name": "test",
+ "project": "prowler",
+ "terraform": "true",
+ }
+
+ def test_unroll_tags_unique(self):
unique_dict_list = [
{
"test1": "value1",
@@ -105,14 +114,26 @@ class TestOutputs:
"test3": "value3",
}
]
- assert (
- unroll_tags(dict_list)
- == "name=test | project=prowler | environment=dev | terraform=true"
- )
- assert (
- unroll_tags(unique_dict_list)
- == "test1=value1 | test2=value2 | test3=value3"
- )
+ assert unroll_tags(unique_dict_list) == {
+ "test1": "value1",
+ "test2": "value2",
+ "test3": "value3",
+ }
+
+ def test_unroll_tags_lowercase(self):
+ dict_list = [
+ {"key": "name", "value": "test"},
+ {"key": "project", "value": "prowler"},
+ {"key": "environment", "value": "dev"},
+ {"key": "terraform", "value": "true"},
+ ]
+
+ assert unroll_tags(dict_list) == {
+ "environment": "dev",
+ "name": "test",
+ "project": "prowler",
+ "terraform": "true",
+ }
def test_unroll_dict(self):
test_compliance_dict = {
@@ -156,18 +177,18 @@ class TestOutputs:
"FedRAMP-Low-Revision-4": ["sc-13"],
}
assert (
- unroll_dict(test_compliance_dict)
+ unroll_dict(test_compliance_dict, separator=": ")
== "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13"
)
def test_unroll_dict_to_list(self):
dict_A = {"A": "B"}
- list_A = ["A: B"]
+ list_A = ["A:B"]
assert unroll_dict_to_list(dict_A) == list_A
dict_B = {"A": ["B", "C"]}
- list_B = ["A: B, C"]
+ list_B = ["A:B, C"]
assert unroll_dict_to_list(dict_B) == list_B
diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py
index 829f1cd458..e0e4d3d2b4 100644
--- a/tests/providers/aws/aws_provider_test.py
+++ b/tests/providers/aws/aws_provider_test.py
@@ -270,7 +270,7 @@ class TestAWSProvider:
assert isinstance(aws_provider.organizations_metadata, AWSOrganizationsInfo)
assert aws_provider.organizations_metadata.account_email == "master@example.com"
assert aws_provider.organizations_metadata.account_name == "master"
- assert aws_provider.organizations_metadata.account_tags == ["tagged:true"]
+ assert aws_provider.organizations_metadata.account_tags == {"tagged": "true"}
assert (
aws_provider.organizations_metadata.organization_account_arn
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:account/{organization['Id']}/{AWS_ACCOUNT_NUMBER}"
@@ -351,7 +351,7 @@ class TestAWSProvider:
assert isinstance(aws_provider.organizations_metadata, AWSOrganizationsInfo)
assert aws_provider.organizations_metadata.account_email == "master@example.com"
assert aws_provider.organizations_metadata.account_name == "master"
- assert aws_provider.organizations_metadata.account_tags == ["tagged:true"]
+ assert aws_provider.organizations_metadata.account_tags == {"tagged": "true"}
assert (
aws_provider.organizations_metadata.organization_account_arn
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:account/{organization['Id']}/{AWS_ACCOUNT_NUMBER}"
diff --git a/tests/providers/aws/lib/organizations/organizations_test.py b/tests/providers/aws/lib/organizations/organizations_test.py
index 1f60aa211d..ad6667bca4 100644
--- a/tests/providers/aws/lib/organizations/organizations_test.py
+++ b/tests/providers/aws/lib/organizations/organizations_test.py
@@ -42,7 +42,7 @@ class Test_AWS_Organizations:
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:organization/{org_id}"
)
assert org.organization_id == org_id
- assert org.account_tags == ["key:value"]
+ assert org.account_tags == {"key": "value"}
def test_parse_organizations_metadata(self):
tags = {"Tags": [{"Key": "test-key", "Value": "test-value"}]}
@@ -70,4 +70,4 @@ class Test_AWS_Organizations:
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:account/{organization_name}/{AWS_ACCOUNT_NUMBER}"
)
assert org.organization_arn == arn
- assert org.account_tags == ["test-key:test-value"]
+ assert org.account_tags == {"test-key": "test-value"}
diff --git a/tests/providers/aws/lib/s3/s3_test.py b/tests/providers/aws/lib/s3/s3_test.py
index 16fb4663f6..0bcecfc809 100644
--- a/tests/providers/aws/lib/s3/s3_test.py
+++ b/tests/providers/aws/lib/s3/s3_test.py
@@ -26,7 +26,7 @@ FINDING = generate_finding_output(
resource_uid="resource-123",
resource_name="Example Resource",
resource_details="Detailed information about the resource",
- resource_tags="tag1,tag2",
+ resource_tags={"key1": "tag1", "key2": "tag2"},
partition="aws",
description="Description of the finding",
risk="High",
diff --git a/tests/providers/gcp/gcp_provider_test.py b/tests/providers/gcp/gcp_provider_test.py
index 7526c00dfd..1762137eea 100644
--- a/tests/providers/gcp/gcp_provider_test.py
+++ b/tests/providers/gcp/gcp_provider_test.py
@@ -29,7 +29,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -75,7 +75,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -148,7 +148,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -200,7 +200,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -242,7 +242,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -292,7 +292,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -341,7 +341,7 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
)
}
@@ -390,14 +390,14 @@ class TestGCPProvider:
number="55555555",
id="project/55555555",
name="test-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
),
"test-excluded-project": GCPProject(
number="12345678",
id="project/12345678",
name="test-excluded-project",
- labels=["test:value"],
+ labels={"test": "value"},
lifecycle_state="",
),
}