security_governance.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """Cross-domain data security and security engineering APIs."""
  2. from __future__ import annotations
  3. from datetime import UTC, datetime
  4. from flask import current_app, g, jsonify, request
  5. from app import db
  6. from app.api.system import bp
  7. from app.core.system.permissions import (
  8. SECURITY_GOVERNANCE_MANAGE,
  9. SECURITY_GOVERNANCE_OPERATE,
  10. SECURITY_GOVERNANCE_READ,
  11. require_permissions,
  12. )
  13. from app.core.system.security_delivery import HttpsWebhookSyslogTransport
  14. from app.core.system.security_governance import SecurityGovernanceService
  15. from app.core.system.security_governance_repository import (
  16. SqlAlchemySecurityGovernanceRepository,
  17. WorkCenterSecurityApprovalGateway,
  18. )
  19. from app.models.result import failed, success
  20. def _service():
  21. allowlist = {
  22. item.strip().lower()
  23. for item in str(current_app.config.get("SECURITY_SIEM_HOST_ALLOWLIST") or "").split(",")
  24. if item.strip()
  25. }
  26. return SecurityGovernanceService(
  27. SqlAlchemySecurityGovernanceRepository(db.session),
  28. approval_gateway=WorkCenterSecurityApprovalGateway(db.session),
  29. siem_transport=HttpsWebhookSyslogTransport(),
  30. siem_host_allowlist=allowlist,
  31. commit=db.session.commit,
  32. rollback=db.session.rollback,
  33. )
  34. def _version():
  35. value = str(request.headers.get("If-Match") or "").strip().removeprefix("W/").strip('"')
  36. if not value.isdigit():
  37. raise ValueError("missing valid If-Match version")
  38. return int(value)
  39. def _etag(response, record):
  40. if record.get("current_version"):
  41. response.headers["ETag"] = f'"{record["current_version"]}"'
  42. return response
  43. def _execute(operation, *, created=False, versioned=False):
  44. try:
  45. result = operation()
  46. response = jsonify(success(result, code=201 if created else 200))
  47. if versioned and isinstance(result, dict):
  48. response = _etag(response, result)
  49. return (response, 201) if created else response
  50. except Exception as exc:
  51. db.session.rollback()
  52. if isinstance(exc, LookupError):
  53. status = 404
  54. elif isinstance(exc, PermissionError):
  55. status = 403
  56. elif isinstance(exc, RuntimeError):
  57. status = 409
  58. else:
  59. status = 400
  60. return jsonify(failed(str(exc), code=status)), status
  61. @bp.route("/security-governance/dashboard", methods=["GET"])
  62. @require_permissions(SECURITY_GOVERNANCE_READ)
  63. def security_dashboard():
  64. return _execute(_service().dashboard)
  65. @bp.route("/security-governance/profiles", methods=["GET"])
  66. @require_permissions(SECURITY_GOVERNANCE_READ)
  67. def security_list_profiles():
  68. return _execute(lambda: _service().list_profiles(business_domain_uid=request.args.get("business_domain_uid")))
  69. @bp.route("/security-governance/profiles", methods=["POST"])
  70. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  71. def security_create_profile():
  72. return _execute(
  73. lambda: _service().create_classification_profile(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]),
  74. created=True,
  75. )
  76. @bp.route("/security-governance/scans", methods=["GET"])
  77. @require_permissions(SECURITY_GOVERNANCE_READ)
  78. def security_list_scans():
  79. return _execute(lambda: _service().list_classification_scans(business_domain_uid=request.args.get("business_domain_uid")))
  80. @bp.route("/security-governance/scans", methods=["POST"])
  81. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  82. def security_create_scan():
  83. return _execute(
  84. lambda: _service().scan_sensitive_sample(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]),
  85. created=True,
  86. )
  87. @bp.route("/security-governance/findings", methods=["GET"])
  88. @require_permissions(SECURITY_GOVERNANCE_READ)
  89. def security_findings():
  90. return _execute(lambda: _service().list_classification_findings(status=request.args.get("status")))
  91. @bp.route("/security-governance/findings/<finding_uid>/review", methods=["POST"])
  92. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  93. def security_review_finding(finding_uid):
  94. return _execute(
  95. lambda: _service().review_classification_finding(
  96. finding_uid, request.get_json(silent=True) or {}, expected_version=_version(), actor_uid=g.current_user["id"]
  97. ),
  98. versioned=True,
  99. )
  100. @bp.route("/security-governance/access-policies", methods=["GET"])
  101. @require_permissions(SECURITY_GOVERNANCE_READ)
  102. def security_list_access_policies():
  103. return _execute(lambda: _service().list_access_policies(business_domain_uid=request.args.get("business_domain_uid"), status=request.args.get("status")))
  104. @bp.route("/security-governance/access-policies", methods=["POST"])
  105. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  106. def security_create_access_policy():
  107. return _execute(
  108. lambda: _service().create_access_policy(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]),
  109. created=True,
  110. )
  111. @bp.route("/security-governance/access-decisions", methods=["GET"])
  112. @require_permissions(SECURITY_GOVERNANCE_READ)
  113. def security_access_decisions():
  114. return _execute(lambda: _service().list_access_decisions(decision=request.args.get("decision")))
  115. @bp.route("/security-governance/access/evaluate", methods=["POST"])
  116. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  117. def security_evaluate_access():
  118. body = request.get_json(silent=True) or {}
  119. body["user_uid"] = g.current_user["id"]
  120. body["roles"] = list(g.current_user["roles"])
  121. return _execute(lambda: _service().evaluate_access(body), created=True)
  122. @bp.route("/security-governance/egress", methods=["GET"])
  123. @require_permissions(SECURITY_GOVERNANCE_READ)
  124. def security_list_egress():
  125. return _execute(lambda: _service().list_egress_requests(status=request.args.get("status")))
  126. @bp.route("/security-governance/egress", methods=["POST"])
  127. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  128. def security_create_egress():
  129. return _execute(
  130. lambda: _service().submit_egress_request(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]),
  131. created=True,
  132. versioned=True,
  133. )
  134. @bp.route("/security-governance/egress/<request_uid>/reconcile", methods=["POST"])
  135. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  136. def security_reconcile_egress(request_uid):
  137. return _execute(
  138. lambda: _service().reconcile_egress_request(request_uid, expected_version=_version(), actor_uid=g.current_user["id"]),
  139. versioned=True,
  140. )
  141. @bp.route("/security-governance/retention", methods=["GET"])
  142. @require_permissions(SECURITY_GOVERNANCE_READ)
  143. def security_list_retention():
  144. return _execute(_service().list_retention_policies)
  145. @bp.route("/security-governance/retention", methods=["POST"])
  146. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  147. def security_create_retention():
  148. return _execute(
  149. lambda: _service().create_retention_policy(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]),
  150. created=True,
  151. )
  152. @bp.route("/security-governance/retention/candidates", methods=["GET"])
  153. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  154. def security_retention_candidates():
  155. value = request.args.get("as_of") or datetime.now(UTC).isoformat()
  156. return _execute(lambda: _service().retention_candidates(as_of=value, limit=int(request.args.get("limit", 100))))
  157. @bp.route("/security-governance/siem/sinks", methods=["GET"])
  158. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  159. def security_list_siem_sinks():
  160. return _execute(_service().list_siem_sinks)
  161. @bp.route("/security-governance/siem/sinks", methods=["POST"])
  162. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  163. def security_create_siem_sink():
  164. return _execute(
  165. lambda: _service().create_siem_sink(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]),
  166. created=True,
  167. )
  168. @bp.route("/security-governance/siem/deliveries", methods=["GET"])
  169. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  170. def security_siem_deliveries():
  171. return _execute(lambda: _service().list_siem_deliveries(sink_uid=request.args.get("sink_uid")))
  172. @bp.route("/security-governance/siem/sinks/<sink_uid>/dispatch", methods=["POST"])
  173. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  174. def security_siem_dispatch(sink_uid):
  175. return _execute(lambda: _service().dispatch_siem_events(sink_uid, request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]), created=True)
  176. @bp.route("/security-governance/sboms", methods=["GET"])
  177. @require_permissions(SECURITY_GOVERNANCE_READ)
  178. def security_list_sboms():
  179. return _execute(_service().list_sboms)
  180. @bp.route("/security-governance/sboms", methods=["POST"])
  181. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  182. def security_register_sbom():
  183. return _execute(lambda: _service().register_sbom(request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]), created=True)
  184. @bp.route("/security-governance/sboms/<sbom_uid>/vulnerabilities", methods=["POST"])
  185. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  186. def security_import_vulnerabilities(sbom_uid):
  187. return _execute(lambda: _service().ingest_vulnerabilities(sbom_uid, request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]), created=True)
  188. @bp.route("/security-governance/vulnerabilities", methods=["GET"])
  189. @require_permissions(SECURITY_GOVERNANCE_READ)
  190. def security_vulnerabilities():
  191. return _execute(lambda: _service().list_vulnerabilities(status=request.args.get("status"), severity=request.args.get("severity")))
  192. def _vulnerability_change(finding_uid, action):
  193. service = _service()
  194. method = getattr(service, f"{action}_vulnerability")
  195. return _execute(
  196. lambda: method(finding_uid, request.get_json(silent=True) or {}, expected_version=_version(), actor_uid=g.current_user["id"]),
  197. versioned=True,
  198. )
  199. @bp.route("/security-governance/vulnerabilities/<finding_uid>/assign", methods=["POST"])
  200. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  201. def security_assign_vulnerability(finding_uid):
  202. return _vulnerability_change(finding_uid, "assign")
  203. @bp.route("/security-governance/vulnerabilities/<finding_uid>/resolve", methods=["POST"])
  204. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  205. def security_resolve_vulnerability(finding_uid):
  206. return _vulnerability_change(finding_uid, "resolve")
  207. @bp.route("/security-governance/vulnerabilities/<finding_uid>/close", methods=["POST"])
  208. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  209. def security_close_vulnerability(finding_uid):
  210. return _vulnerability_change(finding_uid, "close")