From 6d3617938e29f53fc2331ccc3f1cf17346e2418a Mon Sep 17 00:00:00 2001 From: kitos Date: Fri, 12 Jun 2026 12:59:11 +0200 Subject: [PATCH] fix(security): resolve Snyk/bandit code analysis findings - config.py: move REPORT_OUTPUT_DIR from /tmp (world-writable) to /app/reports to prevent CWE-377 symlink attack vector (B108, only real security issue) - main.py: log startup seed failures instead of silently swallowing them (B110) - Add # nosec annotations to intentional try/except patterns that are by design: Jira integration errors, email failures, DetachedInstanceError, storage errors, and Jira session timeout (all B110/B112 false positives) - Add # nosec B105 to false positives where bandit misidentifies config key names and masking strings as hardcoded passwords - Add .bandit config to skip B311 in seed_demo.py (random used for fake demo data generation, not cryptographic purposes) --- backend/.bandit | 4 ++++ backend/app/config.py | 2 +- backend/app/main.py | 8 ++++---- backend/app/routers/admin_config.py | 2 +- backend/app/routers/system.py | 4 ++-- backend/app/routers/tests.py | 10 +++++----- backend/app/routers/webhooks.py | 2 +- backend/app/schemas/test.py | 2 +- backend/app/services/notification_service.py | 2 +- backend/app/services/tempo_service.py | 2 +- 10 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 backend/.bandit diff --git a/backend/.bandit b/backend/.bandit new file mode 100644 index 0000000..909b3c1 --- /dev/null +++ b/backend/.bandit @@ -0,0 +1,4 @@ +[bandit] +# B311: seed_demo.py uses random exclusively for fake demo data generation, +# not for any cryptographic or security-sensitive purpose. +skips = B311 diff --git a/backend/app/config.py b/backend/app/config.py index 3b4e42b..500410b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -109,7 +109,7 @@ class Settings(BaseSettings): # ── Reporting ───────────────────────────────────────────────────── REPORT_TEMPLATES_DIR: str = "app/templates/reports" # Assign REPORT_OUTPUT_DIR = "/tmp/aegis_reports" - REPORT_OUTPUT_DIR: str = "/tmp/aegis_reports" + REPORT_OUTPUT_DIR: str = "/app/reports" # Assign COMPANY_NAME = "Organization" COMPANY_NAME: str = "Organization" # Assign COMPANY_LOGO_PATH = "app/templates/reports/assets/logo.png" diff --git a/backend/app/main.py b/backend/app/main.py index 9410886..d930722 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -125,8 +125,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: db = SessionLocal() try: seed_decay_policies(db) - except Exception: - pass + except Exception as e: + logger.warning("seed_decay_policies failed at startup: %s", e) finally: db.close() # Seed operational alert system rules @@ -134,8 +134,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: try: from app.services.operational_alert_service import seed_system_rules seed_system_rules(db2) - except Exception: - pass + except Exception as e: + logger.warning("seed_system_rules failed at startup: %s", e) finally: db2.close() yield diff --git a/backend/app/routers/admin_config.py b/backend/app/routers/admin_config.py index 43a608b..d009ec5 100644 --- a/backend/app/routers/admin_config.py +++ b/backend/app/routers/admin_config.py @@ -149,7 +149,7 @@ def export_config( "email": u.email if hasattr(u, "email") else None, "role": u.role, "is_active": u.is_active, - "must_change_password": True, # force password reset on new instance + "must_change_password": True, # force password reset on new instance # nosec B105 } for u in db.query(User).order_by(User.username).all() ] diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index c643e72..4337d77 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -72,7 +72,7 @@ _SMTP_KEYS = { "host": "smtp.host", "port": "smtp.port", "username": "smtp.username", - "password": "smtp.password", + "password": "smtp.password", # nosec B105 "from_email": "smtp.from_email", "use_tls": "smtp.use_tls", } @@ -355,7 +355,7 @@ def test_jira_connection( # 10-second timeout so we never block Cloudflare into a 524 try: jira._session.timeout = 10 # type: ignore[attr-defined] - except Exception: + except Exception: # nosec B110 pass myself = jira.myself() logger.info("Jira myself() response keys: %s", list(myself.keys()) if isinstance(myself, dict) else type(myself)) diff --git a/backend/app/routers/tests.py b/backend/app/routers/tests.py index e929748..6b2b369 100644 --- a/backend/app/routers/tests.py +++ b/backend/app/routers/tests.py @@ -281,7 +281,7 @@ def create_test( from app.services.jira_service import auto_create_test_issue auto_create_test_issue(db, test, current_user) db.commit() - except Exception: + except Exception: # nosec B110 pass # jira_service already logs warnings internally return test @@ -374,8 +374,8 @@ def create_test_from_template( from app.services.jira_service import auto_create_test_issue auto_create_test_issue(db, test, current_user) db.commit() - except Exception: - pass + except Exception: # nosec B110 + pass # jira_service already logs warnings internally return test @@ -1485,7 +1485,7 @@ def import_rt( continue try: img_bytes = base64.b64decode(ev.data) - except Exception: + except Exception: # nosec B112 continue # malformed base64 — skip if len(img_bytes) > _MAX_EVIDENCE_BYTES: continue # over size limit — skip @@ -1493,7 +1493,7 @@ def import_rt( key = f"{test.id}/{uuid.uuid4()}_{safe_name}" try: upload_file(img_bytes, key) - except Exception: + except Exception: # nosec B112 continue # storage error — skip but don't abort evidence_obj = Evidence( test_id=test.id, diff --git a/backend/app/routers/webhooks.py b/backend/app/routers/webhooks.py index 81dc276..8397d77 100644 --- a/backend/app/routers/webhooks.py +++ b/backend/app/routers/webhooks.py @@ -36,7 +36,7 @@ def _mask_secret(wh) -> WebhookConfigOut: """Return a WebhookConfigOut with the secret masked.""" out = WebhookConfigOut.model_validate(wh) if wh.secret: - out.secret = "***" + out.secret = "***" # nosec B105 else: out.secret = None return out diff --git a/backend/app/schemas/test.py b/backend/app/schemas/test.py index 6262747..2b61b62 100644 --- a/backend/app/schemas/test.py +++ b/backend/app/schemas/test.py @@ -266,7 +266,7 @@ class TestOut(BaseModel): if hasattr(obj, "technique") and obj.technique is not None: obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id obj.__dict__["technique_name"] = obj.technique.name - except Exception: + except Exception: # nosec B110 pass # DetachedInstanceError or similar — leave technique fields None # Only split evidences when they are already in memory (loaded via joinedload) diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py index 6fe5b03..0561f25 100644 --- a/backend/app/services/notification_service.py +++ b/backend/app/services/notification_service.py @@ -275,7 +275,7 @@ def notify_role_with_email( if email_fn and user.email: try: email_fn(user.email) - except Exception: + except Exception: # nosec B110 pass # email failures never crash notification flow diff --git a/backend/app/services/tempo_service.py b/backend/app/services/tempo_service.py index 8dcc197..552bebb 100644 --- a/backend/app/services/tempo_service.py +++ b/backend/app/services/tempo_service.py @@ -78,7 +78,7 @@ def _get_tempo_base_url(db=None) -> str: ).first() if row and row.value: return row.value.rstrip("/") - except Exception: + except Exception: # nosec B110 pass # DB unavailable — fall through to defaults env_url = getattr(settings, "TEMPO_BASE_URL", None)