diff --git a/prowler/lib/outputs/utils.py b/prowler/lib/outputs/utils.py index 1d974770d3..2e7ee76dc8 100644 --- a/prowler/lib/outputs/utils.py +++ b/prowler/lib/outputs/utils.py @@ -1,3 +1,6 @@ +from math import isfinite + + def unroll_list(listed_items: list, separator: str = "|") -> str: """ Unrolls a list of items into a single string, separated by a specified separator. @@ -208,7 +211,10 @@ def sanitize_csv_value(value): ): return value try: - float(value) - return value + # float() also accepts nan/inf and padded forms, which a spreadsheet + # renders as text, so only a finite number is left untouched + if isfinite(float(value)) and value == value.strip(): + return value except ValueError: - return f"'{value}" + pass + return f"'{value}" diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index 1eff388c25..b95fea7410 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -1331,3 +1331,13 @@ class TestSanitizeCSVValue: @pytest.mark.parametrize("value", [None, True, -1, -1.5, ""]) def test_non_string_and_empty_values_are_untouched(self, value): assert sanitize_csv_value(value) == value + + @pytest.mark.parametrize( + "value", ["-nan", "+nan", "-inf", "+inf", "-infinity", "+Infinity"] + ) + def test_non_finite_float_literal_is_prefixed(self, value): + assert sanitize_csv_value(value) == f"'{value}" + + @pytest.mark.parametrize("value", ["-1 ", "+1\t", "-1\n"]) + def test_padded_number_is_prefixed(self, value): + assert sanitize_csv_value(value) == f"'{value}" diff --git a/tests/providers/aws/lib/quick_inventory/quick_inventory_test.py b/tests/providers/aws/lib/quick_inventory/quick_inventory_test.py new file mode 100644 index 0000000000..52375c67b8 --- /dev/null +++ b/tests/providers/aws/lib/quick_inventory/quick_inventory_test.py @@ -0,0 +1,52 @@ +import csv +from unittest.mock import MagicMock + +from prowler.providers.aws.lib.quick_inventory.quick_inventory import create_output + + +def _args(tmp_path): + args = MagicMock() + args.output_directory = str(tmp_path) + args.output_filename = "inventory" + return args + + +def _provider(): + provider = MagicMock() + provider.identity.account = "123456789012" + return provider + + +def _rows(tmp_path): + with open(tmp_path / "inventory.csv", newline="") as handle: + return list(csv.reader(handle)) + + +class TestQuickInventoryCsvOutput: + def test_formula_initiator_in_tags_is_neutralised(self, tmp_path): + resources = [ + { + "arn": "arn:aws:s3:eu-west-1:123456789012:bucket-one", + "tags": '=cmd|" /C calc"!A0', + } + ] + + create_output(resources, _provider(), _args(tmp_path)) + + header, row = _rows(tmp_path) + assert row[header.index("AWS_Tags")] == '\'=cmd|" /C calc"!A0' + + def test_header_and_ordinary_values_are_untouched(self, tmp_path): + resources = [ + { + "arn": "arn:aws:s3:eu-west-1:123456789012:bucket-one", + "tags": "env=prod", + } + ] + + create_output(resources, _provider(), _args(tmp_path)) + + header, row = _rows(tmp_path) + assert header[0] == "AWS_AccountID" + assert row[header.index("AWS_Tags")] == "env=prod" + assert row[header.index("AWS_Region")] == "eu-west-1"