@cryptotaxi247 / CoPilot / commits / 5ad9e7cb

feat(event-sources): add displayed_columns config field (issue #833 slice 1/3) (#868)

* feat(event-sources): add displayed_columns config field (issue #833, slice 1a) First slice of issue #833 — custom columns for the event-search table. SOC admins will be able to choose which fields show up in the table per event source; both the SOC portal and the customer portal will render those columns. Storage decision: per EventSource row, JSON-typed, nullable. NULL/empty falls back to the frontend's hardcoded defaults so behaviour is unchanged for un-customised sources. This commit covers the model + Pydantic schema only — Alembic migration will be added in a follow-up commit on this branch. Slices 2 and 3 (SOC config UI, customer-portal render) ship as separate PRs. Changes: - universal_models.py: add `displayed_columns: Optional[List[dict]]` to EventSources via `Column(JSON, nullable=True)`. Imports gain `JSON` from sqlalchemy and `List` from typing. `update_from_model` now propagates the field. - siem/schema/event_sources.py: new `DisplayColumn` Pydantic model ({key, label, width?}). Wire `displayed_columns: Optional[List[DisplayColumn]]` into EventSourceCreate, EventSourceUpdate, EventSourceResponse. CRUD service uses `model_dump()` so the new field flows through without service-layer changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(event-sources): add upgrade and downgrade for displayed_columns in event_sources table * style: standardize string quotes and import order in add_display_event_source_columns migration --------- Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

taylorcopilot committed May 9, 2026 at 12:18 UTC 5ad9e7cbeefde1544894dd9238ca90fa6e42e386
3 files changed +58
backend/alembic/versions/0912fd37e41c_add_display_event_source_columns.py new
+31
@@ -0,0 +1,31 @@
1 +"""Add display event source columns
2 +
3 +Revision ID: 0912fd37e41c
4 +Revises: 260371af0a48
5 +Create Date: 2026-05-09 12:11:03.609820
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +
13 +from alembic import op
14 +
15 +# revision identifiers, used by Alembic.
16 +revision: str = "0912fd37e41c"
17 +down_revision: Union[str, None] = "260371af0a48"
18 +branch_labels: Union[str, Sequence[str], None] = None
19 +depends_on: Union[str, Sequence[str], None] = None
20 +
21 +
22 +def upgrade() -> None:
23 + # ### commands auto generated by Alembic - please adjust! ###
24 + op.add_column("event_sources", sa.Column("displayed_columns", sa.JSON(), nullable=True))
25 + # ### end Alembic commands ###
26 +
27 +
28 +def downgrade() -> None:
29 + # ### commands auto generated by Alembic - please adjust! ###
30 + op.drop_column("event_sources", "displayed_columns")
31 + # ### end Alembic commands ###
backend/app/db/universal_models.py
+10
@@ -1,7 +1,9 @@
1 from datetime import datetime
2 +from typing import List
3 from typing import Optional
4
5 from loguru import logger
6 +from sqlalchemy import JSON
7 from sqlalchemy import Column
8 from sqlalchemy import Float
9 from sqlalchemy import LargeBinary
@@ -518,6 +520,12 @@ class EventSources(SQLModel, table=True):
520 event_type: str = Field(max_length=50, nullable=False) # EDR, EPP, Cloud Integration, Network Security
521 time_field: str = Field(max_length=255, nullable=False, default="timestamp")
522 enabled: bool = Field(default=True)
523 + # List of {key, label, width?} dicts. NULL/empty means "use the frontend's
524 + # hardcoded defaults" so behaviour for un-customised sources is unchanged.
525 + displayed_columns: Optional[List[dict]] = Field(
526 + default=None,
527 + sa_column=Column(JSON, nullable=True),
528 + )
529 created_at: datetime = Field(default_factory=datetime.utcnow)
530 updated_at: datetime = Field(default_factory=datetime.utcnow)
531
@@ -534,6 +542,8 @@ class EventSources(SQLModel, table=True):
542 self.time_field = source_data.time_field
543 if hasattr(source_data, "enabled"):
544 self.enabled = source_data.enabled
545 + if hasattr(source_data, "displayed_columns"):
546 + self.displayed_columns = source_data.displayed_columns
547 self.updated_at = datetime.utcnow()
548
549
backend/app/siem/schema/event_sources.py
+17
@@ -15,6 +15,17 @@ class EventType(str, Enum):
15 NETWORK_SECURITY = "Network Security"
16
17
18 +class DisplayColumn(BaseModel):
19 + """One column the SOC has chosen to surface in the event-search table."""
20 +
21 + key: str = Field(
22 + ...,
23 + description="Field path in the event source's _source object (dotted, e.g. 'agent.name' or 'data.win.eventdata.targetUserName').",
24 + )
25 + label: str = Field(..., description="Human-readable column header.")
26 + width: Optional[int] = Field(None, description="Optional pixel width hint for the table column.")
27 +
28 +
29 class EventSourceCreate(BaseModel):
30 customer_code: str = Field(..., max_length=50)
31 name: str = Field(..., max_length=255)
@@ -22,6 +33,10 @@ class EventSourceCreate(BaseModel):
33 event_type: EventType
34 time_field: str = Field("timestamp", max_length=255)
35 enabled: bool = True
36 + displayed_columns: Optional[List[DisplayColumn]] = Field(
37 + None,
38 + description="Per-source column layout for the event-search table. None or empty falls back to the frontend's hardcoded defaults.",
39 + )
40
41
42 class EventSourceUpdate(BaseModel):
@@ -30,6 +45,7 @@ class EventSourceUpdate(BaseModel):
45 event_type: Optional[EventType] = None
46 time_field: Optional[str] = Field(None, max_length=255)
47 enabled: Optional[bool] = None
48 + displayed_columns: Optional[List[DisplayColumn]] = None
49
50
51 class EventSourceResponse(BaseModel):
@@ -40,6 +56,7 @@ class EventSourceResponse(BaseModel):
56 event_type: str
57 time_field: str
58 enabled: bool
59 + displayed_columns: Optional[List[DisplayColumn]] = None
60 created_at: datetime
61 updated_at: datetime
62 model_config = ConfigDict(from_attributes=True)