Skip to content

Commit 6dec350

Browse files
authored
Merge branch 'release/v12.0.0' into backlog/fix-soc-ai-capability-group-prefixes
2 parents cf8b402 + 7cf512a commit 6dec350

65 files changed

Lines changed: 2662 additions & 1444 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/modules/alerts/connectors/repository.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type AlertRepository interface {
1919
CountOpenAlerts(ctx context.Context) (int64, error)
2020
CountByStatus(ctx context.Context, status int) (int64, error)
2121
SearchByIDs(ctx context.Context, alertIDs []string) ([]domain.UtmAlert, error)
22+
ListEchoes(ctx context.Context, parentID string, from, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error)
2223
GetRawByID(ctx context.Context, alertID string) (json.RawMessage, error)
2324
RelatedLogRefs(ctx context.Context, steps []domain.CorrelationStep, anchorTS time.Time, maxSize int) (refs []domain.LogRef, truncated bool, err error)
2425
}

backend/modules/alerts/connectors/usecase.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type AlertUsecase interface {
2525
ConvertToIncident(ctx context.Context, userLogin string, req dto.ConvertToIncidentRequest) error
2626
CountOpenAlerts(ctx context.Context) (*dto.CountOpenAlertsResponse, error)
2727
RelatedLogs(ctx context.Context, alertID string) (*dto.RelatedLogsResponse, error)
28+
ListEchoes(ctx context.Context, parentID string, page, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error)
2829
SetCorrelationResolver(r CorrelationResolver)
2930
}
3031

backend/modules/alerts/handler/alerts.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,37 @@ func (h *AlertHandler) CountOpenAlerts(c *gin.Context) {
185185
// Java returns bare Long — match that contract.
186186
c.JSON(http.StatusOK, resp.Count)
187187
}
188+
189+
// @Summary List echoes of an alert
190+
// @Description Returns the child alerts (parentId == :id) of a parent alert, paginated and sorted.
191+
// @Tags Alerts
192+
// @Security BearerAuth
193+
// @Produce json
194+
// @Param id path string true "Parent alert id"
195+
// @Param page query int false "Page number (1-based, default 1)"
196+
// @Param size query int false "Page size (default 20, max 100)"
197+
// @Param sortBy query string false "Sort field (default @timestamp)"
198+
// @Param sortOrder query string false "Sort order: asc|desc (default desc)"
199+
// @Success 200 {array} domain.UtmAlert
200+
// @Header 200 {string} X-Total-Count "Total matching echoes"
201+
// @Failure 400 {object} map[string]string
202+
// @Failure 500 {object} map[string]string
203+
// @Router /utm-alerts/{id}/echoes [get]
204+
func (h *AlertHandler) ListEchoes(c *gin.Context) {
205+
parentID := c.Param("id")
206+
if parentID == "" {
207+
c.JSON(http.StatusBadRequest, gin.H{"error": "missing alert id"})
208+
return
209+
}
210+
page := queryInt(c, "page", 1)
211+
size := queryInt(c, "size", 20)
212+
sortBy := c.Query("sortBy")
213+
sortOrder := c.DefaultQuery("sortOrder", "desc")
214+
215+
items, total, err := h.usecase.ListEchoes(c.Request.Context(), parentID, page, size, sortBy, sortOrder)
216+
if err != nil {
217+
writeAlertError(c, err)
218+
return
219+
}
220+
writePagedArray(c, items, total)
221+
}

backend/modules/alerts/repository/alert_os.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,20 @@ func (r *osAlertRepo) SearchByIDs(ctx context.Context, alertIDs []string) ([]dom
201201
return alerts, nil
202202
}
203203

204+
func (r *osAlertRepo) ListEchoes(ctx context.Context, parentID string, from, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error) {
205+
query := termQuery("parentId.keyword", parentID)
206+
raws, total, err := osSearchPage(ctx, alertIndex, query, from, size, sortBy, sortOrder)
207+
if err != nil {
208+
return nil, 0, err
209+
}
210+
alerts := make([]domain.UtmAlert, 0, len(raws))
211+
for _, raw := range raws {
212+
var a domain.UtmAlert
213+
if err := json.Unmarshal(raw, &a); err != nil {
214+
continue
215+
}
216+
alerts = append(alerts, a)
217+
}
218+
return alerts, total, nil
219+
}
220+

backend/modules/alerts/repository/osquery.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,37 @@ func osSearchSources(ctx context.Context, index string, query map[string]any, si
100100
return docs, nil
101101
}
102102

103+
// osSearchPage runs a paged + sorted search and returns the raw source docs
104+
// alongside the matching total count.
105+
func osSearchPage(ctx context.Context, index string, query map[string]any, from, size int, sortBy, sortOrder string) ([]json.RawMessage, int64, error) {
106+
body := map[string]any{
107+
"from": from,
108+
"size": size,
109+
"track_total_hits": true,
110+
}
111+
if query != nil {
112+
body["query"] = query
113+
}
114+
if sortBy != "" {
115+
body["sort"] = []map[string]any{
116+
{sortBy: map[string]any{"order": sortOrder}},
117+
}
118+
}
119+
res, err := osdk.RawSearch(ctx, []string{index}, body)
120+
if err != nil {
121+
return nil, 0, err
122+
}
123+
docs := make([]json.RawMessage, 0, len(res.Hits.Hits))
124+
for _, h := range res.Hits.Hits {
125+
b, err := json.Marshal(h.Source)
126+
if err != nil {
127+
return nil, 0, err
128+
}
129+
docs = append(docs, b)
130+
}
131+
return docs, res.Hits.Total.Value, nil
132+
}
133+
103134
func osCount(ctx context.Context, index string, query map[string]any) (int64, error) {
104135
body := map[string]any{"size": 0, "track_total_hits": true}
105136
if query != nil {

backend/modules/alerts/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func RegisterRoutes(api *gin.RouterGroup, m *Module, userAuth gin.HandlerFunc) {
2424

2525
ag.GET("/count-open-alerts", read, h.CountOpenAlerts)
2626
ag.GET("/related-logs", read, h.RelatedLogs)
27+
ag.GET("/:id/echoes", read, h.ListEchoes)
2728

2829
tg := api.Group("/utm-alert-tags", userAuth)
2930
tg.POST("", write, th.Create)

backend/modules/alerts/usecase/alert.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,29 @@ func (u *alertUsecase) CountOpenAlerts(ctx context.Context) (*dto.CountOpenAlert
160160
return &dto.CountOpenAlertsResponse{Count: count}, nil
161161
}
162162

163+
func (u *alertUsecase) ListEchoes(ctx context.Context, parentID string, page, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error) {
164+
if parentID == "" {
165+
return nil, 0, domain.ErrMissingAlertID
166+
}
167+
if page < 1 {
168+
page = 1
169+
}
170+
if size < 1 {
171+
size = 20
172+
}
173+
if size > 100 {
174+
size = 100
175+
}
176+
if sortBy == "" {
177+
sortBy = "@timestamp"
178+
}
179+
if sortOrder != "asc" && sortOrder != "desc" {
180+
sortOrder = "desc"
181+
}
182+
from := (page - 1) * size
183+
return u.repo.ListEchoes(ctx, parentID, from, size, sortBy, sortOrder)
184+
}
185+
163186
// ---------------------------------------------------------------------------
164187
// Internal helpers for HistoryEntry construction
165188
// ---------------------------------------------------------------------------

collectors/forwarder/collector/config.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func SyncCollectorConfig() error {
5555

5656
for key := range cnf.Integrations {
5757
if _, exists := config.ProtoPorts[config.DataType(key)]; !exists {
58+
integration := cnf.Integrations[key]
59+
if integration.HTTP != nil || integration.HTTPS != nil {
60+
continue
61+
}
5862
delete(cnf.Integrations, key)
5963
modified = true
6064
utils.Logger.Info("Removed obsolete integration from config: %s", key)
@@ -252,8 +256,6 @@ func IsPortBindable(port string, proto string) bool {
252256
}
253257
}
254258

255-
256-
257259
func ChangeFileIntegrationStatus(logTyp string, isEnabled bool) ([]string, error) {
258260
cnf, err := schema.ReadCollectorConfig()
259261
if err != nil {
@@ -319,11 +321,11 @@ func GetFileIntegrationPaths(logTyp string) ([]string, error) {
319321
}
320322

321323
type HTTPIntegrationOptions struct {
322-
Proto string // "http" or "https"
323-
Port string
324-
Path string // URL path, default "/logs"
325-
Bind string // bind address, default "127.0.0.1"
326-
Auth string // "" | "bearer" | "hmac"
324+
Proto string // "http" or "https"
325+
Port string
326+
Path string // URL path, default "/logs"
327+
Bind string // bind address, default "127.0.0.1"
328+
Auth string // "" | "bearer" | "hmac"
327329
SignatureHeader string // e.g. "X-Hub-Signature-256"
328330
}
329331

@@ -352,11 +354,11 @@ func EnableHTTPIntegration(logTyp string, opts HTTPIntegrationOptions) error {
352354

353355
integration := cfg.Integrations[logTyp]
354356
httpPort := &schema.HTTPPort{
355-
Enabled: true,
356-
Port: opts.Port,
357-
Path: opts.Path,
358-
Bind: opts.Bind,
359-
Auth: opts.Auth,
357+
Enabled: true,
358+
Port: opts.Port,
359+
Path: opts.Path,
360+
Bind: opts.Bind,
361+
Auth: opts.Auth,
360362
SignatureHeader: opts.SignatureHeader,
361363
}
362364

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
import { ListFilter, Loader2, Search } from 'lucide-react'
3+
import { useTranslation } from 'react-i18next'
4+
import { cn } from '@/shared/lib/utils'
5+
import { Button } from '@/shared/components/ui/button'
6+
import { Input } from '@/shared/components/ui/input'
7+
import { alertsHttpService as svc } from '../services/alerts-http.service'
8+
import { FILTER_FIELDS, FILTER_OPS, SELECT_CLS, fieldKey } from '../lib/alert-meta'
9+
import type { CustomFilter } from '../types/alert.types'
10+
11+
export function AddFilterButton({ onAdd }: { onAdd: (f: CustomFilter) => void }) {
12+
const { t } = useTranslation()
13+
const [open, setOpen] = useState(false)
14+
const [field, setField] = useState(FILTER_FIELDS[0].field)
15+
const [operator, setOperator] = useState('IS')
16+
const [values, setValues] = useState<{ value: string; count: number }[]>([])
17+
const [loadingValues, setLoadingValues] = useState(false)
18+
const [vq, setVq] = useState('')
19+
const ref = useRef<HTMLDivElement>(null)
20+
21+
const op = FILTER_OPS.find((o) => o.id === operator)
22+
const needsValue = op?.needsValue ?? true
23+
24+
useEffect(() => {
25+
if (!open) return
26+
const onDoc = (e: MouseEvent) => ref.current && !ref.current.contains(e.target as Node) && setOpen(false)
27+
document.addEventListener('mousedown', onDoc)
28+
return () => document.removeEventListener('mousedown', onDoc)
29+
}, [open])
30+
31+
// Fetch the field's real existing values when the field changes.
32+
useEffect(() => {
33+
if (!open || !needsValue) return
34+
setLoadingValues(true)
35+
setValues([])
36+
svc
37+
.fieldValues(field)
38+
.then(setValues)
39+
.catch(() => setValues([]))
40+
.finally(() => setLoadingValues(false))
41+
}, [open, field, needsValue])
42+
43+
const add = (value: string) => {
44+
const fdef = FILTER_FIELDS.find((f) => f.field === field)!
45+
onAdd({ field, label: fdef.label, operator, value })
46+
setOpen(false)
47+
setVq('')
48+
}
49+
50+
const filtered = values.filter((v) => (vq ? String(v.value).toLowerCase().includes(vq.toLowerCase()) : true))
51+
52+
return (
53+
<div className="relative" ref={ref}>
54+
<button
55+
onClick={() => setOpen((v) => !v)}
56+
className="inline-flex items-center gap-1.5 rounded-full border border-dashed border-border px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
57+
>
58+
<ListFilter size={12} /> {t('alerts.filters.add')}
59+
</button>
60+
{open && (
61+
<div className="absolute left-0 top-full z-30 mt-1 w-80 rounded-md border border-border bg-popover p-3 shadow-lg">
62+
<div className="space-y-2">
63+
<select value={field} onChange={(e) => setField(e.target.value)} className={cn(SELECT_CLS, 'w-full')}>
64+
{FILTER_FIELDS.map((f) => (
65+
<option key={f.field} value={f.field}>
66+
{t(`alerts.fields.${fieldKey(f.field)}`)}
67+
</option>
68+
))}
69+
</select>
70+
<select value={operator} onChange={(e) => setOperator(e.target.value)} className={cn(SELECT_CLS, 'w-full')}>
71+
{FILTER_OPS.map((o) => (
72+
<option key={o.id} value={o.id}>
73+
{t(`alerts.ops.${o.id}`)}
74+
</option>
75+
))}
76+
</select>
77+
78+
{needsValue ? (
79+
<>
80+
<div className="relative">
81+
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
82+
<Input
83+
value={vq}
84+
onChange={(e) => setVq(e.target.value)}
85+
placeholder={t('alerts.filters.filterValues')}
86+
className="h-8 pl-8 text-xs"
87+
autoFocus
88+
/>
89+
</div>
90+
<div className="max-h-48 overflow-y-auto rounded-md border border-border">
91+
{loadingValues ? (
92+
<div className="flex items-center gap-1.5 px-3 py-3 text-xs text-muted-foreground">
93+
<Loader2 className="h-3.5 w-3.5 animate-spin" /> {t('alerts.filters.loadingValues')}
94+
</div>
95+
) : filtered.length === 0 ? (
96+
<div className="px-3 py-3 text-xs text-muted-foreground">{t('alerts.filters.noValues')}</div>
97+
) : (
98+
filtered.map((v) => (
99+
<button
100+
key={v.value}
101+
onClick={() => add(v.value)}
102+
className="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-xs hover:bg-muted"
103+
>
104+
<span className="truncate font-mono">{v.value || t('alerts.filters.empty')}</span>
105+
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">{v.count.toLocaleString()}</span>
106+
</button>
107+
))
108+
)}
109+
</div>
110+
<p className="text-[10px] text-muted-foreground">{t('alerts.filters.pickValue')}</p>
111+
</>
112+
) : (
113+
<div className="flex justify-end gap-2 pt-1">
114+
<Button variant="outline" size="sm" onClick={() => setOpen(false)}>
115+
{t('alerts.filters.cancel')}
116+
</Button>
117+
<Button size="sm" onClick={() => add('')}>
118+
{t('alerts.filters.addBtn')}
119+
</Button>
120+
</div>
121+
)}
122+
</div>
123+
</div>
124+
)}
125+
</div>
126+
)
127+
}

0 commit comments

Comments
 (0)