mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-26 21:34:19 +00:00
fix(outputs): neutralise formula initiators in CSV cells
This commit is contained in:
10 files changed
+124
-5
No files matched your search
@@ -136,6 +136,28 @@ class TestAWSGenericCompliance:
|
||||
|
||||
assert content == expected_csv
|
||||
|
||||
def test_batch_write_neutralises_formula_initiators(self):
|
||||
mock_file = StringIO()
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
status_extended="=cmd",
|
||||
resource_uid="-1",
|
||||
compliance={"NIST-800-53-Revision-4": "ac_2_4"},
|
||||
)
|
||||
]
|
||||
output = GenericCompliance(findings, NIST_800_53_REVISION_4_AWS)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
output.batch_write_data_to_file()
|
||||
|
||||
mock_file.seek(0)
|
||||
header, row, _ = mock_file.read().splitlines()
|
||||
cells = dict(zip(header.split(";"), row.split(";")))
|
||||
|
||||
assert cells["STATUSEXTENDED"] == "'=cmd"
|
||||
assert cells["RESOURCEID"] == "-1"
|
||||
|
||||
def test_csv_row_count_matches_framework_checks_not_stored_compliance(self):
|
||||
"""Regression test for PROWLER-1763.
|
||||
|
||||
|
||||
@@ -228,6 +228,34 @@ class TestCSVFileWrite:
|
||||
assert "REQUIREMENTS_ATTRIBUTES_SECTION" in content
|
||||
assert "IAM" in content
|
||||
|
||||
def test_batch_write_neutralises_formula_initiators(self, tmp_path):
|
||||
reqs = [
|
||||
UniversalComplianceRequirement(
|
||||
id="1.1",
|
||||
description="test",
|
||||
attributes={"Section": "IAM"},
|
||||
checks={"aws": ["check_a"]},
|
||||
),
|
||||
]
|
||||
fw = _make_framework(reqs, [AttributeMetadata(key="Section", type="str")])
|
||||
|
||||
finding = _make_finding("check_a", "PASS", {"TestFW-1.0": ["1.1"]})
|
||||
finding.status_extended = "=cmd"
|
||||
finding.resource_name = "-1"
|
||||
filepath = str(tmp_path / "test.csv")
|
||||
|
||||
output = UniversalComplianceOutput(
|
||||
findings=[finding], framework=fw, file_path=filepath
|
||||
)
|
||||
output.batch_write_data_to_file()
|
||||
|
||||
with open(filepath, "r") as f:
|
||||
header, row = f.read().splitlines()
|
||||
cells = dict(zip(header.split(";"), row.split(";")))
|
||||
|
||||
assert cells["STATUSEXTENDED"] == "'=cmd"
|
||||
assert cells["RESOURCENAME"] == "-1"
|
||||
|
||||
|
||||
class TestNoFindings:
|
||||
def test_empty_findings_no_data(self, tmp_path):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import tempfile
|
||||
from csv import DictReader
|
||||
from datetime import datetime
|
||||
from io import StringIO, TextIOWrapper
|
||||
from typing import List
|
||||
@@ -131,6 +132,29 @@ class TestCSV:
|
||||
|
||||
assert content == expected_csv
|
||||
|
||||
def test_csv_write_neutralises_formula_initiators(self):
|
||||
mock_file = StringIO()
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
resource_name='=HYPERLINK("http://attacker.example")',
|
||||
resource_details="-1",
|
||||
resource_tags={"=cmd": "x", "env": "prod"},
|
||||
)
|
||||
]
|
||||
|
||||
output = CSV(findings)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
output.batch_write_data_to_file()
|
||||
|
||||
mock_file.seek(0)
|
||||
cells = next(DictReader(mock_file, delimiter=";"))
|
||||
|
||||
assert cells["RESOURCE_NAME"] == '\'=HYPERLINK("http://attacker.example")'
|
||||
assert cells["RESOURCE_DETAILS"] == "-1"
|
||||
assert cells["RESOURCE_TAGS"] == "'=cmd=x | env=prod"
|
||||
|
||||
def test_batch_write_data_to_file_without_findings(self):
|
||||
assert not CSV([])._file_descriptor
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from prowler.lib.outputs.outputs import (
|
||||
from prowler.lib.outputs.utils import (
|
||||
parse_html_string,
|
||||
parse_json_tags,
|
||||
sanitize_csv_value,
|
||||
unroll_dict,
|
||||
unroll_dict_to_list,
|
||||
unroll_list,
|
||||
@@ -1313,3 +1314,20 @@ class TestReport:
|
||||
with mock.patch("builtins.print") as mocked_print:
|
||||
report([finding], provider, output_options)
|
||||
mocked_print.assert_called()
|
||||
|
||||
|
||||
class TestSanitizeCSVValue:
|
||||
@pytest.mark.parametrize("initiator", ["=", "+", "-", "@", "\t", "\r"])
|
||||
def test_leading_formula_initiator_is_prefixed(self, initiator):
|
||||
assert sanitize_csv_value(f"{initiator}cmd") == f"'{initiator}cmd"
|
||||
|
||||
def test_value_containing_equals_is_untouched(self):
|
||||
assert sanitize_csv_value("key=value") == "key=value"
|
||||
|
||||
@pytest.mark.parametrize("number", ["-1", "+1", "-1.5", "-1e3"])
|
||||
def test_signed_number_is_untouched(self, number):
|
||||
assert sanitize_csv_value(number) == number
|
||||
|
||||
@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
|
||||
Reference in new issue
Block a user