mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(sdk): replace detect-secrets library with kingfisher (#11694)
This commit is contained in:
committed by
GitHub
parent
ed1fec8866
commit
5dac8a0a53
@@ -129,47 +129,6 @@ class Test_AWS_Enums:
|
||||
assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == {}
|
||||
|
||||
|
||||
class Test_AWS_Detect_Secrets_Plugins:
|
||||
def test_plugin_without_limit(self):
|
||||
out = _validate({"detect_secrets_plugins": [{"name": "AWSKeyDetector"}]})
|
||||
assert out == {"detect_secrets_plugins": [{"name": "AWSKeyDetector"}]}
|
||||
|
||||
def test_plugin_with_limit(self):
|
||||
out = _validate(
|
||||
{
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Base64HighEntropyString", "limit": 6.0}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert out == {
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Base64HighEntropyString", "limit": 6.0}
|
||||
]
|
||||
}
|
||||
|
||||
def test_plugin_missing_name_drops_whole_field(self):
|
||||
# ``name`` is required by the upstream library.
|
||||
out = _validate({"detect_secrets_plugins": [{"limit": 6.0}]})
|
||||
assert out == {}
|
||||
|
||||
def test_extra_plugin_kwargs_pass_through(self):
|
||||
# Plugins can have arbitrary extra params (extra="allow" on the
|
||||
# nested model). They must round-trip.
|
||||
out = _validate(
|
||||
{
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Custom", "my_param": "abc", "other": 42}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert out == {
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Custom", "my_param": "abc", "other": 42}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class Test_AWS_Booleans:
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
@@ -214,9 +173,5 @@ class Test_AWS_Full_Default_Config_Round_Trips:
|
||||
"threat_detection_enumeration_threshold": 0.3,
|
||||
"threat_detection_llm_jacking_threshold": 0.4,
|
||||
"ec2_high_risk_ports": [25, 110, 8088],
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "AWSKeyDetector"},
|
||||
{"name": "Base64HighEntropyString", "limit": 6.0},
|
||||
],
|
||||
}
|
||||
assert _validate(raw) == raw
|
||||
|
||||
@@ -342,38 +342,6 @@ class TestTrustedIpsValidator:
|
||||
assert _has_error_for(errors, "aws.trusted_ips")
|
||||
|
||||
|
||||
class TestDetectSecretsEntropyBound:
|
||||
"""`detect_secrets_plugins[].limit` is Shannon entropy: 0..10."""
|
||||
|
||||
@pytest.mark.parametrize("value", [0.0, 3.5, 4.5, 8.0, 10.0])
|
||||
def test_valid(self, value):
|
||||
assert (
|
||||
validate_scan_config(
|
||||
{
|
||||
"aws": {
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Base64HighEntropyString", "limit": value}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("value", [-0.1, 10.01, 50])
|
||||
def test_invalid(self, value):
|
||||
errors = validate_scan_config(
|
||||
{
|
||||
"aws": {
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Base64HighEntropyString", "limit": value}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
assert _has_error_for(errors, "aws.detect_secrets_plugins")
|
||||
|
||||
|
||||
class TestAdapterRobustness:
|
||||
"""Top-level adapter behaviour the Prowler App backend depends on."""
|
||||
|
||||
|
||||
@@ -193,21 +193,8 @@ class Test_OpenStack_Schema:
|
||||
raw = {
|
||||
"image_sharing_threshold": 5,
|
||||
"secrets_ignore_patterns": ["AKIA[0-9A-Z]{16}"],
|
||||
"detect_secrets_plugins": [
|
||||
{"name": "Base64HighEntropyString", "limit": 4.5}
|
||||
],
|
||||
}
|
||||
assert _validate("openstack", raw) == raw
|
||||
|
||||
def test_zero_threshold_dropped(self):
|
||||
assert _validate("openstack", {"image_sharing_threshold": 0}) == {}
|
||||
|
||||
def test_invalid_plugin_entropy_dropped(self):
|
||||
# Reuses the AWS _DetectSecretsPlugin entropy bound (0..10).
|
||||
assert (
|
||||
_validate(
|
||||
"openstack",
|
||||
{"detect_secrets_plugins": [{"name": "X", "limit": 50}]},
|
||||
)
|
||||
== {}
|
||||
)
|
||||
|
||||
+187
-63
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from time import mktime
|
||||
@@ -7,7 +8,8 @@ import pytest
|
||||
from mock import patch
|
||||
|
||||
from prowler.lib.utils.utils import (
|
||||
detect_secrets_scan,
|
||||
SecretsScanError,
|
||||
detect_secrets_scan_batch,
|
||||
file_exists,
|
||||
get_file_permissions,
|
||||
hash_sha512,
|
||||
@@ -20,6 +22,95 @@ from prowler.lib.utils.utils import (
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@@ -108,75 +199,108 @@ class Test_utils_validate_ip_address:
|
||||
assert not validate_ip_address("Not an IP")
|
||||
|
||||
|
||||
class Test_detect_secrets_scan:
|
||||
def test_detect_secrets_scan_data(self):
|
||||
data = "password=password"
|
||||
secrets_detected = detect_secrets_scan(data=data, excluded_secrets=[])
|
||||
assert type(secrets_detected) is list
|
||||
assert len(secrets_detected) == 1
|
||||
assert "filename" in secrets_detected[0]
|
||||
assert "hashed_secret" in secrets_detected[0]
|
||||
assert "is_verified" in secrets_detected[0]
|
||||
assert secrets_detected[0]["line_number"] == 1
|
||||
assert secrets_detected[0]["type"] == "Secret Keyword"
|
||||
|
||||
def test_detect_secrets_scan_no_secrets_data(self):
|
||||
data = ""
|
||||
assert detect_secrets_scan(data=data) is None
|
||||
|
||||
def test_detect_secrets_scan_file_with_secrets(self):
|
||||
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_data_file.write(b"password=password")
|
||||
temp_data_file.seek(0)
|
||||
secrets_detected = detect_secrets_scan(
|
||||
file=temp_data_file.name, excluded_secrets=[]
|
||||
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 type(secrets_detected) is list
|
||||
assert len(secrets_detected) == 1
|
||||
assert "filename" in secrets_detected[0]
|
||||
assert "hashed_secret" in secrets_detected[0]
|
||||
assert "is_verified" in secrets_detected[0]
|
||||
assert secrets_detected[0]["line_number"] == 1
|
||||
assert secrets_detected[0]["type"] == "Secret Keyword"
|
||||
os.remove(temp_data_file.name)
|
||||
assert "a" in results
|
||||
assert results["a"][0]["type"] == "Generic Password"
|
||||
# keys without findings are omitted
|
||||
assert "b" not in results
|
||||
|
||||
def test_detect_secrets_scan_file_no_secrets(self):
|
||||
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_data_file.write(b"no secrets")
|
||||
temp_data_file.seek(0)
|
||||
assert detect_secrets_scan(file=temp_data_file.name) is None
|
||||
os.remove(temp_data_file.name)
|
||||
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_detect_secrets_using_regex(self):
|
||||
data = "MYSQL_ALLOW_EMPTY_PASSWORD=password"
|
||||
secrets_detected = detect_secrets_scan(
|
||||
data=data, excluded_secrets=[".*password"]
|
||||
def test_batch_excluded_secrets_filters(self):
|
||||
results = detect_secrets_scan_batch(
|
||||
{"a": 'DB_ALLOW_EMPTY_PASSWORD = "Tr0ub4dor3xKq9vLmZ"'},
|
||||
excluded_secrets=[".*ALLOW_EMPTY_PASSWORD.*"],
|
||||
)
|
||||
assert secrets_detected is None
|
||||
assert results == {}
|
||||
|
||||
def test_detect_secrets_using_regex_file(self):
|
||||
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_data_file.write(b"MYSQL_ALLOW_EMPTY_PASSWORD=password")
|
||||
temp_data_file.seek(0)
|
||||
secrets_detected = detect_secrets_scan(
|
||||
file=temp_data_file.name, excluded_secrets=[".*password"]
|
||||
)
|
||||
assert secrets_detected is None
|
||||
os.remove(temp_data_file.name)
|
||||
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_detect_secrets_secrets_using_regex(self):
|
||||
data = "MYSQL_ALLOW_EMPTY_PASSWORD=password, MYSQL_PASSWORD=password"
|
||||
# Update the regex to exclude only the exact key "MYSQL_ALLOW_EMPTY_PASSWORD"
|
||||
secrets_detected = detect_secrets_scan(
|
||||
data=data, excluded_secrets=["^MYSQL_ALLOW_EMPTY_PASSWORD$"]
|
||||
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 type(secrets_detected) is list
|
||||
assert len(secrets_detected) == 1
|
||||
assert "filename" in secrets_detected[0]
|
||||
assert "hashed_secret" in secrets_detected[0]
|
||||
assert "is_verified" in secrets_detected[0]
|
||||
assert secrets_detected[0]["line_number"] == 1
|
||||
assert secrets_detected[0]["type"] == "Secret Keyword"
|
||||
assert "x" in results
|
||||
|
||||
|
||||
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:
|
||||
|
||||
+7
-3
@@ -104,7 +104,7 @@ class Test_autoscaling_find_secrets_ec2_launch_configuration:
|
||||
InstanceType="t1.micro",
|
||||
KeyName="the_keys",
|
||||
SecurityGroups=["default", "default2"],
|
||||
UserData="DB_PASSWORD=foobar123",
|
||||
UserData='DB_PASSWORD="Tr0ub4dor3xKq9vLmZ"',
|
||||
)
|
||||
launch_configuration_arn = autoscaling_client.describe_launch_configurations(
|
||||
LaunchConfigurationNames=[launch_configuration_name]
|
||||
@@ -341,7 +341,9 @@ class Test_autoscaling_find_secrets_ec2_launch_configuration:
|
||||
check = autoscaling_find_secrets_ec2_launch_configuration()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not decode User Data" in result[0].status_extended
|
||||
|
||||
@mock_aws
|
||||
def test_one_autoscaling_file_invalid_gzip_error(self):
|
||||
@@ -381,4 +383,6 @@ class Test_autoscaling_find_secrets_ec2_launch_configuration:
|
||||
check = autoscaling_find_secrets_ec2_launch_configuration()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not decode User Data" in result[0].status_extended
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
DB_PASSWORD=foobar123
|
||||
DB_PASSWORD="Tr0ub4dor3xKq9vLmZ"
|
||||
DB_USER=foo
|
||||
API_KEY=12345abcd
|
||||
SERVICE_PASSWORD=bbaabb45
|
||||
API_KEY=s3rv1c3Acc0untS3cr3tV4lu3x9
|
||||
SERVICE_PASSWORD="Xy9zPq2wKmRtVbN4"
|
||||
|
||||
BIN
Binary file not shown.
+34
-2
@@ -19,7 +19,7 @@ LAMBDA_FUNCTION_RUNTIME = "nodejs4.3"
|
||||
LAMBDA_FUNCTION_ARN = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{LAMBDA_FUNCTION_NAME}"
|
||||
LAMBDA_FUNCTION_CODE_WITH_SECRETS = """
|
||||
def lambda_handler(event, context):
|
||||
db_password = "test-password"
|
||||
db_password = "Tr0ub4dor3xKq9vLmZ"
|
||||
print("custom log event")
|
||||
return event
|
||||
"""
|
||||
@@ -126,7 +126,7 @@ class Test_awslambda_function_no_secrets_in_code:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in Lambda function {LAMBDA_FUNCTION_NAME} code -> lambda_function.py: Secret Keyword on line 3."
|
||||
== f"Potential secret found in Lambda function {LAMBDA_FUNCTION_NAME} code -> lambda_function.py: Generic Password on line 3."
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
@@ -201,3 +201,35 @@ class Test_awslambda_function_no_secrets_in_code:
|
||||
== f"No secrets found in Lambda function {LAMBDA_FUNCTION_NAME} code."
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_scan_failure_reports_manual_not_pass(self):
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
|
||||
lambda_client = mock.MagicMock
|
||||
lambda_client.functions = {LAMBDA_FUNCTION_ARN: create_lambda_function()}
|
||||
lambda_client._get_function_code = mock_get_function_codewith_secrets
|
||||
lambda_client.audit_config = {"secrets_ignore_patterns": []}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_code.awslambda_function_no_secrets_in_code.awslambda_client",
|
||||
new=lambda_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_code.awslambda_function_no_secrets_in_code.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher exited with code 1"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_code.awslambda_function_no_secrets_in_code import (
|
||||
awslambda_function_no_secrets_in_code,
|
||||
)
|
||||
|
||||
check = awslambda_function_no_secrets_in_code()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not scan" in result[0].status_extended
|
||||
|
||||
+142
-16
@@ -97,7 +97,7 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
arn=function_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
runtime=function_runtime,
|
||||
environment={"db_password": "test-password"},
|
||||
environment={"db_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in Lambda function {function_name} variables -> Secret Keyword in variable db_password."
|
||||
== f"Potential secret found in Lambda function {function_name} variables -> Generic Password in variable db_password."
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
@@ -145,7 +145,69 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
arn=function_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
runtime=function_runtime,
|
||||
environment={"db_password": "srv://admin:pass@db"},
|
||||
environment={
|
||||
"db_password": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.awslambda_client",
|
||||
new=lambda_client,
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables import (
|
||||
awslambda_function_no_secrets_in_variables,
|
||||
)
|
||||
|
||||
check = awslambda_function_no_secrets_in_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == function_name
|
||||
assert result[0].resource_arn == function_arn
|
||||
assert result[0].status == "FAIL"
|
||||
# Kingfisher reports both the generic keyword rule and the JWT rule
|
||||
# for the same value; their order is not guaranteed, so assert on
|
||||
# presence rather than a fixed concatenation order.
|
||||
assert result[0].status_extended.startswith(
|
||||
f"Potential secret found in Lambda function {function_name} variables -> "
|
||||
)
|
||||
assert (
|
||||
"Generic Password in variable db_password" in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"JSON Web Token (base64url-encoded) in variable db_password"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_function_secrets_in_variables_telegram_token(self):
|
||||
lambda_client = mock.MagicMock
|
||||
function_name = "test-lambda"
|
||||
function_runtime = "nodejs4.3"
|
||||
function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}"
|
||||
lambda_client.audit_config = {"secrets_ignore_patterns": []}
|
||||
lambda_client.functions = {
|
||||
"function_name": Function(
|
||||
name=function_name,
|
||||
security_groups=[],
|
||||
arn=function_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
runtime=function_runtime,
|
||||
environment={
|
||||
# The Telegram bot-token rule is no longer enabled in
|
||||
# Kingfisher's built-in ruleset, so a detectable JWT
|
||||
# is used to keep this token-in-variable case meaningful.
|
||||
"TELEGRAM_BOT_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,16 +236,22 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in Lambda function {function_name} variables -> Secret Keyword in variable db_password, Basic Auth Credentials in variable db_password."
|
||||
== f"Potential secret found in Lambda function {function_name} variables -> JSON Web Token (base64url-encoded) in variable TELEGRAM_BOT_TOKEN."
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_function_secrets_in_variables_telegram_token(self):
|
||||
def test_function_with_verified_secret(self):
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
lambda_client = mock.MagicMock
|
||||
function_name = "test-lambda"
|
||||
function_runtime = "nodejs4.3"
|
||||
function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}"
|
||||
lambda_client.audit_config = {"secrets_ignore_patterns": []}
|
||||
lambda_client.audit_config = {
|
||||
"secrets_ignore_patterns": [],
|
||||
"secrets_validate": True,
|
||||
}
|
||||
|
||||
lambda_client.functions = {
|
||||
"function_name": Function(
|
||||
name=function_name,
|
||||
@@ -191,19 +259,35 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
arn=function_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
runtime=function_runtime,
|
||||
environment={"TELEGRAM_BOT_TOKEN": "telegram-token"},
|
||||
environment={"db_password": "test-value"},
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(),
|
||||
return_value=set_mocked_aws_provider(
|
||||
audit_config={"secrets_validate": True}
|
||||
),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.awslambda_client",
|
||||
new=lambda_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
0: [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables import (
|
||||
@@ -213,16 +297,13 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
check = awslambda_function_no_secrets_in_variables()
|
||||
result = check.execute()
|
||||
|
||||
# The check must forward secrets_validate from the config to the scan.
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
assert len(result) == 1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert result[0].resource_id == function_name
|
||||
assert result[0].resource_arn == function_arn
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"No secrets found in Lambda function {function_name} variables."
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_function_no_secrets_in_variables(self):
|
||||
lambda_client = mock.MagicMock
|
||||
@@ -270,3 +351,48 @@ class Test_awslambda_function_no_secrets_in_variables:
|
||||
== f"No secrets found in Lambda function {function_name} variables."
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_scan_failure_reports_manual_not_pass(self):
|
||||
# A scanner failure must not be treated as "no secrets found".
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
|
||||
lambda_client = mock.MagicMock
|
||||
function_name = "test-lambda"
|
||||
function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}"
|
||||
lambda_client.audit_config = {"secrets_ignore_patterns": []}
|
||||
lambda_client.functions = {
|
||||
"function_name": Function(
|
||||
name=function_name,
|
||||
security_groups=[],
|
||||
arn=function_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
runtime="nodejs4.3",
|
||||
environment={"db_password": "test-value"},
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.awslambda_client",
|
||||
new=lambda_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher exited with code 1"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables import (
|
||||
awslambda_function_no_secrets_in_variables,
|
||||
)
|
||||
|
||||
check = awslambda_function_no_secrets_in_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not scan" in result[0].status_extended
|
||||
assert "manual review is required" in result[0].status_extended
|
||||
|
||||
+5
-2
@@ -38,7 +38,10 @@ class Test_cloudformation_stack_outputs_find_secrets:
|
||||
Stack(
|
||||
arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60",
|
||||
name=stack_name,
|
||||
outputs=["DB_PASSWORD:foobar123", "ENV:DEV"],
|
||||
outputs=[
|
||||
"DB_KEY:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"ENV:DEV",
|
||||
],
|
||||
region=AWS_REGION,
|
||||
)
|
||||
]
|
||||
@@ -66,7 +69,7 @@ class Test_cloudformation_stack_outputs_find_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in CloudFormation Stack {stack_name} Outputs -> Secret Keyword in Output 1."
|
||||
== f"Potential secret found in CloudFormation Stack {stack_name} Outputs -> JSON Web Token (base64url-encoded) in Output 1."
|
||||
)
|
||||
assert result[0].resource_id == "Test-Stack"
|
||||
assert (
|
||||
|
||||
+322
-2
@@ -132,7 +132,7 @@ class Test_cloudwatch_log_group_no_secrets_in_logs:
|
||||
logEvents=[
|
||||
{
|
||||
"timestamp": timestamp,
|
||||
"message": "password = password123",
|
||||
"message": 'password = "Tr0ub4dor3xKq9vLmZ"',
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -174,7 +174,7 @@ class Test_cloudwatch_log_group_no_secrets_in_logs:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in log group test in log stream test stream at {dttimestamp} - Secret Keyword on line 1."
|
||||
== f"Potential secrets found in log group test in log stream test stream at {dttimestamp} - Generic Password on line 1."
|
||||
)
|
||||
assert result[0].resource_id == "test"
|
||||
assert (
|
||||
@@ -222,3 +222,323 @@ class Test_cloudwatch_log_group_no_secrets_in_logs:
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_cloudwatch_multiline_event_all_secrets_ignored_is_pass(self):
|
||||
# Regression: a multiline event whose secrets are all dropped by the
|
||||
# rescan (e.g. filtered by secrets_ignore_patterns) must NOT produce a
|
||||
# FAIL with no actual secret evidence.
|
||||
logs_client = client("logs", region_name=AWS_REGION_US_EAST_1)
|
||||
logs_client.create_log_group(logGroupName="test", tags={"test": "test"})
|
||||
logs_client.create_log_stream(logGroupName="test", logStreamName="test stream")
|
||||
logs_client.put_log_events(
|
||||
logGroupName="test",
|
||||
logStreamName="test stream",
|
||||
logEvents=[
|
||||
{
|
||||
"timestamp": timestamp,
|
||||
# Valid JSON so the rescan expands it to multiple lines.
|
||||
"message": '{"api_key": "AKIAIOSFODNN7EXAMPLE", "note": "x"}',
|
||||
}
|
||||
],
|
||||
)
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
|
||||
)
|
||||
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
aws_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=["cloudwatch_log_group_no_secrets_in_logs"],
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.logs_client",
|
||||
new=Logs(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.detect_secrets_scan_batch",
|
||||
side_effect=[
|
||||
# Phase 1: stream flagged on its single (multiline) event.
|
||||
{
|
||||
("test", "test stream"): [
|
||||
{
|
||||
"type": "AWS Access Key",
|
||||
"line_number": 1,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": False,
|
||||
}
|
||||
]
|
||||
},
|
||||
# Phase 3: rescan drops everything (all secrets ignored).
|
||||
{},
|
||||
],
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs import (
|
||||
cloudwatch_log_group_no_secrets_in_logs,
|
||||
)
|
||||
|
||||
check = cloudwatch_log_group_no_secrets_in_logs()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == "No secrets found in test log group."
|
||||
|
||||
@mock_aws
|
||||
def test_cloudwatch_scan_failure_reports_manual(self):
|
||||
# A scanner failure on the stream scan must surface as MANUAL, not PASS.
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
|
||||
logs_client = client("logs", region_name=AWS_REGION_US_EAST_1)
|
||||
logs_client.create_log_group(logGroupName="test", tags={"test": "test"})
|
||||
logs_client.create_log_stream(logGroupName="test", logStreamName="test stream")
|
||||
logs_client.put_log_events(
|
||||
logGroupName="test",
|
||||
logStreamName="test stream",
|
||||
logEvents=[{"timestamp": timestamp, "message": "some log line"}],
|
||||
)
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
|
||||
)
|
||||
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
aws_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=["cloudwatch_log_group_no_secrets_in_logs"],
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.logs_client",
|
||||
new=Logs(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher exited with code 1"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs import (
|
||||
cloudwatch_log_group_no_secrets_in_logs,
|
||||
)
|
||||
|
||||
check = cloudwatch_log_group_no_secrets_in_logs()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not scan" in result[0].status_extended
|
||||
|
||||
@mock_aws
|
||||
def test_two_multiline_events_same_timestamp_do_not_collide(self):
|
||||
# Regression: a CloudWatch stream can hold several events sharing one
|
||||
# millisecond timestamp. The multiline rescan must be keyed per event
|
||||
# (not only per timestamp), otherwise the later event's payload
|
||||
# overwrites the earlier one and secret evidence is lost.
|
||||
log_group_arn = (
|
||||
f"arn:aws:logs:{AWS_REGION_US_EAST_1}:123456789012:log-group:test:*"
|
||||
)
|
||||
logs_client = client("logs", region_name=AWS_REGION_US_EAST_1)
|
||||
logs_client.create_log_group(logGroupName="test", tags={"test": "test"})
|
||||
logs_client.create_log_stream(logGroupName="test", logStreamName="test stream")
|
||||
# Two distinct multiline (valid JSON) events at the same timestamp.
|
||||
logs_client.put_log_events(
|
||||
logGroupName="test",
|
||||
logStreamName="test stream",
|
||||
logEvents=[
|
||||
{
|
||||
"timestamp": timestamp,
|
||||
"message": '{"api_key": "AKIAIOSFODNN7EXAMPLE", "note": "a"}',
|
||||
},
|
||||
{
|
||||
"timestamp": timestamp,
|
||||
"message": '{"secret": "AKIAI44QH8DHBEXAMPLE", "note": "b"}',
|
||||
},
|
||||
],
|
||||
)
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
|
||||
)
|
||||
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
aws_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=["cloudwatch_log_group_no_secrets_in_logs"],
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.logs_client",
|
||||
new=Logs(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.detect_secrets_scan_batch",
|
||||
side_effect=[
|
||||
# Phase 1: both events flagged (one secret on each line).
|
||||
{
|
||||
(log_group_arn, "test stream"): [
|
||||
{
|
||||
"type": "AWS Access Key",
|
||||
"line_number": 1,
|
||||
"filename": "data",
|
||||
"hashed_secret": "a",
|
||||
"is_verified": False,
|
||||
},
|
||||
{
|
||||
"type": "AWS Access Key",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "b",
|
||||
"is_verified": False,
|
||||
},
|
||||
]
|
||||
},
|
||||
# Phase 3: each event is rescanned under its own key. If the
|
||||
# keys collided, only one of these would survive.
|
||||
{
|
||||
(
|
||||
log_group_arn,
|
||||
"test stream",
|
||||
dttimestamp,
|
||||
0,
|
||||
): [
|
||||
{
|
||||
"type": "AWS Access Key",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "a",
|
||||
"is_verified": False,
|
||||
}
|
||||
],
|
||||
(
|
||||
log_group_arn,
|
||||
"test stream",
|
||||
dttimestamp,
|
||||
1,
|
||||
): [
|
||||
{
|
||||
"type": "AWS Access Key",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "b",
|
||||
"is_verified": False,
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs import (
|
||||
cloudwatch_log_group_no_secrets_in_logs,
|
||||
)
|
||||
|
||||
check = cloudwatch_log_group_no_secrets_in_logs()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
# Both events' secrets must be reported, not just the last one.
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in log group test in log stream test stream at {dttimestamp} - AWS Access Key on line 2."
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_same_group_and_stream_names_in_two_regions_do_not_collide(self):
|
||||
# Regression: log group and stream names are not unique across regions,
|
||||
# so the per-stream key must be region-aware (ARN-based). Otherwise the
|
||||
# secret found in one region would be reused for the same-named group in
|
||||
# another region, producing a false FAIL.
|
||||
group_name = "shared-name"
|
||||
stream_name = "shared stream"
|
||||
|
||||
us_client = client("logs", region_name=AWS_REGION_US_EAST_1)
|
||||
us_client.create_log_group(logGroupName=group_name)
|
||||
us_client.create_log_stream(logGroupName=group_name, logStreamName=stream_name)
|
||||
us_client.put_log_events(
|
||||
logGroupName=group_name,
|
||||
logStreamName=stream_name,
|
||||
logEvents=[
|
||||
{
|
||||
"timestamp": timestamp,
|
||||
"message": 'password = "Tr0ub4dor3xKq9vLmZ"',
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
eu_client = client("logs", region_name=AWS_REGION_EU_WEST_1)
|
||||
eu_client.create_log_group(logGroupName=group_name)
|
||||
eu_client.create_log_stream(logGroupName=group_name, logStreamName=stream_name)
|
||||
eu_client.put_log_events(
|
||||
logGroupName=group_name,
|
||||
logStreamName=stream_name,
|
||||
logEvents=[{"timestamp": timestamp, "message": "just a normal log line"}],
|
||||
)
|
||||
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
|
||||
)
|
||||
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
aws_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=["cloudwatch_log_group_no_secrets_in_logs"],
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs.logs_client",
|
||||
new=Logs(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_secrets_in_logs.cloudwatch_log_group_no_secrets_in_logs import (
|
||||
cloudwatch_log_group_no_secrets_in_logs,
|
||||
)
|
||||
|
||||
check = cloudwatch_log_group_no_secrets_in_logs()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
by_region = {report.region: report for report in result}
|
||||
# Only the region with the real secret must FAIL.
|
||||
assert by_region[AWS_REGION_US_EAST_1].status == "FAIL"
|
||||
assert by_region[AWS_REGION_EU_WEST_1].status == "PASS"
|
||||
|
||||
+187
-14
@@ -1,6 +1,10 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_codebuild_project_no_secrets_in_variables:
|
||||
@@ -202,7 +206,11 @@ class Test_codebuild_project_no_secrets_in_variables:
|
||||
environment_variables=[
|
||||
{
|
||||
"name": "AWS_ACCESS_KEY_ID",
|
||||
"value": "AKIAIOSFODNN7EXAMPLE",
|
||||
# Realistic fake secret that Kingfisher detects. The classic
|
||||
# "AKIAIOSFODNN7EXAMPLE" placeholder is suppressed by
|
||||
# Kingfisher and its AWS Access Key rule is not enabled, so a
|
||||
# detectable provider secret is used instead.
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"type": "PLAINTEXT",
|
||||
}
|
||||
],
|
||||
@@ -231,15 +239,100 @@ class Test_codebuild_project_no_secrets_in_variables:
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
# The JWT paired with a "KEY" variable name yields both a
|
||||
# JWT and a Generic API Key finding; order is non-deterministic.
|
||||
assert result[0].status_extended.startswith(
|
||||
"CodeBuild project SensitiveProject has sensitive environment plaintext credentials in variables:"
|
||||
)
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "CodeBuild project SensitiveProject has sensitive environment plaintext credentials in variables: AWS Access Key in variable AWS_ACCESS_KEY_ID."
|
||||
"JSON Web Token (base64url-encoded) in variable AWS_ACCESS_KEY_ID"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"Generic API Key in variable AWS_ACCESS_KEY_ID"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "SensitiveProject"
|
||||
assert result[0].resource_arn == project_arn
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_project_with_verified_secret(self):
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
codebuild_client = mock.MagicMock()
|
||||
|
||||
from prowler.providers.aws.services.codebuild.codebuild_service import Project
|
||||
|
||||
project_arn = f"arn:aws:codebuild:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:project/SensitiveProject"
|
||||
codebuild_client.projects = {
|
||||
project_arn: Project(
|
||||
name="SensitiveProject",
|
||||
arn=project_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
last_invoked_time=None,
|
||||
buildspec=None,
|
||||
environment_variables=[
|
||||
{
|
||||
"name": "EXAMPLE_VAR",
|
||||
"value": "ExampleValue",
|
||||
"type": "PLAINTEXT",
|
||||
}
|
||||
],
|
||||
tags=[],
|
||||
)
|
||||
}
|
||||
|
||||
codebuild_client.audit_config = {
|
||||
"excluded_sensitive_environment_variables": [],
|
||||
"secrets_validate": True,
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(
|
||||
audit_config={"secrets_validate": True}
|
||||
),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_service.Codebuild",
|
||||
codebuild_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_no_secrets_in_variables.codebuild_project_no_secrets_in_variables.codebuild_client",
|
||||
codebuild_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_no_secrets_in_variables.codebuild_project_no_secrets_in_variables.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
(0, 0): [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 1,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
from prowler.providers.aws.services.codebuild.codebuild_project_no_secrets_in_variables.codebuild_project_no_secrets_in_variables import (
|
||||
codebuild_project_no_secrets_in_variables,
|
||||
)
|
||||
|
||||
check = codebuild_project_no_secrets_in_variables()
|
||||
result = check.execute()
|
||||
|
||||
# The check must forward secrets_validate from the config to the scan.
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert result[0].resource_id == "SensitiveProject"
|
||||
|
||||
def test_project_with_sensitive_plaintext_credentials_exluded(self):
|
||||
codebuild_client = mock.MagicMock
|
||||
|
||||
@@ -373,12 +466,12 @@ class Test_codebuild_project_no_secrets_in_variables:
|
||||
environment_variables=[
|
||||
{
|
||||
"name": "AWS_DUMB_ACCESS_KEY",
|
||||
"value": "AKIAIOSFODNN7EXAMPLE",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"type": "PLAINTEXT",
|
||||
},
|
||||
{
|
||||
"name": "AWS_ACCESS_KEY_ID",
|
||||
"value": "AKIAIOSFODNN7EXAMPLE",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"type": "PLAINTEXT",
|
||||
},
|
||||
],
|
||||
@@ -409,10 +502,21 @@ class Test_codebuild_project_no_secrets_in_variables:
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "CodeBuild project SensitiveProject has sensitive environment plaintext credentials in variables: AWS Access Key in variable AWS_ACCESS_KEY_ID."
|
||||
# AWS_DUMB_ACCESS_KEY is excluded, so only AWS_ACCESS_KEY_ID is
|
||||
# scanned; its JWT + "KEY" name yields both a JWT and a
|
||||
# Generic API Key finding with non-deterministic order.
|
||||
assert result[0].status_extended.startswith(
|
||||
"CodeBuild project SensitiveProject has sensitive environment plaintext credentials in variables:"
|
||||
)
|
||||
assert (
|
||||
"JSON Web Token (base64url-encoded) in variable AWS_ACCESS_KEY_ID"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"Generic API Key in variable AWS_ACCESS_KEY_ID"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert "AWS_DUMB_ACCESS_KEY" not in result[0].status_extended
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "SensitiveProject"
|
||||
assert result[0].resource_arn == project_arn
|
||||
@@ -434,12 +538,12 @@ class Test_codebuild_project_no_secrets_in_variables:
|
||||
environment_variables=[
|
||||
{
|
||||
"name": "AWS_DUMB_ACCESS_KEY",
|
||||
"value": "AKIAIOSFODNN7EXAMPLE",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"type": "PLAINTEXT",
|
||||
},
|
||||
{
|
||||
"name": "AWS_ACCESS_KEY_ID",
|
||||
"value": "AKIAIOSFODNN7EXAMPLE",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"type": "PLAINTEXT",
|
||||
},
|
||||
],
|
||||
@@ -468,11 +572,80 @@ class Test_codebuild_project_no_secrets_in_variables:
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "CodeBuild project SensitiveProject has sensitive environment plaintext credentials in variables: AWS Access Key in variable AWS_DUMB_ACCESS_KEY, AWS Access Key in variable AWS_ACCESS_KEY_ID."
|
||||
# Both variables hold a JWT and have "KEY" in their name, so
|
||||
# each yields a JWT and a Generic API Key finding; order is
|
||||
# non-deterministic.
|
||||
assert result[0].status_extended.startswith(
|
||||
"CodeBuild project SensitiveProject has sensitive environment plaintext credentials in variables:"
|
||||
)
|
||||
for var_name in ("AWS_DUMB_ACCESS_KEY", "AWS_ACCESS_KEY_ID"):
|
||||
assert (
|
||||
f"JSON Web Token (base64url-encoded) in variable {var_name}"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
f"Generic API Key in variable {var_name}"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "SensitiveProject"
|
||||
assert result[0].resource_arn == project_arn
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_scan_failure_reports_manual(self):
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
|
||||
codebuild_client = mock.MagicMock()
|
||||
from prowler.providers.aws.services.codebuild.codebuild_service import Project
|
||||
|
||||
project_arn = f"arn:aws:codebuild:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:project/SensitiveProject"
|
||||
codebuild_client.projects = {
|
||||
project_arn: Project(
|
||||
name="SensitiveProject",
|
||||
arn=project_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
last_invoked_time=None,
|
||||
buildspec=None,
|
||||
environment_variables=[
|
||||
{
|
||||
"name": "EXAMPLE_VAR",
|
||||
"value": "ExampleValue",
|
||||
"type": "PLAINTEXT",
|
||||
}
|
||||
],
|
||||
tags=[],
|
||||
)
|
||||
}
|
||||
codebuild_client.audit_config = {
|
||||
"excluded_sensitive_environment_variables": [],
|
||||
"secrets_ignore_patterns": [],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_service.Codebuild",
|
||||
codebuild_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_no_secrets_in_variables.codebuild_project_no_secrets_in_variables.codebuild_client",
|
||||
codebuild_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_no_secrets_in_variables.codebuild_project_no_secrets_in_variables.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher exited with code 1"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.codebuild.codebuild_project_no_secrets_in_variables.codebuild_project_no_secrets_in_variables import (
|
||||
codebuild_project_no_secrets_in_variables,
|
||||
)
|
||||
|
||||
check = codebuild_project_no_secrets_in_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not scan" in result[0].status_extended
|
||||
|
||||
+109
-5
@@ -100,7 +100,7 @@ class Test_ec2_instance_secrets_user_data:
|
||||
ImageId=EXAMPLE_AMI_ID,
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
UserData="DB_PASSWORD=foobar123",
|
||||
UserData='DB_PASSWORD="Tr0ub4dor3xKq9vLmZ"',
|
||||
)[0]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
@@ -130,7 +130,7 @@ class Test_ec2_instance_secrets_user_data:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in EC2 instance {instance.id} User Data -> Secret Keyword on line 1."
|
||||
== f"Potential secret found in EC2 instance {instance.id} User Data -> Generic Password on line 1."
|
||||
)
|
||||
assert result[0].resource_id == instance.id
|
||||
assert (
|
||||
@@ -233,7 +233,7 @@ class Test_ec2_instance_secrets_user_data:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in EC2 instance {instance.id} User Data -> Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4."
|
||||
== f"Potential secret found in EC2 instance {instance.id} User Data -> Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4."
|
||||
)
|
||||
assert result[0].resource_id == instance.id
|
||||
assert (
|
||||
@@ -327,7 +327,7 @@ class Test_ec2_instance_secrets_user_data:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in EC2 instance {instance.id} User Data -> Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4."
|
||||
== f"Potential secret found in EC2 instance {instance.id} User Data -> Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4."
|
||||
)
|
||||
assert result[0].resource_id == instance.id
|
||||
assert (
|
||||
@@ -337,6 +337,64 @@ class Test_ec2_instance_secrets_user_data:
|
||||
assert result[0].resource_tags is None
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@mock_aws
|
||||
def test_one_ec2_with_verified_secret(self):
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1)
|
||||
instance = ec2.create_instances(
|
||||
ImageId=EXAMPLE_AMI_ID,
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
UserData='STRIPE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"',
|
||||
)[0]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
|
||||
audit_config={"secrets_validate": True},
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_secrets_user_data.ec2_instance_secrets_user_data.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_secrets_user_data.ec2_instance_secrets_user_data.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
0: [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 1,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_secrets_user_data.ec2_instance_secrets_user_data import (
|
||||
ec2_instance_secrets_user_data,
|
||||
)
|
||||
|
||||
check = ec2_instance_secrets_user_data()
|
||||
result = check.execute()
|
||||
|
||||
# The check must forward secrets_validate from the config to the scan.
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert result[0].resource_id == instance.id
|
||||
|
||||
@mock_aws
|
||||
def test_one_secrets_with_unicode_error(self):
|
||||
invalid_utf8_bytes = b"\xc0\xaf"
|
||||
@@ -368,4 +426,50 @@ class Test_ec2_instance_secrets_user_data:
|
||||
check = ec2_instance_secrets_user_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not decode User Data" in result[0].status_extended
|
||||
|
||||
@mock_aws
|
||||
def test_scan_failure_reports_manual(self):
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
|
||||
ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1)
|
||||
instance = ec2.create_instances(
|
||||
ImageId=EXAMPLE_AMI_ID,
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
UserData='password = "Tr0ub4dor3xKq9vLmZ"',
|
||||
)[0]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_secrets_user_data.ec2_instance_secrets_user_data.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_secrets_user_data.ec2_instance_secrets_user_data.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher exited with code 1"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_secrets_user_data.ec2_instance_secrets_user_data import (
|
||||
ec2_instance_secrets_user_data,
|
||||
)
|
||||
|
||||
check = ec2_instance_secrets_user_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not scan" in result[0].status_extended
|
||||
assert result[0].resource_id == instance.id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
DB_PASSWORD=foobar123
|
||||
DB_PASSWORD="Tr0ub4dor3xKq9vLmZ"
|
||||
DB_USER=foo
|
||||
API_KEY=12345abcd
|
||||
SERVICE_PASSWORD=bbaabb45
|
||||
STRIPE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
|
||||
SERVICE_PASSWORD="Xy9zPq2wKmRtVbN4"
|
||||
|
||||
Binary file not shown.
+90
-13
@@ -29,7 +29,9 @@ def mock_make_api_call(self, operation_name, kwarg):
|
||||
"VersionNumber": 123,
|
||||
"LaunchTemplateData": {
|
||||
"UserData": b64encode(
|
||||
"DB_PASSWORD=foobar123".encode(encoding_format_utf_8)
|
||||
'DB_PASSWORD="Tr0ub4dor3xKq9vLmZ"'.encode(
|
||||
encoding_format_utf_8
|
||||
)
|
||||
).decode(encoding_format_utf_8),
|
||||
"NetworkInterfaces": [{"AssociatePublicIpAddress": True}],
|
||||
},
|
||||
@@ -164,7 +166,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Potential secret found in User Data for EC2 Launch Template tester1 in template versions: Version 123: Secret Keyword on line 1."
|
||||
== "Potential secret found in User Data for EC2 Launch Template tester1 in template versions: Version 123: Generic Password on line 1."
|
||||
)
|
||||
assert result[0].resource_id == "lt-1234567890"
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
@@ -212,7 +214,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
)
|
||||
|
||||
ec2_client.launch_templates = [launch_template]
|
||||
ec2_client.audit_config = {"detect_secrets_plugins": None}
|
||||
ec2_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -236,7 +238,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name} in template versions: Version 1: Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4, Version 2: Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4."
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name} in template versions: Version 1: Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4, Version 2: Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4."
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
@@ -290,7 +292,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
)
|
||||
|
||||
ec2_client.launch_templates = [launch_template]
|
||||
ec2_client.audit_config = {"detect_secrets_plugins": None}
|
||||
ec2_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -314,7 +316,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name} in template versions: Version 1: Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4."
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name} in template versions: Version 1: Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4."
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
@@ -358,7 +360,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
)
|
||||
|
||||
ec2_client.launch_templates = [launch_template]
|
||||
ec2_client.audit_config = {"detect_secrets_plugins": None}
|
||||
ec2_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -382,7 +384,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name} in template versions: Version 1: Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4."
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name} in template versions: Version 1: Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4."
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
@@ -391,6 +393,81 @@ class Test_ec2_launch_template_no_secrets:
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_one_launch_template_with_verified_secret(self):
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
ec2_client = mock.MagicMock()
|
||||
launch_template_name = "tester"
|
||||
launch_template_id = "lt-1234567890"
|
||||
launch_template_arn = (
|
||||
f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}"
|
||||
)
|
||||
|
||||
launch_template_data = TemplateData(
|
||||
user_data=b64encode(
|
||||
"This is some user_data".encode(encoding_format_utf_8)
|
||||
).decode(encoding_format_utf_8),
|
||||
associate_public_ip_address=True,
|
||||
)
|
||||
|
||||
launch_template_versions = [
|
||||
LaunchTemplateVersion(
|
||||
version_number=1,
|
||||
template_data=launch_template_data,
|
||||
),
|
||||
]
|
||||
|
||||
launch_template = LaunchTemplate(
|
||||
name=launch_template_name,
|
||||
id=launch_template_id,
|
||||
arn=launch_template_arn,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
versions=launch_template_versions,
|
||||
)
|
||||
|
||||
ec2_client.launch_templates = [launch_template]
|
||||
ec2_client.audit_config = {"secrets_validate": True}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=ec2_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_launch_template_no_secrets.ec2_launch_template_no_secrets.ec2_client",
|
||||
new=ec2_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_launch_template_no_secrets.ec2_launch_template_no_secrets.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
(0, 0): [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 1,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ec2.ec2_launch_template_no_secrets.ec2_launch_template_no_secrets import (
|
||||
ec2_launch_template_no_secrets,
|
||||
)
|
||||
|
||||
check = ec2_launch_template_no_secrets()
|
||||
result = check.execute()
|
||||
|
||||
# The check must forward secrets_validate from the config to the scan.
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert result[0].resource_id == launch_template_id
|
||||
|
||||
@mock_aws
|
||||
def test_one_launch_template_without_user_data(self):
|
||||
launch_template_name = "tester"
|
||||
@@ -506,7 +583,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
launch_template_secrets,
|
||||
launch_template_no_secrets,
|
||||
]
|
||||
ec2_client.audit_config = {"detect_secrets_plugins": None}
|
||||
ec2_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -530,7 +607,7 @@ class Test_ec2_launch_template_no_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name1} in template versions: Version 1: Secret Keyword on line 1, Hex High Entropy String on line 3, Secret Keyword on line 3, Secret Keyword on line 4."
|
||||
== f"Potential secret found in User Data for EC2 Launch Template {launch_template_name1} in template versions: Version 1: Generic Password on line 1, JSON Web Token (base64url-encoded) on line 3, Generic Password on line 4."
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id1
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
@@ -593,10 +670,10 @@ class Test_ec2_launch_template_no_secrets:
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"No secrets found in User Data of any version for EC2 Launch Template {launch_template_name}."
|
||||
f"Could not decode User Data for EC2 Launch Template {launch_template_name}"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
DB_PASSWORD=foobar123
|
||||
DB_PASSWORD="Tr0ub4dor3xKq9vLmZ"
|
||||
DB_USER=foo
|
||||
API_KEY=12345abcd
|
||||
SERVICE_PASSWORD=bbaabb45
|
||||
STRIPE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
|
||||
SERVICE_PASSWORD="Xy9zPq2wKmRtVbN4"
|
||||
|
||||
Binary file not shown.
+60
-12
@@ -11,9 +11,17 @@ CONTAINER_NAME = "test-container"
|
||||
ENV_VAR_NAME_NO_SECRETS = "host"
|
||||
ENV_VAR_VALUE_NO_SECRETS = "localhost:1234"
|
||||
ENV_VAR_NAME_WITH_KEYWORD = "DB_PASSWORD"
|
||||
ENV_VAR_VALUE_WITH_SECRETS = "srv://admin:pass@db"
|
||||
# Realistic fake secrets that Kingfisher actually detects (placeholders such as
|
||||
# the previous "srv://admin:pass@db" basic-auth URL are no longer flagged).
|
||||
# A JWT fires on any line of the dumped JSON (even when followed by a
|
||||
# trailing comma); a keyword-named variable additionally fires the generic
|
||||
# keyword rule when it is the last entry in the dump.
|
||||
ENV_VAR_VALUE_WITH_SECRETS = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
ENV_VAR_NAME_WITH_KEYWORD2 = "DATABASE_PASSWORD"
|
||||
ENV_VAR_VALUE_WITH_SECRETS2 = "srv://admin:password@database"
|
||||
ENV_VAR_VALUE_WITH_SECRETS2 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODc2NTQzMjEwIiwibmFtZSI6IkphbmUifQ.s5LqY8mC2pX1vN0bQwReTyUiOpAsDfGhJkLzXcVbNm0"
|
||||
# Generic password/secret assignment value (detected only on the last entry of
|
||||
# the JSON dump, where there is no trailing comma after the value).
|
||||
ENV_VAR_VALUE_GENERIC_SECRET = "Tr0ub4dor3xKq9vLmZ"
|
||||
|
||||
|
||||
class Test_ecs_task_definitions_no_environment_secrets:
|
||||
@@ -143,7 +151,7 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Basic Auth Credentials on the environment variable host."
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> JSON Web Token (base64url-encoded) on the environment variable host."
|
||||
)
|
||||
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
|
||||
assert result[0].resource_arn == task_arn
|
||||
@@ -167,7 +175,7 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
"environment": [
|
||||
{
|
||||
"name": ENV_VAR_NAME_WITH_KEYWORD,
|
||||
"value": ENV_VAR_VALUE_NO_SECRETS,
|
||||
"value": ENV_VAR_VALUE_GENERIC_SECRET,
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -198,7 +206,7 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD."
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Generic Password on the environment variable DB_PASSWORD."
|
||||
)
|
||||
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
|
||||
assert result[0].resource_arn == task_arn
|
||||
@@ -251,9 +259,20 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
# The keyword-named variable holding a real secret triggers both the
|
||||
# generic keyword rule and the JWT rule on the same line.
|
||||
# Kingfisher emits same-line findings in a non-deterministic order, so
|
||||
# assert both are present without pinning their order.
|
||||
assert result[0].status_extended.startswith(
|
||||
f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> "
|
||||
)
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD."
|
||||
"JSON Web Token (base64url-encoded) on the environment variable DB_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"Generic Password on the environment variable DB_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
|
||||
assert result[0].resource_arn == task_arn
|
||||
@@ -310,9 +329,23 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
# DB_PASSWORD holds a JWT under a keyword name, so it fires
|
||||
# both the JWT rule and the generic keyword rule on the
|
||||
# same line (non-deterministic order); host holds a second JWT.
|
||||
assert result[0].status_extended.startswith(
|
||||
f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> "
|
||||
)
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable host."
|
||||
"JSON Web Token (base64url-encoded) on the environment variable DB_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"Generic Password on the environment variable DB_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"JSON Web Token (base64url-encoded) on the environment variable host"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
|
||||
assert result[0].resource_arn == task_arn
|
||||
@@ -340,7 +373,7 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
},
|
||||
{
|
||||
"name": ENV_VAR_NAME_WITH_KEYWORD2,
|
||||
"value": ENV_VAR_VALUE_WITH_SECRETS2,
|
||||
"value": ENV_VAR_VALUE_GENERIC_SECRET,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -369,9 +402,24 @@ class Test_ecs_task_definitions_no_environment_secrets:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
# DB_PASSWORD holds a JWT under a keyword name, so it fires
|
||||
# both the JWT and the generic keyword rule on the same line
|
||||
# (non-deterministic order); DATABASE_PASSWORD fires the generic
|
||||
# keyword rule on its own line.
|
||||
assert result[0].status_extended.startswith(
|
||||
f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> "
|
||||
)
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DATABASE_PASSWORD, Secret Keyword on the environment variable DATABASE_PASSWORD."
|
||||
"JSON Web Token (base64url-encoded) on the environment variable DB_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"Generic Password on the environment variable DB_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert (
|
||||
"Generic Password on the environment variable DATABASE_PASSWORD"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
|
||||
assert result[0].resource_arn == task_arn
|
||||
|
||||
@@ -27,13 +27,13 @@ class Test_ssm_documents_secrets:
|
||||
document_name = "test-document"
|
||||
document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}"
|
||||
ssm_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
ssm_client.audit_config = {"detect_secrets_plugins": None}
|
||||
ssm_client.audit_config = {}
|
||||
ssm_client.documents = {
|
||||
document_name: Document(
|
||||
arn=document_arn,
|
||||
name=document_name,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
content={"db_password": "test-password"},
|
||||
content={"db_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
account_owners=[],
|
||||
)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class Test_ssm_documents_secrets:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Potential secret found in SSM Document {document_name} -> Secret Keyword on line 2."
|
||||
== f"Potential secret found in SSM Document {document_name} -> Generic Password on line 2."
|
||||
)
|
||||
|
||||
def test_document_no_secrets(self):
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ class Test_stepfunctions_statemachine_no_secrets_in_definition:
|
||||
arn=statemachine_arn,
|
||||
name="TestStateMachine",
|
||||
status=StateMachineStatus.ACTIVE,
|
||||
definition='{"Comment": "Example with secret", "StartAt": "MyTask", "States": {"MyTask": {"Type": "Task", "Parameters": {"api_key": "AKIAIOSFODNN7EXAMPLE"}, "End": true}}}',
|
||||
definition='{"Comment": "Example with secret", "StartAt": "MyTask", "States": {"MyTask": {"Type": "Task", "Parameters": {"api_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"}, "End": true}}}',
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
type=StateMachineType.STANDARD,
|
||||
creation_date=datetime.now(),
|
||||
|
||||
+64
-5
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.providers.openstack.services.blockstorage.blockstorage_service import (
|
||||
SnapshotResource,
|
||||
)
|
||||
@@ -141,7 +142,7 @@ class Test_blockstorage_snapshot_metadata_sensitive_data:
|
||||
status="available",
|
||||
size=50,
|
||||
volume_id="vol-1",
|
||||
metadata={"db_password": "supersecret123"},
|
||||
metadata={"db_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
region=OPENSTACK_REGION,
|
||||
)
|
||||
@@ -179,7 +180,9 @@ class Test_blockstorage_snapshot_metadata_sensitive_data:
|
||||
status="available",
|
||||
size=50,
|
||||
volume_id="vol-1",
|
||||
metadata={"api_key": "sk-1234567890"},
|
||||
metadata={
|
||||
"api_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
},
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
region=OPENSTACK_REGION,
|
||||
)
|
||||
@@ -223,7 +226,9 @@ class Test_blockstorage_snapshot_metadata_sensitive_data:
|
||||
status="available",
|
||||
size=50,
|
||||
volume_id="vol-1",
|
||||
metadata={"ssh_key": "-----BEGIN RSA PRIVATE KEY-----"},
|
||||
metadata={
|
||||
"ssh_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCUzlT9QGi8ZSr5\nk+LTRz/1TaiCCs6o1icW4cur0Q0hdBnbRJXUdjlQsgzmBvCBNkGHI8hb/RUPssvc\nDLU5kOQ3Wp2KgtbphhZ2PfpuJrzwHL1ejcJkRxegm/aTdmpoQKcxGeehAfHbmlLA\nxdfn6wPDfGji973yiRH56JRukJAaqF50HC2a/AVNC5HtZoVlbQ+WvVbYVUnPxNkv\nPpc53PjrBgWiTtdMONEqJ3jDiaqfUBt+TZYF0CFc9HgjnUniRX28OukDyLu+idOz\nFKyZxMXtqexkAvQLDW1PATpZgVQ7hJoCD8UVTXAtcgzPq5fA6AR2URiECHI6ZyL0\nUmixKfMNAgMBAAECggEAJRzp5wjdpmEgDQOkjpfGXJ6sAJUD8mmI8cTKeJWIzhdo\nDH8oVEdRJ65kl6lS6hMXWEZlJgYyrsnj3MPBnjQkKycbRCy6P59s8jwmfbsFI+iz\nFUZLXZm6i5jicGhYBRzc5hrlIYu73863RXOClAnSFDsu6K6rzfYASQFIJeRBwJfs\njqXinuun/h2zGjpiY+TtNsa8c+nC7f3sGsTzNJugDvBPWQzsnAMzXJqiyharre4V\no157XIOvdC0joIp8j/Ib1ZtMfz1K1LcgBgw0szSieIw0Rq8yQ0Ek7GtLh43jG+ap\nvcSEesTD1p4mjPXoWkPG8KYd4iwGedZaePfheVcKKQKBgQDNE03SWv18AH0d4fpB\nlFAtRybCfSvMORzBrt2oilz8wDmK+Zga5o+phCnM8v3eJy1v8BvIQ9RvwQA2uVgZ\nr701wNMpVrTsMujk83oVRhimZLk6Hyw07wmMgEHX7+izkm2Lk4Lk7Zol3VRfnWG6\nmIcUk7xB1yAs3mudsfx0VO0QyQKBgQC5wfdqCLj2hZk4sMZu8Bth+BHKChGItmDk\nAW7aNt+gaPyoryOJoi2OUO8ud8EyuqXiuslSk2pPtjvLhCppkoq6V8kmPAUzaxFk\n4nDEAxT9Un8IJ0j2ebv+koQKsBWjssbVSjrZgIcYIDK1QblgbCp2FSE3ima+V8ip\nOdNjiatWJQKBgEX8lox5nRSanhh6rIuA8DPjmmi5ix7xRs0avm7seXuQppK1R6G2\nmcTCY/mb2+Pa/vi6uuCHtZJGDaqfal+pyCr2GZp8CtapMS4hocJs37C5ozUguld+\nVIXsp4voRkQybsw5lWxHYloVxNu0vEuQDlmJabAWmNZ3OcbhnUSeTyFxAoGAFtkZ\n0owCHChwoT11Gt4jsBgwL/avE27DWigm92Y6eWOQeDsalupAyjmAQenu9Itqrgml\ni6egMu/KSQ0Xnmas86CqmC5XwWxQ9mS31BRA96u2/ky+t7pfej+RSDNCZiEuPbvk\noy4g78G+GvdbktWbH20X6dn3K0Bm6RG4w4yCa5UCgYBs0zAVs0DZmM8SUZJA/HuQ\nN6a1vKKns7xKw5N3SmX1KbDhx5LSZXfbUo2+QktE7iRf9G2f1o0q8kz9l/4AGXi1\nKJNUHupWoaQzGNrzAb27TUtFA0ocMG8KnqxjANWox5oPJS9OU5tw5H5dxeI/Senc\nkYW6eCnRzPcmBqex6Vuw4w==\n-----END PRIVATE KEY-----\n"
|
||||
},
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
region=OPENSTACK_REGION,
|
||||
)
|
||||
@@ -277,7 +282,7 @@ class Test_blockstorage_snapshot_metadata_sensitive_data:
|
||||
status="available",
|
||||
size=50,
|
||||
volume_id="vol-2",
|
||||
metadata={"admin_password": "secret123"},
|
||||
metadata={"admin_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
region=OPENSTACK_REGION,
|
||||
),
|
||||
@@ -318,7 +323,7 @@ class Test_blockstorage_snapshot_metadata_sensitive_data:
|
||||
metadata={
|
||||
"environment": "production",
|
||||
"application": "web-app",
|
||||
"db_password": "supersecret123",
|
||||
"db_password": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"region": "us-east",
|
||||
},
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
@@ -348,3 +353,57 @@ class Test_blockstorage_snapshot_metadata_sensitive_data:
|
||||
# Verify the secret is correctly attributed to 'db_password' key
|
||||
assert "in metadata key 'db_password'" in result[0].status_extended
|
||||
assert result[0].resource_id == "snap-6"
|
||||
|
||||
def test_snapshot_verified_secret_escalates_to_critical(self):
|
||||
"""Test that a confirmed live secret escalates the finding to CRITICAL (FAIL)."""
|
||||
blockstorage_client = mock.MagicMock()
|
||||
blockstorage_client.audit_config = {"secrets_validate": True}
|
||||
blockstorage_client.snapshots = [
|
||||
SnapshotResource(
|
||||
id="snap-verified",
|
||||
name="Verified Secret",
|
||||
status="available",
|
||||
size=50,
|
||||
volume_id="vol-1",
|
||||
metadata={"api_key": "placeholder"},
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
region=OPENSTACK_REGION,
|
||||
)
|
||||
]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_openstack_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.blockstorage.blockstorage_snapshot_metadata_sensitive_data.blockstorage_snapshot_metadata_sensitive_data.blockstorage_client",
|
||||
new=blockstorage_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.blockstorage.blockstorage_snapshot_metadata_sensitive_data.blockstorage_snapshot_metadata_sensitive_data.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
0: [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
from prowler.providers.openstack.services.blockstorage.blockstorage_snapshot_metadata_sensitive_data.blockstorage_snapshot_metadata_sensitive_data import (
|
||||
blockstorage_snapshot_metadata_sensitive_data,
|
||||
)
|
||||
|
||||
check = blockstorage_snapshot_metadata_sensitive_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
|
||||
+71
-5
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.providers.openstack.services.blockstorage.blockstorage_service import (
|
||||
VolumeResource,
|
||||
)
|
||||
@@ -159,7 +160,7 @@ class Test_blockstorage_volume_metadata_sensitive_data:
|
||||
is_bootable=False,
|
||||
is_multiattach=False,
|
||||
attachments=[],
|
||||
metadata={"db_password": "supersecret123"},
|
||||
metadata={"db_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
availability_zone="nova",
|
||||
snapshot_id="",
|
||||
source_volume_id="",
|
||||
@@ -204,7 +205,9 @@ class Test_blockstorage_volume_metadata_sensitive_data:
|
||||
is_bootable=False,
|
||||
is_multiattach=False,
|
||||
attachments=[],
|
||||
metadata={"api_key": "sk-1234567890"},
|
||||
metadata={
|
||||
"api_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
},
|
||||
availability_zone="nova",
|
||||
snapshot_id="",
|
||||
source_volume_id="",
|
||||
@@ -255,7 +258,9 @@ class Test_blockstorage_volume_metadata_sensitive_data:
|
||||
is_bootable=False,
|
||||
is_multiattach=False,
|
||||
attachments=[],
|
||||
metadata={"ssh_key": "-----BEGIN RSA PRIVATE KEY-----"},
|
||||
metadata={
|
||||
"ssh_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCUzlT9QGi8ZSr5\nk+LTRz/1TaiCCs6o1icW4cur0Q0hdBnbRJXUdjlQsgzmBvCBNkGHI8hb/RUPssvc\nDLU5kOQ3Wp2KgtbphhZ2PfpuJrzwHL1ejcJkRxegm/aTdmpoQKcxGeehAfHbmlLA\nxdfn6wPDfGji973yiRH56JRukJAaqF50HC2a/AVNC5HtZoVlbQ+WvVbYVUnPxNkv\nPpc53PjrBgWiTtdMONEqJ3jDiaqfUBt+TZYF0CFc9HgjnUniRX28OukDyLu+idOz\nFKyZxMXtqexkAvQLDW1PATpZgVQ7hJoCD8UVTXAtcgzPq5fA6AR2URiECHI6ZyL0\nUmixKfMNAgMBAAECggEAJRzp5wjdpmEgDQOkjpfGXJ6sAJUD8mmI8cTKeJWIzhdo\nDH8oVEdRJ65kl6lS6hMXWEZlJgYyrsnj3MPBnjQkKycbRCy6P59s8jwmfbsFI+iz\nFUZLXZm6i5jicGhYBRzc5hrlIYu73863RXOClAnSFDsu6K6rzfYASQFIJeRBwJfs\njqXinuun/h2zGjpiY+TtNsa8c+nC7f3sGsTzNJugDvBPWQzsnAMzXJqiyharre4V\no157XIOvdC0joIp8j/Ib1ZtMfz1K1LcgBgw0szSieIw0Rq8yQ0Ek7GtLh43jG+ap\nvcSEesTD1p4mjPXoWkPG8KYd4iwGedZaePfheVcKKQKBgQDNE03SWv18AH0d4fpB\nlFAtRybCfSvMORzBrt2oilz8wDmK+Zga5o+phCnM8v3eJy1v8BvIQ9RvwQA2uVgZ\nr701wNMpVrTsMujk83oVRhimZLk6Hyw07wmMgEHX7+izkm2Lk4Lk7Zol3VRfnWG6\nmIcUk7xB1yAs3mudsfx0VO0QyQKBgQC5wfdqCLj2hZk4sMZu8Bth+BHKChGItmDk\nAW7aNt+gaPyoryOJoi2OUO8ud8EyuqXiuslSk2pPtjvLhCppkoq6V8kmPAUzaxFk\n4nDEAxT9Un8IJ0j2ebv+koQKsBWjssbVSjrZgIcYIDK1QblgbCp2FSE3ima+V8ip\nOdNjiatWJQKBgEX8lox5nRSanhh6rIuA8DPjmmi5ix7xRs0avm7seXuQppK1R6G2\nmcTCY/mb2+Pa/vi6uuCHtZJGDaqfal+pyCr2GZp8CtapMS4hocJs37C5ozUguld+\nVIXsp4voRkQybsw5lWxHYloVxNu0vEuQDlmJabAWmNZ3OcbhnUSeTyFxAoGAFtkZ\n0owCHChwoT11Gt4jsBgwL/avE27DWigm92Y6eWOQeDsalupAyjmAQenu9Itqrgml\ni6egMu/KSQ0Xnmas86CqmC5XwWxQ9mS31BRA96u2/ky+t7pfej+RSDNCZiEuPbvk\noy4g78G+GvdbktWbH20X6dn3K0Bm6RG4w4yCa5UCgYBs0zAVs0DZmM8SUZJA/HuQ\nN6a1vKKns7xKw5N3SmX1KbDhx5LSZXfbUo2+QktE7iRf9G2f1o0q8kz9l/4AGXi1\nKJNUHupWoaQzGNrzAb27TUtFA0ocMG8KnqxjANWox5oPJS9OU5tw5H5dxeI/Senc\nkYW6eCnRzPcmBqex6Vuw4w==\n-----END PRIVATE KEY-----\n"
|
||||
},
|
||||
availability_zone="nova",
|
||||
snapshot_id="",
|
||||
source_volume_id="",
|
||||
@@ -323,7 +328,7 @@ class Test_blockstorage_volume_metadata_sensitive_data:
|
||||
is_bootable=False,
|
||||
is_multiattach=False,
|
||||
attachments=[],
|
||||
metadata={"admin_password": "secret123"},
|
||||
metadata={"admin_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
availability_zone="nova",
|
||||
snapshot_id="",
|
||||
source_volume_id="",
|
||||
@@ -371,7 +376,7 @@ class Test_blockstorage_volume_metadata_sensitive_data:
|
||||
metadata={
|
||||
"environment": "production",
|
||||
"application": "web-app",
|
||||
"db_password": "supersecret123",
|
||||
"db_password": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"region": "us-east",
|
||||
},
|
||||
availability_zone="nova",
|
||||
@@ -404,3 +409,64 @@ class Test_blockstorage_volume_metadata_sensitive_data:
|
||||
# Verify the secret is correctly attributed to 'db_password' key
|
||||
assert "in metadata key 'db_password'" in result[0].status_extended
|
||||
assert result[0].resource_id == "vol-6"
|
||||
|
||||
def test_volume_verified_secret_escalates_to_critical(self):
|
||||
"""Test that a confirmed live secret escalates the finding to CRITICAL (FAIL)."""
|
||||
blockstorage_client = mock.MagicMock()
|
||||
blockstorage_client.audit_config = {"secrets_validate": True}
|
||||
blockstorage_client.volumes = [
|
||||
VolumeResource(
|
||||
id="vol-verified",
|
||||
name="Verified Secret",
|
||||
status="in-use",
|
||||
size=100,
|
||||
volume_type="standard",
|
||||
is_encrypted=False,
|
||||
is_bootable=False,
|
||||
is_multiattach=False,
|
||||
attachments=[],
|
||||
metadata={"api_key": "placeholder"},
|
||||
availability_zone="nova",
|
||||
snapshot_id="",
|
||||
source_volume_id="",
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
region=OPENSTACK_REGION,
|
||||
)
|
||||
]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_openstack_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.blockstorage.blockstorage_volume_metadata_sensitive_data.blockstorage_volume_metadata_sensitive_data.blockstorage_client",
|
||||
new=blockstorage_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.blockstorage.blockstorage_volume_metadata_sensitive_data.blockstorage_volume_metadata_sensitive_data.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
0: [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
from prowler.providers.openstack.services.blockstorage.blockstorage_volume_metadata_sensitive_data.blockstorage_volume_metadata_sensitive_data import (
|
||||
blockstorage_volume_metadata_sensitive_data,
|
||||
)
|
||||
|
||||
check = blockstorage_volume_metadata_sensitive_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
|
||||
+136
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.providers.openstack.services.compute.compute_service import ComputeInstance
|
||||
from tests.providers.openstack.openstack_fixtures import (
|
||||
OPENSTACK_PROJECT_ID,
|
||||
@@ -181,7 +182,7 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
private_v6="",
|
||||
networks={},
|
||||
has_config_drive=False,
|
||||
metadata={"db_password": "supersecret123"},
|
||||
metadata={"db_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
user_data="",
|
||||
trusted_image_certificates=[],
|
||||
)
|
||||
@@ -233,7 +234,9 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
private_v6="",
|
||||
networks={},
|
||||
has_config_drive=False,
|
||||
metadata={"api_key": "sk-1234567890"},
|
||||
metadata={
|
||||
"api_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
},
|
||||
user_data="",
|
||||
trusted_image_certificates=[],
|
||||
)
|
||||
@@ -349,7 +352,9 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
private_v6="",
|
||||
networks={},
|
||||
has_config_drive=False,
|
||||
metadata={"ssh_key": "-----BEGIN RSA PRIVATE KEY-----"},
|
||||
metadata={
|
||||
"ssh_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCUzlT9QGi8ZSr5\nk+LTRz/1TaiCCs6o1icW4cur0Q0hdBnbRJXUdjlQsgzmBvCBNkGHI8hb/RUPssvc\nDLU5kOQ3Wp2KgtbphhZ2PfpuJrzwHL1ejcJkRxegm/aTdmpoQKcxGeehAfHbmlLA\nxdfn6wPDfGji973yiRH56JRukJAaqF50HC2a/AVNC5HtZoVlbQ+WvVbYVUnPxNkv\nPpc53PjrBgWiTtdMONEqJ3jDiaqfUBt+TZYF0CFc9HgjnUniRX28OukDyLu+idOz\nFKyZxMXtqexkAvQLDW1PATpZgVQ7hJoCD8UVTXAtcgzPq5fA6AR2URiECHI6ZyL0\nUmixKfMNAgMBAAECggEAJRzp5wjdpmEgDQOkjpfGXJ6sAJUD8mmI8cTKeJWIzhdo\nDH8oVEdRJ65kl6lS6hMXWEZlJgYyrsnj3MPBnjQkKycbRCy6P59s8jwmfbsFI+iz\nFUZLXZm6i5jicGhYBRzc5hrlIYu73863RXOClAnSFDsu6K6rzfYASQFIJeRBwJfs\njqXinuun/h2zGjpiY+TtNsa8c+nC7f3sGsTzNJugDvBPWQzsnAMzXJqiyharre4V\no157XIOvdC0joIp8j/Ib1ZtMfz1K1LcgBgw0szSieIw0Rq8yQ0Ek7GtLh43jG+ap\nvcSEesTD1p4mjPXoWkPG8KYd4iwGedZaePfheVcKKQKBgQDNE03SWv18AH0d4fpB\nlFAtRybCfSvMORzBrt2oilz8wDmK+Zga5o+phCnM8v3eJy1v8BvIQ9RvwQA2uVgZ\nr701wNMpVrTsMujk83oVRhimZLk6Hyw07wmMgEHX7+izkm2Lk4Lk7Zol3VRfnWG6\nmIcUk7xB1yAs3mudsfx0VO0QyQKBgQC5wfdqCLj2hZk4sMZu8Bth+BHKChGItmDk\nAW7aNt+gaPyoryOJoi2OUO8ud8EyuqXiuslSk2pPtjvLhCppkoq6V8kmPAUzaxFk\n4nDEAxT9Un8IJ0j2ebv+koQKsBWjssbVSjrZgIcYIDK1QblgbCp2FSE3ima+V8ip\nOdNjiatWJQKBgEX8lox5nRSanhh6rIuA8DPjmmi5ix7xRs0avm7seXuQppK1R6G2\nmcTCY/mb2+Pa/vi6uuCHtZJGDaqfal+pyCr2GZp8CtapMS4hocJs37C5ozUguld+\nVIXsp4voRkQybsw5lWxHYloVxNu0vEuQDlmJabAWmNZ3OcbhnUSeTyFxAoGAFtkZ\n0owCHChwoT11Gt4jsBgwL/avE27DWigm92Y6eWOQeDsalupAyjmAQenu9Itqrgml\ni6egMu/KSQ0Xnmas86CqmC5XwWxQ9mS31BRA96u2/ky+t7pfej+RSDNCZiEuPbvk\noy4g78G+GvdbktWbH20X6dn3K0Bm6RG4w4yCa5UCgYBs0zAVs0DZmM8SUZJA/HuQ\nN6a1vKKns7xKw5N3SmX1KbDhx5LSZXfbUo2+QktE7iRf9G2f1o0q8kz9l/4AGXi1\nKJNUHupWoaQzGNrzAb27TUtFA0ocMG8KnqxjANWox5oPJS9OU5tw5H5dxeI/Senc\nkYW6eCnRzPcmBqex6Vuw4w==\n-----END PRIVATE KEY-----\n"
|
||||
},
|
||||
user_data="",
|
||||
trusted_image_certificates=[],
|
||||
)
|
||||
@@ -431,7 +436,7 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
private_v6="",
|
||||
networks={},
|
||||
has_config_drive=False,
|
||||
metadata={"admin_password": "secret123"},
|
||||
metadata={"admin_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
user_data="",
|
||||
trusted_image_certificates=[],
|
||||
),
|
||||
@@ -486,7 +491,7 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
metadata={
|
||||
"environment": "production",
|
||||
"application": "web-app",
|
||||
"db_password": "supersecret123",
|
||||
"db_password": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"region": "us-east",
|
||||
},
|
||||
user_data="",
|
||||
@@ -544,7 +549,7 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
has_config_drive=False,
|
||||
metadata={
|
||||
"first_key": "safe_value",
|
||||
"api_key": "sk-1234567890abcdef",
|
||||
"api_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
"third_key": "also_safe",
|
||||
},
|
||||
user_data="",
|
||||
@@ -574,3 +579,128 @@ class Test_compute_instance_metadata_sensitive_data:
|
||||
# Verify the secret is correctly attributed to 'api_key' key (second in order)
|
||||
assert "in metadata key 'api_key'" in result[0].status_extended
|
||||
assert result[0].resource_id == "instance-8"
|
||||
|
||||
def test_instance_verified_secret_escalates_to_critical(self):
|
||||
"""Test that a confirmed live secret escalates the finding to CRITICAL (FAIL)."""
|
||||
compute_client = mock.MagicMock()
|
||||
compute_client.audit_config = {"secrets_validate": True}
|
||||
compute_client.instances = [
|
||||
ComputeInstance(
|
||||
id="instance-verified",
|
||||
name="Verified Secret",
|
||||
status="ACTIVE",
|
||||
flavor_id="flavor-1",
|
||||
security_groups=["default"],
|
||||
region=OPENSTACK_REGION,
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
is_locked=False,
|
||||
locked_reason="",
|
||||
key_name="",
|
||||
user_id="",
|
||||
access_ipv4="",
|
||||
access_ipv6="",
|
||||
public_v4="",
|
||||
public_v6="",
|
||||
private_v4="",
|
||||
private_v6="",
|
||||
networks={},
|
||||
has_config_drive=False,
|
||||
metadata={"api_key": "placeholder"},
|
||||
user_data="",
|
||||
trusted_image_certificates=[],
|
||||
)
|
||||
]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_openstack_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
0: [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import (
|
||||
compute_instance_metadata_sensitive_data,
|
||||
)
|
||||
|
||||
check = compute_instance_metadata_sensitive_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
|
||||
def test_scan_failure_reports_manual(self):
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
|
||||
compute_client = mock.MagicMock()
|
||||
compute_client.audit_config = {"secrets_ignore_patterns": []}
|
||||
compute_client.instances = [
|
||||
ComputeInstance(
|
||||
id="instance-scan-fail",
|
||||
name="Scan Fail",
|
||||
status="ACTIVE",
|
||||
flavor_id="flavor-1",
|
||||
security_groups=["default"],
|
||||
region=OPENSTACK_REGION,
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
is_locked=False,
|
||||
locked_reason="",
|
||||
key_name="",
|
||||
user_id="",
|
||||
access_ipv4="",
|
||||
access_ipv6="",
|
||||
public_v4="",
|
||||
public_v6="",
|
||||
private_v4="",
|
||||
private_v6="",
|
||||
networks={},
|
||||
has_config_drive=False,
|
||||
metadata={"api_key": "placeholder"},
|
||||
user_data="",
|
||||
trusted_image_certificates=[],
|
||||
)
|
||||
]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_openstack_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher exited with code 1"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import (
|
||||
compute_instance_metadata_sensitive_data,
|
||||
)
|
||||
|
||||
check = compute_instance_metadata_sensitive_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not scan" in result[0].status_extended
|
||||
|
||||
+63
-2
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.providers.openstack.services.objectstorage.objectstorage_service import (
|
||||
ObjectStorageContainer,
|
||||
)
|
||||
@@ -157,7 +158,7 @@ class Test_objectstorage_container_metadata_sensitive_data:
|
||||
history_location="",
|
||||
sync_to="",
|
||||
sync_key="",
|
||||
metadata={"db_password": "supersecret123"},
|
||||
metadata={"db_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -217,7 +218,7 @@ class Test_objectstorage_container_metadata_sensitive_data:
|
||||
history_location="",
|
||||
sync_to="",
|
||||
sync_key="",
|
||||
metadata={"admin_password": "secret123"},
|
||||
metadata={"admin_password": "Tr0ub4dor3xKq9vLmZ"},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -241,3 +242,63 @@ class Test_objectstorage_container_metadata_sensitive_data:
|
||||
assert len(result) == 2
|
||||
assert len([r for r in result if r.status == "PASS"]) == 1
|
||||
assert len([r for r in result if r.status == "FAIL"]) == 1
|
||||
|
||||
def test_container_verified_secret_escalates_to_critical(self):
|
||||
"""Test that a confirmed live secret escalates the finding to CRITICAL (FAIL)."""
|
||||
objectstorage_client = mock.MagicMock()
|
||||
objectstorage_client.audit_config = {"secrets_validate": True}
|
||||
objectstorage_client.containers = [
|
||||
ObjectStorageContainer(
|
||||
id="container-verified",
|
||||
name="verified-secret",
|
||||
region=OPENSTACK_REGION,
|
||||
project_id=OPENSTACK_PROJECT_ID,
|
||||
object_count=0,
|
||||
bytes_used=0,
|
||||
read_ACL="",
|
||||
write_ACL="",
|
||||
versioning_enabled=False,
|
||||
versions_location="",
|
||||
history_location="",
|
||||
sync_to="",
|
||||
sync_key="",
|
||||
metadata={"api_key": "placeholder"},
|
||||
)
|
||||
]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_openstack_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.objectstorage_client",
|
||||
new=objectstorage_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.detect_secrets_scan_batch",
|
||||
return_value={
|
||||
0: [
|
||||
{
|
||||
"type": "JSON Web Token (base64url-encoded)",
|
||||
"line_number": 2,
|
||||
"filename": "data",
|
||||
"hashed_secret": "x",
|
||||
"is_verified": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
) as mock_scan,
|
||||
):
|
||||
from prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data import (
|
||||
objectstorage_container_metadata_sensitive_data,
|
||||
)
|
||||
|
||||
check = objectstorage_container_metadata_sensitive_data()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].check_metadata.Severity == Severity.critical
|
||||
assert "confirmed to be live" in result[0].status_extended
|
||||
assert mock_scan.call_args.kwargs.get("validate") is True
|
||||
|
||||
Reference in New Issue
Block a user