feat: complete production workflow migration
This commit is contained in:
@@ -7,7 +7,9 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_EXCLUDED_DIRECTORIES = frozenset({"var", ".git", ".venv", ".learnings"})
|
||||
DEFAULT_EXCLUDED_DIRECTORIES = frozenset(
|
||||
{"var", ".git", ".venv", ".learnings", "build", "dist"}
|
||||
)
|
||||
|
||||
_ASSIGNMENT_PATTERN = re.compile(
|
||||
r"""
|
||||
@@ -21,7 +23,7 @@ _ASSIGNMENT_PATTERN = re.compile(
|
||||
access[_-]?token | refresh[_-]?token | auth[_-]?token
|
||||
)
|
||||
[\"']?
|
||||
\s*(?::|(?<![=!<>])=(?!=))\s*
|
||||
\s*(?P<separator>:|(?<![=!<>])=(?!=))\s*
|
||||
(?:
|
||||
\"(?P<double>[^\"\r\n]*)\" |
|
||||
'(?P<single>[^'\r\n]*)' |
|
||||
@@ -46,6 +48,32 @@ _NAMED_LONG_HEX_CREDENTIAL_PATTERN = re.compile(
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
_SENSITIVE_ENVIRONMENT_FALLBACK_PATTERN = re.compile(
|
||||
r"""
|
||||
[\"']?
|
||||
(?:[a-z0-9]+[_\-.])*
|
||||
(?:
|
||||
password | passwd | pwd |
|
||||
api[_-]?key | app[_-]?secret | client[_-]?secret | consumer[_-]?secret |
|
||||
secret[_-]?key |
|
||||
secret[_-]?access[_-]?key | access[_-]?key(?:[_-]?id)? |
|
||||
access[_-]?token | refresh[_-]?token | auth[_-]?token
|
||||
)
|
||||
[\"']?
|
||||
\s*(?::|(?<![=!<>])=(?!=))\s*
|
||||
[^\r\n]*?
|
||||
(?:os\.)?getenv\(
|
||||
\s*[\"'][^\"'\r\n]+[\"']\s*,\s*
|
||||
(?:\"(?P<double>[^\"\r\n]*)\"|'(?P<single>[^'\r\n]*)')
|
||||
\s*,?
|
||||
\s*\)
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
_SAFE_REFERENCE_PATTERN = re.compile(
|
||||
r"[A-Za-z_]\w*(?:(?:\.[A-Za-z_]\w*)|(?:\[(?:[\"'][^\"'\r\n]+[\"']|\d+)\]))+"
|
||||
)
|
||||
|
||||
_PLACEHOLDER_WORDS = frozenset(
|
||||
{
|
||||
@@ -98,7 +126,10 @@ def scan_repository(
|
||||
findings: list[SecretFinding] = []
|
||||
for directory, directory_names, file_names in os.walk(repository, followlinks=False):
|
||||
directory_names[:] = sorted(
|
||||
name for name in directory_names if name not in excluded_directories
|
||||
name
|
||||
for name in directory_names
|
||||
if name not in excluded_directories
|
||||
and not name.casefold().endswith(".egg-info")
|
||||
)
|
||||
current_directory = Path(directory)
|
||||
for file_name in sorted(file_names):
|
||||
@@ -128,15 +159,31 @@ def _read_text(path: Path) -> str | None:
|
||||
|
||||
def _scan_text(relative_file: str, text: str) -> list[SecretFinding]:
|
||||
findings: list[SecretFinding] = []
|
||||
long_hex_lines: set[int] = set()
|
||||
if relative_file.casefold().endswith((".py", ".pyw")):
|
||||
for match in _NAMED_LONG_HEX_CREDENTIAL_PATTERN.finditer(text):
|
||||
line_number = text.count("\n", 0, match.start()) + 1
|
||||
long_hex_lines.add(line_number)
|
||||
findings.append(
|
||||
SecretFinding(
|
||||
relative_file,
|
||||
text.count("\n", 0, match.start()) + 1,
|
||||
line_number,
|
||||
"hardcoded-long-hex-credential",
|
||||
)
|
||||
)
|
||||
for match in _SENSITIVE_ENVIRONMENT_FALLBACK_PATTERN.finditer(text):
|
||||
line_number = text.count("\n", 0, match.start()) + 1
|
||||
if (
|
||||
line_number not in long_hex_lines
|
||||
and _is_plaintext_value(match, allow_safe_reference=False)
|
||||
):
|
||||
findings.append(
|
||||
SecretFinding(
|
||||
relative_file,
|
||||
line_number,
|
||||
"plaintext-environment-fallback",
|
||||
)
|
||||
)
|
||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||
if _PRIVATE_KEY_PATTERN.search(line):
|
||||
findings.append(
|
||||
@@ -144,16 +191,43 @@ def _scan_text(relative_file: str, text: str) -> list[SecretFinding]:
|
||||
)
|
||||
if _CREDENTIAL_URL_PATTERN.search(line):
|
||||
findings.append(SecretFinding(relative_file, line_number, "credential-in-url"))
|
||||
if any(_is_plaintext_value(match) for match in _ASSIGNMENT_PATTERN.finditer(line)):
|
||||
assignments = (
|
||||
match
|
||||
for match in _ASSIGNMENT_PATTERN.finditer(line)
|
||||
if not _is_python_lambda_parameter(relative_file, line, match)
|
||||
)
|
||||
if any(_is_plaintext_value(match) for match in assignments):
|
||||
findings.append(
|
||||
SecretFinding(relative_file, line_number, "plaintext-credential")
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _is_plaintext_value(match: re.Match[str]) -> bool:
|
||||
def _is_python_lambda_parameter(
|
||||
relative_file: str,
|
||||
line: str,
|
||||
match: re.Match[str],
|
||||
) -> bool:
|
||||
if not relative_file.casefold().endswith((".py", ".pyw")):
|
||||
return False
|
||||
if match.groupdict().get("separator") != ":":
|
||||
return False
|
||||
prefix = line[: match.start()]
|
||||
lambda_position = prefix.rfind("lambda")
|
||||
return lambda_position >= 0 and ":" not in prefix[lambda_position:]
|
||||
|
||||
|
||||
def _is_plaintext_value(
|
||||
match: re.Match[str],
|
||||
*,
|
||||
allow_safe_reference: bool = True,
|
||||
) -> bool:
|
||||
value = next(
|
||||
(candidate for candidate in match.group("double", "single", "bare") if candidate is not None),
|
||||
(
|
||||
match.groupdict().get(name)
|
||||
for name in ("double", "single", "bare")
|
||||
if match.groupdict().get(name) is not None
|
||||
),
|
||||
"",
|
||||
).strip()
|
||||
if not value:
|
||||
@@ -167,7 +241,7 @@ def _is_plaintext_value(match: re.Match[str]) -> bool:
|
||||
return False
|
||||
if value.startswith(("$", "%(", "{{", "{")):
|
||||
return False
|
||||
if re.fullmatch(r"[a-z_]\w*(?:\.[a-z_]\w*)+", lowered):
|
||||
if allow_safe_reference and _SAFE_REFERENCE_PATTERN.fullmatch(value):
|
||||
return False
|
||||
if any(marker in lowered for marker in ("getenv(", "environ[", "secretmanager", "keyvault")):
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user