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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/sentry/incidents/endpoints/serializers/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
OFFSET = 10**9


def get_fake_id_from_object_id(obj_id: int) -> int:
"""
Return object_id + 1 billion to avoid any ID collisions with the model whose ID we're faking.
"""
return obj_id + OFFSET


def get_object_id_from_fake_id(fake_id: int) -> int:
"""
Undo the object_id + offset operation to recover the object ID.
"""
return fake_id - OFFSET
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
get_input_channel_id,
human_desc,
)
from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id
from sentry.notifications.models.notificationaction import ActionService, ActionTarget
from sentry.notifications.notification_action.group_type_notification_registry.handlers.metric_alert_registry_handler import (
MetricAlertRegistryHandler,
Expand Down Expand Up @@ -49,13 +50,19 @@ def get_alert_rule_trigger_id(self, action: Action) -> int | None:
).values("workflow")
)
).values("detector__workflow_condition_group")
detector_trigger = DataCondition.objects.filter(
detector_trigger = DataCondition.objects.get(
condition_result__in=Subquery(action_filter_data_condition.values("comparison")),
condition_group__in=detector_dcg,
)
return DataConditionAlertRuleTrigger.objects.values_list(
"alert_rule_trigger_id", flat=True
).get(data_condition__in=detector_trigger)
try:
alert_rule_trigger_id = DataConditionAlertRuleTrigger.objects.values_list(
"alert_rule_trigger_id", flat=True
).get(data_condition=detector_trigger)
Comment on lines +58 to +60

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High severity vulnerability may affect your project—review required:
Line 59 lists a dependency (django) with a known High severity vulnerability.

ℹ️ Why this matters

Affected versions of Django are vulnerable to Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'). SQL injection in Django's ORM column aliases: when using QuerySet.annotate(), QuerySet.alias(), QuerySet.aggregate(), or QuerySet.extra() with dictionary expansion (**kwargs), the dictionary keys are used unescaped as SQL column aliases. On MySQL and MariaDB backends, an attacker who can influence those keys (for example, by passing a crafted dict of annotations) can inject arbitrary SQL into the generated query.

References: GHSA, CVE

To resolve this comment:
Check if you are using Django with MySQL or MariaDB.

  • If you're affected, upgrade this dependency to at least version 5.2.7 at uv.lock.
  • If you're not affected, comment /fp we don't use this [condition]
💬 Ignore this finding

To ignore this, reply with:

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

You can view more details on this finding in the Semgrep AppSec Platform here.

return alert_rule_trigger_id
except DataConditionAlertRuleTrigger.DoesNotExist:
# this data condition does not have an analog in the old system,
# but we need to return *something*
return get_fake_id_from_object_id(detector_trigger.id)

def serialize(
self, obj: Action, attrs: Mapping[str, Any], user: User | RpcUser | AnonymousUser, **kwargs
Expand All @@ -65,7 +72,10 @@ def serialize(
"""
from sentry.incidents.serializers import ACTION_TARGET_TYPE_TO_STRING

aarta = ActionAlertRuleTriggerAction.objects.get(action=obj.id)
try:
aarta = ActionAlertRuleTriggerAction.objects.get(action=obj.id)
except ActionAlertRuleTriggerAction.DoesNotExist:
aarta = None
priority = obj.data.get("priority")
type_value = ActionService.get_value(obj.type)
target = MetricAlertRegistryHandler.target(obj)
Expand All @@ -81,8 +91,12 @@ def serialize(
sentry_app_config = obj.data.get("settings")

result = {
"id": str(aarta.alert_rule_trigger_action_id),
"alertRuleTriggerId": str(self.get_alert_rule_trigger_id(aarta.action)),
"id": (
str(aarta.alert_rule_trigger_action_id)
if aarta is not None
else str(get_fake_id_from_object_id(obj.id))
),
"alertRuleTriggerId": str(self.get_alert_rule_trigger_id(obj)),
"type": obj.type,
"targetType": ACTION_TARGET_TYPE_TO_STRING[ActionTarget(target_type)],
"targetIdentifier": get_identifier_from_action(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db.models import Subquery

from sentry.api.serializers import Serializer, serialize
from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id
from sentry.incidents.endpoints.serializers.workflow_engine_action import (
WorkflowEngineActionSerializer,
)
Expand Down Expand Up @@ -89,13 +90,22 @@ def serialize(
) -> dict[str, Any]:
# XXX: we are assuming that the obj/DataCondition is a detector trigger
detector = Detector.objects.get(workflow_condition_group=obj.condition_group)
try:
alert_rule_trigger_id = DataConditionAlertRuleTrigger.objects.values_list(
"alert_rule_trigger_id", flat=True
).get(data_condition=obj)
except DataConditionAlertRuleTrigger.DoesNotExist:
# this data condition does not have an analog in the old system,
# but we need to return *something*
alert_rule_trigger_id = get_fake_id_from_object_id(obj.id)
try:
alert_rule_id = AlertRuleDetector.objects.values_list("alert_rule_id", flat=True).get(
detector=detector.id
)
except AlertRuleDetector.DoesNotExist:
# this detector does not have an analog in the old system
alert_rule_id = get_fake_id_from_object_id(detector.id)

alert_rule_trigger_id = DataConditionAlertRuleTrigger.objects.values_list(
"alert_rule_trigger_id", flat=True
).get(data_condition=obj)
alert_rule_id = AlertRuleDetector.objects.values_list("alert_rule_id", flat=True).get(
detector=detector.id
)
if obj.type == Condition.ANOMALY_DETECTION:
threshold_type = obj.comparison["threshold_type"]
resolve_threshold = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

from sentry.api.serializers import Serializer, serialize
from sentry.incidents.endpoints.serializers.alert_rule import AlertRuleSerializerResponse
from sentry.incidents.endpoints.serializers.utils import (
get_fake_id_from_object_id,
get_object_id_from_fake_id,
)
from sentry.incidents.endpoints.serializers.workflow_engine_data_condition import (
WorkflowEngineDataConditionSerializer,
)
Expand Down Expand Up @@ -77,9 +81,13 @@ def add_triggers_and_actions(
errors = []
alert_rule_id = serialized.get("alertRuleId")
assert alert_rule_id
detector_id = AlertRuleDetector.objects.values_list("detector_id", flat=True).get(
alert_rule_id=alert_rule_id
)
try:
detector_id = AlertRuleDetector.objects.values_list("detector_id", flat=True).get(
alert_rule_id=alert_rule_id
)
except AlertRuleDetector.DoesNotExist:
detector_id = get_object_id_from_fake_id(int(alert_rule_id))

detector = detectors[int(detector_id)]
alert_rule_triggers = result[detector].setdefault("triggers", [])

Expand Down Expand Up @@ -320,17 +328,22 @@ def get_attrs(

def serialize(self, obj: Detector, attrs, user, **kwargs) -> AlertRuleSerializerResponse:
triggers = attrs.get("triggers", [])
alert_rule_detector_id = None
alert_rule_id = None

if triggers:
alert_rule_detector_id = triggers[0].get("alertRuleId")
alert_rule_id = triggers[0].get("alertRuleId")
else:
alert_rule_detector_id = AlertRuleDetector.objects.values_list(
"alert_rule_id", flat=True
).get(detector=obj)
try:
alert_rule_id = AlertRuleDetector.objects.values_list(
"alert_rule_id", flat=True
).get(detector=obj)
Comment on lines +337 to +339
Copy link

@semgrep-code-getsentry semgrep-code-getsentry bot Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: Affected versions of Django are vulnerable to Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'). SQL injection in Django's ORM column aliases: when using QuerySet.annotate(), QuerySet.alias(), QuerySet.aggregate(), or QuerySet.extra() with dictionary expansion (**kwargs), the dictionary keys are used unescaped as SQL column aliases. On MySQL and MariaDB backends, an attacker who can influence those keys (for example, by passing a crafted dict of annotations) can inject arbitrary SQL into the generated query.

Manual Review Advice: A vulnerability from this advisory is reachable if you are using Django with MySQL or MariaDB

Fix: Upgrade this library to at least version 5.2.7 at sentry/uv.lock:305.

Reference(s): GHSA-hpr9-3m2g-3j9p, CVE-2025-59681

🎉 Fixed in commit 8c89442 🎉

Comment on lines +337 to +339
Copy link

@semgrep-code-getsentry semgrep-code-getsentry bot Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: Affected versions of Django are vulnerable to Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'). SQL injection in Django's ORM column aliases: when using QuerySet.annotate(), QuerySet.alias(), QuerySet.aggregate(), or QuerySet.extra() with dictionary expansion (**kwargs), the dictionary keys are used unescaped as SQL column aliases. On MySQL and MariaDB backends, an attacker who can influence those keys (for example, by passing a crafted dict of annotations) can inject arbitrary SQL into the generated query.

Manual Review Advice: A vulnerability from this advisory is reachable if you are using Django with MySQL or MariaDB

Fix: Upgrade this library to at least version 5.2.7 at sentry/uv.lock:305.

Reference(s): GHSA-hpr9-3m2g-3j9p, CVE-2025-59681

🍰 Fixed in commit e878183 🍰

except AlertRuleDetector.DoesNotExist:
# this detector does not have an analog in the old system,
# but we need to return *something*
alert_rule_id = get_fake_id_from_object_id(obj.id)

data: AlertRuleSerializerResponse = {
"id": str(alert_rule_detector_id),
"id": str(alert_rule_id),
"name": obj.name,
"organizationId": str(obj.project.organization_id),
"status": (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,28 @@
migrate_metric_data_conditions,
migrate_resolve_threshold_data_condition,
)
from sentry.workflow_engine.models import AlertRuleDetector, DataConditionAlertRuleTrigger
from sentry.workflow_engine.models.action_alertruletriggeraction import ActionAlertRuleTriggerAction
from tests.sentry.incidents.serializers.test_workflow_engine_base import (
TestWorkflowEngineSerializer,
)

OFFSET = 10**9


class TestDetectorSerializer(TestWorkflowEngineSerializer):
def setUp(self) -> None:
super().setUp()
self.add_warning_trigger()

def test_simple(self) -> None:
self.add_warning_trigger()
serialized_detector = serialize(
self.detector, self.user, WorkflowEngineDetectorSerializer()
)
assert serialized_detector == self.expected

def test_latest_incident(self) -> None:
self.add_warning_trigger()
# add some other workflow engine objects to ensure that our filtering is working properly
other_alert_rule = self.create_alert_rule()
critical_trigger = self.create_alert_rule_trigger(
Expand Down Expand Up @@ -67,6 +72,7 @@ def test_latest_incident(self) -> None:

@patch("sentry.sentry_apps.components.SentryAppComponentPreparer.run")
def test_sentry_app(self, mock_sentry_app_components_preparer: Any) -> None:
self.add_warning_trigger()
sentry_app = self.create_sentry_app(
organization=self.organization,
published=True,
Expand Down Expand Up @@ -177,3 +183,34 @@ def test_sentry_app(self, mock_sentry_app_components_preparer: Any) -> None:
WorkflowEngineDetectorSerializer(prepare_component_fields=True),
)
assert serialized_detector == sentry_app_expected

def test_new_models_only(self) -> None:
# test that we can still serialize if objects do not have lookup table entries
# this is required for firing actions
ard = AlertRuleDetector.objects.filter(detector_id=self.detector.id)
dcart = DataConditionAlertRuleTrigger.objects.filter(
data_condition_id=self.critical_detector_trigger.id
)
aarta = ActionAlertRuleTriggerAction.objects.filter(action_id=self.critical_action.id)

ard.delete()
dcart.delete()
aarta.delete()

serialized_detector = serialize(
self.detector, self.user, WorkflowEngineDetectorSerializer()
)
self.expected.update({"id": str(self.detector.id + OFFSET)})
self.expected["triggers"][0].update(
{
"id": str(self.critical_detector_trigger.id + OFFSET),
"alertRuleId": str(self.detector.id + OFFSET),
}
)
self.expected["triggers"][0]["actions"][0].update(
{
"id": str(self.critical_action.id + OFFSET),
"alertRuleTriggerId": str(self.critical_detector_trigger.id + OFFSET),
}
)
assert serialized_detector == self.expected
Loading