Skip to content

Commit e6360ef

Browse files
🛡️ Sentinel: implement security enhancements and robustness improvements
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.
1 parent 5d2556d commit e6360ef

4 files changed

Lines changed: 57 additions & 18 deletions

File tree

‎.dockerignore‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@ LICENSE
1212
.devcontainer
1313

1414
.coverage
15-
coverage.xml
15+
coverage.xml
16+
17+
# Secrets and credentials
18+
.env*
19+
*.key
20+
*.pem
21+
*.crt

‎project/app.py‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
from click import UsageError, command, option, secho, version_option
22

33

4+
class _LazyVersion:
5+
"""Lazy version loader to avoid overhead and handle missing metadata."""
6+
7+
def __str__(self) -> str:
8+
import re
9+
from importlib.metadata import PackageNotFoundError, version
10+
from pathlib import Path
11+
12+
# Try pyproject.toml first (dev/script run)
13+
pyproject = Path(__file__).parent.parent / "pyproject.toml"
14+
if pyproject.exists():
15+
with pyproject.open(encoding="utf-8") as f:
16+
content = f.read()
17+
match = re.search(r'^version\s*=\s*"(.*)"', content, re.MULTILINE)
18+
if match:
19+
return match.group(1)
20+
21+
# Fallback to importlib.metadata (installed package)
22+
try:
23+
return version("project") # project-name
24+
except PackageNotFoundError:
25+
return "0.0.0"
26+
27+
428
@command(
529
name="app",
630
context_settings={"help_option_names": ["-h", "--help"]},
@@ -15,7 +39,7 @@
1539
show_default=True,
1640
metavar="<name>",
1741
)
18-
@version_option(None, "-V", "--version")
42+
@version_option(_LazyVersion(), "-V", "--version")
1943
def main(name: str = "World"):
2044
"""
2145
Say hello to the given name.
@@ -24,9 +48,11 @@ def main(name: str = "World"):
2448
name: the name to be greeted
2549
"""
2650
if len(name) > 100:
27-
raise UsageError("Invalid name: maximum length is 100 characters.")
51+
msg = "Invalid name: maximum length is 100 characters."
52+
raise UsageError(msg)
2853
if any(not c.isprintable() for c in name):
29-
raise UsageError("Invalid name: control characters are not allowed.")
54+
msg = "Invalid name: control characters are not allowed."
55+
raise UsageError(msg)
3056

3157
secho(f"Hello {name}! 👋", fg="green", bold=True)
3258

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ build-backend = "hatchling.build"
3636
line-length = 120
3737

3838
[tool.ruff.lint]
39-
select = ["E", "I", "S"]
39+
select = ["E", "I", "S", "B", "TRY", "PTH"]
4040

4141
[tool.ruff.lint.per-file-ignores]
4242
"tests/*" = ["S101"]

‎scripts/rename.py‎

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,22 +77,26 @@ def main(name: str, description: str, author: str, email: str, github: str):
7777
("github", github),
7878
]:
7979
if len(value) > 100:
80-
raise UsageError(f"Invalid {label}: maximum length is 100 characters.")
80+
msg = f"Invalid {label}: maximum length is 100 characters."
81+
raise UsageError(msg)
8182
if any(not c.isprintable() for c in value):
82-
raise UsageError(f"Invalid {label}: control characters are not allowed.")
83+
msg = f"Invalid {label}: control characters are not allowed."
84+
raise UsageError(msg)
8385
if label != "description" and '"' in value:
84-
raise UsageError(f"Invalid {label}: double quotes are not allowed.")
86+
msg = f"Invalid {label}: double quotes are not allowed."
87+
raise UsageError(msg)
8588

8689
if not re.match(r"^[a-zA-Z0-9_-]+$", name):
87-
raise UsageError(
88-
f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed."
89-
)
90+
msg = f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed."
91+
raise UsageError(msg)
9092

9193
if not re.match(r"^[a-zA-Z0-9-]+$", github):
92-
raise UsageError(f"Invalid GitHub username '{github}'. Only alphanumeric characters and dashes are allowed.")
94+
msg = f"Invalid GitHub username '{github}'. Only alphanumeric characters and dashes are allowed."
95+
raise UsageError(msg)
9396

9497
if not re.match(r"^[^@]+@[^@]+\.[^@]+$", email):
95-
raise UsageError(f"Invalid email address '{email}'.")
98+
msg = f"Invalid email address '{email}'."
99+
raise UsageError(msg)
96100

97101
# Sanitize for TOML double-quoted strings (escape backslashes and double quotes)
98102
def toml_escape(s: str) -> str:
@@ -124,11 +128,12 @@ def print_field(label: str, value: str):
124128
secho(f"\nInitializing project '{name}'... 🚀", fg="green", bold=True)
125129

126130
# 1. Rename project directory
127-
if os.path.isdir("project"):
131+
if Path("project").is_dir():
128132
shutil.move("project", source)
129133
secho(f"Renamed 'project' directory to '{source}'", fg="blue")
130-
elif not os.path.isdir(source):
131-
raise ClickException(f"Error: Neither 'project' nor '{source}' directory found.")
134+
elif not Path(source).is_dir():
135+
msg = f"Error: Neither 'project' nor '{source}' directory found."
136+
raise ClickException(msg)
132137

133138
# 2. File modifications
134139
replacements = [
@@ -137,9 +142,10 @@ def print_field(label: str, value: str):
137142
("mkdocs.yml", r"^repo_url: .*", f"repo_url: https://github.com/{github}/{name}"),
138143
("pyproject.toml", r"^source = \[.*\]", f'source = ["{source}"]'),
139144
("pyproject.toml", r'^app = "project\.app:main"', f'app = "{source}.app:main"'),
145+
(f"project/app.py", r'version\("project"\) # project-name', f'version("{source}") # project-name'),
140146
("pyproject.toml", r'^name = ".*"', f'name = "{source}"'),
141147
("pyproject.toml", r'^description = ".*"', f'description = "{description}"'),
142-
("pyproject.toml", r"^authors = \[.*\]", f'authors = ["{author} <{email}>"]'),
148+
("pyproject.toml", r"^authors = \[.*\]", f'authors = [{{name = "{author}", email = "{email}"}}]'),
143149
("docs/README.md", r"^# .*", f"# {description}"),
144150
(".github/CODEOWNERS", r"@.*", f"@{github}"),
145151
(".github/FUNDING.yml", r"^github: \[.*\]", f"github: [{github}]"),
@@ -153,7 +159,8 @@ def print_field(label: str, value: str):
153159

154160
content = path.read_text()
155161
# Use a lambda for replacement to avoid regex backreference injection
156-
new_content = re.sub(pattern, lambda _: replacement, content, flags=re.MULTILINE)
162+
# Use a default argument to capture the current value of replacement
163+
new_content = re.sub(pattern, lambda _, r=replacement: r, content, flags=re.MULTILINE)
157164
path.write_text(new_content)
158165
secho(f" Updated {filepath} ✅", fg="blue")
159166

0 commit comments

Comments
 (0)