mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
514 lines
22 KiB
Python
514 lines
22 KiB
Python
import os
|
|
import subprocess
|
|
import tempfile
|
|
from datetime import datetime
|
|
from time import mktime
|
|
|
|
import pytest
|
|
import yaml
|
|
from mock import patch
|
|
|
|
from prowler.lib.utils.utils import (
|
|
SecretsScanError,
|
|
detect_secrets_scan_batch,
|
|
file_exists,
|
|
get_file_permissions,
|
|
hash_sha512,
|
|
is_owned_by_root,
|
|
open_file,
|
|
outputs_unix_timestamp,
|
|
parse_json_file,
|
|
secrets_rules_path,
|
|
strip_ansi_codes,
|
|
validate_ip_address,
|
|
)
|
|
|
|
|
|
def _fake_kingfisher_run(output_content=None, returncode=0, stderr=""):
|
|
"""Build a ``subprocess.run`` replacement that mimics a Kingfisher call.
|
|
|
|
When ``output_content`` is given it is written to the ``--output`` path from
|
|
the command (so the reader sees realistic file content); the call returns a
|
|
CompletedProcess with the requested ``returncode``/``stderr``.
|
|
"""
|
|
|
|
def _run(command, *_args, **_kwargs):
|
|
if output_content is not None:
|
|
output_path = command[command.index("--output") + 1]
|
|
with open(output_path, "w") as output_file:
|
|
output_file.write(output_content)
|
|
return subprocess.CompletedProcess(
|
|
command, returncode, stdout="", stderr=stderr
|
|
)
|
|
|
|
return _run
|
|
|
|
|
|
def _fake_kingfisher_run_with_findings(findings):
|
|
"""Build a ``subprocess.run`` replacement that emits crafted findings.
|
|
|
|
Each entry in ``findings`` is a ``(payload_index, line)`` pair: the finding
|
|
is mapped back to the temp file named ``str(payload_index)`` (the basename
|
|
``_scan_batch_chunk`` writes per payload) and given the requested ``line``
|
|
value (omitted entirely when ``line`` is the sentinel ``_OMIT``). Returns a
|
|
success exit code so only the finding shape is under test.
|
|
"""
|
|
|
|
def _run(command, *_args, **_kwargs):
|
|
output_path = command[command.index("--output") + 1]
|
|
entries = []
|
|
for payload_index, line in findings:
|
|
finding = {"path": str(payload_index), "snippet": "secret"}
|
|
if line is not _OMIT:
|
|
finding["line"] = line
|
|
entries.append({"finding": finding, "rule": {"name": "Generic Secret"}})
|
|
import json as _json
|
|
|
|
with open(output_path, "w") as output_file:
|
|
output_file.write(_json.dumps({"findings": entries}))
|
|
return subprocess.CompletedProcess(command, 200, stdout="", stderr="")
|
|
|
|
return _run
|
|
|
|
|
|
_OMIT = object()
|
|
|
|
|
|
class Test_detect_secrets_scan_batch_invalid_line:
|
|
"""Kingfisher's ``line`` is consumed as a trusted 1-based index by checks
|
|
(e.g. CloudWatch ``events[line_number - 1]``). A malformed line must fail
|
|
closed as SecretsScanError, never return a finding with a bad index."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"line",
|
|
[_OMIT, None, "2", 0, -1, 5, True],
|
|
ids=["missing", "none", "string", "zero", "negative", "out_of_range", "bool"],
|
|
)
|
|
def test_invalid_line_raises(self, line):
|
|
# Payload "data" is a single line, so any line other than 1 is invalid.
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run_with_findings([(0, line)]),
|
|
):
|
|
with pytest.raises(SecretsScanError) as exc:
|
|
detect_secrets_scan_batch({"a": "data"})
|
|
assert "invalid line number" in str(exc.value)
|
|
|
|
def test_valid_line_is_returned(self):
|
|
# A valid in-range line must still pass through to the caller.
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run_with_findings([(0, 1)]),
|
|
):
|
|
results = detect_secrets_scan_batch({"a": "data"})
|
|
assert results["a"][0]["line_number"] == 1
|
|
|
|
def test_one_invalid_line_aborts_the_whole_scan(self):
|
|
# Even mixed with a valid finding, a single invalid line fails closed.
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run_with_findings([(0, 1), (1, 0)]),
|
|
):
|
|
with pytest.raises(SecretsScanError):
|
|
detect_secrets_scan_batch({"a": "data", "b": "data"})
|
|
|
|
|
|
class Test_utils_open_file:
|
|
def test_open_read_file(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
mode = "r"
|
|
f = open_file(temp_data_file.name, mode)
|
|
assert f.__class__.__name__ == "TextIOWrapper"
|
|
os.remove(temp_data_file.name)
|
|
|
|
def test_open_raise_too_many_open_files(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
mode = "r"
|
|
with patch("prowler.lib.utils.utils.open") as mock_open:
|
|
mock_open.side_effect = OSError(1, "Too many open files")
|
|
with pytest.raises(SystemExit) as exception:
|
|
open_file(temp_data_file.name, mode)
|
|
assert exception.type == SystemExit
|
|
assert exception.value.code == 1
|
|
os.remove(temp_data_file.name)
|
|
|
|
def test_open_raise_os_error(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
mode = "r"
|
|
with patch("prowler.lib.utils.utils.open") as mock_open:
|
|
mock_open.side_effect = OSError(1, "Another OS error")
|
|
with pytest.raises(SystemExit) as exception:
|
|
open_file(temp_data_file.name, mode)
|
|
assert exception.type == SystemExit
|
|
assert exception.value.code == 1
|
|
os.remove(temp_data_file.name)
|
|
|
|
def test_open_raise_exception(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
mode = "r"
|
|
with patch("prowler.lib.utils.utils.open") as mock_open:
|
|
mock_open.side_effect = Exception()
|
|
with pytest.raises(SystemExit) as exception:
|
|
open_file(temp_data_file.name, mode)
|
|
assert exception.type == SystemExit
|
|
assert exception.value.code == 1
|
|
os.remove(temp_data_file.name)
|
|
|
|
|
|
class Test_parse_json_file:
|
|
def test_parse_json_file_invalid(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
with pytest.raises(SystemExit) as exception:
|
|
parse_json_file(temp_data_file)
|
|
|
|
assert exception.type == SystemExit
|
|
assert exception.value.code == 1
|
|
os.remove(temp_data_file.name)
|
|
|
|
def test_parse_json_file_valid(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
temp_data_file.write(b"{}")
|
|
temp_data_file.seek(0)
|
|
f = parse_json_file(temp_data_file)
|
|
assert f == {}
|
|
|
|
|
|
class Test_file_exists:
|
|
def test_file_exists_false(self):
|
|
assert not file_exists("not_existing.txt")
|
|
|
|
def test_file_exists(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
assert file_exists(temp_data_file.name)
|
|
os.remove(temp_data_file.name)
|
|
|
|
def test_file_exists_raised_exception(self):
|
|
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
|
with patch("prowler.lib.utils.utils.exists") as mock_exists:
|
|
mock_exists.side_effect = Exception()
|
|
with pytest.raises(SystemExit) as exception:
|
|
file_exists(temp_data_file.name)
|
|
|
|
assert exception.type == SystemExit
|
|
assert exception.value.code == 1
|
|
|
|
os.remove(temp_data_file.name)
|
|
|
|
|
|
class Test_utils_validate_ip_address:
|
|
def test_validate_ip_address(self):
|
|
assert validate_ip_address("88.26.151.198")
|
|
assert not validate_ip_address("Not an IP")
|
|
|
|
|
|
class Test_detect_secrets_scan_batch:
|
|
def test_batch_returns_findings_per_key(self):
|
|
results = detect_secrets_scan_batch(
|
|
{
|
|
"a": 'password = "Tr0ub4dor3xKq9vLmZ"',
|
|
"b": "just a normal config = value",
|
|
}
|
|
)
|
|
assert "a" in results
|
|
assert results["a"][0]["type"] == "Generic Password"
|
|
# keys without findings are omitted
|
|
assert "b" not in results
|
|
|
|
def test_batch_no_dedup_reports_identical_secret_in_each_key(self):
|
|
# The same secret in two payloads must be reported for both (matches
|
|
# scanning each payload individually).
|
|
secret = "token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
|
results = detect_secrets_scan_batch({"a": secret, "b": secret})
|
|
assert "a" in results
|
|
assert "b" in results
|
|
|
|
def test_batch_excluded_secrets_filters(self):
|
|
results = detect_secrets_scan_batch(
|
|
{"a": 'DB_ALLOW_EMPTY_PASSWORD = "Tr0ub4dor3xKq9vLmZ"'},
|
|
excluded_secrets=[".*ALLOW_EMPTY_PASSWORD.*"],
|
|
)
|
|
assert results == {}
|
|
|
|
@pytest.mark.parametrize("separator", ["\x1c", "\x1d", "\x1e"])
|
|
def test_batch_excluded_secrets_uses_lf_line_numbers(self, separator):
|
|
payload = (
|
|
f'const characterTable = "prefix{separator}suffix";\n'
|
|
'DB_ALLOW_EMPTY_PASSWORD = "Tr0ub4dor3xKq9vLmZ"'
|
|
)
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run_with_findings([(0, 2)]),
|
|
):
|
|
results = detect_secrets_scan_batch(
|
|
{"a": payload},
|
|
excluded_secrets=[".*ALLOW_EMPTY_PASSWORD.*"],
|
|
)
|
|
|
|
assert results == {}
|
|
|
|
def test_batch_chunking_maps_all_keys(self):
|
|
payloads = {f"k{i}": f'password = "S3cr3tV4lu3xy{i}z"' for i in range(5)}
|
|
results = detect_secrets_scan_batch(payloads, chunk_size=2)
|
|
assert sorted(results.keys()) == ["k0", "k1", "k2", "k3", "k4"]
|
|
|
|
def test_batch_empty_payloads(self):
|
|
assert detect_secrets_scan_batch({}) == {}
|
|
|
|
def test_batch_accepts_iterable_of_pairs(self):
|
|
results = detect_secrets_scan_batch(
|
|
iter([("x", 'password = "Tr0ub4dor3xKq9vLmZ"')])
|
|
)
|
|
assert "x" in results
|
|
|
|
|
|
JDBC_RULE = "JDBC connection string with embedded credentials"
|
|
|
|
|
|
class Test_detect_secrets_scan_batch_jdbc:
|
|
"""The bundled override of Kingfisher's built-in ``kingfisher.jdbc.1``.
|
|
|
|
The built-in rule matches a bare ``jdbc:<scheme>:`` prefix followed by any 10
|
|
non-space characters, so every JDBC connection string was reported as an
|
|
embedded credential. The override in
|
|
``prowler/lib/utils/kingfisher_rules/kingfisher_jdbc_1.yaml`` requires an
|
|
actual credential; these tests pin both halves of that behavior.
|
|
"""
|
|
|
|
def _jdbc_findings(self, connection_string):
|
|
results = detect_secrets_scan_batch({"a": connection_string})
|
|
return [f for f in results.get("a", []) if f["type"] == JDBC_RULE]
|
|
|
|
def test_override_keeps_every_non_pattern_field_of_the_builtin(self):
|
|
"""Replacing the built-in rule drops any field the override omits.
|
|
|
|
Losing ``validation`` would silently stop ``--scan-secrets-validate``
|
|
from confirming a JDBC credential is live, and losing
|
|
``pattern_requirements`` would stop placeholder values being discarded —
|
|
neither of which any behavioral test would catch. Only ``pattern`` and
|
|
``examples`` are meant to diverge.
|
|
"""
|
|
with open(
|
|
os.path.join(secrets_rules_path, "kingfisher_jdbc_1.yaml"),
|
|
encoding="utf-8",
|
|
) as f:
|
|
rule = yaml.safe_load(f)["rules"][0]
|
|
|
|
# Verbatim from crates/kingfisher-rules/data/rules/jdbc.yml upstream.
|
|
assert rule["id"] == "kingfisher.jdbc.1"
|
|
assert rule["name"] == JDBC_RULE
|
|
assert rule["confidence"] == "medium"
|
|
assert rule["min_entropy"] == 3.3
|
|
assert rule["validation"] == {"type": "Jdbc"}
|
|
assert rule["tls_mode"] == "lax"
|
|
assert rule["pattern_requirements"] == {
|
|
"min_special_chars": 2,
|
|
"special_chars": ";=/?@&",
|
|
"ignore_if_contains": ["****", "xxxx", "example"],
|
|
}
|
|
assert rule["references"]
|
|
|
|
def test_rules_path_is_passed_to_kingfisher(self):
|
|
"""The override is only in effect if the directory is actually shipped
|
|
and handed to Kingfisher."""
|
|
assert os.path.isdir(secrets_rules_path)
|
|
assert os.path.isfile(
|
|
os.path.join(secrets_rules_path, "kingfisher_jdbc_1.yaml")
|
|
)
|
|
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run(output_content="{}"),
|
|
) as mocked_run:
|
|
detect_secrets_scan_batch({"a": "data"})
|
|
|
|
command = mocked_run.call_args[0][0]
|
|
assert "--rules-path" in command
|
|
assert command[command.index("--rules-path") + 1] == secrets_rules_path
|
|
|
|
@pytest.mark.parametrize(
|
|
"connection_string",
|
|
[
|
|
"jdbc:postgresql://mydb.cluster-abc123.eu-west-1.rds.amazonaws.com:5432/appdb", # trufflehog:ignore
|
|
"jdbc:oracle:thin:@ora.corp.internal:1521/ORCLPDB1", # trufflehog:ignore
|
|
"jdbc:oracle:thin:@//ora.corp.internal:1521/SVC", # trufflehog:ignore
|
|
"jdbc:mysql://prod.internal:3306/inventory?useSSL=true", # trufflehog:ignore
|
|
"jdbc:sqlserver://sql.corp.internal:1433;databaseName=inv;integratedSecurity=true", # trufflehog:ignore
|
|
"jdbc:redshift://cluster.abc.us-east-1.redshift.amazonaws.com:5439/dev", # trufflehog:ignore
|
|
# A username alone is not a credential.
|
|
"jdbc:mysql://prod.internal:3306/inventory?user=admin", # trufflehog:ignore
|
|
# An empty password is not a credential.
|
|
"jdbc:postgresql://pg.corp.internal/app?password=", # trufflehog:ignore
|
|
"jdbc:mysql://(host=db.internal,user=alice,password=)/app", # trufflehog:ignore
|
|
"jdbc:mysql://address=(host=db.internal)(user=alice)(password=)/app", # trufflehog:ignore
|
|
# Connector/J host-list credentials require a non-empty username.
|
|
"jdbc:mysql://(host=db.internal,user=,password=Zq81ncPl42)/app", # trufflehog:ignore
|
|
"jdbc:mysql://address=(host=db.internal)(user=)(password=Zq81ncPl42)/app", # trufflehog:ignore
|
|
# Connector/J host-list syntax must not apply to other drivers.
|
|
"jdbc:postgresql://(host=db.internal,user=alice,password=Zq81ncPl42)/app", # trufflehog:ignore
|
|
# An `@` in the query string must not turn the host and port into
|
|
# `user:password`: without the userinfo alternative being anchored
|
|
# to `//`, `db.internal:3306?user=alice` reads as a credential.
|
|
"jdbc:mysql://db.internal:3306?user=alice@corp.internal", # trufflehog:ignore
|
|
# The same backtrack against the `user/password@` alternative.
|
|
"jdbc:mysql://db.internal:3306?owner=team/ops@corp.internal", # trufflehog:ignore
|
|
"jdbc:mysql://db.internal:3306?path=a:b/c@corp.internal", # trufflehog:ignore
|
|
# And against a `;`-delimited property list.
|
|
"jdbc:sqlserver://sql.corp.internal:1433;user=sa@corp.internal", # trufflehog:ignore
|
|
# `user/password@` is Oracle TNS syntax and a credential only after
|
|
# an Oracle prefix. Every other subprotocol reads `a/b@c` as part of
|
|
# a path or a host, so the alternative must not apply to them.
|
|
"jdbc:derby:team/ops@corp.internal", # trufflehog:ignore
|
|
"jdbc:sqlite:team/ops@corp.internal", # trufflehog:ignore
|
|
"jdbc:h2:file:team/ops@corp.internal", # trufflehog:ignore
|
|
# The exact payload shape of a CloudFormation Output
|
|
# ("OutputKey:OutputValue"), which is how this was reported.
|
|
"DatabaseUrl:jdbc:postgresql://mydb.eu-west-1.rds.amazonaws.com:5432/appdb", # trufflehog:ignore
|
|
],
|
|
)
|
|
def test_credential_free_connection_string_is_not_reported(self, connection_string):
|
|
assert self._jdbc_findings(connection_string) == []
|
|
|
|
@pytest.mark.parametrize(
|
|
"connection_string",
|
|
[
|
|
# URL userinfo.
|
|
"jdbc:mysql://admin:s3cr3t@prod.internal:3306/inventory", # trufflehog:ignore
|
|
# MySQL Connector/J host-list credentials.
|
|
"jdbc:mysql://(host=db.internal,user=alice,password=Zq81ncPl42)/app", # trufflehog:ignore
|
|
"jdbc:mysql://address=(host=db.internal)(user=alice)(password=Zq81ncPl42)/app", # trufflehog:ignore
|
|
# Password as a query parameter.
|
|
"jdbc:postgresql://pg.corp.internal:5432/app?user=admin&password=Tr0ub4dor3", # trufflehog:ignore
|
|
"jdbc:postgresql://pg.corp.internal/app?password=Xk29fjWa02", # trufflehog:ignore
|
|
"jdbc:mysql://prod.internal/db?user=a&pwd=Zq81ncPl42", # trufflehog:ignore
|
|
# Password as a semicolon-delimited property.
|
|
"jdbc:sqlserver://sql.corp.internal:1433;databaseName=inv;user=sa;password=S3cr3t99", # trufflehog:ignore
|
|
"jdbc:sqlserver://sql.corp.internal:1433;Password=Vb73msQr18;user=sa", # trufflehog:ignore
|
|
# Oracle TNS userinfo, for each driver type.
|
|
"jdbc:oracle:thin:scott/tiger99@ora.corp.internal:1521:ORCL", # trufflehog:ignore
|
|
"jdbc:oracle:oci:scott/tiger99@ora.corp.internal:1521:ORCL", # trufflehog:ignore
|
|
# Two-character scheme, which the built-in pattern could not match.
|
|
"jdbc:h2:file:./data/store;CIPHER=AES;PASSWORD=Nf62kdTp07", # trufflehog:ignore
|
|
],
|
|
)
|
|
def test_embedded_credential_is_still_reported(self, connection_string):
|
|
assert self._jdbc_findings(connection_string) != []
|
|
|
|
|
|
class Test_detect_secrets_scan_batch_failures:
|
|
"""A scanner failure must surface as SecretsScanError, never as empty
|
|
results (which a caller would read as 'no secrets found')."""
|
|
|
|
def test_non_zero_exit_code_raises(self):
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run(returncode=1, stderr="boom"),
|
|
):
|
|
with pytest.raises(SecretsScanError) as exc:
|
|
detect_secrets_scan_batch({"a": "data"})
|
|
assert "exited with code 1" in str(exc.value)
|
|
assert "boom" in str(exc.value)
|
|
|
|
def test_timeout_raises(self):
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=subprocess.TimeoutExpired(cmd="kingfisher", timeout=300),
|
|
):
|
|
with pytest.raises(SecretsScanError) as exc:
|
|
detect_secrets_scan_batch({"a": "data"})
|
|
assert "timed out" in str(exc.value)
|
|
|
|
def test_malformed_json_output_raises(self):
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run(
|
|
output_content="{not valid json", returncode=0
|
|
),
|
|
):
|
|
with pytest.raises(SecretsScanError):
|
|
detect_secrets_scan_batch({"a": "data"})
|
|
|
|
def test_missing_binary_raises(self):
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=FileNotFoundError("kingfisher binary not found"),
|
|
):
|
|
with pytest.raises(SecretsScanError):
|
|
detect_secrets_scan_batch({"a": "data"})
|
|
|
|
def test_empty_output_is_not_a_failure(self):
|
|
# Empty output means the scan ran and found nothing; it must NOT raise.
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run(output_content="", returncode=0),
|
|
):
|
|
assert detect_secrets_scan_batch({"a": "data"}) == {}
|
|
|
|
def test_failure_in_any_chunk_aborts_the_whole_scan(self):
|
|
# A failure in any chunk must abort the whole scan, not silently return
|
|
# partial results from the chunks that happened to succeed first.
|
|
payloads = {f"k{i}": "data" for i in range(4)}
|
|
with patch(
|
|
"prowler.lib.utils.utils.subprocess.run",
|
|
side_effect=_fake_kingfisher_run(returncode=2, stderr="boom"),
|
|
):
|
|
with pytest.raises(SecretsScanError):
|
|
detect_secrets_scan_batch(payloads, chunk_size=2)
|
|
|
|
|
|
class Test_hash_sha512:
|
|
def test_hash_sha512(self):
|
|
assert hash_sha512("test") == "ee26b0dd4"
|
|
|
|
|
|
class Test_outputs_unix_timestamp:
|
|
def test_outputs_unix_timestamp_false(self):
|
|
time = datetime.now()
|
|
assert outputs_unix_timestamp(False, time) == time.isoformat()
|
|
|
|
def test_outputs_unix_timestamp_true(self):
|
|
time = datetime.now()
|
|
assert outputs_unix_timestamp(True, time) == mktime(time.timetuple())
|
|
|
|
|
|
class TestFilePermissions:
|
|
def test_get_file_permissions(self):
|
|
# Create a temporary file with known permissions
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
temp_file.close()
|
|
os.chmod(temp_file.name, 0o644) # Set permissions to 644 (-rw-r--r--)
|
|
permissions = get_file_permissions(temp_file.name)
|
|
assert permissions == "0o644"
|
|
os.unlink(temp_file.name)
|
|
assert not get_file_permissions("not_existing_file")
|
|
|
|
def test_is_owned_by_root(self):
|
|
# Create a temporary file with known permissions
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
temp_file.close()
|
|
os.chmod(temp_file.name, 0o644) # Set permissions to 644 (-rw-r--r--)
|
|
# Check ownership for the temporary file
|
|
assert not is_owned_by_root(temp_file.name)
|
|
os.unlink(temp_file.name)
|
|
|
|
assert not is_owned_by_root("not_existing_file")
|
|
# Not valid for darwin systems
|
|
# assert is_owned_by_root("/etc/passwd")
|
|
|
|
|
|
class TestStripAnsiCodes:
|
|
def test_strip_ansi_codes_no_alteration(self):
|
|
input_string = "\x1b[31mHello\x1b[0m World"
|
|
expected_output = "Hello World"
|
|
|
|
actual_output = strip_ansi_codes(input_string)
|
|
|
|
assert actual_output == expected_output
|
|
|
|
def test_strip_ansi_codes_empty_string(self):
|
|
input_string = ""
|
|
expected_output = ""
|
|
|
|
actual_output = strip_ansi_codes(input_string)
|
|
|
|
assert actual_output == expected_output
|