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:
Hugo Pereira Brito
2026-07-01 09:03:25 +01:00
committed by GitHub
parent af6918d57b
commit fd38a0ac03
6 changed files with 246 additions and 12 deletions
+1
View File
@@ -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
+14
View File
@@ -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
+1
View File
@@ -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=(
@@ -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:
+23
View File
@@ -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",
@@ -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",