Skip to content
Open
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
7 changes: 5 additions & 2 deletions packages/tracecat-registry/tracecat_registry/core/ee/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ async def delete_task(
str,
Doc("The ID of the task to delete."),
],
) -> None:
) -> dict[str, str]:
"""Delete a case task."""
await get_context().cases.delete_task(task_id)
ctx = get_context()
task = await ctx.cases.get_task(task_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: delete_task now requires case:read in addition to case:delete, introducing an RBAC regression for delete-only actors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tracecat-registry/tracecat_registry/core/ee/tasks.py, line 194:

<comment>`delete_task` now requires `case:read` in addition to `case:delete`, introducing an RBAC regression for delete-only actors.</comment>

<file context>
@@ -188,6 +188,9 @@ async def delete_task(
     """Delete a case task."""
-    await get_context().cases.delete_task(task_id)
+    ctx = get_context()
+    task = await ctx.cases.get_task(task_id)
+    await ctx.cases.delete_task(task_id)
+    return {"case_id": str(task["case_id"])}
</file context>

await ctx.cases.delete_task(task_id)
return {"case_id": str(task["case_id"])}
22 changes: 22 additions & 0 deletions tests/registry/test_core_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
import tracecat_registry.core.cases as cases_core
import tracecat_registry.core.ee.tasks as case_tasks_core
import tracecat_registry.types as registry_types
from tracecat_registry.core.cases import (
add_case_tag,
Expand All @@ -36,6 +37,7 @@
upload_attachment,
upload_attachment_from_url,
)
from tracecat_registry.core.ee.tasks import delete_task
from tracecat_registry.sdk.exceptions import (
TracecatValidationError,
)
Expand Down Expand Up @@ -878,6 +880,26 @@ async def test_delete_case_success(
assert result is None


@pytest.mark.anyio
class TestCoreDeleteTask:
"""Test cases for case task deletion UDF artifact context."""

async def test_delete_task_returns_parent_case_marker(self):
"""Delete task returns parent case ID for artifact projection."""
task_id = str(uuid.uuid4())
case_id = str(uuid.uuid4())
mock_cases_client = AsyncMock()
mock_cases_client.get_task.return_value = {"id": task_id, "case_id": case_id}
fake_ctx = SimpleNamespace(cases=mock_cases_client)

with patch.object(case_tasks_core, "get_context", return_value=fake_ctx):
result = await delete_task(task_id=task_id)

mock_cases_client.get_task.assert_awaited_once_with(task_id)
mock_cases_client.delete_task.assert_awaited_once_with(task_id)
assert result == {"case_id": case_id}


@pytest.mark.anyio
class TestCoreAssignUser:
"""Test cases for the assign_user UDF."""
Expand Down
153 changes: 153 additions & 0 deletions tests/unit/test_agent_stream_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from tracecat.artifacts.resolution import resolve_artifact_side_effects
from tracecat.artifacts.schemas import ArtifactAdapter
from tracecat.auth.types import Role
from tracecat.cases.enums import CaseSeverity, CaseStatus
from tracecat.chat import tokens
from tracecat.exceptions import TracecatNotFoundError
from tracecat.redis.client import RedisClient
Expand Down Expand Up @@ -271,6 +272,158 @@ async def fake_get_table_by_name(
assert resolved == []


@pytest.mark.anyio
async def test_resolve_artifact_side_effects_resolves_case_comment_ref(
monkeypatch: pytest.MonkeyPatch,
) -> None:
comment_id = uuid.UUID("33333333-3333-4333-8333-333333333333")
case_id = uuid.UUID("44444444-4444-4444-8444-444444444444")

async def fake_get_comment(
_service: object,
requested_comment_id: uuid.UUID,
) -> SimpleNamespace:
assert requested_comment_id == comment_id
return SimpleNamespace(case_id=case_id)

async def fake_get_case(
_service: object,
requested_case_id: uuid.UUID,
) -> SimpleNamespace:
assert requested_case_id == case_id
return SimpleNamespace(
id=case_id,
summary="Suspicious login",
severity=CaseSeverity.HIGH,
status=CaseStatus.IN_PROGRESS,
)

monkeypatch.setattr(
"tracecat.artifacts.resolution.CaseCommentsService.get_comment",
fake_get_comment,
)
monkeypatch.setattr(
"tracecat.artifacts.resolution.CasesService.get_case",
fake_get_case,
)

artifact = ArtifactAdapter.validate_python(
{
"type": "case",
"id": str(comment_id),
"title": str(comment_id),
"severity": "unknown",
"status": "unknown",
}
)

resolved = await resolve_artifact_side_effects(
[
ArtifactSideEffect(
op="upsert",
artifact=artifact,
identity_ref=ArtifactIdentityRef(
artifact_type="case",
ref=str(comment_id),
ref_kind="comment_id",
),
)
],
session=cast(AsyncSession, object()),
role=Role(
type="service",
service_id="tracecat-api",
workspace_id=uuid.uuid4(),
organization_id=uuid.uuid4(),
),
)

assert len(resolved) == 1
case_artifact = resolved[0].artifact
assert case_artifact.type == "case"
assert case_artifact.id == str(case_id)
assert case_artifact.title == "Suspicious login"
assert case_artifact.severity == CaseSeverity.HIGH
assert case_artifact.status == CaseStatus.IN_PROGRESS
assert resolved[0].identity_ref is None


@pytest.mark.anyio
async def test_resolve_artifact_side_effects_resolves_case_task_ref(
monkeypatch: pytest.MonkeyPatch,
) -> None:
task_id = uuid.UUID("55555555-5555-4555-8555-555555555555")
case_id = uuid.UUID("66666666-6666-4666-8666-666666666666")

async def fake_get_task(
_service: object,
requested_task_id: uuid.UUID,
) -> SimpleNamespace:
assert requested_task_id == task_id
return SimpleNamespace(case_id=case_id)

async def fake_get_case(
_service: object,
requested_case_id: uuid.UUID,
) -> SimpleNamespace:
assert requested_case_id == case_id
return SimpleNamespace(
id=case_id,
summary="Containment",
severity=CaseSeverity.MEDIUM,
status=CaseStatus.NEW,
)

monkeypatch.setattr(
"tracecat.artifacts.resolution.CaseTasksService.get_task",
fake_get_task,
)
monkeypatch.setattr(
"tracecat.artifacts.resolution.CasesService.get_case",
fake_get_case,
)

artifact = ArtifactAdapter.validate_python(
{
"type": "case",
"id": str(task_id),
"title": str(task_id),
"severity": "unknown",
"status": "unknown",
}
)

resolved = await resolve_artifact_side_effects(
[
ArtifactSideEffect(
op="upsert",
artifact=artifact,
identity_ref=ArtifactIdentityRef(
artifact_type="case",
ref=str(task_id),
ref_kind="task_id",
),
)
],
session=cast(AsyncSession, object()),
role=Role(
type="service",
service_id="tracecat-api",
workspace_id=uuid.uuid4(),
organization_id=uuid.uuid4(),
),
)

assert len(resolved) == 1
case_artifact = resolved[0].artifact
assert case_artifact.type == "case"
assert case_artifact.id == str(case_id)
assert case_artifact.title == "Containment"
assert case_artifact.severity == CaseSeverity.MEDIUM
assert case_artifact.status == CaseStatus.NEW
assert resolved[0].identity_ref is None


@pytest.mark.anyio
async def test_tool_result_artifact_pipeline_persists_and_streams_data_part() -> None:
"""Cover tool result -> projection -> Redis event -> Vercel data-artifact."""
Expand Down
Loading
Loading