Skip to content

Commit 8172588

Browse files
committed
feat(#912): exploration statistique en CLI et dans l'API
- netcross_core.stats : explore_stats (dict correlate() ou liste de Flow), build_events_by_segment (deplace de netcross_gtk4.stats_view, reexporte), stats_format_for, render_stats - CLI : --stats FICHIER (CSV si .csv, JSON sinon), --stats-group-by, --stats-sort, --stats-top-n (0 = toutes), --stats-format - API : GET /analyses/{analysis_id}/stats (group_by, sort, top_n, format json|csv) - parite des surfaces, api-rest, guide « Lire le rapport », openapi.yaml, diagramme ; 21 tests
1 parent d6f8bd5 commit 8172588

10 files changed

Lines changed: 502 additions & 34 deletions

File tree

‎docs/api-rest.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ La spécification OpenAPI est disponible sur :
8080
| `POST` | `/tools/export` | Sous-ensemble filtré (`file`, `bpf`, `time_start`, `time_end`, `endpoints`, `format`) : `--export-pcap` (issue #886) |
8181
| `POST` | `/tools/adjust-time` | Recalage (`file`, puis `time_offset`, `normalize` ou `align_to`, `format`) : `--adjust-time-output` (issue #887) |
8282
| `POST` | `/tools/replay` | Rejeu (`file`, `interface`, `speed`, `loop`) : `--replay`. ÉMET du trafic réel ; refusé (403) sauf si `interface` figure dans `NETCROSS_REPLAY_INTERFACES` (liste séparée par des virgules, vide par défaut) ; 503 si tcpreplay absent (issue #871) |
83+
| `GET` | `/analyses/{id}/stats` | Exploration statistique : `group_by` (`endpoint`, `protocol`, `segment`, `flow`), `sort` (`packets`, `bytes`, `duration_ms`, `throughput_bps`, `latency_ms`, `events`), `top_n` (0 = toutes), `format=json\|csv` ; `flow_keys` par ligne pour le drill-down ; équiv. `--stats` (issue #912) |
8384
| `GET` | `/health` | Health check |
8485

8586
## Exemples curl

‎docs/class-diagram.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
> Il remplace l'ancienne section 3 de `docs/features-backlog.md`, tenue à la main, qui avait dérivé
1212
> (voir `docs/sessions/session-36.md`, issue #140).
1313
14-
194 modules · 282 classes · 671 fonctions publiques de module.
14+
194 modules · 282 classes · 676 fonctions publiques de module.
1515

1616
Conventions : `+` public, `-` privé (préfixe `_`) ; `int?` = `int | None` ; `list~str~` = `list[str]` ;
1717
`<<module>>` regroupe les fonctions publiques d'un module ; `A --> B : champ` = `A` a un champ annoté
@@ -32,17 +32,17 @@ flowchart TD
3232
netcross_ai["netcross_ai"]
3333
netcross_core["netcross_core"]
3434
pcap_parser["pcap_parser"]
35-
CLI -->|"20 imports"| netcross_report
35+
CLI -->|"21 imports"| netcross_report
3636
CLI -->|"8 imports"| netcross_ai
37-
CLI -->|"41 imports"| netcross_core
37+
CLI -->|"45 imports"| netcross_core
3838
CLI -->|"8 imports"| pcap_parser
3939
netcross_gtk4 -->|"31 imports"| netcross_report
4040
netcross_gtk4 -->|"4 imports"| netcross_ai
41-
netcross_gtk4 -->|"109 imports"| netcross_core
41+
netcross_gtk4 -->|"110 imports"| netcross_core
4242
netcross_gtk4 -->|"6 imports"| pcap_parser
4343
netcross_api -->|"19 imports"| netcross_report
4444
netcross_api -->|"5 imports"| netcross_ai
45-
netcross_api -->|"42 imports"| netcross_core
45+
netcross_api -->|"44 imports"| netcross_core
4646
netcross_api -->|"3 imports"| pcap_parser
4747
netcross_report -->|"40 imports"| netcross_core
4848
netcross_ai -->|"10 imports"| netcross_core
@@ -1700,6 +1700,10 @@ classDiagram
17001700
+compute_stats(flows, report, all_packets, query, events_by_segment) list~StatRow~
17011701
+export_csv(rows) str
17021702
+export_json(rows) list~dict~
1703+
+build_events_by_segment(findings) dict~str, list~
1704+
+explore_stats(flows, report, findings, group_by, sort_by, top_n) list~StatRow~
1705+
+stats_format_for(path, fmt) str
1706+
+render_stats(rows, fmt) str
17031707
}
17041708
17051709
%% ===== netcross_core.tls_diagnostics =====
@@ -3936,6 +3940,7 @@ classDiagram
39363940
+get_analysis_pdf(analysis_id, topn, _auth) StreamingResponse
39373941
+get_analysis_csv(analysis_id, _auth) PlainTextResponse
39383942
+get_analysis_sequence(analysis_id, max_flows, _auth) JSONResponse
3943+
+get_analysis_stats(analysis_id, group_by, sort, top_n, format, _auth) Response
39393944
+get_analysis_siem(analysis_id, format, _auth) PlainTextResponse
39403945
+forensic_search(analysis_id, body, _auth) JSONResponse
39413946
+extract_contents(analysis_id, files, labels, kinds, _auth) StreamingResponse

‎docs/guide/lire-le-rapport.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ présentes.
9696
Quand Netcross ne peut pas conclure, il l'écrit (« indéterminé »,
9797
« impossible de conclure ») plutôt que de deviner. C'est volontaire.
9898

99+
## L'exploration statistique
100+
101+
La vue « Exploration statistique » de la GUI (Top-N, tri, regroupement)
102+
s'exporte aussi en ligne de commande, en CSV ou en JSON :
103+
104+
```bash
105+
netcross --capture LAN=lan.pcapng --capture WAN=wan.pcapng --stats stats.csv \
106+
--stats-group-by protocol --stats-sort throughput_bps --stats-top-n 20
107+
netcross --capture LAN=lan.pcapng --stats stats.json # 10 hôtes les plus volumineux
108+
```
109+
110+
Regroupements : `endpoint`, `protocol`, `segment`, `flow`. Tris (décroissants) :
111+
`packets`, `bytes`, `duration_ms`, `throughput_bps`, `latency_ms`, `events`.
112+
`--stats-top-n 0` garde toutes les lignes. La colonne `flow_keys` liste les
113+
flux de chaque ligne, pour descendre au détail comme en cliquant une ligne
114+
dans la GUI. Par l'API : `GET /analyses/{id}/stats` avec les mêmes paramètres.
115+
99116
## Les autres formats
100117

101118
- **PDF** (`--pdf-report`) : destiné à être transmis. Il s'ouvre sur « Par

‎docs/openapi.yaml‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,95 @@ paths:
654654
application/json:
655655
schema:
656656
$ref: '#/components/schemas/HTTPValidationError'
657+
/analyses/{analysis_id}/stats:
658+
get:
659+
tags:
660+
- analyses
661+
summary: Get Analysis Stats
662+
description: 'Exploration statistique (équiv. ``--stats``, vue GUI « Exploration
663+
statistique », issue #912).
664+
665+
666+
Chaque ligne porte ``flow_keys`` : les clés des flux regroupés (drill-down).'
667+
operationId: get_analysis_stats_analyses__analysis_id__stats_get
668+
security:
669+
- APIKeyHeader: []
670+
parameters:
671+
- name: analysis_id
672+
in: path
673+
required: true
674+
schema:
675+
type: string
676+
title: Analysis Id
677+
- name: group_by
678+
in: query
679+
required: false
680+
schema:
681+
type: string
682+
description: endpoint, protocol, segment ou flow
683+
default: endpoint
684+
title: Group By
685+
description: endpoint, protocol, segment ou flow
686+
- name: sort
687+
in: query
688+
required: false
689+
schema:
690+
type: string
691+
description: packets, bytes, duration_ms, throughput_bps, latency_ms ou
692+
events
693+
default: bytes
694+
title: Sort
695+
description: packets, bytes, duration_ms, throughput_bps, latency_ms ou events
696+
- name: top_n
697+
in: query
698+
required: false
699+
schema:
700+
type: integer
701+
minimum: 0
702+
description: N premières lignes (0 = toutes)
703+
default: 10
704+
title: Top N
705+
description: N premières lignes (0 = toutes)
706+
- name: format
707+
in: query
708+
required: false
709+
schema:
710+
type: string
711+
description: json ou csv
712+
default: json
713+
title: Format
714+
description: json ou csv
715+
responses:
716+
'200':
717+
description: Successful Response
718+
content:
719+
application/json:
720+
schema: {}
721+
text/csv: {}
722+
'401':
723+
content:
724+
application/json:
725+
schema:
726+
$ref: '#/components/schemas/ErrorResponse'
727+
description: Unauthorized
728+
'404':
729+
content:
730+
application/json:
731+
schema:
732+
$ref: '#/components/schemas/ErrorResponse'
733+
description: Not Found
734+
'409':
735+
content:
736+
application/json:
737+
schema:
738+
$ref: '#/components/schemas/ErrorResponse'
739+
description: Conflict
740+
'422':
741+
description: Validation Error
742+
content:
743+
application/json:
744+
schema:
745+
$ref: '#/components/schemas/HTTPValidationError'
657746
/analyses/{analysis_id}/siem:
658747
get:
659748
tags:

‎docs/parite-surfaces.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ ici fait donc échouer la CI.
1414

1515
| Surface | Points d'entrée | Capacités |
1616
|---|---|---|
17-
| CLI | `cross_capture_analyzer_cli.py` (110 options), `cross_capture_diff_cli.py` (27), `cross_capture_batch_cli.py` (11), `cross_history_cli.py`, `netcross_lua_doc_cli.py`, `netcross_ai_models_cli.py` | Surface de référence : tout y est |
17+
| CLI | `cross_capture_analyzer_cli.py` (115 options), `cross_capture_diff_cli.py` (27), `cross_capture_batch_cli.py` (11), `cross_history_cli.py`, `netcross_lua_doc_cli.py`, `netcross_ai_models_cli.py` | Surface de référence : tout y est |
1818
| GUI | `netcross_gtk4` : 3 pages, 43 cases à cocher, 31 réglages numériques | Analyse interactive et exploration visuelle |
19-
| API | 41 routes FastAPI | Analyse avec sécurité ; options NAT, TLS, QUIC, anonymisation ; exports texte, PDF, CSV (issue #670) ; options avancées (#672) ; comparaison baseline/courant (#669) ; recherche forensic, extraction, comparaison de postes, NetFlow (#675) |
19+
| API | 42 routes FastAPI | Analyse avec sécurité ; options NAT, TLS, QUIC, anonymisation ; exports texte, PDF, CSV (issue #670) ; options avancées (#672) ; comparaison baseline/courant (#669) ; recherche forensic, extraction, comparaison de postes, NetFlow (#675) |
2020

2121

2222
Légende : **oui** = disponible ; **non** = absent ; **auto** = toujours
@@ -90,6 +90,7 @@ actif, non réglable.
9090
| Graphiques Top-N du PDF | oui (« Top-N graphiques ») | `--topn-charts` | `topn` de `GET /analyses/{analysis_id}/pdf` (issue #865) |
9191
| Rapport de sécurité HTML / JSON | oui (section Sécurité, 2 boutons) | `--security-html` ; clé `security_report` de `--json-report` | `GET /analyses/{analysis_id}/security` (liste simplifiée) |
9292
| Diagramme de séquence | oui (« Diagramme de sequence » de la configuration, dans l'export PDF ; issue #674) | `--sequence-diagram` | `GET /analyses/{analysis_id}/sequence` (issue #674), `format`, `max_flows` |
93+
| Exploration statistique (Top-N, tri, regroupement, drill-down, export CSV/JSON) | oui (vue « Exploration statistique » : regroupement, tri, Top-N, sélection d'une ligne, « Exporter CSV/JSON ») | `--stats FICHIER` (CSV si `.csv`, JSON sinon), `--stats-group-by`, `--stats-sort`, `--stats-top-n`, `--stats-format` ; `flow_keys` de chaque ligne pour le drill-down (issue #912) | `GET /analyses/{analysis_id}/stats` : `group_by`, `sort`, `top_n`, `format` (`json` ou `csv` ; issue #912) |
9394
| Export SIEM (CEF, LEEF, STIX) | oui (panneau « SIEM, ticket de support, historique » de la page Résultats, après un rapport de sécurité, même fichier ; issue #674) | `--siem-export`, `--siem-output` | `GET /analyses/{analysis_id}/siem` (issue #674), `format` |
9495
| Historique SQLite | oui (« Historique... » et « Etiquette » de la configuration, derniers runs dans le panneau « SIEM, ticket de support, historique » ; issue #674) | `--history-db`, `--history-label`, `--history-show` ; `netcross-history` | `history`, `history_label` de `POST /captures` et `POST /captures/multi` (base fixée par `NETCROSS_HISTORY_DB`, `history_id` ou `history_error` dans le résumé ; issue #890), consultation `GET /history` ; persistance propre des analyses (`NETCROSS_DB_PATH`), `GET /analyses` |
9596
| Ticket de support anonymisé | oui (case de consentement et « Ticket de support... » du panneau « SIEM, ticket de support, historique », ticket « diagnostic », toutes portées ; issue #674) | `--support-ticket`, `--support-consent`, `--support-scope`, `--support-map`, `--support-marker` | `POST /analyses/{analysis_id}/support-ticket` (issue #674), `consent`, `kind`, `scopes`, `marker`, `include_map` (issue #877) |
@@ -192,12 +193,11 @@ fenêtre « Outils de capture » (bouton de la barre de titre).
192193
| Cartographie des communications (filtres protocole, Top-N, « Anomalies seulement ») | Qui parle à qui, anomalies en rouge |
193194
| Annotations / signets | Étiquettes sur des trames, enregistrées à côté de la capture |
194195
| Dashboard analytique | Sélection croisée segments / flux / hôtes / protocoles / événements |
195-
| Exploration statistique (export CSV et JSON) | Top-N, tri, regroupement, drill-down |
196196

197197
Ce sont des vues d'exploration interactives. Leur calcul vit déjà hors de
198198
la GUI (`netcross_report/comm_map.py`, `netcross_gtk4/stats_view.py`,
199199
`netcross_gtk4/dashboard_context.py`), ce qui permettrait de les exposer
200-
ailleurs : c'est l'objet de #909 à #912 (règle sans exemption, #330).
200+
ailleurs : c'est l'objet de #909 à #911 (exploration statistique : #912, livrée) (règle sans exemption, #330).
201201

202202
## Inventaire des cases de la GUI
203203

@@ -296,6 +296,7 @@ l'option CLI de même nom) : `nat_tolerant`, `nat_window_ms`, `tls`,
296296
| `GET /lua-doc/search` | Recherche dans la documentation Lua, `q`, `limit`, `full` (issue #873) |
297297
| `GET /comparisons/{comparison_id}/redact-map` | Table d'anonymisation commune baseline/courant (issue #876) |
298298
| `POST /tools/replay` | Rejeu sur une interface autorisée (issue #871) |
299+
| `GET /analyses/{analysis_id}/stats` | Exploration statistique, JSON ou CSV (issue #912) |
299300

300301
Réglages par variables d'environnement (`NETCROSS_API_TOKEN` pour
301302
l'en-tête `X-API-Key`, `NETCROSS_MAX_UPLOAD_MB`, `NETCROSS_API_MAX_FILES`,
@@ -325,8 +326,7 @@ Classés par impact pour un utilisateur :
325326
Révisé le 11/10/2026 (#330) : plus aucune exemption, toute fonction
326327
doit exister en CLI, GTK4 et API. Restent : l'API de la capture en
327328
direct (#902 à #908) et la CLI et l'API des vues propres à la GUI
328-
(#909 cartographie, #910 annotations, #911 dashboard, #912
329-
exploration statistique).
329+
(#909 cartographie, #910 annotations, #911 dashboard ; #912 livrée).
330330

331331
Correctifs apportés à l'audit du 24/09 (commentaire de #330) :
332332

‎src/cross_capture_analyzer_cli.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@
156156
from netcross_core.security import close_db, connect_cve_db
157157
from netcross_core.security import findings as security_findings
158158
from netcross_core.security.cve_seed import open_seed_db
159+
from netcross_core.stats import EXPORT_FORMATS as STATS_EXPORT_FORMATS
160+
from netcross_core.stats import GROUP_BY as STATS_GROUP_BY
161+
from netcross_core.stats import SORT_BY as STATS_SORT_BY
162+
from netcross_core.stats import explore_stats, render_stats, stats_format_for
159163
from netcross_core.support import (
160164
SCOPES as SUPPORT_SCOPES,
161165
)
@@ -1290,6 +1294,23 @@ def _run_content_extraction(captures, out_dir, kinds) -> None:
12901294
logger.debug("_run_content_extraction: fin")
12911295

12921296

1297+
def _check_stats_args(args) -> None:
1298+
"""Validations de l'issue #912."""
1299+
modifies = (
1300+
args.stats_group_by != "endpoint"
1301+
or args.stats_sort != "bytes"
1302+
or args.stats_top_n != 10
1303+
or args.stats_format is not None
1304+
)
1305+
if modifies and not args.stats:
1306+
print("--stats-group-by/--stats-sort/--stats-top-n/--stats-format necessitent --stats.", file=sys.stderr)
1307+
sys.exit(1)
1308+
if args.stats_top_n < 0:
1309+
print("--stats-top-n : 0 (toutes) ou plus.", file=sys.stderr)
1310+
sys.exit(1)
1311+
logger.debug("_check_stats_args: fin")
1312+
1313+
12931314
def _check_live_report_args(args) -> None:
12941315
"""Validations de l'issue #274."""
12951316
logger.debug("_check_live_report_args: args={}", summarize(args, "args"))
@@ -1601,6 +1622,32 @@ def main():
16011622
)
16021623
ap.add_argument("--order", help="Ordre physique des points sur le chemin reseau, ex: LAN,WAN,DC")
16031624
ap.add_argument("--detail-csv", help="Chemin de sortie pour le detail par flux (CSV)")
1625+
# Issue #912 : exploration statistique (vue GUI « Exploration statistique »)
1626+
ap.add_argument(
1627+
"--stats",
1628+
metavar="FICHIER",
1629+
help="Exploration statistique (Top-N, tri, regroupement) : CSV si FICHIER finit par .csv, JSON sinon "
1630+
"(--stats-format pour forcer) ; flow_keys de chaque ligne = drill-down vers les flux",
1631+
)
1632+
ap.add_argument(
1633+
"--stats-group-by",
1634+
choices=STATS_GROUP_BY,
1635+
default="endpoint",
1636+
help="Statistiques : regroupement (defaut endpoint)",
1637+
)
1638+
ap.add_argument(
1639+
"--stats-sort", choices=STATS_SORT_BY, default="bytes", help="Statistiques : tri decroissant (defaut bytes)"
1640+
)
1641+
ap.add_argument(
1642+
"--stats-top-n",
1643+
type=int,
1644+
default=10,
1645+
metavar="N",
1646+
help="Statistiques : N premieres lignes (defaut 10, 0 = toutes)",
1647+
)
1648+
ap.add_argument(
1649+
"--stats-format", choices=STATS_EXPORT_FORMATS, help="Statistiques : csv ou json (defaut : extension)"
1650+
)
16041651
ap.add_argument(
16051652
"--names",
16061653
metavar="PATH",
@@ -2734,6 +2781,7 @@ def main():
27342781
sys.exit(1)
27352782
extract_kinds = _check_extraction_args(args)
27362783
_check_live_report_args(args)
2784+
_check_stats_args(args)
27372785
if args.redact and (args.tls or args.quic or args.client_group):
27382786
print(
27392787
"--redact n'est pas disponible avec --tls/--quic/--client-group : ces "
@@ -3168,6 +3216,24 @@ def main():
31683216
if args.detail_csv:
31693217
write_detail_csv(args.detail_csv, flows, r.points, names=names)
31703218
print(f"\nDetail par flux ecrit dans {args.detail_csv}")
3219+
if args.stats:
3220+
from netcross_report import build_findings as _build_findings
3221+
3222+
rows = explore_stats(
3223+
flows,
3224+
r,
3225+
findings if findings is not None else _build_findings(r),
3226+
group_by=args.stats_group_by,
3227+
sort_by=args.stats_sort,
3228+
top_n=args.stats_top_n,
3229+
)
3230+
fmt = stats_format_for(args.stats, args.stats_format)
3231+
with open(args.stats, "w", encoding="utf-8", newline="") as f:
3232+
f.write(render_stats(rows, fmt))
3233+
print(
3234+
f"\nStatistiques ({fmt.upper()}, par {args.stats_group_by}, tri {args.stats_sort}) "
3235+
f"ecrites dans {args.stats} : {len(rows)} ligne(s)"
3236+
)
31713237

31723238
# Objets de contrat de la Session 0 (FEATURES.md section 13.3) --
31733239
# construits UNE fois ici, puis partages par la console

‎src/netcross_api/app.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile
4545
from fastapi.concurrency import run_in_threadpool
46-
from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse
46+
from fastapi.responses import JSONResponse, PlainTextResponse, Response, StreamingResponse
4747
from fastapi.security import APIKeyHeader
4848

4949
from netcross_api.models import (
@@ -1687,6 +1687,47 @@ async def get_analysis_sequence(
16871687
return JSONResponse({"sequence_views": [asdict(v) for v in views]})
16881688

16891689

1690+
@app.get(
1691+
"/analyses/{analysis_id}/stats",
1692+
tags=["analyses"],
1693+
responses={**_ANALYSIS_RESPONSES, 404: {"model": ErrorResponse}, 200: {"content": {"text/csv": {}}}},
1694+
)
1695+
async def get_analysis_stats(
1696+
analysis_id: str,
1697+
group_by: str = Query(default="endpoint", description="endpoint, protocol, segment ou flow"),
1698+
sort: str = Query(default="bytes", description="packets, bytes, duration_ms, throughput_bps, latency_ms ou events"),
1699+
top_n: int = Query(default=10, ge=0, description="N premières lignes (0 = toutes)"),
1700+
format: str = Query(default="json", description="json ou csv"),
1701+
_auth: None = Depends(_verify_api_key),
1702+
) -> Response:
1703+
"""Exploration statistique (équiv. ``--stats``, vue GUI « Exploration statistique », issue #912).
1704+
1705+
Chaque ligne porte ``flow_keys`` : les clés des flux regroupés (drill-down)."""
1706+
from netcross_core.stats import explore_stats, render_stats, stats_format_for
1707+
1708+
try:
1709+
fmt = stats_format_for("", format)
1710+
except ValueError as exc:
1711+
logger.debug("get_analysis_stats: format invalide ({})", exc)
1712+
raise HTTPException(status_code=400, detail=str(exc)) from exc
1713+
entry = _completed_entry(analysis_id)
1714+
flows = entry.get("flows")
1715+
report_obj = entry.get("report_obj")
1716+
if flows is None or report_obj is None:
1717+
logger.debug("get_analysis_stats: pas de flux ou de rapport -> 404")
1718+
raise HTTPException(status_code=404, detail="Aucun flux disponible pour les statistiques")
1719+
try:
1720+
rows = explore_stats(flows, report_obj, entry.get("findings"), group_by=group_by, sort_by=sort, top_n=top_n)
1721+
except ValueError as exc:
1722+
logger.debug("get_analysis_stats: paramètre invalide ({})", exc)
1723+
raise HTTPException(status_code=400, detail=str(exc)) from exc
1724+
if fmt == "csv":
1725+
return PlainTextResponse(render_stats(rows, "csv"), media_type="text/csv")
1726+
from netcross_core.stats import export_json
1727+
1728+
return JSONResponse({"group_by": group_by, "sort": sort, "rows": export_json(rows)})
1729+
1730+
16901731
@app.get(
16911732
"/analyses/{analysis_id}/siem",
16921733
tags=["analyses"],

0 commit comments

Comments
 (0)