fix(security): resolve Snyk/bandit code analysis findings
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
- 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)
This commit is contained in:
@@ -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
|
||||||
@@ -109,7 +109,7 @@ class Settings(BaseSettings):
|
|||||||
# ── Reporting ─────────────────────────────────────────────────────
|
# ── Reporting ─────────────────────────────────────────────────────
|
||||||
REPORT_TEMPLATES_DIR: str = "app/templates/reports"
|
REPORT_TEMPLATES_DIR: str = "app/templates/reports"
|
||||||
# Assign REPORT_OUTPUT_DIR = "/tmp/aegis_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"
|
# Assign COMPANY_NAME = "Organization"
|
||||||
COMPANY_NAME: str = "Organization"
|
COMPANY_NAME: str = "Organization"
|
||||||
# Assign COMPANY_LOGO_PATH = "app/templates/reports/assets/logo.png"
|
# Assign COMPANY_LOGO_PATH = "app/templates/reports/assets/logo.png"
|
||||||
|
|||||||
+4
-4
@@ -125,8 +125,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
seed_decay_policies(db)
|
seed_decay_policies(db)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning("seed_decay_policies failed at startup: %s", e)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
# Seed operational alert system rules
|
# Seed operational alert system rules
|
||||||
@@ -134,8 +134,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
try:
|
try:
|
||||||
from app.services.operational_alert_service import seed_system_rules
|
from app.services.operational_alert_service import seed_system_rules
|
||||||
seed_system_rules(db2)
|
seed_system_rules(db2)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning("seed_system_rules failed at startup: %s", e)
|
||||||
finally:
|
finally:
|
||||||
db2.close()
|
db2.close()
|
||||||
yield
|
yield
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ def export_config(
|
|||||||
"email": u.email if hasattr(u, "email") else None,
|
"email": u.email if hasattr(u, "email") else None,
|
||||||
"role": u.role,
|
"role": u.role,
|
||||||
"is_active": u.is_active,
|
"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()
|
for u in db.query(User).order_by(User.username).all()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ _SMTP_KEYS = {
|
|||||||
"host": "smtp.host",
|
"host": "smtp.host",
|
||||||
"port": "smtp.port",
|
"port": "smtp.port",
|
||||||
"username": "smtp.username",
|
"username": "smtp.username",
|
||||||
"password": "smtp.password",
|
"password": "smtp.password", # nosec B105
|
||||||
"from_email": "smtp.from_email",
|
"from_email": "smtp.from_email",
|
||||||
"use_tls": "smtp.use_tls",
|
"use_tls": "smtp.use_tls",
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ def test_jira_connection(
|
|||||||
# 10-second timeout so we never block Cloudflare into a 524
|
# 10-second timeout so we never block Cloudflare into a 524
|
||||||
try:
|
try:
|
||||||
jira._session.timeout = 10 # type: ignore[attr-defined]
|
jira._session.timeout = 10 # type: ignore[attr-defined]
|
||||||
except Exception:
|
except Exception: # nosec B110
|
||||||
pass
|
pass
|
||||||
myself = jira.myself()
|
myself = jira.myself()
|
||||||
logger.info("Jira myself() response keys: %s", list(myself.keys()) if isinstance(myself, dict) else type(myself))
|
logger.info("Jira myself() response keys: %s", list(myself.keys()) if isinstance(myself, dict) else type(myself))
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ def create_test(
|
|||||||
from app.services.jira_service import auto_create_test_issue
|
from app.services.jira_service import auto_create_test_issue
|
||||||
auto_create_test_issue(db, test, current_user)
|
auto_create_test_issue(db, test, current_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception:
|
except Exception: # nosec B110
|
||||||
pass # jira_service already logs warnings internally
|
pass # jira_service already logs warnings internally
|
||||||
|
|
||||||
return test
|
return test
|
||||||
@@ -374,8 +374,8 @@ def create_test_from_template(
|
|||||||
from app.services.jira_service import auto_create_test_issue
|
from app.services.jira_service import auto_create_test_issue
|
||||||
auto_create_test_issue(db, test, current_user)
|
auto_create_test_issue(db, test, current_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception:
|
except Exception: # nosec B110
|
||||||
pass
|
pass # jira_service already logs warnings internally
|
||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
@@ -1485,7 +1485,7 @@ def import_rt(
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
img_bytes = base64.b64decode(ev.data)
|
img_bytes = base64.b64decode(ev.data)
|
||||||
except Exception:
|
except Exception: # nosec B112
|
||||||
continue # malformed base64 — skip
|
continue # malformed base64 — skip
|
||||||
if len(img_bytes) > _MAX_EVIDENCE_BYTES:
|
if len(img_bytes) > _MAX_EVIDENCE_BYTES:
|
||||||
continue # over size limit — skip
|
continue # over size limit — skip
|
||||||
@@ -1493,7 +1493,7 @@ def import_rt(
|
|||||||
key = f"{test.id}/{uuid.uuid4()}_{safe_name}"
|
key = f"{test.id}/{uuid.uuid4()}_{safe_name}"
|
||||||
try:
|
try:
|
||||||
upload_file(img_bytes, key)
|
upload_file(img_bytes, key)
|
||||||
except Exception:
|
except Exception: # nosec B112
|
||||||
continue # storage error — skip but don't abort
|
continue # storage error — skip but don't abort
|
||||||
evidence_obj = Evidence(
|
evidence_obj = Evidence(
|
||||||
test_id=test.id,
|
test_id=test.id,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def _mask_secret(wh) -> WebhookConfigOut:
|
|||||||
"""Return a WebhookConfigOut with the secret masked."""
|
"""Return a WebhookConfigOut with the secret masked."""
|
||||||
out = WebhookConfigOut.model_validate(wh)
|
out = WebhookConfigOut.model_validate(wh)
|
||||||
if wh.secret:
|
if wh.secret:
|
||||||
out.secret = "***"
|
out.secret = "***" # nosec B105
|
||||||
else:
|
else:
|
||||||
out.secret = None
|
out.secret = None
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ class TestOut(BaseModel):
|
|||||||
if hasattr(obj, "technique") and obj.technique is not None:
|
if hasattr(obj, "technique") and obj.technique is not None:
|
||||||
obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id
|
obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id
|
||||||
obj.__dict__["technique_name"] = obj.technique.name
|
obj.__dict__["technique_name"] = obj.technique.name
|
||||||
except Exception:
|
except Exception: # nosec B110
|
||||||
pass # DetachedInstanceError or similar — leave technique fields None
|
pass # DetachedInstanceError or similar — leave technique fields None
|
||||||
|
|
||||||
# Only split evidences when they are already in memory (loaded via joinedload)
|
# Only split evidences when they are already in memory (loaded via joinedload)
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ def notify_role_with_email(
|
|||||||
if email_fn and user.email:
|
if email_fn and user.email:
|
||||||
try:
|
try:
|
||||||
email_fn(user.email)
|
email_fn(user.email)
|
||||||
except Exception:
|
except Exception: # nosec B110
|
||||||
pass # email failures never crash notification flow
|
pass # email failures never crash notification flow
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def _get_tempo_base_url(db=None) -> str:
|
|||||||
).first()
|
).first()
|
||||||
if row and row.value:
|
if row and row.value:
|
||||||
return row.value.rstrip("/")
|
return row.value.rstrip("/")
|
||||||
except Exception:
|
except Exception: # nosec B110
|
||||||
pass # DB unavailable — fall through to defaults
|
pass # DB unavailable — fall through to defaults
|
||||||
|
|
||||||
env_url = getattr(settings, "TEMPO_BASE_URL", None)
|
env_url = getattr(settings, "TEMPO_BASE_URL", None)
|
||||||
|
|||||||
Reference in New Issue
Block a user