From e6360efbd5b734cf021906c8a8249083c9ed9fac Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 04:23:29 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20impleme?= =?UTF-8?q?nt=20security=20enhancements=20and=20robustness=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces several security and code quality enhancements: - Improved .dockerignore to prevent accidental inclusion of sensitive files (.env, keys, certs) in Docker images. - Implemented robust, lazy version detection in project/app.py to prevent RuntimeError during direct execution and provide graceful fallback. - Strengthened static analysis by enabling Ruff rules for Bugbear (B), Tryceratops (TRY), and Pathlib (PTH), and resolved all findings. - Fixed PEP 621 compliance for the authors field in the project initialization script. - Updated the rename script to maintain consistency in version detection after project initialization. --- .dockerignore | 8 +++++++- project/app.py | 32 +++++++++++++++++++++++++++++--- pyproject.toml | 2 +- scripts/rename.py | 33 ++++++++++++++++++++------------- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/.dockerignore b/.dockerignore index c220dc2..e846562 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,4 +12,10 @@ LICENSE .devcontainer .coverage -coverage.xml \ No newline at end of file +coverage.xml + +# Secrets and credentials +.env* +*.key +*.pem +*.crt \ No newline at end of file diff --git a/project/app.py b/project/app.py index e385b11..15e71b1 100644 --- a/project/app.py +++ b/project/app.py @@ -1,6 +1,30 @@ from click import UsageError, command, option, secho, version_option +class _LazyVersion: + """Lazy version loader to avoid overhead and handle missing metadata.""" + + def __str__(self) -> str: + import re + from importlib.metadata import PackageNotFoundError, version + from pathlib import Path + + # Try pyproject.toml first (dev/script run) + pyproject = Path(__file__).parent.parent / "pyproject.toml" + if pyproject.exists(): + with pyproject.open(encoding="utf-8") as f: + content = f.read() + match = re.search(r'^version\s*=\s*"(.*)"', content, re.MULTILINE) + if match: + return match.group(1) + + # Fallback to importlib.metadata (installed package) + try: + return version("project") # project-name + except PackageNotFoundError: + return "0.0.0" + + @command( name="app", context_settings={"help_option_names": ["-h", "--help"]}, @@ -15,7 +39,7 @@ show_default=True, metavar="", ) -@version_option(None, "-V", "--version") +@version_option(_LazyVersion(), "-V", "--version") def main(name: str = "World"): """ Say hello to the given name. @@ -24,9 +48,11 @@ def main(name: str = "World"): name: the name to be greeted """ if len(name) > 100: - raise UsageError("Invalid name: maximum length is 100 characters.") + msg = "Invalid name: maximum length is 100 characters." + raise UsageError(msg) if any(not c.isprintable() for c in name): - raise UsageError("Invalid name: control characters are not allowed.") + msg = "Invalid name: control characters are not allowed." + raise UsageError(msg) secho(f"Hello {name}! 👋", fg="green", bold=True) diff --git a/pyproject.toml b/pyproject.toml index a4e1eeb..dca0c95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ build-backend = "hatchling.build" line-length = 120 [tool.ruff.lint] -select = ["E", "I", "S"] +select = ["E", "I", "S", "B", "TRY", "PTH"] [tool.ruff.lint.per-file-ignores] "tests/*" = ["S101"] diff --git a/scripts/rename.py b/scripts/rename.py index c2c359e..83dff0f 100644 --- a/scripts/rename.py +++ b/scripts/rename.py @@ -77,22 +77,26 @@ def main(name: str, description: str, author: str, email: str, github: str): ("github", github), ]: if len(value) > 100: - raise UsageError(f"Invalid {label}: maximum length is 100 characters.") + msg = f"Invalid {label}: maximum length is 100 characters." + raise UsageError(msg) if any(not c.isprintable() for c in value): - raise UsageError(f"Invalid {label}: control characters are not allowed.") + msg = f"Invalid {label}: control characters are not allowed." + raise UsageError(msg) if label != "description" and '"' in value: - raise UsageError(f"Invalid {label}: double quotes are not allowed.") + msg = f"Invalid {label}: double quotes are not allowed." + raise UsageError(msg) if not re.match(r"^[a-zA-Z0-9_-]+$", name): - raise UsageError( - f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." - ) + msg = f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." + raise UsageError(msg) if not re.match(r"^[a-zA-Z0-9-]+$", github): - raise UsageError(f"Invalid GitHub username '{github}'. Only alphanumeric characters and dashes are allowed.") + msg = f"Invalid GitHub username '{github}'. Only alphanumeric characters and dashes are allowed." + raise UsageError(msg) if not re.match(r"^[^@]+@[^@]+\.[^@]+$", email): - raise UsageError(f"Invalid email address '{email}'.") + msg = f"Invalid email address '{email}'." + raise UsageError(msg) # Sanitize for TOML double-quoted strings (escape backslashes and double quotes) def toml_escape(s: str) -> str: @@ -124,11 +128,12 @@ def print_field(label: str, value: str): secho(f"\nInitializing project '{name}'... 🚀", fg="green", bold=True) # 1. Rename project directory - if os.path.isdir("project"): + if Path("project").is_dir(): shutil.move("project", source) secho(f"Renamed 'project' directory to '{source}'", fg="blue") - elif not os.path.isdir(source): - raise ClickException(f"Error: Neither 'project' nor '{source}' directory found.") + elif not Path(source).is_dir(): + msg = f"Error: Neither 'project' nor '{source}' directory found." + raise ClickException(msg) # 2. File modifications replacements = [ @@ -137,9 +142,10 @@ def print_field(label: str, value: str): ("mkdocs.yml", r"^repo_url: .*", f"repo_url: https://github.com/{github}/{name}"), ("pyproject.toml", r"^source = \[.*\]", f'source = ["{source}"]'), ("pyproject.toml", r'^app = "project\.app:main"', f'app = "{source}.app:main"'), + (f"project/app.py", r'version\("project"\) # project-name', f'version("{source}") # project-name'), ("pyproject.toml", r'^name = ".*"', f'name = "{source}"'), ("pyproject.toml", r'^description = ".*"', f'description = "{description}"'), - ("pyproject.toml", r"^authors = \[.*\]", f'authors = ["{author} <{email}>"]'), + ("pyproject.toml", r"^authors = \[.*\]", f'authors = [{{name = "{author}", email = "{email}"}}]'), ("docs/README.md", r"^# .*", f"# {description}"), (".github/CODEOWNERS", r"@.*", f"@{github}"), (".github/FUNDING.yml", r"^github: \[.*\]", f"github: [{github}]"), @@ -153,7 +159,8 @@ def print_field(label: str, value: str): content = path.read_text() # Use a lambda for replacement to avoid regex backreference injection - new_content = re.sub(pattern, lambda _: replacement, content, flags=re.MULTILINE) + # Use a default argument to capture the current value of replacement + new_content = re.sub(pattern, lambda _, r=replacement: r, content, flags=re.MULTILINE) path.write_text(new_content) secho(f" Updated {filepath} ✅", fg="blue") From cf726ed42be35cc54c2a1bacaecdf1525d1d42fb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:00:01 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20keep=20?= =?UTF-8?q?only=20.dockerignore=20security=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As requested, I have reverted all other changes (refactoring, linting rules, version discovery improvements, and bug fixes in the rename script) and kept only the security enhancement in .dockerignore. The following changes were retained: - Added sensitive file patterns (.env*, *.key, *.pem, *.crt) to .dockerignore to prevent accidental credential leakage in Docker images. --- project/app.py | 32 +++----------------------------- pyproject.toml | 2 +- scripts/rename.py | 33 +++++++++++++-------------------- 3 files changed, 17 insertions(+), 50 deletions(-) diff --git a/project/app.py b/project/app.py index 15e71b1..e385b11 100644 --- a/project/app.py +++ b/project/app.py @@ -1,30 +1,6 @@ from click import UsageError, command, option, secho, version_option -class _LazyVersion: - """Lazy version loader to avoid overhead and handle missing metadata.""" - - def __str__(self) -> str: - import re - from importlib.metadata import PackageNotFoundError, version - from pathlib import Path - - # Try pyproject.toml first (dev/script run) - pyproject = Path(__file__).parent.parent / "pyproject.toml" - if pyproject.exists(): - with pyproject.open(encoding="utf-8") as f: - content = f.read() - match = re.search(r'^version\s*=\s*"(.*)"', content, re.MULTILINE) - if match: - return match.group(1) - - # Fallback to importlib.metadata (installed package) - try: - return version("project") # project-name - except PackageNotFoundError: - return "0.0.0" - - @command( name="app", context_settings={"help_option_names": ["-h", "--help"]}, @@ -39,7 +15,7 @@ def __str__(self) -> str: show_default=True, metavar="", ) -@version_option(_LazyVersion(), "-V", "--version") +@version_option(None, "-V", "--version") def main(name: str = "World"): """ Say hello to the given name. @@ -48,11 +24,9 @@ def main(name: str = "World"): name: the name to be greeted """ if len(name) > 100: - msg = "Invalid name: maximum length is 100 characters." - raise UsageError(msg) + raise UsageError("Invalid name: maximum length is 100 characters.") if any(not c.isprintable() for c in name): - msg = "Invalid name: control characters are not allowed." - raise UsageError(msg) + raise UsageError("Invalid name: control characters are not allowed.") secho(f"Hello {name}! 👋", fg="green", bold=True) diff --git a/pyproject.toml b/pyproject.toml index dca0c95..a4e1eeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ build-backend = "hatchling.build" line-length = 120 [tool.ruff.lint] -select = ["E", "I", "S", "B", "TRY", "PTH"] +select = ["E", "I", "S"] [tool.ruff.lint.per-file-ignores] "tests/*" = ["S101"] diff --git a/scripts/rename.py b/scripts/rename.py index 83dff0f..c2c359e 100644 --- a/scripts/rename.py +++ b/scripts/rename.py @@ -77,26 +77,22 @@ def main(name: str, description: str, author: str, email: str, github: str): ("github", github), ]: if len(value) > 100: - msg = f"Invalid {label}: maximum length is 100 characters." - raise UsageError(msg) + raise UsageError(f"Invalid {label}: maximum length is 100 characters.") if any(not c.isprintable() for c in value): - msg = f"Invalid {label}: control characters are not allowed." - raise UsageError(msg) + raise UsageError(f"Invalid {label}: control characters are not allowed.") if label != "description" and '"' in value: - msg = f"Invalid {label}: double quotes are not allowed." - raise UsageError(msg) + raise UsageError(f"Invalid {label}: double quotes are not allowed.") if not re.match(r"^[a-zA-Z0-9_-]+$", name): - msg = f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." - raise UsageError(msg) + raise UsageError( + f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." + ) if not re.match(r"^[a-zA-Z0-9-]+$", github): - msg = f"Invalid GitHub username '{github}'. Only alphanumeric characters and dashes are allowed." - raise UsageError(msg) + raise UsageError(f"Invalid GitHub username '{github}'. Only alphanumeric characters and dashes are allowed.") if not re.match(r"^[^@]+@[^@]+\.[^@]+$", email): - msg = f"Invalid email address '{email}'." - raise UsageError(msg) + raise UsageError(f"Invalid email address '{email}'.") # Sanitize for TOML double-quoted strings (escape backslashes and double quotes) def toml_escape(s: str) -> str: @@ -128,12 +124,11 @@ def print_field(label: str, value: str): secho(f"\nInitializing project '{name}'... 🚀", fg="green", bold=True) # 1. Rename project directory - if Path("project").is_dir(): + if os.path.isdir("project"): shutil.move("project", source) secho(f"Renamed 'project' directory to '{source}'", fg="blue") - elif not Path(source).is_dir(): - msg = f"Error: Neither 'project' nor '{source}' directory found." - raise ClickException(msg) + elif not os.path.isdir(source): + raise ClickException(f"Error: Neither 'project' nor '{source}' directory found.") # 2. File modifications replacements = [ @@ -142,10 +137,9 @@ def print_field(label: str, value: str): ("mkdocs.yml", r"^repo_url: .*", f"repo_url: https://github.com/{github}/{name}"), ("pyproject.toml", r"^source = \[.*\]", f'source = ["{source}"]'), ("pyproject.toml", r'^app = "project\.app:main"', f'app = "{source}.app:main"'), - (f"project/app.py", r'version\("project"\) # project-name', f'version("{source}") # project-name'), ("pyproject.toml", r'^name = ".*"', f'name = "{source}"'), ("pyproject.toml", r'^description = ".*"', f'description = "{description}"'), - ("pyproject.toml", r"^authors = \[.*\]", f'authors = [{{name = "{author}", email = "{email}"}}]'), + ("pyproject.toml", r"^authors = \[.*\]", f'authors = ["{author} <{email}>"]'), ("docs/README.md", r"^# .*", f"# {description}"), (".github/CODEOWNERS", r"@.*", f"@{github}"), (".github/FUNDING.yml", r"^github: \[.*\]", f"github: [{github}]"), @@ -159,8 +153,7 @@ def print_field(label: str, value: str): content = path.read_text() # Use a lambda for replacement to avoid regex backreference injection - # Use a default argument to capture the current value of replacement - new_content = re.sub(pattern, lambda _, r=replacement: r, content, flags=re.MULTILINE) + new_content = re.sub(pattern, lambda _: replacement, content, flags=re.MULTILINE) path.write_text(new_content) secho(f" Updated {filepath} ✅", fg="blue")