-
Notifications
You must be signed in to change notification settings - Fork 4
[RELEASE] v1.4.1 - security fixes, template inspection repairs #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Normalise line endings so template files are never checked out with CRLF. | ||
| # | ||
| # A generated project's shell scripts run inside Linux containers; a CRLF | ||
| # `scripts/pre-start.sh` makes the app container die with | ||
| # `env: 'bash\r': No such file or directory`. Contributors on Windows (or with | ||
| # `core.autocrlf=true`) would otherwise ship CRLF into the templates. | ||
|
|
||
| # Let Git decide for anything it detects as text, but never rewrite to CRLF. | ||
| * text=auto eol=lf | ||
|
|
||
| # Template payload files (the `-tpl` marker is stripped at generation time). | ||
| *-tpl text eol=lf | ||
| *.sh-tpl text eol=lf | ||
| Dockerfile-tpl text eol=lf | ||
| Makefile-tpl text eol=lf | ||
|
|
||
| # Scripts and container files in this repository itself. | ||
| *.sh text eol=lf | ||
| *.bash text eol=lf | ||
| Dockerfile text eol=lf | ||
| Makefile text eol=lf | ||
|
|
||
| # Binary assets must never be touched. | ||
| *.png binary | ||
| *.jpg binary | ||
| *.jpeg binary | ||
| *.gif binary | ||
| *.ico binary | ||
| *.pdf binary | ||
| *.zip binary | ||
| *.gz binary | ||
| *.woff binary | ||
| *.woff2 binary | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,20 +32,15 @@ jobs: | |
|
|
||
| - name: Run security scan on templates | ||
| id: scan | ||
| env: | ||
| TEMPLATES_INPUT: ${{ github.event.inputs.templates }} | ||
| run: | | ||
| TEMPLATE_DIR="src/fastapi_fastkit/fastapi_project_template" | ||
| RESULTS_FILE="security_scan_results.json" | ||
| SCAN_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | ||
|
|
||
| # Initialize results | ||
| echo '{' > $RESULTS_FILE | ||
| echo ' "scan_date": "'$SCAN_DATE'",' >> $RESULTS_FILE | ||
| echo ' "templates": [' >> $RESULTS_FILE | ||
|
|
||
| TEMPLATES_INPUT="${{ github.event.inputs.templates }}" | ||
| FIRST_TEMPLATE=true | ||
| TOTAL_VULNERABILITIES=0 | ||
| AFFECTED_TEMPLATES="" | ||
| TEMP_DIR=$(mktemp -d) | ||
| MANIFEST="$TEMP_DIR/manifest.txt" | ||
| : > "$MANIFEST" | ||
|
|
||
| for template_dir in $TEMPLATE_DIR/fastapi-*/; do | ||
| template_name=$(basename "$template_dir") | ||
|
|
@@ -65,44 +60,83 @@ jobs: | |
| temp_req=$(mktemp) | ||
| cp "$req_file" "$temp_req" | ||
|
|
||
| # Run pip-audit and capture output | ||
| audit_output=$(pip-audit -r "$temp_req" --format json 2>/dev/null || echo '[]') | ||
| rm "$temp_req" | ||
|
|
||
| # Count vulnerabilities | ||
| vuln_count=$(echo "$audit_output" | python3 -c "import sys, json; data = json.load(sys.stdin); print(len(data))" 2>/dev/null || echo "0") | ||
|
|
||
| if [ "$vuln_count" -gt 0 ]; then | ||
| TOTAL_VULNERABILITIES=$((TOTAL_VULNERABILITIES + vuln_count)) | ||
| AFFECTED_TEMPLATES="$AFFECTED_TEMPLATES $template_name" | ||
| echo "⚠️ Found $vuln_count vulnerabilities in $template_name" | ||
| else | ||
| echo "✅ No vulnerabilities in $template_name" | ||
| fi | ||
| audit_file="$TEMP_DIR/${template_name}.json" | ||
| err_file="$TEMP_DIR/${template_name}.err" | ||
|
|
||
| # Add to JSON | ||
| if [ "$FIRST_TEMPLATE" = true ]; then | ||
| FIRST_TEMPLATE=false | ||
| else | ||
| echo ' ,' >> $RESULTS_FILE | ||
| if ! pip-audit -r "$temp_req" --format json > "$audit_file" 2>"$err_file"; then | ||
| echo "⚠️ pip-audit failed for $template_name (recorded in results)" | ||
| fi | ||
| rm -f "$temp_req" | ||
|
|
||
| echo ' {' >> $RESULTS_FILE | ||
| echo ' "name": "'$template_name'",' >> $RESULTS_FILE | ||
| echo ' "vulnerability_count": '$vuln_count',' >> $RESULTS_FILE | ||
| echo ' "vulnerabilities": '$audit_output >> $RESULTS_FILE | ||
| echo ' }' >> $RESULTS_FILE | ||
| echo "$template_name" >> "$MANIFEST" | ||
| fi | ||
| done | ||
|
|
||
| echo ' ],' >> $RESULTS_FILE | ||
| echo ' "total_vulnerabilities": '$TOTAL_VULNERABILITIES',' >> $RESULTS_FILE | ||
| echo ' "affected_templates": "'$(echo $AFFECTED_TEMPLATES | xargs)'"' >> $RESULTS_FILE | ||
| echo '}' >> $RESULTS_FILE | ||
|
|
||
| # Set outputs for later steps | ||
| echo "total_vulnerabilities=$TOTAL_VULNERABILITIES" >> $GITHUB_OUTPUT | ||
| echo "affected_templates=$AFFECTED_TEMPLATES" >> $GITHUB_OUTPUT | ||
| # Assemble valid JSON results (and never do it via shell string concatenation) | ||
| TEMP_DIR="$TEMP_DIR" RESULTS_FILE="$RESULTS_FILE" SCAN_DATE="$SCAN_DATE" python3 - <<'PYEOF' | ||
| import json | ||
| import os | ||
|
|
||
| temp_dir = os.environ["TEMP_DIR"] | ||
| results_file = os.environ["RESULTS_FILE"] | ||
| scan_date = os.environ["SCAN_DATE"] | ||
|
|
||
| manifest_path = os.path.join(temp_dir, "manifest.txt") | ||
| with open(manifest_path) as f: | ||
| template_names = [line.strip() for line in f if line.strip()] | ||
|
|
||
| templates = [] | ||
| total_vulnerabilities = 0 | ||
| affected_templates = [] | ||
|
|
||
| for name in template_names: | ||
| audit_file = os.path.join(temp_dir, f"{name}.json") | ||
| err_file = os.path.join(temp_dir, f"{name}.err") | ||
|
|
||
| entry = {"template": name, "vulnerability_count": 0, "vulnerabilities": []} | ||
|
|
||
| try: | ||
| with open(audit_file) as af: | ||
| data = json.load(af) | ||
| dependencies = data.get("dependencies", []) | ||
| vulnerable_deps = [dep for dep in dependencies if dep.get("vulns")] | ||
| vuln_count = sum(len(dep.get("vulns", [])) for dep in dependencies) | ||
| entry["vulnerability_count"] = vuln_count | ||
| entry["vulnerabilities"] = vulnerable_deps | ||
| except Exception as exc: | ||
| error_msg = "" | ||
| if os.path.exists(err_file): | ||
| with open(err_file) as ef: | ||
| error_msg = ef.read().strip() | ||
| entry["error"] = error_msg or str(exc) | ||
|
|
||
| if entry["vulnerability_count"] > 0: | ||
| total_vulnerabilities += entry["vulnerability_count"] | ||
| affected_templates.append(name) | ||
| print(f"⚠️ Found {entry['vulnerability_count']} vulnerabilities in {name}") | ||
| elif "error" not in entry: | ||
| print(f"✅ No vulnerabilities in {name}") | ||
| else: | ||
| print(f"❌ pip-audit failed for {name}: {entry['error']}") | ||
|
|
||
| templates.append(entry) | ||
|
|
||
| results = { | ||
| "scan_date": scan_date, | ||
| "templates": templates, | ||
| "total_vulnerabilities": total_vulnerabilities, | ||
| "affected_templates": " ".join(affected_templates), | ||
| } | ||
|
|
||
| with open(results_file, "w") as rf: | ||
| json.dump(results, rf, indent=2) | ||
|
|
||
| github_output = os.environ.get("GITHUB_OUTPUT") | ||
| if github_output: | ||
| with open(github_output, "a") as gh: | ||
| gh.write(f"total_vulnerabilities={total_vulnerabilities}\n") | ||
| gh.write(f"affected_templates={' '.join(affected_templates)}\n") | ||
| PYEOF | ||
|
|
||
| - name: Upload scan results | ||
| uses: actions/upload-artifact@v4 | ||
|
|
@@ -127,15 +161,27 @@ jobs: | |
| const results = JSON.parse(fs.readFileSync('security_scan_results.json', 'utf8')); | ||
|
|
||
| results.templates.forEach(template => { | ||
| if (template.error) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| issueBody += `### ❌ ${template.template}\n\n`; | ||
| issueBody += `pip-audit failed to scan this template:\n\n`; | ||
| issueBody += "```\n" + template.error + "\n```\n\n"; | ||
| return; | ||
| } | ||
|
|
||
| if (template.vulnerability_count > 0) { | ||
| issueBody += `### ⚠️ ${template.name}\n\n`; | ||
| issueBody += `### ⚠️ ${template.template}\n\n`; | ||
| issueBody += `Found **${template.vulnerability_count}** vulnerabilities:\n\n`; | ||
| issueBody += `| Package | Installed | Fix Versions | Vulnerability ID |\n`; | ||
| issueBody += `|---------|-----------|--------------|------------------|\n`; | ||
|
|
||
| template.vulnerabilities.forEach(vuln => { | ||
| const fixVersions = vuln.fix_versions ? vuln.fix_versions.join(', ') : 'N/A'; | ||
| issueBody += `| ${vuln.name} | ${vuln.version} | ${fixVersions} | ${vuln.id} |\n`; | ||
| issueBody += `| Package | Installed | Fix Versions | Vulnerability ID / Aliases |\n`; | ||
| issueBody += `|---------|-----------|--------------|------------------------------|\n`; | ||
|
|
||
| template.vulnerabilities.forEach(dep => { | ||
| (dep.vulns || []).forEach(vuln => { | ||
| const fixVersions = (vuln.fix_versions && vuln.fix_versions.length) | ||
| ? vuln.fix_versions.join(', ') | ||
| : 'no fix available'; | ||
| const idAliases = [vuln.id, ...(vuln.aliases || [])].filter(Boolean).join(', '); | ||
| issueBody += `| ${dep.name} | ${dep.version} | ${fixVersions} | ${idAliases} |\n`; | ||
| }); | ||
| }); | ||
|
|
||
| issueBody += `\n`; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template | |
| │ Dependency 1 │ fastapi │ | ||
| │ Dependency 2 │ uvicorn │ | ||
| │ Dependency 3 │ pydantic │ | ||
| │ Dependency 4 │ python-jose │ | ||
| │ Dependency 5 │ passlib │ | ||
| │ Dependency 4 │ PyJWT │ | ||
| │ Dependency 5 │ pwdlib[argon2] │ | ||
| │ Dependency 6 │ python-multipart│ | ||
| │ Dependency 7 │ mcp │ | ||
| └──────────────┴────────────────┘ | ||
|
|
@@ -131,21 +131,21 @@ ai-integrated-api/ | |
| ```python | ||
| from datetime import datetime, timedelta | ||
| from typing import Optional, Dict, Any | ||
| from jose import JWTError, jwt | ||
| from passlib.context import CryptContext | ||
| import jwt | ||
| from pwdlib import PasswordHash | ||
|
|
||
| from src.core.config import settings | ||
|
|
||
| # Password hashing | ||
| pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | ||
| # Password hashing — argon2id, the recommended default for new applications. | ||
| password_hash = PasswordHash.recommended() | ||
|
|
||
| def verify_password(plain_password: str, hashed_password: str) -> bool: | ||
| """Password verification""" | ||
| return pwd_context.verify(plain_password, hashed_password) | ||
| return password_hash.verify(plain_password, hashed_password) | ||
|
|
||
| def get_password_hash(password: str) -> str: | ||
| """Password hashing""" | ||
| return pwd_context.hash(password) | ||
| return password_hash.hash(password) | ||
|
|
||
| def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: | ||
| """Access token generation""" | ||
|
|
@@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: | |
| algorithms=[settings.ALGORITHM] | ||
| ) | ||
| return payload | ||
| except JWTError: | ||
| except jwt.PyJWTError: | ||
| return None | ||
|
|
||
| def verify_token(token: str, token_type: str = "access") -> Optional[str]: | ||
|
|
@@ -452,8 +452,6 @@ user_db = UserDatabase() | |
| from typing import Optional, List | ||
| from fastapi import Depends, HTTPException, status, Security | ||
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader | ||
| from jose import JWTError | ||
|
|
||
| from src.auth.jwt_handler import decode_token, token_manager | ||
| from src.auth.models import User, UserInDB, Permission, user_db | ||
|
|
||
|
|
@@ -486,7 +484,7 @@ async def get_current_user( | |
| if user_id is None: | ||
| raise credentials_exception | ||
|
|
||
| except JWTError: | ||
| except jwt.PyJWTError: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This Useful? React with 👍 / 👎. |
||
| raise credentials_exception | ||
|
|
||
| user = user_db.get_user_by_id(user_id) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The broad
*-tpl textrule also matches supported binary payloads such aslogo.png-tpl; the later*.png binaryrule does not match that suffixed filename. On checkout Git will therefore normalize the PNG signature's CRLF bytes before the transducer can copy the file unchanged, corrupting such assets—the addedtest_binary_files_are_copied_untouchedcase assumes exactly this filename remains byte-for-byte intact. Add binary rules for the.<binary-extension>-tplforms or avoid forcing every template payload to text.Useful? React with 👍 / 👎.