mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(awslambda): add secrets_ignore_files to skip false-positive files (e.g. *.deps.json) in no-secrets-in-code check (#11222)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
committed by
GitHub
parent
af6918d57b
commit
fd38a0ac03
@@ -26,6 +26,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
- Replaced the `detect-secrets` library with [Kingfisher](https://github.com/mongodb/kingfisher) as the engine for the secret-scanning checks; scans run fully offline by default and obvious placeholder values are no longer reported as findings [(#11694)](https://github.com/prowler-cloud/prowler/pull/11694)
|
||||
- Removed the `detect_secrets_plugins` configuration option, which is no longer used by the new secret-scanning engine [(#11694)](https://github.com/prowler-cloud/prowler/pull/11694)
|
||||
- `awslambda_function_no_secrets_in_code` now supports a `secrets_ignore_files` audit-config option to skip files inside the deployment package by glob pattern (e.g. `*.deps.json`), suppressing .NET dependency-manifest false positives without masking real secrets [(#11222)](https://github.com/prowler-cloud/prowler/pull/11222)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
|
||||
@@ -449,6 +449,20 @@ aws:
|
||||
# Patterns to ignore in the secrets checks
|
||||
secrets_ignore_patterns: []
|
||||
|
||||
# aws.awslambda_function_no_secrets_in_code
|
||||
# Glob patterns of file names inside the Lambda deployment package to skip
|
||||
# when scanning for secrets. Useful to suppress known false positives such
|
||||
# as .NET dependency manifests.
|
||||
# Example:
|
||||
# secrets_ignore_files:
|
||||
# - "*.deps.json"
|
||||
# WARNING: use at your own risk. Any file whose name matches one of these
|
||||
# patterns is fully excluded from secret scanning, so a real secret placed
|
||||
# in such a file will NOT be detected. Keep patterns as narrow and specific
|
||||
# as possible; this is not recommended unless you have confirmed the matched
|
||||
# files only ever contain false positives.
|
||||
secrets_ignore_files: []
|
||||
|
||||
# Validate discovered secrets by checking whether they are live against the
|
||||
# provider APIs. WARNING: this makes outbound network calls that authenticate
|
||||
# with the discovered secret itself; the credential is exercised against the
|
||||
|
||||
@@ -426,6 +426,7 @@ class AWSProviderConfig(ProviderConfigBase):
|
||||
|
||||
# --- Secrets ---------------------------------------------------------
|
||||
secrets_ignore_patterns: Optional[list[str]] = None
|
||||
secrets_ignore_files: Optional[list[str]] = None
|
||||
secrets_validate: Optional[bool] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
|
||||
+27
-12
@@ -1,3 +1,4 @@
|
||||
import fnmatch
|
||||
import os
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
@@ -20,14 +21,20 @@ class awslambda_function_no_secrets_in_code(Check):
|
||||
secrets_ignore_patterns = awslambda_client.audit_config.get(
|
||||
"secrets_ignore_patterns", []
|
||||
)
|
||||
# Glob patterns of file names inside the deployment package to skip
|
||||
# when scanning for secrets (e.g. "*.deps.json" for .NET Lambdas).
|
||||
secrets_ignore_files = (
|
||||
awslambda_client.audit_config.get("secrets_ignore_files", []) or []
|
||||
)
|
||||
validate = awslambda_client.audit_config.get("secrets_validate", False)
|
||||
|
||||
# Scan the top-level files of every function's package in batched
|
||||
# Scan files of every function's package in batched
|
||||
# Kingfisher invocations instead of one subprocess per file per function.
|
||||
# Each package is extracted one at a time and its top-level files are
|
||||
# Each package is extracted one at a time and its files are
|
||||
# read (byte-faithfully via latin-1) before the extraction is released,
|
||||
# so only a single package is on disk at a time. Findings are keyed by
|
||||
# (function index, file name) so they can be grouped back per function.
|
||||
# (function index, package-relative file name) so they can be grouped
|
||||
# back per function.
|
||||
functions_with_code = []
|
||||
|
||||
def code_payloads():
|
||||
@@ -38,15 +45,23 @@ class awslambda_function_no_secrets_in_code(Check):
|
||||
functions_with_code.append(function)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_name:
|
||||
function_code.code_zip.extractall(tmp_dir_name)
|
||||
for file_name in next(os.walk(tmp_dir_name))[2]:
|
||||
try:
|
||||
with open(
|
||||
os.path.join(tmp_dir_name, file_name), "rb"
|
||||
) as code_file:
|
||||
content = code_file.read().decode("latin-1")
|
||||
except Exception:
|
||||
continue
|
||||
yield (index, file_name), content
|
||||
for root, _, files in os.walk(tmp_dir_name):
|
||||
for file_name in files:
|
||||
file_path = os.path.join(root, file_name)
|
||||
relative_file_path = os.path.relpath(
|
||||
file_path, tmp_dir_name
|
||||
)
|
||||
if any(
|
||||
fnmatch.fnmatch(relative_file_path, pattern)
|
||||
for pattern in secrets_ignore_files
|
||||
):
|
||||
continue
|
||||
try:
|
||||
with open(file_path, "rb") as code_file:
|
||||
content = code_file.read().decode("latin-1")
|
||||
except Exception:
|
||||
continue
|
||||
yield (index, relative_file_path), content
|
||||
|
||||
scan_error = None
|
||||
try:
|
||||
|
||||
@@ -176,6 +176,29 @@ class Test_AWS_Enums:
|
||||
assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == {}
|
||||
|
||||
|
||||
class Test_AWS_Secrets_Ignore_Files:
|
||||
def test_valid_file_patterns_round_trip(self):
|
||||
files = ["*.deps.json", "vendor/*.js"]
|
||||
assert _validate({"secrets_ignore_files": files}) == {
|
||||
"secrets_ignore_files": files
|
||||
}
|
||||
|
||||
def test_empty_list_is_valid(self):
|
||||
assert _validate({"secrets_ignore_files": []}) == {"secrets_ignore_files": []}
|
||||
|
||||
def test_exposed_in_scan_config_schema(self):
|
||||
aws_properties = SCAN_CONFIG_SCHEMA["properties"]["aws"]["properties"]
|
||||
|
||||
assert aws_properties["secrets_ignore_files"] == {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"default": None,
|
||||
"title": "Secrets Ignore Files",
|
||||
}
|
||||
|
||||
|
||||
class Test_AWS_Booleans:
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
|
||||
+180
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import zipfile
|
||||
from unittest import mock
|
||||
|
||||
@@ -53,6 +54,57 @@ def get_lambda_code_with_secrets(code):
|
||||
)
|
||||
|
||||
|
||||
LAMBDA_DEPS_JSON_WITH_SECRET = """
|
||||
{
|
||||
"runtimeTarget": { "name": ".NETCoreApp,Version=v8.0" },
|
||||
"libraries": {
|
||||
"AWSSDK.SecretsManager/3.7.0": {
|
||||
"type": "package",
|
||||
"password": "test-deps-json-password"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
LAMBDA_VENDOR_JS_WITH_SECRET = """
|
||||
const dbPassword = "test-vendor-password";
|
||||
"""
|
||||
|
||||
|
||||
def get_lambda_code_from_files(files: dict) -> LambdaCode:
|
||||
# The check only calls code_zip.extractall(dir); mock it to drop the
|
||||
# given files into the temporary directory the check creates, so no
|
||||
# real archive needs to be built.
|
||||
code_zip = mock.MagicMock()
|
||||
|
||||
def _extractall(path):
|
||||
for name, content in files.items():
|
||||
os.makedirs(os.path.dirname(f"{path}/{name}"), exist_ok=True)
|
||||
with open(f"{path}/{name}", "w") as fd:
|
||||
fd.write(content)
|
||||
|
||||
code_zip.extractall.side_effect = _extractall
|
||||
return LambdaCode(location="", code_zip=code_zip)
|
||||
|
||||
|
||||
def mock_get_function_code_with_deps_json_secret():
|
||||
yield create_lambda_function(), get_lambda_code_from_files(
|
||||
{
|
||||
"lambda_function.py": LAMBDA_FUNCTION_CODE_WITHOUT_SECRETS,
|
||||
"myapp.deps.json": LAMBDA_DEPS_JSON_WITH_SECRET,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def mock_get_function_code_with_nested_vendor_secret():
|
||||
yield create_lambda_function(), get_lambda_code_from_files(
|
||||
{
|
||||
"lambda_function.py": LAMBDA_FUNCTION_CODE_WITHOUT_SECRETS,
|
||||
"vendor/package.js": LAMBDA_VENDOR_JS_WITH_SECRET,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def mock_get_function_codewith_secrets():
|
||||
yield create_lambda_function(), get_lambda_code_with_secrets(
|
||||
LAMBDA_FUNCTION_CODE_WITH_SECRETS
|
||||
@@ -202,6 +254,133 @@ class Test_awslambda_function_no_secrets_in_code:
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_function_code_deps_json_secret_not_ignored(self):
|
||||
lambda_client = mock.MagicMock
|
||||
lambda_client.functions = {LAMBDA_FUNCTION_ARN: create_lambda_function()}
|
||||
lambda_client._get_function_code = mock_get_function_code_with_deps_json_secret
|
||||
lambda_client.audit_config = {
|
||||
"secrets_ignore_patterns": [],
|
||||
"secrets_ignore_files": None,
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
):
|
||||
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 == "FAIL"
|
||||
assert "myapp.deps.json" in result[0].status_extended
|
||||
|
||||
def test_function_code_nested_vendor_secret_not_ignored(self):
|
||||
lambda_client = mock.MagicMock
|
||||
lambda_client.functions = {LAMBDA_FUNCTION_ARN: create_lambda_function()}
|
||||
lambda_client._get_function_code = (
|
||||
mock_get_function_code_with_nested_vendor_secret
|
||||
)
|
||||
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,
|
||||
),
|
||||
):
|
||||
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 == "FAIL"
|
||||
assert "vendor/package.js" in result[0].status_extended
|
||||
|
||||
def test_function_code_nested_vendor_secret_ignored_by_file_pattern(self):
|
||||
lambda_client = mock.MagicMock
|
||||
lambda_client.functions = {LAMBDA_FUNCTION_ARN: create_lambda_function()}
|
||||
lambda_client._get_function_code = (
|
||||
mock_get_function_code_with_nested_vendor_secret
|
||||
)
|
||||
lambda_client.audit_config = {
|
||||
"secrets_ignore_patterns": [],
|
||||
"secrets_ignore_files": ["vendor/*.js"],
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
):
|
||||
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 == "PASS"
|
||||
|
||||
def test_function_code_deps_json_secret_ignored_by_file_pattern(self):
|
||||
lambda_client = mock.MagicMock
|
||||
lambda_client.functions = {LAMBDA_FUNCTION_ARN: create_lambda_function()}
|
||||
lambda_client._get_function_code = mock_get_function_code_with_deps_json_secret
|
||||
lambda_client.audit_config = {
|
||||
"secrets_ignore_patterns": [],
|
||||
"secrets_ignore_files": ["*.deps.json"],
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
):
|
||||
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].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == LAMBDA_FUNCTION_NAME
|
||||
assert result[0].resource_arn == LAMBDA_FUNCTION_ARN
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== 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
|
||||
|
||||
@@ -209,6 +388,7 @@ class Test_awslambda_function_no_secrets_in_code:
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user