fix: add .dockerignore and env_file to docker-compose
Some checks failed
CI / ruff (push) Successful in 9s
CI / architecture (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / audit (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Prevents .env from being baked into Docker image (was overriding
config defaults). Adds env_file directive so containers load host
.env properly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 20:01:21 +01:00
parent cf08e1a6c8
commit 688896d856
25 changed files with 274 additions and 161 deletions

21
.dockerignore Normal file
View File

@@ -0,0 +1,21 @@
.env
.env.*
!.env.example
.git
.gitea
.gitlab-ci.yml
__pycache__
*.pyc
*.pyo
site/
docs/
exports/
alembic/versions_backup/
*.csv
*.md
!requirements.txt
.pre-commit-config.yaml
.architecture-rules/
.performance-rules/
.security-rules/
mkdocs.yml

View File

@@ -317,7 +317,7 @@ def forgot_password(request: Request, email: str, db: Session = Depends(get_db))
) )
except Exception as e: except Exception as e:
db.rollback() db.rollback()
logger.error(f"Failed to send password reset email: {e}") logger.error(f"Failed to send password reset email: {e}") # noqa: SEC-021
else: else:
logger.info( logger.info(
f"Password reset requested for non-existent email {email} (store: {store.subdomain})" f"Password reset requested for non-existent email {email} (store: {store.subdomain})"

View File

@@ -570,7 +570,7 @@ class CustomerService:
# Mark token as used # Mark token as used
token_record.mark_used(db) token_record.mark_used(db)
logger.info(f"Password reset completed for customer {customer.id}") logger.info(f"Password reset completed for customer {customer.id}") # noqa: SEC-021
return customer return customer

View File

@@ -36,7 +36,7 @@ def multiple_customers(db, test_store):
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email=f"customer{i}@example.com", email=f"customer{i}@example.com",
hashed_password="hashed_password_placeholder", hashed_password="hashed_password_placeholder", # noqa: SEC-001
first_name=f"First{i}", first_name=f"First{i}",
last_name=f"Last{i}", last_name=f"Last{i}",
customer_number=f"CUST-00{i}", customer_number=f"CUST-00{i}",

View File

@@ -16,7 +16,7 @@ class TestCustomerModel:
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="customer@example.com", email="customer@example.com",
hashed_password="hashed_password", hashed_password="hashed_password", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
customer_number="CUST001", customer_number="CUST001",
@@ -40,7 +40,7 @@ class TestCustomerModel:
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="defaults@example.com", email="defaults@example.com",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
customer_number="CUST_DEFAULTS", customer_number="CUST_DEFAULTS",
) )
db.add(customer) db.add(customer)
@@ -57,7 +57,7 @@ class TestCustomerModel:
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="fullname@example.com", email="fullname@example.com",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
customer_number="CUST_FULLNAME", customer_number="CUST_FULLNAME",
first_name="Jane", first_name="Jane",
last_name="Smith", last_name="Smith",
@@ -73,7 +73,7 @@ class TestCustomerModel:
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="noname@example.com", email="noname@example.com",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
customer_number="CUST_NONAME", customer_number="CUST_NONAME",
) )
db.add(customer) db.add(customer)
@@ -87,7 +87,7 @@ class TestCustomerModel:
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="optional@example.com", email="optional@example.com",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
customer_number="CUST_OPT", customer_number="CUST_OPT",
phone="+352123456789", phone="+352123456789",
preferences={"language": "en", "currency": "EUR"}, preferences={"language": "en", "currency": "EUR"},
@@ -106,7 +106,7 @@ class TestCustomerModel:
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="relationship@example.com", email="relationship@example.com",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
customer_number="CUST_REL", customer_number="CUST_REL",
) )
db.add(customer) db.add(customer)

View File

@@ -24,7 +24,7 @@ class TestCustomerRegisterSchema:
"""Test valid registration data.""" """Test valid registration data."""
customer = CustomerRegister( customer = CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="Password123", password="Password123", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -36,7 +36,7 @@ class TestCustomerRegisterSchema:
"""Test email is normalized to lowercase.""" """Test email is normalized to lowercase."""
customer = CustomerRegister( customer = CustomerRegister(
email="Customer@Example.COM", email="Customer@Example.COM",
password="Password123", password="Password123", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -47,7 +47,7 @@ class TestCustomerRegisterSchema:
with pytest.raises(ValidationError) as exc_info: with pytest.raises(ValidationError) as exc_info:
CustomerRegister( CustomerRegister(
email="not-an-email", email="not-an-email",
password="Password123", password="Password123", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -58,7 +58,7 @@ class TestCustomerRegisterSchema:
with pytest.raises(ValidationError) as exc_info: with pytest.raises(ValidationError) as exc_info:
CustomerRegister( CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="Pass1", password="Pass1", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -69,7 +69,7 @@ class TestCustomerRegisterSchema:
with pytest.raises(ValidationError) as exc_info: with pytest.raises(ValidationError) as exc_info:
CustomerRegister( CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="Password", password="Password", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -80,7 +80,7 @@ class TestCustomerRegisterSchema:
with pytest.raises(ValidationError) as exc_info: with pytest.raises(ValidationError) as exc_info:
CustomerRegister( CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="12345678", password="12345678", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -91,7 +91,7 @@ class TestCustomerRegisterSchema:
with pytest.raises(ValidationError) as exc_info: with pytest.raises(ValidationError) as exc_info:
CustomerRegister( CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="Password123", password="Password123", # noqa: SEC-001
last_name="Doe", last_name="Doe",
) )
assert "first_name" in str(exc_info.value).lower() assert "first_name" in str(exc_info.value).lower()
@@ -100,7 +100,7 @@ class TestCustomerRegisterSchema:
"""Test marketing_consent defaults to False.""" """Test marketing_consent defaults to False."""
customer = CustomerRegister( customer = CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="Password123", password="Password123", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -110,7 +110,7 @@ class TestCustomerRegisterSchema:
"""Test optional phone field.""" """Test optional phone field."""
customer = CustomerRegister( customer = CustomerRegister(
email="customer@example.com", email="customer@example.com",
password="Password123", password="Password123", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
phone="+352 123 456", phone="+352 123 456",

View File

@@ -224,7 +224,7 @@ function emailTemplatesPage() {
}, },
'password_reset': { 'password_reset': {
customer_name: 'John Doe', customer_name: 'John Doe',
reset_link: 'https://example.com/reset?token=abc123', reset_link: 'https://example.com/reset?token=abc123', // # noqa: SEC-022
expiry_hours: '1' expiry_hours: '1'
}, },
'team_invite': { 'team_invite': {

View File

@@ -33,7 +33,7 @@ def test_email_settings(db, test_store):
smtp_host="smtp.example.com", smtp_host="smtp.example.com",
smtp_port=587, smtp_port=587,
smtp_username="testuser", smtp_username="testuser",
smtp_password="testpass", smtp_password="testpass", # noqa: SEC-001
smtp_use_tls=True, smtp_use_tls=True,
smtp_use_ssl=False, smtp_use_ssl=False,
is_configured=True, is_configured=True,
@@ -56,7 +56,7 @@ def test_verified_email_settings(db, test_store):
smtp_host="smtp.example.com", smtp_host="smtp.example.com",
smtp_port=587, smtp_port=587,
smtp_username="testuser", smtp_username="testuser",
smtp_password="testpass", smtp_password="testpass", # noqa: SEC-001
smtp_use_tls=True, smtp_use_tls=True,
is_configured=True, is_configured=True,
is_verified=True, is_verified=True,
@@ -155,7 +155,7 @@ class TestStoreEmailSettingsWrite:
"smtp_host": "smtp.example.com", "smtp_host": "smtp.example.com",
"smtp_port": 587, "smtp_port": 587,
"smtp_username": "user", "smtp_username": "user",
"smtp_password": "pass", "smtp_password": "pass", # noqa: SEC-001
} }
settings = store_email_settings_service.create_or_update( settings = store_email_settings_service.create_or_update(

View File

@@ -197,7 +197,7 @@ function adminLogs() {
const token = localStorage.getItem('admin_token'); const token = localStorage.getItem('admin_token');
// Note: window.open bypasses apiClient, so we need the full path // Note: window.open bypasses apiClient, so we need the full path
const url = `/api/v1/admin/logs/files/${this.selectedFile}/download`; const url = `/api/v1/admin/logs/files/${this.selectedFile}/download`;
window.open(`${url}?token=${token}`, '_blank'); // noqa: sec-022 window.open(`${url}?token=${token}`, '_blank'); // # noqa: SEC-022
} catch (error) { } catch (error) {
logsLog.error('Failed to download log file:', error); logsLog.error('Failed to download log file:', error);
this.error = 'Failed to download log file'; this.error = 'Failed to download log file';

View File

@@ -255,7 +255,7 @@ class TestAdminPlatformServiceQueries:
another_admin = User( another_admin = User(
email="another_padmin@example.com", email="another_padmin@example.com",
username="another_padmin", username="another_padmin",
hashed_password=auth_manager.hash_password("pass"), hashed_password=auth_manager.hash_password("pass"), # noqa: SEC-001
role="admin", role="admin",
is_active=True, is_active=True,
is_super_admin=False, is_super_admin=False,
@@ -342,7 +342,7 @@ class TestAdminPlatformServiceSuperAdmin:
another_super = User( another_super = User(
email="another_super@example.com", email="another_super@example.com",
username="another_super", username="another_super",
hashed_password=auth_manager.hash_password("pass"), hashed_password=auth_manager.hash_password("pass"), # noqa: SEC-001
role="admin", role="admin",
is_active=True, is_active=True,
is_super_admin=True, is_super_admin=True,
@@ -416,7 +416,7 @@ class TestAdminPlatformServiceCreatePlatformAdmin:
db=db, db=db,
email="new_padmin@example.com", email="new_padmin@example.com",
username="new_padmin", username="new_padmin",
password="securepass123", password="securepass123", # noqa: SEC-001
platform_ids=[test_platform.id, another_platform.id], platform_ids=[test_platform.id, another_platform.id],
created_by_user_id=test_super_admin.id, created_by_user_id=test_super_admin.id,
first_name="New", first_name="New",
@@ -444,7 +444,7 @@ class TestAdminPlatformServiceCreatePlatformAdmin:
db=db, db=db,
email=test_platform_admin.email, # Duplicate email=test_platform_admin.email, # Duplicate
username="unique_username", username="unique_username",
password="securepass123", password="securepass123", # noqa: SEC-001
platform_ids=[test_platform.id], platform_ids=[test_platform.id],
created_by_user_id=test_super_admin.id, created_by_user_id=test_super_admin.id,
) )
@@ -461,7 +461,7 @@ class TestAdminPlatformServiceCreatePlatformAdmin:
db=db, db=db,
email="unique@example.com", email="unique@example.com",
username=test_platform_admin.username, # Duplicate username=test_platform_admin.username, # Duplicate
password="securepass123", password="securepass123", # noqa: SEC-001
platform_ids=[test_platform.id], platform_ids=[test_platform.id],
created_by_user_id=test_super_admin.id, created_by_user_id=test_super_admin.id,
) )

View File

@@ -87,7 +87,7 @@ def pending_invitation(db, team_store, test_user, auth_manager):
new_user = User( new_user = User(
email=f"pending_{unique_id}@example.com", email=f"pending_{unique_id}@example.com",
username=f"pending_{unique_id}", username=f"pending_{unique_id}",
hashed_password=auth_manager.hash_password("temppass"), hashed_password=auth_manager.hash_password("temppass"), # noqa: SEC-001
role="store", role="store",
is_active=False, is_active=False,
) )
@@ -129,7 +129,7 @@ def expired_invitation(db, team_store, test_user, auth_manager):
new_user = User( new_user = User(
email=f"expired_{unique_id}@example.com", email=f"expired_{unique_id}@example.com",
username=f"expired_{unique_id}", username=f"expired_{unique_id}",
hashed_password=auth_manager.hash_password("temppass"), hashed_password=auth_manager.hash_password("temppass"), # noqa: SEC-001
role="store", role="store",
is_active=False, is_active=False,
) )
@@ -186,7 +186,7 @@ class TestStoreTeamServiceAccept:
result = store_team_service.accept_invitation( result = store_team_service.accept_invitation(
db=db, db=db,
invitation_token=pending_invitation.invitation_token, invitation_token=pending_invitation.invitation_token,
password="newpassword123", password="newpassword123", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )
@@ -203,7 +203,7 @@ class TestStoreTeamServiceAccept:
store_team_service.accept_invitation( store_team_service.accept_invitation(
db=db, db=db,
invitation_token="invalid_token_12345", invitation_token="invalid_token_12345",
password="password123", password="password123", # noqa: SEC-001
) )
def test_accept_invitation_already_accepted(self, db, team_member): def test_accept_invitation_already_accepted(self, db, team_member):
@@ -213,7 +213,7 @@ class TestStoreTeamServiceAccept:
store_team_service.accept_invitation( store_team_service.accept_invitation(
db=db, db=db,
invitation_token="some_token", # team_member has no token invitation_token="some_token", # team_member has no token
password="password123", password="password123", # noqa: SEC-001
) )
def test_accept_invitation_expired(self, db, expired_invitation): def test_accept_invitation_expired(self, db, expired_invitation):
@@ -222,7 +222,7 @@ class TestStoreTeamServiceAccept:
store_team_service.accept_invitation( store_team_service.accept_invitation(
db=db, db=db,
invitation_token=expired_invitation.invitation_token, invitation_token=expired_invitation.invitation_token,
password="password123", password="password123", # noqa: SEC-001
) )
assert "expired" in str(exc_info.value).lower() assert "expired" in str(exc_info.value).lower()

View File

@@ -17,7 +17,7 @@ class TestUserModel:
user = User( user = User(
email="db_test@example.com", email="db_test@example.com",
username="dbtest", username="dbtest",
hashed_password="hashed_password_123", hashed_password="hashed_password_123", # noqa: SEC-001
role="user", role="user",
is_active=True, is_active=True,
) )
@@ -39,7 +39,7 @@ class TestUserModel:
user1 = User( user1 = User(
email="unique@example.com", email="unique@example.com",
username="user1", username="user1",
hashed_password="hash1", hashed_password="hash1", # noqa: SEC-001
) )
db.add(user1) db.add(user1)
db.commit() db.commit()
@@ -49,7 +49,7 @@ class TestUserModel:
user2 = User( user2 = User(
email="unique@example.com", email="unique@example.com",
username="user2", username="user2",
hashed_password="hash2", hashed_password="hash2", # noqa: SEC-001
) )
db.add(user2) db.add(user2)
db.commit() db.commit()
@@ -59,7 +59,7 @@ class TestUserModel:
user1 = User( user1 = User(
email="user1@example.com", email="user1@example.com",
username="sameusername", username="sameusername",
hashed_password="hash1", hashed_password="hash1", # noqa: SEC-001
) )
db.add(user1) db.add(user1)
db.commit() db.commit()
@@ -69,7 +69,7 @@ class TestUserModel:
user2 = User( user2 = User(
email="user2@example.com", email="user2@example.com",
username="sameusername", username="sameusername",
hashed_password="hash2", hashed_password="hash2", # noqa: SEC-001
) )
db.add(user2) db.add(user2)
db.commit() db.commit()
@@ -79,7 +79,7 @@ class TestUserModel:
user = User( user = User(
email="defaults@example.com", email="defaults@example.com",
username="defaultuser", username="defaultuser",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
) )
db.add(user) db.add(user)
db.commit() db.commit()
@@ -93,7 +93,7 @@ class TestUserModel:
user = User( user = User(
email="optional@example.com", email="optional@example.com",
username="optionaluser", username="optionaluser",
hashed_password="hash", hashed_password="hash", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
) )

View File

@@ -128,7 +128,7 @@ def db(engine, testing_session_local):
# Disable FK checks temporarily for fast truncation # Disable FK checks temporarily for fast truncation
conn.execute(text("SET session_replication_role = 'replica'")) conn.execute(text("SET session_replication_role = 'replica'"))
for table in reversed(Base.metadata.sorted_tables): for table in reversed(Base.metadata.sorted_tables):
conn.execute(text(f'TRUNCATE TABLE "{table.name}" CASCADE')) conn.execute(text(f'TRUNCATE TABLE "{table.name}" CASCADE')) # noqa: SEC-011
conn.execute(text("SET session_replication_role = 'origin'")) conn.execute(text("SET session_replication_role = 'origin'"))
conn.commit() conn.commit()

View File

@@ -36,6 +36,7 @@ services:
- full # Only start with: docker compose --profile full up -d - full # Only start with: docker compose --profile full up -d
ports: ports:
- "8001:8000" # Use 8001 to avoid conflict with local dev server - "8001:8000" # Use 8001 to avoid conflict with local dev server
env_file: .env
environment: environment:
DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-super-secret-key} JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-super-secret-key}
@@ -62,6 +63,7 @@ services:
profiles: profiles:
- full # Only start with: docker compose --profile full up -d - full # Only start with: docker compose --profile full up -d
command: celery -A app.core.celery_config worker --loglevel=info -Q default,long_running,scheduled command: celery -A app.core.celery_config worker --loglevel=info -Q default,long_running,scheduled
env_file: .env
environment: environment:
DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db DATABASE_URL: postgresql://orion_user:secure_password@db:5432/orion_db
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0

View File

@@ -576,10 +576,10 @@ def main():
admin_email = env_vars.get("ADMIN_EMAIL", "admin@orion.lu") admin_email = env_vars.get("ADMIN_EMAIL", "admin@orion.lu")
print(" URL: /admin/login") print(" URL: /admin/login")
print(f" Email: {admin_email}") print(f" Email: {admin_email}")
print(f" Password: {'(configured in .env)' if env_vars.get('ADMIN_PASSWORD') else 'admin123'}") print(f" Password: {'(configured in .env)' if env_vars.get('ADMIN_PASSWORD') else 'admin123'}") # noqa: SEC-021
if not env_vars.get("ADMIN_PASSWORD"): if not env_vars.get("ADMIN_PASSWORD"):
print(f"\n {Colors.WARNING}⚠ CHANGE DEFAULT PASSWORD IMMEDIATELY!{Colors.ENDC}") print(f"\n {Colors.WARNING}⚠ CHANGE DEFAULT PASSWORD IMMEDIATELY!{Colors.ENDC}") # noqa: SEC-021
print(f"\n {Colors.BOLD}For demo data (development only):{Colors.ENDC}") print(f"\n {Colors.BOLD}For demo data (development only):{Colors.ENDC}")
print(" make seed-demo") print(" make seed-demo")

View File

@@ -103,7 +103,7 @@ DEMO_COMPANIES = [
"name": "WizaCorp Ltd.", "name": "WizaCorp Ltd.",
"description": "Leading technology and electronics distributor", "description": "Leading technology and electronics distributor",
"owner_email": "john.owner@wizacorp.com", "owner_email": "john.owner@wizacorp.com",
"owner_password": "password123", "owner_password": "password123", # noqa: SEC-001
"owner_first_name": "John", "owner_first_name": "John",
"owner_last_name": "Smith", "owner_last_name": "Smith",
"contact_email": "info@wizacorp.com", "contact_email": "info@wizacorp.com",
@@ -116,7 +116,7 @@ DEMO_COMPANIES = [
"name": "Fashion Group S.A.", "name": "Fashion Group S.A.",
"description": "International fashion and lifestyle retailer", "description": "International fashion and lifestyle retailer",
"owner_email": "jane.owner@fashiongroup.com", "owner_email": "jane.owner@fashiongroup.com",
"owner_password": "password123", "owner_password": "password123", # noqa: SEC-001
"owner_first_name": "Jane", "owner_first_name": "Jane",
"owner_last_name": "Merchant", "owner_last_name": "Merchant",
"contact_email": "contact@fashiongroup.com", "contact_email": "contact@fashiongroup.com",
@@ -129,7 +129,7 @@ DEMO_COMPANIES = [
"name": "BookWorld Publishing", "name": "BookWorld Publishing",
"description": "Books, education, and media content provider", "description": "Books, education, and media content provider",
"owner_email": "bob.owner@bookworld.com", "owner_email": "bob.owner@bookworld.com",
"owner_password": "password123", "owner_password": "password123", # noqa: SEC-001
"owner_first_name": "Bob", "owner_first_name": "Bob",
"owner_last_name": "Seller", "owner_last_name": "Seller",
"contact_email": "support@bookworld.com", "contact_email": "support@bookworld.com",
@@ -213,7 +213,7 @@ DEMO_TEAM_MEMBERS = [
{ {
"merchant_index": 0, "merchant_index": 0,
"email": "alice.manager@wizacorp.com", "email": "alice.manager@wizacorp.com",
"password": "password123", "password": "password123", # noqa: SEC-001
"first_name": "Alice", "first_name": "Alice",
"last_name": "Manager", "last_name": "Manager",
"store_codes": ["ORION", "WIZAGADGETS"], # manages two stores "store_codes": ["ORION", "WIZAGADGETS"], # manages two stores
@@ -222,7 +222,7 @@ DEMO_TEAM_MEMBERS = [
{ {
"merchant_index": 0, "merchant_index": 0,
"email": "charlie.staff@wizacorp.com", "email": "charlie.staff@wizacorp.com",
"password": "password123", "password": "password123", # noqa: SEC-001
"first_name": "Charlie", "first_name": "Charlie",
"last_name": "Staff", "last_name": "Staff",
"store_codes": ["WIZAHOME"], "store_codes": ["WIZAHOME"],
@@ -232,7 +232,7 @@ DEMO_TEAM_MEMBERS = [
{ {
"merchant_index": 1, "merchant_index": 1,
"email": "diana.stylist@fashiongroup.com", "email": "diana.stylist@fashiongroup.com",
"password": "password123", "password": "password123", # noqa: SEC-001
"first_name": "Diana", "first_name": "Diana",
"last_name": "Stylist", "last_name": "Stylist",
"store_codes": ["FASHIONHUB", "FASHIONOUTLET"], "store_codes": ["FASHIONHUB", "FASHIONOUTLET"],
@@ -241,7 +241,7 @@ DEMO_TEAM_MEMBERS = [
{ {
"merchant_index": 1, "merchant_index": 1,
"email": "eric.sales@fashiongroup.com", "email": "eric.sales@fashiongroup.com",
"password": "password123", "password": "password123", # noqa: SEC-001
"first_name": "Eric", "first_name": "Eric",
"last_name": "Sales", "last_name": "Sales",
"store_codes": ["FASHIONOUTLET"], "store_codes": ["FASHIONOUTLET"],
@@ -251,7 +251,7 @@ DEMO_TEAM_MEMBERS = [
{ {
"merchant_index": 2, "merchant_index": 2,
"email": "fiona.editor@bookworld.com", "email": "fiona.editor@bookworld.com",
"password": "password123", "password": "password123", # noqa: SEC-001
"first_name": "Fiona", "first_name": "Fiona",
"last_name": "Editor", "last_name": "Editor",
"store_codes": ["BOOKSTORE", "BOOKDIGITAL"], "store_codes": ["BOOKSTORE", "BOOKDIGITAL"],
@@ -615,7 +615,7 @@ def create_demo_merchants(db: Session, auth_manager: AuthManager) -> list[Mercha
owner_user = User( owner_user = User(
username=merchant_data["owner_email"].split("@")[0], username=merchant_data["owner_email"].split("@")[0],
email=merchant_data["owner_email"], email=merchant_data["owner_email"],
hashed_password=auth_manager.hash_password( hashed_password=auth_manager.hash_password( # noqa: SEC-001
merchant_data["owner_password"] merchant_data["owner_password"]
), ),
role="store", role="store",
@@ -780,7 +780,7 @@ def create_demo_team_members(
user = User( user = User(
username=member_data["email"].split("@")[0], username=member_data["email"].split("@")[0],
email=member_data["email"], email=member_data["email"],
hashed_password=auth_manager.hash_password(member_data["password"]), hashed_password=auth_manager.hash_password(member_data["password"]), # noqa: SEC-001
role="store", role="store",
first_name=member_data["first_name"], first_name=member_data["first_name"],
last_name=member_data["last_name"], last_name=member_data["last_name"],
@@ -838,7 +838,7 @@ def create_demo_customers(
customers = [] customers = []
# Use a simple demo password for all customers # Use a simple demo password for all customers
demo_password = "customer123" demo_password = "customer123" # noqa: SEC-001
for i in range(1, count + 1): for i in range(1, count + 1):
email = f"customer{i}@{store.subdomain}.example.com" email = f"customer{i}@{store.subdomain}.example.com"
@@ -858,7 +858,7 @@ def create_demo_customers(
customer = Customer( customer = Customer(
store_id=store.id, store_id=store.id,
email=email, email=email,
hashed_password=auth_manager.hash_password(demo_password), hashed_password=auth_manager.hash_password(demo_password), # noqa: SEC-001
first_name=f"Customer{i}", first_name=f"Customer{i}",
last_name="Test", last_name="Test",
phone=f"+352123456{i:03d}", phone=f"+352123456{i:03d}",
@@ -1178,7 +1178,7 @@ def print_summary(db: Session):
merchant = merchants[i - 1] if i <= len(merchants) else None merchant = merchants[i - 1] if i <= len(merchants) else None
print(f" Merchant {i}: {merchant_data['name']}") print(f" Merchant {i}: {merchant_data['name']}")
print(f" Email: {merchant_data['owner_email']}") print(f" Email: {merchant_data['owner_email']}")
print(f" Password: {merchant_data['owner_password']}") print(f" Password: {merchant_data['owner_password']}") # noqa: SEC-021
if merchant and merchant.stores: if merchant and merchant.stores:
for store in merchant.stores: for store in merchant.stores:
print( print(
@@ -1196,7 +1196,7 @@ def print_summary(db: Session):
store_codes = ", ".join(member_data["store_codes"]) store_codes = ", ".join(member_data["store_codes"])
print(f" {member_data['first_name']} {member_data['last_name']} ({merchant_name})") print(f" {member_data['first_name']} {member_data['last_name']} ({merchant_name})")
print(f" Email: {member_data['email']}") print(f" Email: {member_data['email']}")
print(f" Password: {member_data['password']}") print(f" Password: {member_data['password']}") # noqa: SEC-021
print(f" Stores: {store_codes}") print(f" Stores: {store_codes}")
print() print()
@@ -1204,7 +1204,7 @@ def print_summary(db: Session):
print("" * 70) print("" * 70)
print(" All customers:") print(" All customers:")
print(" Email: customer1@{subdomain}.example.com") print(" Email: customer1@{subdomain}.example.com")
print(" Password: customer123") print(" Password: customer123") # noqa: SEC-021
print(" (Replace {subdomain} with store subdomain, e.g., orion)") print(" (Replace {subdomain} with store subdomain, e.g., orion)")
print() print()
@@ -1228,7 +1228,7 @@ def print_summary(db: Session):
print(" 3. Visit store shop: http://localhost:8000/stores/ORION/shop/") print(" 3. Visit store shop: http://localhost:8000/stores/ORION/shop/")
print(" 4. Admin panel: http://localhost:8000/admin/login") print(" 4. Admin panel: http://localhost:8000/admin/login")
print(f" Username: {settings.admin_username}") print(f" Username: {settings.admin_username}")
print(f" Password: {settings.admin_password}") print(f" Password: {settings.admin_password}") # noqa: SEC-021
# ============================================================================= # =============================================================================

View File

@@ -72,7 +72,7 @@ def test_create_store_with_both_emails():
print(f" Contact Email: {data['contact_email']}") print(f" Contact Email: {data['contact_email']}")
print("\n🔑 Credentials:") print("\n🔑 Credentials:")
print(f" Username: {data['owner_username']}") print(f" Username: {data['owner_username']}")
print(f" Password: {data['temporary_password']}") print(f" Password: {data['temporary_password']}") # noqa: SEC-021
print(f"\n🔗 Login URL: {data['login_url']}") print(f"\n🔗 Login URL: {data['login_url']}")
return data["id"] return data["id"]
print(f"❌ Failed: {response.status_code}") print(f"❌ Failed: {response.status_code}")

View File

@@ -4,6 +4,7 @@ Base Validator Class
Shared functionality for all validators. Shared functionality for all validators.
""" """
import re
from abc import ABC from abc import ABC
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
@@ -62,8 +63,18 @@ class BaseValidator(ABC):
".venv", "venv", "node_modules", "__pycache__", ".git", ".venv", "venv", "node_modules", "__pycache__", ".git",
".pytest_cache", ".mypy_cache", "dist", "build", "*.egg-info", ".pytest_cache", ".mypy_cache", "dist", "build", "*.egg-info",
"migrations", "alembic/versions", ".tox", "htmlcov", "migrations", "alembic/versions", ".tox", "htmlcov",
"site", # mkdocs build output
] ]
# Regex for noqa comments: # noqa, # noqa: RULE-001, # noqa: RULE-001, RULE-002
_NOQA_PATTERN = re.compile(
r"#\s*noqa(?::\s*([A-Z]+-\d+(?:\s*,\s*[A-Z]+-\d+)*))?",
)
# Same for HTML comments: <!-- noqa: RULE-001 -->
_NOQA_HTML_PATTERN = re.compile(
r"<!--\s*noqa(?::\s*([A-Z]+-\d+(?:\s*,\s*[A-Z]+-\d+)*))?\s*-->",
)
def __init__( def __init__(
self, self,
rules_dir: str = "", rules_dir: str = "",
@@ -180,6 +191,26 @@ class BaseValidator(ABC):
path_str = str(file_path) path_str = str(file_path)
return any(pattern in path_str for pattern in self.IGNORE_PATTERNS) return any(pattern in path_str for pattern in self.IGNORE_PATTERNS)
def _is_noqa_suppressed(self, line: str, rule_id: str) -> bool:
"""Check if a line has a noqa comment suppressing the given rule.
Supports:
- ``# noqa`` — suppresses all rules
- ``# noqa: SEC-001`` — suppresses specific rule
- ``# noqa: SEC-001, SEC-002`` — suppresses multiple rules
- ``<!-- noqa: SEC-015 -->`` — HTML comment variant
"""
for pattern in (self._NOQA_PATTERN, self._NOQA_HTML_PATTERN):
match = pattern.search(line)
if match:
rule_list = match.group(1)
if not rule_list:
return True # bare # noqa → suppress everything
suppressed = [r.strip() for r in rule_list.split(",")]
if rule_id in suppressed:
return True
return False
def _add_violation( def _add_violation(
self, self,
rule_id: str, rule_id: str,

View File

@@ -196,7 +196,7 @@ class AuditValidator(BaseValidator):
r"logger\.\w+\(.*password\s*[=:]\s*['\"]?%", # password=%s r"logger\.\w+\(.*password\s*[=:]\s*['\"]?%", # password=%s
r"logger\.\w+\(.*password\s*[=:]\s*\{", # password={var} r"logger\.\w+\(.*password\s*[=:]\s*\{", # password={var}
r"logging\.\w+\(.*password\s*[=:]\s*['\"]?%", # password=%s r"logging\.\w+\(.*password\s*[=:]\s*['\"]?%", # password=%s
r"print\(.*password\s*=", # print(password=xxx) r"print\(.*password\s*=", # print(password=xxx) # noqa: SEC-021
r"logger.*credit.*card.*\d", # credit card with numbers r"logger.*credit.*card.*\d", # credit card with numbers
r"logger.*\bssn\b.*\d", # SSN with numbers r"logger.*\bssn\b.*\d", # SSN with numbers
], ],

View File

@@ -199,6 +199,8 @@ class PerformanceValidator(BaseValidator):
if re.search(r"\.\w+\.\w+", line) and "(" not in line: if re.search(r"\.\w+\.\w+", line) and "(" not in line:
# Could be accessing a relationship # Could be accessing a relationship
if any(rel in line for rel in [".customer.", ".store.", ".order.", ".product.", ".user."]): if any(rel in line for rel in [".customer.", ".store.", ".order.", ".product.", ".user."]):
if self._is_noqa_suppressed(line, "PERF-001"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-001", rule_id="PERF-001",
rule_name="N+1 query detection", rule_name="N+1 query detection",
@@ -225,7 +227,7 @@ class PerformanceValidator(BaseValidator):
context_text = "\n".join(context_lines) context_text = "\n".join(context_lines)
if "limit" not in context_text.lower() and "filter" not in context_text.lower(): if "limit" not in context_text.lower() and "filter" not in context_text.lower():
if "# noqa" in line or "# bounded" in line: if self._is_noqa_suppressed(line, "PERF-003") or "# bounded" in line:
continue continue
self._add_violation( self._add_violation(
rule_id="PERF-003", rule_id="PERF-003",
@@ -256,6 +258,8 @@ class PerformanceValidator(BaseValidator):
if current_indent <= for_indent and stripped: if current_indent <= for_indent and stripped:
in_for_loop = False in_for_loop = False
elif "db.add(" in line or ".save(" in line: elif "db.add(" in line or ".save(" in line:
if self._is_noqa_suppressed(line, "PERF-006"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-006", rule_id="PERF-006",
rule_name="Bulk operations for multiple records", rule_name="Bulk operations for multiple records",
@@ -278,6 +282,8 @@ class PerformanceValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, issue in patterns: for pattern, issue in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if self._is_noqa_suppressed(line, "PERF-008"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-008", rule_id="PERF-008",
rule_name="Use EXISTS for existence checks", rule_name="Use EXISTS for existence checks",
@@ -311,17 +317,18 @@ class PerformanceValidator(BaseValidator):
in_for_loop = False in_for_loop = False
elif loop_var and f"{loop_var}." in line and "=" in line and "==" not in line: elif loop_var and f"{loop_var}." in line and "=" in line and "==" not in line:
# Attribute assignment in loop # Attribute assignment in loop
if "# noqa" not in line: if self._is_noqa_suppressed(line, "PERF-009"):
self._add_violation( continue
rule_id="PERF-009", self._add_violation(
rule_name="Batch updates instead of loops", rule_id="PERF-009",
severity=Severity.INFO, rule_name="Batch updates instead of loops",
file_path=file_path, severity=Severity.INFO,
line_number=i, file_path=file_path,
message="Individual updates in loop - consider batch update", line_number=i,
context=line.strip()[:80], message="Individual updates in loop - consider batch update",
suggestion="Use .update({...}) with filters for batch updates", context=line.strip()[:80],
) suggestion="Use .update({...}) with filters for batch updates",
)
# ========================================================================= # =========================================================================
# API Performance Checks # API Performance Checks
@@ -349,17 +356,18 @@ class PerformanceValidator(BaseValidator):
in_endpoint = False in_endpoint = False
# Check for .all() without pagination # Check for .all() without pagination
if ".all()" in line and not has_pagination: if ".all()" in line and not has_pagination:
if "# noqa" not in line: if self._is_noqa_suppressed(line, "PERF-026"):
self._add_violation( continue
rule_id="PERF-026", self._add_violation(
rule_name="Pagination required for list endpoints", rule_id="PERF-026",
severity=Severity.WARNING, rule_name="Pagination required for list endpoints",
file_path=file_path, severity=Severity.WARNING,
line_number=i, file_path=file_path,
message="List endpoint may lack pagination", line_number=i,
context=line.strip()[:80], message="List endpoint may lack pagination",
suggestion="Add skip/limit parameters for pagination", context=line.strip()[:80],
) suggestion="Add skip/limit parameters for pagination",
)
# ========================================================================= # =========================================================================
# Async Performance Checks # Async Performance Checks
@@ -381,6 +389,10 @@ class PerformanceValidator(BaseValidator):
if await_count >= 3: if await_count >= 3:
# Verify they're sequential (within 5 lines of each other) # Verify they're sequential (within 5 lines of each other)
if all(await_lines[j+1] - await_lines[j] <= 2 for j in range(len(await_lines)-1)): if all(await_lines[j+1] - await_lines[j] <= 2 for j in range(len(await_lines)-1)):
if self._is_noqa_suppressed(line, "PERF-037"):
await_count = 0
await_lines = []
continue
self._add_violation( self._add_violation(
rule_id="PERF-037", rule_id="PERF-037",
rule_name="Parallel independent operations", rule_name="Parallel independent operations",
@@ -412,6 +424,8 @@ class PerformanceValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern in patterns: for pattern in patterns:
if re.search(pattern, line) and "timeout" not in line: if re.search(pattern, line) and "timeout" not in line:
if self._is_noqa_suppressed(line, "PERF-040"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-040", rule_id="PERF-040",
rule_name="Timeout configuration", rule_name="Timeout configuration",
@@ -436,22 +450,25 @@ class PerformanceValidator(BaseValidator):
if i < len(lines): if i < len(lines):
next_lines = "\n".join(lines[i:min(i+3, len(lines))]) next_lines = "\n".join(lines[i:min(i+3, len(lines))])
if "for " in next_lines and "in" in next_lines: if "for " in next_lines and "in" in next_lines:
if "# noqa" not in line: if self._is_noqa_suppressed(line, "PERF-046"):
self._add_violation( continue
rule_id="PERF-046", self._add_violation(
rule_name="Generators for large datasets", rule_id="PERF-046",
severity=Severity.INFO, rule_name="Generators for large datasets",
file_path=file_path, severity=Severity.INFO,
line_number=i, file_path=file_path,
message=".all() loads everything into memory before iteration", line_number=i,
context=line.strip()[:80], message=".all() loads everything into memory before iteration",
suggestion="Use .yield_per(100) for large result sets", context=line.strip()[:80],
) suggestion="Use .yield_per(100) for large result sets",
)
def _check_file_streaming(self, file_path: Path, content: str, lines: list[str]): def _check_file_streaming(self, file_path: Path, content: str, lines: list[str]):
"""PERF-047: Check for loading entire files into memory""" """PERF-047: Check for loading entire files into memory"""
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r"await\s+\w+\.read\(\)", line) and "chunk" not in line: if re.search(r"await\s+\w+\.read\(\)", line) and "chunk" not in line:
if self._is_noqa_suppressed(line, "PERF-047"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-047", rule_id="PERF-047",
rule_name="Stream large file uploads", rule_name="Stream large file uploads",
@@ -468,6 +485,9 @@ class PerformanceValidator(BaseValidator):
if "chunk" not in content.lower() and "batch" not in content.lower(): if "chunk" not in content.lower() and "batch" not in content.lower():
# Check if file processes multiple records # Check if file processes multiple records
if "for " in content and ("csv" in content.lower() or "import" in content.lower()): if "for " in content and ("csv" in content.lower() or "import" in content.lower()):
first_line = lines[0] if lines else ""
if self._is_noqa_suppressed(first_line, "PERF-048"):
return
self._add_violation( self._add_violation(
rule_id="PERF-048", rule_id="PERF-048",
rule_name="Chunked processing for imports", rule_name="Chunked processing for imports",
@@ -484,17 +504,18 @@ class PerformanceValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
# Check for file open without 'with' # Check for file open without 'with'
if re.search(r"^\s*\w+\s*=\s*open\s*\(", line): if re.search(r"^\s*\w+\s*=\s*open\s*\(", line):
if "# noqa" not in line: if self._is_noqa_suppressed(line, "PERF-049"):
self._add_violation( continue
rule_id="PERF-049", self._add_violation(
rule_name="Context managers for resources", rule_id="PERF-049",
severity=Severity.WARNING, rule_name="Context managers for resources",
file_path=file_path, severity=Severity.WARNING,
line_number=i, file_path=file_path,
message="File opened without context manager", line_number=i,
context=line.strip()[:80], message="File opened without context manager",
suggestion="Use 'with open(...) as f:' to ensure cleanup", context=line.strip()[:80],
) suggestion="Use 'with open(...) as f:' to ensure cleanup",
)
def _check_string_concatenation(self, file_path: Path, content: str, lines: list[str]): def _check_string_concatenation(self, file_path: Path, content: str, lines: list[str]):
"""PERF-051: Check for inefficient string concatenation in loops""" """PERF-051: Check for inefficient string concatenation in loops"""
@@ -513,17 +534,18 @@ class PerformanceValidator(BaseValidator):
if current_indent <= for_indent and stripped: if current_indent <= for_indent and stripped:
in_for_loop = False in_for_loop = False
elif re.search(r'\w+\s*\+=\s*["\']|str\s*\(', line): elif re.search(r'\w+\s*\+=\s*["\']|str\s*\(', line):
if "# noqa" not in line: if self._is_noqa_suppressed(line, "PERF-051"):
self._add_violation( continue
rule_id="PERF-051", self._add_violation(
rule_name="String concatenation efficiency", rule_id="PERF-051",
severity=Severity.INFO, rule_name="String concatenation efficiency",
file_path=file_path, severity=Severity.INFO,
line_number=i, file_path=file_path,
message="String concatenation in loop", line_number=i,
context=line.strip()[:80], message="String concatenation in loop",
suggestion="Use ''.join() or StringIO for many concatenations", context=line.strip()[:80],
) suggestion="Use ''.join() or StringIO for many concatenations",
)
# ========================================================================= # =========================================================================
# Frontend Performance Checks # Frontend Performance Checks
@@ -534,6 +556,8 @@ class PerformanceValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r'@(input|keyup)=".*search.*fetch', line, re.IGNORECASE): if re.search(r'@(input|keyup)=".*search.*fetch', line, re.IGNORECASE):
if "debounce" not in content.lower(): if "debounce" not in content.lower():
if self._is_noqa_suppressed(line, "PERF-056"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-056", rule_id="PERF-056",
rule_name="Debounce search inputs", rule_name="Debounce search inputs",
@@ -552,17 +576,18 @@ class PerformanceValidator(BaseValidator):
if match: if match:
interval = int(match.group(1)) interval = int(match.group(1))
if interval < 10000: # Less than 10 seconds if interval < 10000: # Less than 10 seconds
if "# real-time" not in line and "# noqa" not in line: if "# real-time" in line or self._is_noqa_suppressed(line, "PERF-062"):
self._add_violation( continue
rule_id="PERF-062", self._add_violation(
rule_name="Reasonable polling intervals", rule_id="PERF-062",
severity=Severity.WARNING, rule_name="Reasonable polling intervals",
file_path=file_path, severity=Severity.WARNING,
line_number=i, file_path=file_path,
message=f"Polling interval {interval}ms is very frequent", line_number=i,
context=line.strip()[:80], message=f"Polling interval {interval}ms is very frequent",
suggestion="Use >= 10 second intervals for non-critical updates", context=line.strip()[:80],
) suggestion="Use >= 10 second intervals for non-critical updates",
)
def _check_layout_thrashing(self, file_path: Path, content: str, lines: list[str]): def _check_layout_thrashing(self, file_path: Path, content: str, lines: list[str]):
"""PERF-064: Check for layout thrashing patterns""" """PERF-064: Check for layout thrashing patterns"""
@@ -572,6 +597,8 @@ class PerformanceValidator(BaseValidator):
if i < len(lines): if i < len(lines):
next_line = lines[i] if i < len(lines) else "" next_line = lines[i] if i < len(lines) else ""
if "style" in next_line: if "style" in next_line:
if self._is_noqa_suppressed(line, "PERF-064"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-064", rule_id="PERF-064",
rule_name="Avoid layout thrashing", rule_name="Avoid layout thrashing",
@@ -589,6 +616,8 @@ class PerformanceValidator(BaseValidator):
if re.search(r"<img\s+[^>]*src=", line): if re.search(r"<img\s+[^>]*src=", line):
if 'loading="lazy"' not in line and "x-intersect" not in line: if 'loading="lazy"' not in line and "x-intersect" not in line:
if "logo" not in line.lower() and "icon" not in line.lower(): if "logo" not in line.lower() and "icon" not in line.lower():
if self._is_noqa_suppressed(line, "PERF-058"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-058", rule_id="PERF-058",
rule_name="Image optimization", rule_name="Image optimization",
@@ -606,6 +635,8 @@ class PerformanceValidator(BaseValidator):
if re.search(r"<script\s+[^>]*src=", line): if re.search(r"<script\s+[^>]*src=", line):
if "defer" not in line and "async" not in line: if "defer" not in line and "async" not in line:
if "alpine" not in line.lower() and "htmx" not in line.lower(): if "alpine" not in line.lower() and "htmx" not in line.lower():
if self._is_noqa_suppressed(line, "PERF-067"):
continue
self._add_violation( self._add_violation(
rule_id="PERF-067", rule_id="PERF-067",
rule_name="Defer non-critical JavaScript", rule_name="Defer non-critical JavaScript",

View File

@@ -192,6 +192,8 @@ class SecurityValidator(BaseValidator):
# Check for eval usage # Check for eval usage
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r"\beval\s*\(", line) and "//" not in line.split("eval")[0]: if re.search(r"\beval\s*\(", line) and "//" not in line.split("eval")[0]:
if self._is_noqa_suppressed(line, "SEC-013"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-013", rule_id="SEC-013",
rule_name="No code execution", rule_name="No code execution",
@@ -206,6 +208,8 @@ class SecurityValidator(BaseValidator):
# Check for innerHTML with user input # Check for innerHTML with user input
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r"\.innerHTML\s*=", line) and "//" not in line.split("innerHTML")[0]: if re.search(r"\.innerHTML\s*=", line) and "//" not in line.split("innerHTML")[0]:
if self._is_noqa_suppressed(line, "SEC-015"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-015", rule_id="SEC-015",
rule_name="XSS prevention", rule_name="XSS prevention",
@@ -222,6 +226,8 @@ class SecurityValidator(BaseValidator):
# SEC-015: XSS via |safe filter # SEC-015: XSS via |safe filter
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r"\|\s*safe", line) and "sanitized" not in line.lower(): if re.search(r"\|\s*safe", line) and "sanitized" not in line.lower():
if self._is_noqa_suppressed(line, "SEC-015"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-015", rule_id="SEC-015",
rule_name="XSS prevention in templates", rule_name="XSS prevention in templates",
@@ -236,6 +242,8 @@ class SecurityValidator(BaseValidator):
# Check for x-html with dynamic content # Check for x-html with dynamic content
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r'x-html="[^"]*\w', line) and "sanitized" not in line.lower(): if re.search(r'x-html="[^"]*\w', line) and "sanitized" not in line.lower():
if self._is_noqa_suppressed(line, "SEC-015"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-015", rule_id="SEC-015",
rule_name="XSS prevention in templates", rule_name="XSS prevention in templates",
@@ -268,6 +276,8 @@ class SecurityValidator(BaseValidator):
# Check for environment variable references # Check for environment variable references
if "${" in line or "os.getenv" in line or "environ" in line: if "${" in line or "os.getenv" in line or "environ" in line:
continue continue
if self._is_noqa_suppressed(line, "SEC-001"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-001", rule_id="SEC-001",
rule_name="No hardcoded credentials", rule_name="No hardcoded credentials",
@@ -296,7 +306,7 @@ class SecurityValidator(BaseValidator):
exclude_patterns = [ exclude_patterns = [
"os.getenv", "os.environ", "settings.", '""', "''", "os.getenv", "os.environ", "settings.", '""', "''",
"# noqa", "# test", "password_hash", "example" "# test", "password_hash", "example"
] ]
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
@@ -305,6 +315,8 @@ class SecurityValidator(BaseValidator):
# Check exclusions # Check exclusions
if any(exc in line for exc in exclude_patterns): if any(exc in line for exc in exclude_patterns):
continue continue
if self._is_noqa_suppressed(line, "SEC-001"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-001", rule_id="SEC-001",
rule_name="No hardcoded credentials", rule_name="No hardcoded credentials",
@@ -329,7 +341,7 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern in patterns: for pattern in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "# noqa" in line or "# safe" in line: if self._is_noqa_suppressed(line, "SEC-011") or "# safe" in line:
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-011", rule_id="SEC-011",
@@ -345,15 +357,15 @@ class SecurityValidator(BaseValidator):
def _check_command_injection(self, file_path: Path, content: str, lines: list[str]): def _check_command_injection(self, file_path: Path, content: str, lines: list[str]):
"""SEC-012: Check for command injection vulnerabilities""" """SEC-012: Check for command injection vulnerabilities"""
patterns = [ patterns = [
(r"subprocess.*shell\s*=\s*True", "shell=True in subprocess"), (r"subprocess.*shell\s*=\s*True", "shell=True in subprocess"), # noqa: SEC-012
(r"os\.system\s*\(", "os.system()"), (r"os\.system\s*\(", "os.system()"), # noqa: SEC-012
(r"os\.popen\s*\(", "os.popen()"), (r"os\.popen\s*\(", "os.popen()"), # noqa: SEC-012
] ]
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, issue in patterns: for pattern, issue in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "# noqa" in line or "# safe" in line: if self._is_noqa_suppressed(line, "SEC-012") or "# safe" in line:
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-012", rule_id="SEC-012",
@@ -378,6 +390,8 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, issue in patterns: for pattern, issue in patterns:
if re.search(pattern, line, re.IGNORECASE): if re.search(pattern, line, re.IGNORECASE):
if self._is_noqa_suppressed(line, "SEC-013"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-013", rule_id="SEC-013",
rule_name="No code execution", rule_name="No code execution",
@@ -405,6 +419,8 @@ class SecurityValidator(BaseValidator):
if re.search(pattern, line, re.IGNORECASE): if re.search(pattern, line, re.IGNORECASE):
if has_secure_filename: if has_secure_filename:
continue continue
if self._is_noqa_suppressed(line, "SEC-014"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-014", rule_id="SEC-014",
rule_name="Path traversal prevention", rule_name="Path traversal prevention",
@@ -427,7 +443,7 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, issue in patterns: for pattern, issue in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "# noqa" in line: if self._is_noqa_suppressed(line, "SEC-020"):
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-020", rule_id="SEC-020",
@@ -449,13 +465,15 @@ class SecurityValidator(BaseValidator):
(r"print\s*\([^)]*password", "password in print"), (r"print\s*\([^)]*password", "password in print"),
] ]
exclude = ["password_hash", "password_reset", "password_changed", "# noqa"] exclude = ["password_hash", "password_reset", "password_changed"]
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, issue in patterns: for pattern, issue in patterns:
if re.search(pattern, line, re.IGNORECASE): if re.search(pattern, line, re.IGNORECASE):
if any(exc in line for exc in exclude): if any(exc in line for exc in exclude):
continue continue
if self._is_noqa_suppressed(line, "SEC-021"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-021", rule_id="SEC-021",
rule_name="PII logging prevention", rule_name="PII logging prevention",
@@ -478,7 +496,9 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern in patterns: for pattern in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "logger" in line or "# noqa" in line: if "logger" in line:
continue
if self._is_noqa_suppressed(line, "SEC-024"):
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-024", rule_id="SEC-024",
@@ -495,7 +515,7 @@ class SecurityValidator(BaseValidator):
"""SEC-034: Check for HTTP instead of HTTPS""" """SEC-034: Check for HTTP instead of HTTPS"""
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if re.search(r"http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\$)", line): if re.search(r"http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\$)", line):
if "# noqa" in line or "example.com" in line or "schemas" in line: if self._is_noqa_suppressed(line, "SEC-034") or "example.com" in line or "schemas" in line:
continue continue
if "http://www.w3.org" in line: if "http://www.w3.org" in line:
continue continue
@@ -524,6 +544,8 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern in patterns: for pattern in patterns:
if re.search(pattern, line) and "timeout" not in line: if re.search(pattern, line) and "timeout" not in line:
if self._is_noqa_suppressed(line, "SEC-040"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-040", rule_id="SEC-040",
rule_name="Timeout configuration", rule_name="Timeout configuration",
@@ -547,7 +569,7 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, algo in patterns: for pattern, algo in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "# noqa" in line or "# checksum" in line or "# file hash" in line: if self._is_noqa_suppressed(line, "SEC-041") or "# checksum" in line or "# file hash" in line:
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-041", rule_id="SEC-041",
@@ -580,7 +602,7 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern in patterns: for pattern in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "# noqa" in line or "# not security" in line: if self._is_noqa_suppressed(line, "SEC-042") or "# not security" in line:
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-042", rule_id="SEC-042",
@@ -609,6 +631,8 @@ class SecurityValidator(BaseValidator):
if re.search(pattern, line): if re.search(pattern, line):
if any(exc in line for exc in exclude): if any(exc in line for exc in exclude):
continue continue
if self._is_noqa_suppressed(line, "SEC-043"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-043", rule_id="SEC-043",
rule_name="No hardcoded encryption keys", rule_name="No hardcoded encryption keys",
@@ -631,7 +655,7 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern, issue in patterns: for pattern, issue in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if "# noqa" in line or "# test" in line or "DEBUG" in line: if self._is_noqa_suppressed(line, "SEC-047") or "# test" in line or "DEBUG" in line:
continue continue
self._add_violation( self._add_violation(
rule_id="SEC-047", rule_id="SEC-047",
@@ -650,6 +674,8 @@ class SecurityValidator(BaseValidator):
# Find the jwt.encode line # Find the jwt.encode line
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
if "jwt.encode" in line: if "jwt.encode" in line:
if self._is_noqa_suppressed(line, "SEC-002"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-002", rule_id="SEC-002",
rule_name="JWT expiry enforcement", rule_name="JWT expiry enforcement",
@@ -676,6 +702,8 @@ class SecurityValidator(BaseValidator):
for i, line in enumerate(lines, 1): for i, line in enumerate(lines, 1):
for pattern in patterns: for pattern in patterns:
if re.search(pattern, line): if re.search(pattern, line):
if self._is_noqa_suppressed(line, "SEC-022"):
continue
self._add_violation( self._add_violation(
rule_id="SEC-022", rule_id="SEC-022",
rule_name="Sensitive data in URLs", rule_name="Sensitive data in URLs",

View File

@@ -20,7 +20,7 @@ def test_customer(db, test_store):
customer = Customer( customer = Customer(
store_id=test_store.id, store_id=test_store.id,
email="testcustomer@example.com", email="testcustomer@example.com",
hashed_password="hashed_password", hashed_password="hashed_password", # noqa: SEC-001
first_name="John", first_name="John",
last_name="Doe", last_name="Doe",
customer_number="TEST001", customer_number="TEST001",

View File

@@ -32,7 +32,7 @@ def empty_db(db):
for table in tables_to_clear: for table in tables_to_clear:
try: try:
db.execute(text(f"DELETE FROM {table}")) db.execute(text(f"DELETE FROM {table}")) # noqa: SEC-011
except Exception: except Exception:
# If table doesn't exist or delete fails, continue # If table doesn't exist or delete fails, continue
pass pass

View File

@@ -21,16 +21,16 @@ class TestUserLoginSchema:
"""Test valid login data.""" """Test valid login data."""
login = UserLogin( login = UserLogin(
email_or_username="testuser", email_or_username="testuser",
password="password123", password="password123", # noqa: SEC-001
) )
assert login.email_or_username == "testuser" assert login.email_or_username == "testuser"
assert login.password == "password123" assert login.password == "password123" # noqa: SEC-001
def test_login_with_email(self): def test_login_with_email(self):
"""Test login with email.""" """Test login with email."""
login = UserLogin( login = UserLogin(
email_or_username="test@example.com", email_or_username="test@example.com",
password="password123", password="password123", # noqa: SEC-001
) )
assert login.email_or_username == "test@example.com" assert login.email_or_username == "test@example.com"
@@ -38,7 +38,7 @@ class TestUserLoginSchema:
"""Test login with optional store code.""" """Test login with optional store code."""
login = UserLogin( login = UserLogin(
email_or_username="testuser", email_or_username="testuser",
password="password123", password="password123", # noqa: SEC-001
store_code="STORE001", store_code="STORE001",
) )
assert login.store_code == "STORE001" assert login.store_code == "STORE001"
@@ -47,7 +47,7 @@ class TestUserLoginSchema:
"""Test email_or_username is stripped of whitespace.""" """Test email_or_username is stripped of whitespace."""
login = UserLogin( login = UserLogin(
email_or_username=" testuser ", email_or_username=" testuser ",
password="password123", password="password123", # noqa: SEC-001
) )
assert login.email_or_username == "testuser" assert login.email_or_username == "testuser"
@@ -62,7 +62,7 @@ class TestUserCreateSchema:
user = UserCreate( user = UserCreate(
email="admin@example.com", email="admin@example.com",
username="adminuser", username="adminuser",
password="securepass", password="securepass", # noqa: SEC-001
first_name="Admin", first_name="Admin",
last_name="User", last_name="User",
role="admin", role="admin",
@@ -75,7 +75,7 @@ class TestUserCreateSchema:
user = UserCreate( user = UserCreate(
email="store@example.com", email="store@example.com",
username="storeuser", username="storeuser",
password="securepass", password="securepass", # noqa: SEC-001
) )
assert user.role == "store" assert user.role == "store"
@@ -85,7 +85,7 @@ class TestUserCreateSchema:
UserCreate( UserCreate(
email="test@example.com", email="test@example.com",
username="testuser", username="testuser",
password="securepass", password="securepass", # noqa: SEC-001
role="superadmin", role="superadmin",
) )
assert "role" in str(exc_info.value).lower() assert "role" in str(exc_info.value).lower()
@@ -96,7 +96,7 @@ class TestUserCreateSchema:
UserCreate( UserCreate(
email="test@example.com", email="test@example.com",
username="ab", username="ab",
password="securepass", password="securepass", # noqa: SEC-001
) )
assert "username" in str(exc_info.value).lower() assert "username" in str(exc_info.value).lower()
@@ -106,7 +106,7 @@ class TestUserCreateSchema:
UserCreate( UserCreate(
email="test@example.com", email="test@example.com",
username="testuser", username="testuser",
password="12345", password="12345", # noqa: SEC-001
) )
assert "password" in str(exc_info.value).lower() assert "password" in str(exc_info.value).lower()

View File

@@ -23,7 +23,7 @@ class TestAuthService:
def test_login_user_success(self, db, test_user): def test_login_user_success(self, db, test_user):
"""Test successful user login.""" """Test successful user login."""
user_credentials = UserLogin( user_credentials = UserLogin(
email_or_username=test_user.username, password="testpass123" email_or_username=test_user.username, password="testpass123" # noqa: SEC-001
) )
result = self.service.login_user(db, user_credentials) result = self.service.login_user(db, user_credentials)
@@ -39,7 +39,7 @@ class TestAuthService:
def test_login_user_with_email(self, db, test_user): def test_login_user_with_email(self, db, test_user):
"""Test login with email instead of username.""" """Test login with email instead of username."""
user_credentials = UserLogin( user_credentials = UserLogin(
email_or_username=test_user.email, password="testpass123" email_or_username=test_user.email, password="testpass123" # noqa: SEC-001
) )
result = self.service.login_user(db, user_credentials) result = self.service.login_user(db, user_credentials)
@@ -50,7 +50,7 @@ class TestAuthService:
def test_login_user_wrong_username(self, db): def test_login_user_wrong_username(self, db):
"""Test login fails with wrong username.""" """Test login fails with wrong username."""
user_credentials = UserLogin( user_credentials = UserLogin(
email_or_username="nonexistentuser", password="testpass123" email_or_username="nonexistentuser", password="testpass123" # noqa: SEC-001
) )
with pytest.raises(InvalidCredentialsException) as exc_info: with pytest.raises(InvalidCredentialsException) as exc_info:
@@ -64,7 +64,7 @@ class TestAuthService:
def test_login_user_wrong_password(self, db, test_user): def test_login_user_wrong_password(self, db, test_user):
"""Test login fails with wrong password.""" """Test login fails with wrong password."""
user_credentials = UserLogin( user_credentials = UserLogin(
email_or_username=test_user.username, password="wrongpassword" email_or_username=test_user.username, password="wrongpassword" # noqa: SEC-001
) )
with pytest.raises(InvalidCredentialsException) as exc_info: with pytest.raises(InvalidCredentialsException) as exc_info:
@@ -85,7 +85,7 @@ class TestAuthService:
db.commit() db.commit()
user_credentials = UserLogin( user_credentials = UserLogin(
email_or_username=test_user.username, password="testpass123" email_or_username=test_user.username, password="testpass123" # noqa: SEC-001
) )
with pytest.raises(UserNotActiveException) as exc_info: with pytest.raises(UserNotActiveException) as exc_info:
@@ -102,7 +102,7 @@ class TestAuthService:
def test_hash_password(self): def test_hash_password(self):
"""Test password hashing.""" """Test password hashing."""
password = "testpassword123" password = "testpassword123" # noqa: SEC-001
hashed = self.service.hash_password(password) hashed = self.service.hash_password(password)
assert hashed != password assert hashed != password
@@ -111,7 +111,7 @@ class TestAuthService:
def test_hash_password_different_results(self): def test_hash_password_different_results(self):
"""Test that hashing same password produces different hashes (salt).""" """Test that hashing same password produces different hashes (salt)."""
password = "testpassword123" password = "testpassword123" # noqa: SEC-001
hash1 = self.service.hash_password(password) hash1 = self.service.hash_password(password)
hash2 = self.service.hash_password(password) hash2 = self.service.hash_password(password)