Skip to content

Commit b1db4b9

Browse files
committed
feat(#909): cartographie des communications en CLI et dans l'API
- netcross_report.comm_map : comm_map_to_dict (JSON), comm_map_to_dot (Graphviz, anomalies en rouge), write_comm_map (DOT si .dot/.gv, JSON sinon) - CLI : --comm-map FICHIER, --comm-map-protocol, --comm-map-top-n, --comm-map-anomalies-only (filtres sans --comm-map refuses) - API : GET /analyses/{analysis_id}/comm-map (protocol, top_n, anomalies_only, format json|dot) - parite des surfaces, api-rest, guide « Lire le rapport », openapi.yaml, diagramme ; 22 tests
1 parent d6f8bd5 commit b1db4b9

9 files changed

Lines changed: 480 additions & 9 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}/comm-map` | Cartographie des communications (qui parle à qui) : `protocol` (répétable), `top_n`, `anomalies_only`, `format=json\|dot` (DOT Graphviz, anomalies en rouge) ; équiv. `--comm-map` (issue #909) |
8384
| `GET` | `/health` | Health check |
8485

8586
## Exemples curl

‎docs/class-diagram.md‎

Lines changed: 8 additions & 3 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,15 +32,15 @@ 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
3737
CLI -->|"41 imports"| netcross_core
3838
CLI -->|"8 imports"| pcap_parser
3939
netcross_gtk4 -->|"31 imports"| netcross_report
4040
netcross_gtk4 -->|"4 imports"| netcross_ai
4141
netcross_gtk4 -->|"109 imports"| netcross_core
4242
netcross_gtk4 -->|"6 imports"| pcap_parser
43-
netcross_api -->|"19 imports"| netcross_report
43+
netcross_api -->|"20 imports"| netcross_report
4444
netcross_api -->|"5 imports"| netcross_ai
4545
netcross_api -->|"42 imports"| netcross_core
4646
netcross_api -->|"3 imports"| pcap_parser
@@ -3486,6 +3486,9 @@ classDiagram
34863486
+available_protocols(flows) list~str~
34873487
+build_comm_map(flows, protocols, top_n, only_anomalies) CommMap
34883488
+format_comm_map(cmap, top_n) str
3489+
+comm_map_to_dict(cmap) dict
3490+
+comm_map_to_dot(cmap) str
3491+
+write_comm_map(path, cmap) str
34893492
}
34903493
34913494
%% ===== netcross_report.expert_events =====
@@ -3936,6 +3939,7 @@ classDiagram
39363939
+get_analysis_pdf(analysis_id, topn, _auth) StreamingResponse
39373940
+get_analysis_csv(analysis_id, _auth) PlainTextResponse
39383941
+get_analysis_sequence(analysis_id, max_flows, _auth) JSONResponse
3942+
+get_analysis_comm_map(analysis_id, protocol, top_n, anomalies_only, format, _auth) Response
39393943
+get_analysis_siem(analysis_id, format, _auth) PlainTextResponse
39403944
+forensic_search(analysis_id, body, _auth) JSONResponse
39413945
+extract_contents(analysis_id, files, labels, kinds, _auth) StreamingResponse
@@ -4909,6 +4913,7 @@ classDiagram
49094913
%% ===== cross_capture_analyzer_cli =====
49104914
class mod_cross_capture_analyzer_cli["cross_capture_analyzer_cli"] {
49114915
<<module>>
4916+
+comm_map_protocols(values) list~str~?
49124917
+main()
49134918
}
49144919

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,22 @@ 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+
## La cartographie des communications
100+
101+
La vue « Cartographie » de la GUI (qui parle à qui, anomalies en rouge)
102+
s'exporte aussi en ligne de commande :
103+
104+
```bash
105+
netcross --capture LAN=lan.pcapng --capture WAN=wan.pcapng --comm-map carte.dot --comm-map-anomalies-only
106+
dot -Tsvg carte.dot -o carte.svg # rendu SVG avec Graphviz
107+
netcross --capture LAN=lan.pcapng --comm-map carte.json --comm-map-protocol TCP,DNS --comm-map-top-n 20
108+
```
109+
110+
Un nom en `.dot` ou `.gv` donne un graphe Graphviz, tout autre nom un JSON
111+
(nœuds, arêtes orientées, volumes, protocoles, anomalies). Les arêtes sont
112+
orientées : un échange TCP en donne deux, ce qui montre un sens sans réponse.
113+
Par l'API : `GET /analyses/{id}/comm-map` avec les mêmes filtres.
114+
99115
## Les autres formats
100116

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

‎docs/openapi.yaml‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,94 @@ paths:
654654
application/json:
655655
schema:
656656
$ref: '#/components/schemas/HTTPValidationError'
657+
/analyses/{analysis_id}/comm-map:
658+
get:
659+
tags:
660+
- analyses
661+
summary: Get Analysis Comm Map
662+
description: 'Cartographie des communications (équiv. ``--comm-map``, vue GUI
663+
« Cartographie », issue #909).'
664+
operationId: get_analysis_comm_map_analyses__analysis_id__comm_map_get
665+
security:
666+
- APIKeyHeader: []
667+
parameters:
668+
- name: analysis_id
669+
in: path
670+
required: true
671+
schema:
672+
type: string
673+
title: Analysis Id
674+
- name: protocol
675+
in: query
676+
required: false
677+
schema:
678+
anyOf:
679+
- type: array
680+
items:
681+
type: string
682+
- type: 'null'
683+
description: Seulement ces protocoles (répétable ou séparés par des virgules)
684+
title: Protocol
685+
description: Seulement ces protocoles (répétable ou séparés par des virgules)
686+
- name: top_n
687+
in: query
688+
required: false
689+
schema:
690+
anyOf:
691+
- type: integer
692+
minimum: 1
693+
- type: 'null'
694+
description: 'N arêtes les plus volumineuses (défaut : toutes)'
695+
title: Top N
696+
description: 'N arêtes les plus volumineuses (défaut : toutes)'
697+
- name: anomalies_only
698+
in: query
699+
required: false
700+
schema:
701+
type: boolean
702+
description: Seulement les arêtes avec anomalies
703+
default: false
704+
title: Anomalies Only
705+
description: Seulement les arêtes avec anomalies
706+
- name: format
707+
in: query
708+
required: false
709+
schema:
710+
type: string
711+
description: json ou dot (Graphviz, anomalies en rouge)
712+
default: json
713+
title: Format
714+
description: json ou dot (Graphviz, anomalies en rouge)
715+
responses:
716+
'200':
717+
description: Successful Response
718+
content:
719+
application/json:
720+
schema: {}
721+
'401':
722+
content:
723+
application/json:
724+
schema:
725+
$ref: '#/components/schemas/ErrorResponse'
726+
description: Unauthorized
727+
'404':
728+
content:
729+
application/json:
730+
schema:
731+
$ref: '#/components/schemas/ErrorResponse'
732+
description: Not Found
733+
'409':
734+
content:
735+
application/json:
736+
schema:
737+
$ref: '#/components/schemas/ErrorResponse'
738+
description: Conflict
739+
'422':
740+
description: Validation Error
741+
content:
742+
application/json:
743+
schema:
744+
$ref: '#/components/schemas/HTTPValidationError'
657745
/analyses/{analysis_id}/siem:
658746
get:
659747
tags:

‎docs/parite-surfaces.md‎

Lines changed: 6 additions & 5 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` (114 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+
| Cartographie des communications (qui parle à qui, anomalies en rouge) | oui (vue « Cartographie » : filtres protocole, Top-N, « Anomalies seulement ») | `--comm-map FICHIER` (DOT Graphviz si `.dot`/`.gv`, JSON sinon), `--comm-map-protocol`, `--comm-map-top-n`, `--comm-map-anomalies-only` (issue #909) | `GET /analyses/{analysis_id}/comm-map` : `protocol`, `top_n`, `anomalies_only`, `format` (`json` ou `dot` ; issue #909) |
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) |
@@ -189,15 +190,14 @@ fenêtre « Outils de capture » (bouton de la barre de titre).
189190

190191
| Vue | Rôle |
191192
|---|---|
192-
| Cartographie des communications (filtres protocole, Top-N, « Anomalies seulement ») | Qui parle à qui, anomalies en rouge |
193193
| Annotations / signets | Étiquettes sur des trames, enregistrées à côté de la capture |
194194
| Dashboard analytique | Sélection croisée segments / flux / hôtes / protocoles / événements |
195195
| 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 #910 à #912 (cartographie : #909, 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}/comm-map` | Cartographie des communications, JSON ou DOT (issue #909) |
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,7 +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+
(#910 annotations, #911 dashboard, #912
329330
exploration statistique).
330331

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

‎src/cross_capture_analyzer_cli.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
write_support_map_csv,
168168
write_ticket,
169169
)
170+
from netcross_report.comm_map import build_comm_map, write_comm_map
170171
from netcross_report.security_report import build_security_report, print_security_report
171172
from pcap_parser.capture import split_by_interface
172173
from pcap_parser.ek_source import TsharkError, TsharkNotFoundError
@@ -1290,6 +1291,24 @@ def _run_content_extraction(captures, out_dir, kinds) -> None:
12901291
logger.debug("_run_content_extraction: fin")
12911292

12921293

1294+
def comm_map_protocols(values) -> list[str] | None:
1295+
"""Protocoles de --comm-map-protocol (repetable, virgules acceptees)."""
1296+
protos = [p.strip() for v in values or [] for p in v.split(",") if p.strip()]
1297+
return protos or None
1298+
1299+
1300+
def _check_comm_map_args(args) -> None:
1301+
"""Validations de l'issue #909."""
1302+
filtres = args.comm_map_protocol or args.comm_map_top_n is not None or args.comm_map_anomalies_only
1303+
if filtres and not args.comm_map:
1304+
print("--comm-map-protocol/--comm-map-top-n/--comm-map-anomalies-only necessitent --comm-map.", file=sys.stderr)
1305+
sys.exit(1)
1306+
if args.comm_map_top_n is not None and args.comm_map_top_n < 1:
1307+
print("--comm-map-top-n : 1 minimum.", file=sys.stderr)
1308+
sys.exit(1)
1309+
logger.debug("_check_comm_map_args: fin")
1310+
1311+
12931312
def _check_live_report_args(args) -> None:
12941313
"""Validations de l'issue #274."""
12951314
logger.debug("_check_live_report_args: args={}", summarize(args, "args"))
@@ -1601,6 +1620,30 @@ def main():
16011620
)
16021621
ap.add_argument("--order", help="Ordre physique des points sur le chemin reseau, ex: LAN,WAN,DC")
16031622
ap.add_argument("--detail-csv", help="Chemin de sortie pour le detail par flux (CSV)")
1623+
# Issue #909 : cartographie des communications (vue GUI « Cartographie »)
1624+
ap.add_argument(
1625+
"--comm-map",
1626+
metavar="FICHIER",
1627+
help="Cartographie des communications (qui parle a qui) : DOT Graphviz si FICHIER finit par .dot/.gv "
1628+
"(anomalies en rouge ; SVG : dot -Tsvg), JSON sinon",
1629+
)
1630+
ap.add_argument(
1631+
"--comm-map-protocol",
1632+
action="append",
1633+
metavar="PROTO[,PROTO...]",
1634+
help="Cartographie : seulement ces protocoles (repetable ou separes par des virgules)",
1635+
)
1636+
ap.add_argument(
1637+
"--comm-map-top-n",
1638+
type=int,
1639+
metavar="N",
1640+
help="Cartographie : N aretes les plus volumineuses (defaut : toutes)",
1641+
)
1642+
ap.add_argument(
1643+
"--comm-map-anomalies-only",
1644+
action="store_true",
1645+
help="Cartographie : seulement les aretes avec retransmissions ou signaux d'expertise",
1646+
)
16041647
ap.add_argument(
16051648
"--names",
16061649
metavar="PATH",
@@ -2734,6 +2777,7 @@ def main():
27342777
sys.exit(1)
27352778
extract_kinds = _check_extraction_args(args)
27362779
_check_live_report_args(args)
2780+
_check_comm_map_args(args)
27372781
if args.redact and (args.tls or args.quic or args.client_group):
27382782
print(
27392783
"--redact n'est pas disponible avec --tls/--quic/--client-group : ces "
@@ -3168,6 +3212,18 @@ def main():
31683212
if args.detail_csv:
31693213
write_detail_csv(args.detail_csv, flows, r.points, names=names)
31703214
print(f"\nDetail par flux ecrit dans {args.detail_csv}")
3215+
if args.comm_map:
3216+
cmap = build_comm_map(
3217+
flows,
3218+
protocols=comm_map_protocols(args.comm_map_protocol),
3219+
top_n=args.comm_map_top_n,
3220+
only_anomalies=args.comm_map_anomalies_only,
3221+
)
3222+
fmt = write_comm_map(args.comm_map, cmap)
3223+
print(
3224+
f"\nCartographie ({fmt.upper()}) ecrite dans {args.comm_map} : "
3225+
f"{len(cmap.edges)} arete(s) sur {cmap.total_edges}, {len(cmap.nodes)} hote(s)"
3226+
)
31713227

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

‎src/netcross_api/app.py‎

Lines changed: 37 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,42 @@ async def get_analysis_sequence(
16871687
return JSONResponse({"sequence_views": [asdict(v) for v in views]})
16881688

16891689

1690+
_COMM_MAP_PROTOCOL_QUERY = Query(
1691+
default=None, description="Seulement ces protocoles (répétable ou séparés par des virgules)"
1692+
)
1693+
1694+
1695+
@app.get(
1696+
"/analyses/{analysis_id}/comm-map",
1697+
tags=["analyses"],
1698+
responses={**_ANALYSIS_RESPONSES, 404: {"model": ErrorResponse}},
1699+
)
1700+
async def get_analysis_comm_map(
1701+
analysis_id: str,
1702+
protocol: list[str] | None = _COMM_MAP_PROTOCOL_QUERY,
1703+
top_n: int | None = Query(default=None, ge=1, description="N arêtes les plus volumineuses (défaut : toutes)"),
1704+
anomalies_only: bool = Query(default=False, description="Seulement les arêtes avec anomalies"),
1705+
format: str = Query(default="json", description="json ou dot (Graphviz, anomalies en rouge)"),
1706+
_auth: None = Depends(_verify_api_key),
1707+
) -> Response:
1708+
"""Cartographie des communications (équiv. ``--comm-map``, vue GUI « Cartographie », issue #909)."""
1709+
from netcross_report.comm_map import build_comm_map, comm_map_to_dict, comm_map_to_dot
1710+
1711+
if format not in ("json", "dot"):
1712+
logger.debug("get_analysis_comm_map: format {} -> 400", format)
1713+
raise HTTPException(status_code=400, detail="format : json ou dot")
1714+
entry = _completed_entry(analysis_id)
1715+
flows = entry.get("flows")
1716+
if flows is None:
1717+
logger.debug("get_analysis_comm_map: pas de flux -> 404")
1718+
raise HTTPException(status_code=404, detail="Aucun flux disponible pour la cartographie")
1719+
protos = [p.strip() for v in protocol or [] for p in v.split(",") if p.strip()] or None
1720+
cmap = build_comm_map(flows, protocols=protos, top_n=top_n, only_anomalies=anomalies_only)
1721+
if format == "dot":
1722+
return PlainTextResponse(comm_map_to_dot(cmap), media_type="text/vnd.graphviz")
1723+
return JSONResponse(comm_map_to_dict(cmap))
1724+
1725+
16901726
@app.get(
16911727
"/analyses/{analysis_id}/siem",
16921728
tags=["analyses"],

0 commit comments

Comments
 (0)