Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 42 additions & 114 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
@@ -1,154 +1,82 @@
name: Security Scan

on:
pull_request_target:
types: [opened, synchronize, reopened]
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write
issues: write
checks: write
security-events: write
statuses: write

jobs:
security-scan:
runs-on: ubuntu-latest

steps:
- name: Checkout PR
- name: Checkout PR Code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit safety
pip install bandit pip-audit

- name: Run Bandit
run: |
echo "Running Bandit..."
bandit -r . -f txt -o bandit-results.txt || true

- name: Run pip-audit
run: |
echo "Running pip-audit..."
pip-audit -r requirements.txt > pip-audit-results.txt || true

- name: Run Security Scan
id: security_scan
env:
BANDIT_SKIP_IDS: ${{ secrets.BANDIT_SKIP_IDS }}
- name: Combine results
run: |
# Run bandit recursively on all Python files
echo "Running Bandit security scan..."

bandit -r . \
--severity-level medium \
--skip "${BANDIT_SKIP_IDS}" \
-f txt \
-o bandit-results.txt || true

# Run Safety check on requirements
if [ -f "requirements.txt" ]; then
echo "Checking dependencies with Safety..."
safety scan -r requirements.txt --output text > safety-results.txt || true
fi

# Combine results
echo "🔒 Security Scan Results" > security-scan-results.txt
echo "=========================" >> security-scan-results.txt
echo "" >> security-scan-results.txt

if [ -f "bandit-results.txt" ]; then
echo "Bandit Scan Results:" >> security-scan-results.txt
echo "-------------------" >> security-scan-results.txt
cat bandit-results.txt >> security-scan-results.txt
echo "" >> security-scan-results.txt
fi

if [ -f "safety-results.txt" ]; then
echo "Dependency Check Results:" >> security-scan-results.txt
echo "-----------------------" >> security-scan-results.txt
cat safety-results.txt >> security-scan-results.txt
fi

# Check for critical issues
if grep -iE "Severity\:\ High|Severity\:\ Critical" bandit-results.txt > /dev/null 2>&1; then
echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
elif [ -f "safety-results.txt" ] && grep -iE "critical" safety-results.txt > /dev/null 2>&1; then
echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
else
echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT
fi

- name: Create comment body
id: create-comment
if: always()
run: |
if [ -f security-scan-results.txt ]; then
SCAN_RESULTS=$(cat security-scan-results.txt)
if [ "${{ steps.security_scan.outputs.vulnerabilities_found }}" == "true" ]; then
echo 'comment_body<<EOF' >> $GITHUB_ENV
echo '## 🔒 Security Scan Results' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '```' >> $GITHUB_ENV
echo "$SCAN_RESULTS" >> $GITHUB_ENV
echo '```' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '⛔️ **Critical vulnerabilities detected. Please review and address these security issues before merging.**' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '### Next Steps:' >> $GITHUB_ENV
echo '1. Review each critical finding above and fix them according to OWASP top 10 mitigations.' >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
else
echo 'comment_body<<EOF' >> $GITHUB_ENV
echo '## 🔒 Security Scan Results' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '```' >> $GITHUB_ENV
echo "$SCAN_RESULTS" >> $GITHUB_ENV
echo '```' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '✅ **No critical security issues detected.**' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo 'The code has passed all critical security checks.' >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
fi
else
echo 'comment_body<<EOF' >> $GITHUB_ENV
echo '## 🔒 Security Scan Results' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '⚠️ **Error: The security scan failed to complete. Please review the workflow logs for more information.**' >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
fi
echo "" >> security-scan-results.txt
echo "📘 Bandit Scan:" >> security-scan-results.txt
echo "-------------------" >> security-scan-results.txt
cat bandit-results.txt >> security-scan-results.txt

- name: Comment PR
uses: peter-evans/create-or-update-comment@v4
if: always()
with:
issue-number: ${{ github.event.pull_request.number }}
body: ${{ env.comment_body }}
echo "" >> security-scan-results.txt
echo "📦 pip-audit:" >> security-scan-results.txt
echo "-------------------" >> security-scan-results.txt
cat pip-audit-results.txt >> security-scan-results.txt

- name: Upload scan artifacts
if: always()
- name: Upload scan results
uses: actions/upload-artifact@v4
with:
name: security-scan-results
path: |
security-scan-results.txt
bandit-results.txt
safety-results.txt
pip-audit-results.txt
retention-days: 5

- name: Fail if vulnerabilities found
if: steps.security_scan.outputs.vulnerabilities_found == 'true'
- name: Create PR comment body
id: comment_body
run: |
echo "::error::Critical security vulnerabilities were detected. Please review the findings and address them before merging."
exit 1
echo 'comment<<EOF' >> $GITHUB_ENV
echo '## 🔒 Security Scan Summary' >> $GITHUB_ENV
echo '' >> $GITHUB_ENV
echo '```' >> $GITHUB_ENV
cat security-scan-results.txt >> $GITHUB_ENV
echo '```' >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV

- name: Post Comment on PR
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: ${{ env.comment }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ __pycache__/
env/
.env
.ipynb_checkpoints/
*.db
logs/app.log
59 changes: 59 additions & 0 deletions api/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from datetime import datetime, timezone
from models.user import UserProfile
from models import db
from flask import Blueprint, jsonify, request
from flask import current_app as app

sync_bp = Blueprint('sync', __name__)


@sync_bp.route('/update', methods=['POST'])
def update_sync_time():
try:
user_id = request.json.get('user_id')
if not user_id:
app.logger.warning(f"[sync/update] missing user_id in payload")
return jsonify({'error' : 'Missing user_id'}), 400

user = db.session.get(UserProfile, user_id)
if not user:
app.logger.warning(f"[sync/update] user not found: {user_id}")
return jsonify({'error' : 'User not found'}), 404

user.last_synced = datetime.now(timezone.utc)
db.session.commit()
ts = user.last_synced.strftime('%Y-%m-%dT%H:%M:%SZ')
app.logger.info(f"[sync/update] updated last_synced for user {user_id}: {ts}")
return jsonify({'message' : 'Last sync time updated',
'user_id' : str(user_id),
'last_synced' : ts
}), 200
except Exception as e:
app.logger.error(f"[sync/update] error for user {user_id}: {e}", exc_info=True)
return jsonify({'error': 'Internal server error'}), 500



@sync_bp.route('/last', methods=['GET'])
def get_last_sync_time():
try:
user_id = request.args.get('user_id')
if not user_id:
app.logger.warning(f"[sync/last] missing user_id in query")
return jsonify({'error': 'Missing user_id'}), 400

user = db.session.get(UserProfile, user_id)
if not user:
app.logger.warning(f"[sync/last] user not found: {user_id}")
return jsonify({'error' : 'User not found'}), 404
ts = user.last_synced.strftime('%Y-%m-%dT%H:%M:%SZ') if user.last_synced else None
app.logger.info(f"[sync/last] fetched last_synced for user {user_id}: {ts}")
return jsonify({
'user_id' : str(user.id),
'last_synced' : ts
}), 200
except Exception as e:
app.logger.error(f"Error fetching sync status: {str(e)}")
return jsonify({'error': 'Internal server error'}), 500


24 changes: 24 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from flask import Flask, jsonify, session, render_template, request, redirect
from flask_migrate import Migrate
from flask_cors import CORS
from api.routes import api
from api.goals import goals_bp
Expand All @@ -8,18 +9,38 @@
from api.activity import activity_bp
from models import db
from dotenv import load_dotenv
from api.sync import sync_bp
from logging.handlers import RotatingFileHandler
import logging
import os
import pyrebase

# Import scripts here
from scripts.add_default_user import add_default_user

# ensure a folder for logs exists
os.makedirs('logs', exist_ok=True)

# Load environment variables from .env file
load_dotenv()

app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})


# set up a rotating file handler
file_handler = RotatingFileHandler(
'logs/app.log', maxBytes=10*1024, backupCount=5
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s in %(module)s: %(message)s'
))
file_handler.setLevel(logging.INFO)

app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info("🟢 App startup complete")

# Firebase configuration
config = {
'apiKey': os.getenv('FIREBASE_API_KEY'),
Expand All @@ -42,6 +63,8 @@

# Initialize database
db.init_app(app)
migrate = Migrate(app, db)

with app.app_context():
db.create_all()

Expand All @@ -53,6 +76,7 @@
app.register_blueprint(goals_bp, url_prefix='/api/goals')
app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard')
app.register_blueprint(profile_api, url_prefix='/api/profile')
app.register_blueprint(sync_bp, url_prefix='/api/synced')
app.register_blueprint(body_insight_bp, url_prefix='/api/body_insight')
app.register_blueprint(activity_bp, url_prefix='/api/activity')

Expand Down
27 changes: 27 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest
from app import app as flask_app, db
from models.user import UserProfile

@pytest.fixture
def app():
# tell Flask to use testing config & in‑memory sqlite
flask_app.config.update({
"TESTING": True,
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
})
with flask_app.app_context():
db.create_all()
yield flask_app
db.drop_all()

@pytest.fixture
def client(app):
return app.test_client()

@pytest.fixture
def sample_user(app):
u = UserProfile(name="Test", account="acct1", birthDate="2000-01-01", gender="F")
db.session.add(u)
db.session.commit()
return u
Loading