Ferdlance commited on
Commit
9738b32
·
verified ·
1 Parent(s): c1a6d5f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +641 -316
app.py CHANGED
@@ -11,447 +11,772 @@ import subprocess
11
  import shutil
12
  from datetime import datetime
13
  from pathlib import Path
14
-
15
- # Streamlit et visualisation
16
  import streamlit as st
17
  import pandas as pd
18
  import plotly.express as px
 
 
 
19
 
20
- # Parsing HTML
21
- import html2text
22
-
23
- # Importation du module de configuration (supposé exister)
24
  from config import app_config as config
25
 
26
- # --- CONFIGURATION DE LA PAGE ET LOGGING ---
27
-
28
  st.set_page_config(
29
  page_title="DevSecOps Data Bot",
30
  layout="wide",
31
  initial_sidebar_state="expanded"
32
  )
33
 
34
- # Initialisation de l'état de la session (géré par le fichier config)
35
  config.init_session_state()
36
 
 
 
 
 
 
37
  def setup_logging():
38
- """Configure un logger pour tracer l'exécution dans un fichier et la console."""
39
  log_dir = Path("logs")
40
  log_dir.mkdir(exist_ok=True)
41
- log_file = log_dir / f"data_collector_{datetime.now().strftime('%Y%m%d')}.log"
42
-
43
- # Évite d'ajouter des handlers multiples si la fonction est appelée plusieurs fois
44
- logger = logging.getLogger(__name__)
45
- if not logger.handlers:
46
- logger.setLevel(logging.INFO)
47
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
48
-
49
- file_handler = logging.FileHandler(log_file)
50
- file_handler.setFormatter(formatter)
51
- logger.addHandler(file_handler)
52
-
53
- stream_handler = logging.StreamHandler(sys.stdout)
54
- stream_handler.setFormatter(formatter)
55
- logger.addHandler(stream_handler)
56
-
57
- return logger
58
 
59
  logger = setup_logging()
60
- h_parser = html2text.HTML2Text()
61
- h_parser.ignore_links = True
62
-
63
- # --- GESTION DU SERVEUR LLM LOCAL ---
64
 
 
65
  def check_server_status():
66
- """Vérifie si le serveur LLM est actif."""
67
  try:
68
- response = requests.get(config.LLM_SERVER_URL.replace("/completion", "/health"), timeout=3)
69
- if response.status_code == 200 and response.json().get('status') == 'ok':
70
  st.session_state.server_status = "Actif"
71
  return True
 
 
 
72
  except requests.exceptions.RequestException:
73
- pass
74
- st.session_state.server_status = "Inactif"
75
- return False
76
 
77
  def start_llm_server():
78
- """Démarre le serveur llama.cpp en subprocess."""
79
  if check_server_status():
80
- st.toast("Le serveur LLM est déjà actif.", icon="✅")
81
  return
82
-
83
- model_path = Path(config.MODEL_PATH)
84
- server_binary = Path(config.LLAMA_SERVER_PATH)
85
 
 
86
  if not model_path.exists():
87
- st.error(f"Le modèle GGUF est introuvable à : {config.MODEL_PATH}")
88
  return
89
- if not server_binary.exists():
90
- st.error(f"Le binaire du serveur est introuvable : {config.LLAMA_SERVER_PATH}")
 
 
91
  return
92
-
93
- # Commande pour démarrer le serveur
94
- command = [
95
- str(server_binary),
96
- "-m", str(model_path),
97
- "--port", str(config.LLM_PORT),
98
- "--host", "0.0.0.0",
99
- "-c", "4096",
100
- "-ngl", "999", # Nombre de couches GPU, ajuster si nécessaire
101
- "--threads", "8"
102
- ]
103
 
104
- log_file = Path("logs/llama_server.log")
105
- pid_file = Path("server/server.pid")
106
- pid_file.parent.mkdir(exist_ok=True)
107
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  try:
109
- with open(log_file, 'w') as log:
110
- process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT)
111
-
112
- with open(pid_file, 'w') as f:
113
- f.write(str(process.pid))
114
-
115
- st.info("Tentative de démarrage du serveur LLM...")
116
- time.sleep(10) # Laisse le temps au serveur de démarrer
117
-
118
  if check_server_status():
119
- st.success("Serveur LLM démarré avec succès !")
120
  else:
121
- st.error("Le serveur n'a pas pu démarrer. Vérifiez les logs dans `logs/llama_server.log`.")
122
-
123
  except Exception as e:
124
- st.error(f"Erreur lors du démarrage du serveur : {e}")
125
 
126
  def stop_llm_server():
127
- """Arrête le serveur LLM en tuant le processus via son PID."""
128
- pid_file = Path("server/server.pid")
129
- if not pid_file.exists():
130
- st.warning("Aucun fichier PID trouvé. Le serveur est probablement déjà arrêté.")
131
- check_server_status()
132
- return
133
-
 
 
 
 
 
 
 
 
 
134
  try:
135
- with open(pid_file, 'r') as f:
136
- pid = int(f.read().strip())
137
-
138
- # Tente de tuer le processus
139
- os.kill(pid, 9) # SIGKILL
140
- st.info(f"Signal d'arrêt envoyé au processus {pid}.")
141
- os.remove(pid_file)
142
- except (ProcessLookupError, FileNotFoundError):
143
- st.warning("Le processus n'existait pas ou le fichier PID a déjà été supprimé.")
144
- if pid_file.exists():
145
- os.remove(pid_file)
146
  except Exception as e:
147
- st.error(f"Erreur lors de l'arrêt du serveur : {e}")
148
-
149
- time.sleep(3)
150
- if not check_server_status():
151
- st.success("Serveur LLM arrêté avec succès.")
152
- else:
153
- st.warning("Le serveur semble toujours actif. Une vérification manuelle peut être nécessaire.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- # --- LOGIQUE D'ENRICHISSEMENT IA ---
156
 
157
  class IAEnricher:
158
- """Classe pour interagir avec le LLM et enrichir les données."""
159
  def __init__(self):
160
  self.server_url = config.LLM_SERVER_URL
161
  self.available = check_server_status()
162
-
163
- def _query_llm(self, prompt, n_predict=512):
 
 
 
 
164
  if not self.available:
165
  return None
166
 
167
  payload = {
168
  "prompt": prompt,
169
- "n_predict": n_predict,
170
- "temperature": st.session_state.temperature,
171
- "stop": ["<|im_end|>", "</s>", "\n}\n"]
172
  }
173
 
174
  try:
175
- response = requests.post(self.server_url, json=payload, timeout=120)
176
- response.raise_for_status()
177
- return response.json().get('content', '')
 
 
 
178
  except requests.exceptions.RequestException as e:
179
- logger.error(f"Erreur de communication avec le serveur LLM : {e}")
180
  return None
181
 
182
- def _extract_json(self, text):
183
- """Extrait un objet JSON d'une chaîne de texte, de manière plus robuste."""
184
- if not text:
185
- return None
186
 
187
- # Trouve le premier '{' et le dernier '}' pour délimiter le JSON potentiel
188
- start = text.find('{')
189
- end = text.rfind('}')
190
- if start != -1 and end != -1 and end > start:
191
- json_str = text[start:end+1]
 
 
192
  try:
193
- return json.loads(json_str)
194
- except json.JSONDecodeError:
195
- logger.warning(f"Impossible de décoder le JSON extrait : {json_str[:200]}...")
196
- return None
197
-
 
 
 
 
 
 
 
 
 
 
 
 
198
  def analyze_content_relevance(self, content):
199
- """Utilise l'IA pour analyser la pertinence d'un contenu."""
200
  if not self.available or not st.session_state.enable_enrichment:
201
- return {"relevant": True, "attack_signatures": [], "security_tags": [], "it_relevance_score": 50}
202
 
203
- prompt = config.PROMPTS["analyze_relevance"].format(content=content[:1500])
204
- response_text = self._query_llm(prompt, n_predict=256)
205
 
206
- analysis = self._extract_json(response_text)
207
- if analysis:
208
- return analysis
209
 
210
- # Valeur par défaut si l'IA échoue
211
- return {"relevant": True, "attack_signatures": [], "security_tags": [], "it_relevance_score": 50}
212
-
213
-
214
- # --- FONCTIONS DE COLLECTE DE DONNÉES ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  def check_api_keys():
217
- """Vérifie la présence des clés API et met à jour un flag global."""
218
- keys_needed = ['GITHUB_API_TOKEN', 'NVD_API_KEY', 'STACK_EXCHANGE_API_KEY']
219
- missing_keys = [key for key in keys_needed if not os.getenv(key)]
 
 
 
 
 
220
 
221
- if missing_keys:
222
- logger.warning(f"Clés API manquantes : {', '.join(missing_keys)}. Le bot fonctionnera en mode dégradé.")
223
- config.USE_API_KEYS = False
 
 
224
  else:
225
- logger.info("Toutes les clés API nécessaires sont configurées.")
226
- config.USE_API_KEYS = True
227
 
228
- def make_request(url, headers=None, params=None):
229
- """Effectue une requête HTTP avec gestion des pauses et des erreurs."""
230
- # Logique de pause pour éviter le rate-limiting
231
- pause_time = random.uniform(2, 5) if not config.USE_API_KEYS else random.uniform(0.5, 1.5)
232
- time.sleep(pause_time)
 
 
 
 
 
233
 
234
  try:
235
  response = requests.get(url, headers=headers, params=params, timeout=30)
236
- if response.status_code == 429: # Rate limited
237
- retry_after = int(response.headers.get('Retry-After', 15))
 
 
 
 
 
 
238
  logger.warning(f"Limite de débit atteinte. Pause de {retry_after} secondes...")
239
  time.sleep(retry_after)
240
- return make_request(url, headers, params)
241
-
242
- response.raise_for_status() # Lève une exception pour les codes 4xx/5xx
243
- return response
244
-
245
  except requests.exceptions.RequestException as e:
246
- logger.error(f"Erreur de requête pour {url}: {e}")
247
  return None
248
 
249
  def clean_html(html_content):
250
- """Nettoie le contenu HTML pour extraire le texte brut."""
251
  if not html_content:
252
  return ""
253
- return h_parser.handle(html_content)
254
-
255
- def save_data(data):
256
- """Ajoute les données collectées à l'état de la session."""
257
- st.session_state.qa_data.append(data)
258
- st.session_state.total_qa_pairs = len(st.session_state.qa_data)
259
- logger.info(f"Donnée sauvegardée : {data['source']} (Total: {st.session_state.total_qa_pairs})")
260
-
261
- # Mise à jour du log dans l'UI
262
- log_placeholder = st.session_state.get('log_placeholder')
263
- if log_placeholder:
264
- log_placeholder.text(f"Dernière collecte : {data['source']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
- def collect_github_data(query, limit):
268
- """Collecte les problèmes de sécurité depuis des dépôts GitHub."""
269
- logger.info(f"GitHub: Recherche de '{query}'...")
270
  base_url = "https://api.github.com"
271
  headers = {"Accept": "application/vnd.github.v3+json"}
272
  if config.USE_API_KEYS:
273
- headers["Authorization"] = f"token {os.getenv('GITHUB_API_TOKEN')}"
 
274
 
275
- search_url = f"{base_url}/search/repositories"
276
- params = {"q": query, "sort": "stars", "per_page": limit}
277
 
278
- response = make_request(search_url, headers=headers, params=params)
279
- if not response: return
280
-
281
- for repo in response.json().get("items", []):
282
- issues_url = repo["issues_url"].replace("{/number}", "")
283
- issues_params = {"state": "all", "labels": "security,vulnerability", "per_page": 5}
284
- issues_response = make_request(issues_url, headers=headers, params=issues_params)
285
-
286
- if issues_response:
287
- for issue in issues_response.json():
288
- if "pull_request" not in issue and issue.get("body"):
289
- analysis = ia_enricher.analyze_content_relevance(issue['title'] + " " + issue['body'])
290
- if analysis['relevant'] and analysis['it_relevance_score'] >= st.session_state.min_relevance:
291
- save_data({
292
- "question": issue["title"],
293
- "answer": clean_html(issue["body"]),
294
- "category": "devsecops",
295
- "source": f"github_{repo['full_name']}",
296
- "tags": [t['name'] for t in issue.get('labels', [])] + analysis['security_tags']
297
- })
298
-
299
-
300
- def collect_nvd_data(limit):
301
- """Collecte les dernières vulnérabilités CVE depuis le NVD."""
302
- logger.info("NVD: Collecte des dernières vulnérabilités...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  base_url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
304
- headers = {}
305
  if config.USE_API_KEYS:
306
- headers["apiKey"] = os.getenv('NVD_API_KEY')
 
307
 
308
- params = {"resultsPerPage": limit}
309
  response = make_request(base_url, headers=headers, params=params)
310
- if not response: return
311
-
312
- for vuln in response.json().get("vulnerabilities", []):
313
- cve = vuln.get("cve", {})
314
- cve_id = cve.get("id", "N/A")
315
- description = next((d['value'] for d in cve.get('descriptions', []) if d['lang'] == 'en'), "")
316
-
317
- if description:
318
- save_data({
319
- "question": f"Qu'est-ce que la vulnérabilité {cve_id} ?",
320
- "answer": description,
321
- "category": "security",
322
- "source": f"nvd_{cve_id}",
323
- "tags": ["cve", "vulnerability"]
324
- })
325
-
326
- # --- FONCTION PRINCIPALE ET INTERFACE STREAMLIT ---
327
-
328
- def run_data_collection(sources, queries, limits):
329
- """Orchestre la collecte de données depuis les sources sélectionnées."""
330
- st.session_state.bot_status = "En cours d'exécution"
331
 
332
- # Nettoyage de l'état précédent avant de démarrer
333
- st.session_state.qa_data = []
334
- st.session_state.total_qa_pairs = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
  check_api_keys()
337
 
 
 
 
338
  enabled_sources = [s for s, enabled in sources.items() if enabled]
339
- progress_bar = st.progress(0, text="Démarrage de la collecte...")
340
-
341
- for i, source_name in enumerate(enabled_sources):
342
- progress_text = f"Collecte depuis {source_name}... ({i+1}/{len(enabled_sources)})"
343
- progress_bar.progress((i + 1) / len(enabled_sources), text=progress_text)
344
-
345
  try:
346
- if source_name == "GitHub":
347
- for query in queries["GitHub"].split('\n'):
348
- if query.strip():
349
- collect_github_data(query.strip(), limits["GitHub"])
 
 
350
  elif source_name == "NVD":
351
- collect_nvd_data(limits["NVD"])
352
- # Ajouter d'autres sources ici (Kaggle, etc.) de la même manière
353
-
354
  except Exception as e:
355
- logger.error(f"Erreur fatale lors de la collecte depuis {source_name}: {e}")
356
-
357
- progress_bar.empty()
 
 
358
  st.session_state.bot_status = "Arrêté"
359
- st.toast("Collecte des données terminée !", icon="🎉")
 
 
360
 
361
- # Forcer le rafraîchissement de la page pour mettre à jour l'onglet statistiques
362
- time.sleep(2)
363
  st.rerun()
364
 
365
  def main():
366
- """Fonction principale de l'application Streamlit."""
367
- st.title("🤖 DevSecOps Data Bot")
368
- st.markdown("Ce bot collecte et enrichit des données DevSecOps depuis diverses sources.")
369
-
370
- global ia_enricher
371
- ia_enricher = IAEnricher()
372
 
373
- tabs = st.tabs(["▶️ Bot", "📊 Statistiques & Données", "⚙️ Configuration"])
374
 
375
  with tabs[0]:
376
- st.header("Tableau de bord")
377
  col1, col2, col3 = st.columns(3)
378
- col1.metric("Statut du bot", st.session_state.bot_status)
379
- col2.metric("Paires Q/R collectées", st.session_state.total_qa_pairs)
380
- col3.metric("Statut du serveur LLM", st.session_state.server_status)
381
-
382
- # Placeholder pour les logs en direct
383
- st.session_state['log_placeholder'] = st.empty()
384
-
385
- with st.form("collection_form"):
386
- st.subheader("1. Choisir les sources de données")
387
- sources = {
388
- "GitHub": st.checkbox("GitHub (Problèmes de sécurité)", value=True),
389
- "NVD": st.checkbox("NVD (Vulnérabilités CVE)", value=True),
390
- }
391
-
392
- st.subheader("2. Paramètres de la collecte")
393
- queries = {}
394
- limits = {}
395
-
396
- with st.expander("Configuration pour GitHub"):
397
- queries["GitHub"] = st.text_area("Requêtes GitHub (une par ligne)", "language:python security\ntopic:devsecops vulnerability")
398
- limits["GitHub"] = st.number_input("Nombre de dépôts par requête", 1, 50, 5)
399
-
400
- with st.expander("Configuration pour NVD"):
401
- limits["NVD"] = st.number_input("Nombre de CVE à récupérer", 10, 200, 50)
402
-
403
- submitted = st.form_submit_button("🚀 Lancer la collecte", type="primary", use_container_width=True)
404
-
405
- if submitted:
406
- if st.session_state.bot_status == "En cours d'exécution":
407
- st.warning("Une collecte est déjà en cours.")
408
- else:
409
- run_data_collection(sources, queries, limits)
410
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  with tabs[1]:
412
- st.header("Analyse des Données Collectées")
413
  if st.session_state.qa_data:
414
  df = pd.DataFrame(st.session_state.qa_data)
415
 
416
  st.subheader("Aperçu des données")
417
- st.dataframe(df)
418
 
419
  st.subheader("Répartition par source")
420
- source_counts = df['source'].apply(lambda x: x.split('_')[0]).value_counts()
421
- fig_source = px.bar(source_counts, x=source_counts.index, y=source_counts.values,
422
- labels={'x': 'Source', 'y': 'Nombre'}, title="Nombre de paires Q/R par source")
423
  st.plotly_chart(fig_source, use_container_width=True)
424
 
425
- # Bouton de téléchargement
426
- json_data = json.dumps(st.session_state.qa_data, indent=2, ensure_ascii=False)
427
- st.download_button(
428
- label="📥 Télécharger les données (JSON)",
429
- data=json_data,
430
- file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d')}.json",
431
- mime="application/json",
432
- use_container_width=True
433
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  else:
435
- st.info("Aucune donnée à afficher. Lancez une collecte depuis l'onglet 'Bot'.")
436
-
437
  with tabs[2]:
438
  st.header("Configuration Avancée")
439
- st.subheader("Gestion du serveur LLM local")
440
- st.warning("⚠️ Attention : La gestion du serveur est expérimentale sur les conteneurs.")
441
 
442
  llm_col1, llm_col2 = st.columns(2)
443
- if llm_col1.button("Démarrer le serveur LLM", use_container_width=True):
444
- start_llm_server()
445
- st.rerun()
446
-
447
- if llm_col2.button("Arrêter le serveur LLM", type="secondary", use_container_width=True):
448
- stop_llm_server()
449
- st.rerun()
450
-
 
 
 
 
451
  st.subheader("Paramètres d'enrichissement IA")
452
- st.session_state.enable_enrichment = st.toggle("Activer l'enrichissement par IA", value=True)
453
- st.session_state.min_relevance = st.slider("Score de pertinence minimum", 0, 100, 50)
454
- st.session_state.temperature = st.slider("Température de l'IA (créativité)", 0.0, 1.5, 0.5)
 
455
 
456
  if __name__ == "__main__":
457
  main()
 
11
  import shutil
12
  from datetime import datetime
13
  from pathlib import Path
 
 
14
  import streamlit as st
15
  import pandas as pd
16
  import plotly.express as px
17
+ import plotly.graph_objects as go
18
+ from bs4 import BeautifulSoup
19
+ import html2text # CORRECTION 1: Import manquant ajouté
20
 
21
+ # Importation du module de configuration
 
 
 
22
  from config import app_config as config
23
 
24
+ # Configuration de la page Streamlit
 
25
  st.set_page_config(
26
  page_title="DevSecOps Data Bot",
27
  layout="wide",
28
  initial_sidebar_state="expanded"
29
  )
30
 
31
+ # Initialisation des variables de session
32
  config.init_session_state()
33
 
34
+ # Initialisation du parser HTML
35
+ h = html2text.HTML2Text() # CORRECTION 1: Initialisation du parser
36
+ h.ignore_links = True
37
+
38
+ # Configuration du logging
39
  def setup_logging():
 
40
  log_dir = Path("logs")
41
  log_dir.mkdir(exist_ok=True)
42
+ log_file = log_dir / f"data_collector_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format='%(asctime)s - %(levelname)s - %(message)s',
46
+ handlers=[
47
+ logging.FileHandler(log_file),
48
+ logging.StreamHandler(sys.stdout)
49
+ ]
50
+ )
51
+ return logging.getLogger(__name__)
 
 
 
 
 
 
 
52
 
53
  logger = setup_logging()
 
 
 
 
54
 
55
+ # Fonctions pour le serveur LLM (llama.cpp)
56
  def check_server_status():
 
57
  try:
58
+ response = requests.get(config.LLM_SERVER_URL.replace("/completion", "/health"), timeout=2)
59
+ if response.status_code == 200:
60
  st.session_state.server_status = "Actif"
61
  return True
62
+ else:
63
+ st.session_state.server_status = "Inactif"
64
+ return False
65
  except requests.exceptions.RequestException:
66
+ st.session_state.server_status = "Inactif"
67
+ return False
 
68
 
69
  def start_llm_server():
 
70
  if check_server_status():
71
+ st.info("Le serveur llama.cpp est déjà en cours d'exécution.")
72
  return
 
 
 
73
 
74
+ model_path = Path("models/qwen2.5-coder-1.5b-q8_0.gguf")
75
  if not model_path.exists():
76
+ st.error("Le modèle GGUF n'existe pas. Veuillez le placer dans le dossier models/.")
77
  return
78
+
79
+ llama_server = Path("llama.cpp/build/bin/llama-server")
80
+ if not llama_server.exists():
81
+ st.error("llama.cpp n'est pas compilé. Veuillez compiler llama.cpp d'abord.")
82
  return
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ start_script = Path("server/start_server.sh")
85
+ if not start_script.exists():
86
+ with open(start_script, 'w') as f:
87
+ f.write(f"""#!/bin/bash
88
+ MODEL_PATH="{str(model_path)}"
89
+ if [ ! -f "$MODEL_PATH" ]; then
90
+ echo "Le modèle GGUF est introuvable à: $MODEL_PATH"
91
+ exit 1
92
+ fi
93
+ "{str(llama_server)}" \\
94
+ -m "$MODEL_PATH" \\
95
+ --port 8080 \\
96
+ --host 0.0.0.0 \\
97
+ -c 4096 \\
98
+ -ngl 999 \\
99
+ --threads 8 \\
100
+ > "logs/llama_server.log" 2>&1 &
101
+ echo $! > "server/server.pid"
102
+ """)
103
+ os.chmod(start_script, 0o755)
104
+
105
  try:
106
+ subprocess.Popen(["bash", str(start_script)])
107
+ st.success("Le serveur llama.cpp est en cours de démarrage...")
108
+ time.sleep(5)
 
 
 
 
 
 
109
  if check_server_status():
110
+ st.success("Serveur llama.cpp démarré avec succès!")
111
  else:
112
+ st.error("Le serveur n'a pas pu démarrer. Vérifiez les logs dans le dossier logs/.")
 
113
  except Exception as e:
114
+ st.error(f"Erreur lors du démarrage du serveur: {str(e)}")
115
 
116
  def stop_llm_server():
117
+ stop_script = Path("server/stop_server.sh")
118
+ if not stop_script.exists():
119
+ with open(stop_script, 'w') as f:
120
+ f.write("""#!/bin/bash
121
+ PID_FILE="server/server.pid"
122
+ if [ -f "$PID_FILE" ]; then
123
+ PID=$(cat "$PID_FILE")
124
+ kill $PID
125
+ rm "$PID_FILE"
126
+ echo "Serveur llama.cpp arrêté."
127
+ else
128
+ echo "Aucun PID de serveur trouvé."
129
+ fi
130
+ """)
131
+ os.chmod(stop_script, 0o755)
132
+
133
  try:
134
+ subprocess.run(["bash", str(stop_script)])
135
+ st.success("Le serveur llama.cpp est en cours d'arrêt...")
136
+ time.sleep(2)
137
+ if not check_server_status():
138
+ st.success("Serveur llama.cpp arrêté avec succès!")
139
+ else:
140
+ st.warning("Le serveur n'a pas pu être arrêté correctement.")
 
 
 
 
141
  except Exception as e:
142
+ st.error(f"Erreur lors de l'arrêt du serveur: {str(e)}")
143
+
144
+ def load_prompts():
145
+ prompts_file = Path("config/prompts.json")
146
+ if not prompts_file.exists():
147
+ default_prompts = {
148
+ "enrich_qa": {
149
+ "system": "Tu es un expert DevSecOps. Améliore cette paire question/réponse en y ajoutant des tags, des signatures d'attaques potentielles, et en structurant les informations. Réponds uniquement avec un objet JSON.",
150
+ "prompt_template": "Question originale: {question}\nRéponse originale: {answer}\nContexte: {context}\n\nFournis une version améliorée sous forme de JSON:\n{{\n \"question\": \"question améliorée\",\n \"answer\": \"réponse améliorée\",\n \"tags\": [\"tag1\", \"tag2\"],\n \"attack_signatures\": [\"signature1\", \"signature2\"]\n}}"
151
+ },
152
+ "analyze_relevance": {
153
+ "system": "Analyse ce contenu et détermine s'il est pertinent pour DevSecOps. Si pertinent, extrais les signatures d'attaques connues. Réponds uniquement avec un objet JSON.",
154
+ "prompt_template": "Contenu: {content}...\n\nRéponds sous forme de JSON:\n{{\n \"relevant\": true,\n \"attack_signatures\": [\"signature1\", \"signature2\"],\n \"security_tags\": [\"tag1\", \"tag2\"],\n \"it_relevance_score\": 0-100\n}}"
155
+ },
156
+ "generate_queries": {
157
+ "system": "Analyse les données actuelles et génère 5 nouvelles requêtes de recherche pour trouver plus de contenu DevSecOps pertinent, en particulier des signatures d'attaques et vulnérabilités. Réponds uniquement avec un objet JSON.",
158
+ "prompt_template": "Données actuelles: {current_data}...\n\nRéponds sous forme de JSON:\n{{\n \"queries\": [\"query1\", \"query2\", \"query3\", \"query4\", \"query5\"]\n}}"
159
+ }
160
+ }
161
+ with open(prompts_file, 'w') as f:
162
+ json.dump(default_prompts, f, indent=2)
163
+
164
+ with open(prompts_file, 'r', encoding='utf-8') as f:
165
+ return json.load(f)
166
 
167
+ PROMPTS = load_prompts()
168
 
169
  class IAEnricher:
 
170
  def __init__(self):
171
  self.server_url = config.LLM_SERVER_URL
172
  self.available = check_server_status()
173
+ if self.available:
174
+ logger.info("Serveur llama.cpp détecté et prêt.")
175
+ else:
176
+ logger.warning("Serveur llama.cpp non disponible. Les fonctionnalités d'IA seront désactivées.")
177
+
178
+ def _get_qwen_response(self, prompt, **kwargs):
179
  if not self.available:
180
  return None
181
 
182
  payload = {
183
  "prompt": prompt,
184
+ "n_predict": kwargs.get('n_predict', 512),
185
+ "temperature": kwargs.get('temperature', 0.7),
186
+ "stop": ["<|im_end|>", "</s>"]
187
  }
188
 
189
  try:
190
+ response = requests.post(self.server_url, json=payload, timeout=60)
191
+ if response.status_code == 200:
192
+ return response.json().get('content', '')
193
+ else:
194
+ logger.error(f"Erreur du serveur LLM: {response.status_code} - {response.text}")
195
+ return None
196
  except requests.exceptions.RequestException as e:
197
+ logger.error(f"Erreur de connexion au serveur LLM: {str(e)}")
198
  return None
199
 
200
+ def enrich_qa_pair(self, question, answer, context=""):
201
+ if not self.available or not st.session_state.enable_enrichment:
202
+ return question, answer, [], []
 
203
 
204
+ prompt_template = PROMPTS.get("enrich_qa", {}).get("prompt_template", "")
205
+ system_prompt = PROMPTS.get("enrich_qa", {}).get("system", "")
206
+
207
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(question=question, answer=answer, context=context[:500])}"
208
+ response_text = self._get_qwen_response(full_prompt, n_predict=1024)
209
+
210
+ if response_text:
211
  try:
212
+ # CORRECTION 3: Remplacement du regex fragile par une recherche de délimiteurs JSON
213
+ start = response_text.find('{')
214
+ end = response_text.rfind('}')
215
+ if start != -1 and end != -1:
216
+ json_str = response_text[start:end+1]
217
+ enriched_data = json.loads(json_str)
218
+ return (
219
+ enriched_data.get('question', question),
220
+ enriched_data.get('answer', answer),
221
+ enriched_data.get('tags', []),
222
+ enriched_data.get('attack_signatures', [])
223
+ )
224
+ except json.JSONDecodeError as e:
225
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
226
+
227
+ return question, answer, [], []
228
+
229
  def analyze_content_relevance(self, content):
 
230
  if not self.available or not st.session_state.enable_enrichment:
231
+ return True, [], [], 50
232
 
233
+ prompt_template = PROMPTS.get("analyze_relevance", {}).get("prompt_template", "")
234
+ system_prompt = PROMPTS.get("analyze_relevance", {}).get("system", "")
235
 
236
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(content=content[:1500])}"
237
+ response_text = self._get_qwen_response(full_prompt, n_predict=256, temperature=st.session_state.temperature)
 
238
 
239
+ if response_text:
240
+ try:
241
+ # CORRECTION 3: Remplacement du regex fragile
242
+ start = response_text.find('{')
243
+ end = response_text.rfind('}')
244
+ if start != -1 and end != -1:
245
+ json_str = response_text[start:end+1]
246
+ analysis = json.loads(json_str)
247
+ return (
248
+ analysis.get('relevant', True),
249
+ analysis.get('attack_signatures', []),
250
+ analysis.get('security_tags', []),
251
+ analysis.get('it_relevance_score', 50)
252
+ )
253
+ except json.JSONDecodeError as e:
254
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
255
+ return True, [], [], 50
256
+
257
+ def generate_adaptive_queries(self, current_data):
258
+ if not self.available or not st.session_state.enable_enrichment:
259
+ return []
260
+
261
+ prompt_template = PROMPTS.get("generate_queries", {}).get("prompt_template", "")
262
+ system_prompt = PROMPTS.get("generate_queries", {}).get("system", "")
263
+
264
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(current_data=current_data[:1000])}"
265
+ response_text = self._get_qwen_response(full_prompt, n_predict=st.session_state.n_predict)
266
+
267
+ if response_text:
268
+ try:
269
+ # CORRECTION 3: Remplacement du regex fragile
270
+ start = response_text.find('{')
271
+ end = response_text.rfind('}')
272
+ if start != -1 and end != -1:
273
+ json_str = response_text[start:end+1]
274
+ queries_data = json.loads(json_str)
275
+ return queries_data.get('queries', [])
276
+ except json.JSONDecodeError as e:
277
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
278
+ return []
279
+
280
+ ia_enricher = IAEnricher()
281
 
282
  def check_api_keys():
283
+ keys = {
284
+ 'GITHUB_API_TOKEN': os.getenv('GITHUB_API_TOKEN'),
285
+ 'HUGGINGFACE_API_TOKEN': os.getenv('HUGGINGFACE_API_TOKEN'),
286
+ 'NVD_API_KEY': os.getenv('NVD_API_KEY'),
287
+ 'STACK_EXCHANGE_API_KEY': os.getenv('STACK_EXCHANGE_API_KEY')
288
+ }
289
+
290
+ valid_keys = {k: v for k, v in keys.items() if v and v != f'your_{k.lower()}_here'}
291
 
292
+ config.USE_API_KEYS = len(valid_keys) == len(keys)
293
+ if not config.USE_API_KEYS:
294
+ missing = set(keys.keys()) - set(valid_keys.keys())
295
+ logger.warning(f"Clés d'API manquantes ou non configurées: {', '.join(missing)}")
296
+ logger.warning("Le bot fonctionnera en mode dégradé avec des pauses plus longues.")
297
  else:
298
+ logger.info("Toutes les clés d'API sont configurées.")
299
+ return config.USE_API_KEYS
300
 
301
+ def make_request(url, headers=None, params=None, is_api_call=True):
302
+ config.REQUEST_COUNT += 1
303
+
304
+ pause_factor = 1 if config.USE_API_KEYS else 2
305
+
306
+ if config.REQUEST_COUNT >= config.MAX_REQUESTS_BEFORE_PAUSE:
307
+ pause_time = random.uniform(config.MIN_PAUSE * pause_factor, config.MAX_PAUSE * pause_factor)
308
+ logger.info(f"Pause de {pause_time:.2f} secondes après {config.REQUEST_COUNT} requêtes...")
309
+ time.sleep(pause_time)
310
+ config.REQUEST_COUNT = 0
311
 
312
  try:
313
  response = requests.get(url, headers=headers, params=params, timeout=30)
314
+
315
+ if response.status_code == 200:
316
+ return response
317
+ elif response.status_code in [401, 403]:
318
+ logger.warning(f"Accès non autorisé à {url}. Vérifiez vos clés d'API.")
319
+ return None
320
+ elif response.status_code == 429:
321
+ retry_after = int(response.headers.get('Retry-After', 10))
322
  logger.warning(f"Limite de débit atteinte. Pause de {retry_after} secondes...")
323
  time.sleep(retry_after)
324
+ return make_request(url, headers, params, is_api_call)
325
+ else:
326
+ logger.warning(f"Statut HTTP {response.status_code} pour {url}")
327
+ return None
 
328
  except requests.exceptions.RequestException as e:
329
+ logger.error(f"Erreur lors de la requête à {url}: {str(e)}")
330
  return None
331
 
332
  def clean_html(html_content):
 
333
  if not html_content:
334
  return ""
335
+ text = h.handle(html_content)
336
+ text = re.sub(r'\s+', ' ', text).strip()
337
+ return text
338
+
339
+ def save_qa_pair(question, answer, category, subcategory, source, attack_signatures=None, tags=None):
340
+ if ia_enricher.available and st.session_state.enable_enrichment:
341
+ enriched_question, enriched_answer, enriched_tags, enriched_signatures = ia_enricher.enrich_qa_pair(
342
+ question, answer, f"{category}/{subcategory}"
343
+ )
344
+
345
+ question = enriched_question
346
+ answer = enriched_answer
347
+ tags = list(set((tags or []) + enriched_tags))
348
+ attack_signatures = list(set((attack_signatures or []) + enriched_signatures))
349
+
350
+ save_dir = Path("data") / category / "qa"
351
+ save_dir.mkdir(parents=True, exist_ok=True)
352
+
353
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
354
+ filename = f"{subcategory}_{source}_{st.session_state.total_qa_pairs}_{timestamp}.json"
355
+ filename = re.sub(r'[^\w\s-]', '', filename).replace(' ', '_')
356
+
357
+ qa_data = {
358
+ "question": question,
359
+ "answer": answer,
360
+ "category": category,
361
+ "subcategory": subcategory,
362
+ "source": source,
363
+ "timestamp": timestamp,
364
+ "attack_signatures": attack_signatures or [],
365
+ "tags": tags or []
366
+ }
367
+
368
+ try:
369
+ with open(save_dir / filename, "w", encoding="utf-8") as f:
370
+ json.dump(qa_data, f, indent=2, ensure_ascii=False)
371
+
372
+ st.session_state.total_qa_pairs += 1
373
+ st.session_state.qa_data.append(qa_data)
374
+
375
+ logger.info(f"Paire Q/R sauvegardée: {filename} (Total: {st.session_state.total_qa_pairs})")
376
+ st.session_state.logs.append(f"Sauvegardé: {filename}")
377
+ except Exception as e:
378
+ logger.error(f"Erreur lors de la sauvegarde du fichier {filename}: {str(e)}")
379
 
380
+ def collect_kaggle_data(queries):
381
+ logger.info("Début de la collecte des données Kaggle...")
382
+
383
+ os.environ['KAGGLE_USERNAME'] = os.getenv('KAGGLE_USERNAME')
384
+ os.environ['KAGGLE_KEY'] = os.getenv('KAGGLE_KEY')
385
+ import kaggle
386
+ kaggle.api.authenticate()
387
+
388
+ search_queries = queries.split('\n') if queries else ["cybersecurity", "vulnerability"]
389
+
390
+ if ia_enricher.available and st.session_state.enable_enrichment:
391
+ adaptive_queries = ia_enricher.generate_adaptive_queries("Initial data keywords: " + ", ".join(search_queries))
392
+ search_queries.extend(adaptive_queries)
393
+
394
+ for query in list(set(search_queries)):
395
+ logger.info(f"Recherche de datasets Kaggle pour: {query}")
396
+ try:
397
+ datasets = kaggle.api.dataset_list(search=query, max_results=5)
398
+ for dataset in datasets:
399
+ dataset_ref = dataset.ref
400
+ if ia_enricher.available and st.session_state.enable_enrichment:
401
+ is_relevant, _, _, relevance_score = ia_enricher.analyze_content_relevance(dataset.title + " " + dataset.subtitle)
402
+ if not is_relevant or relevance_score < st.session_state.min_relevance:
403
+ logger.info(f"Dataset non pertinent ({relevance_score}%): {dataset_ref}. Ignoré.")
404
+ continue
405
+
406
+ logger.info(f"Traitement du dataset: {dataset_ref}")
407
+ download_dir = Path("data") / "security" / "kaggle" / dataset_ref.replace('/', '_')
408
+ download_dir.mkdir(parents=True, exist_ok=True)
409
+ kaggle.api.dataset_download_files(dataset_ref, path=download_dir, unzip=True)
410
+
411
+ for file_path in download_dir.glob('*'):
412
+ if file_path.is_file() and file_path.suffix.lower() in ['.json', '.csv', '.txt']:
413
+ try:
414
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
415
+ file_content = f.read()[:5000]
416
+ is_relevant, signatures, security_tags, _ = ia_enricher.analyze_content_relevance(file_content)
417
+ if is_relevant:
418
+ save_qa_pair(
419
+ question=f"Quelles informations de sécurité contient le fichier {file_path.name} du dataset '{dataset.title}'?",
420
+ answer=file_content, category="security", subcategory="vulnerability",
421
+ source=f"kaggle_{dataset_ref}", attack_signatures=signatures, tags=security_tags
422
+ )
423
+ except Exception as e:
424
+ logger.error(f"Erreur lors du traitement du fichier {file_path}: {str(e)}")
425
+ time.sleep(random.uniform(2, 4))
426
+ except Exception as e:
427
+ logger.error(f"Erreur lors de la collecte des données Kaggle pour {query}: {str(e)}")
428
+ logger.info("Collecte des données Kaggle terminée.")
429
 
430
+ def collect_github_data(queries):
431
+ logger.info("Début de la collecte des données GitHub...")
 
432
  base_url = "https://api.github.com"
433
  headers = {"Accept": "application/vnd.github.v3+json"}
434
  if config.USE_API_KEYS:
435
+ token = os.getenv('GITHUB_API_TOKEN')
436
+ headers["Authorization"] = f"token {token}"
437
 
438
+ search_queries = queries.split('\n') if queries else ["topic:devsecops", "topic:security"]
 
439
 
440
+ for query in search_queries:
441
+ logger.info(f"Recherche de repositories pour: {query}")
442
+ search_url = f"{base_url}/search/repositories"
443
+ params = {"q": query, "sort": "stars", "per_page": 10}
444
+ response = make_request(search_url, headers=headers, params=params)
445
+ if not response:
446
+ continue
447
+
448
+ data = response.json()
449
+ for repo in data.get("items", []):
450
+ repo_name = repo["full_name"].replace("/", "_")
451
+ logger.info(f"Traitement du repository: {repo['full_name']}")
452
+
453
+ issues_url = f"{base_url}/repos/{repo['full_name']}/issues"
454
+ issues_params = {"state": "closed", "labels": "security,bug,vulnerability", "per_page": 10}
455
+ issues_response = make_request(issues_url, headers=headers, params=issues_params)
456
+
457
+ if issues_response:
458
+ issues_data = issues_response.json()
459
+ for issue in issues_data:
460
+ if "pull_request" in issue: continue
461
+ question = issue.get("title", "")
462
+ body = clean_html(issue.get("body", ""))
463
+ if not question or not body or len(body) < 50: continue
464
+
465
+ comments_url = issue.get("comments_url")
466
+ comments_response = make_request(comments_url, headers=headers)
467
+ answer_parts = []
468
+ if comments_response:
469
+ comments_data = comments_response.json()
470
+ for comment in comments_data:
471
+ comment_body = clean_html(comment.get("body", ""))
472
+ if comment_body: answer_parts.append(comment_body)
473
+
474
+ if answer_parts:
475
+ answer = "\n\n".join(answer_parts)
476
+ save_qa_pair(
477
+ question=f"{question}: {body}", answer=answer, category="devsecops",
478
+ subcategory="github", source=f"github_{repo_name}"
479
+ )
480
+ time.sleep(random.uniform(1, 3))
481
+ logger.info("Collecte des données GitHub terminée.")
482
+
483
+ def collect_huggingface_data(queries):
484
+ logger.info("Début de la collecte des données Hugging Face...")
485
+ base_url = "https://huggingface.co/api"
486
+ headers = {"Accept": "application/json"}
487
+ if config.USE_API_KEYS:
488
+ token = os.getenv('HUGGINGFACE_API_TOKEN')
489
+ headers["Authorization"] = f"Bearer {token}"
490
+
491
+ search_queries = queries.split('\n') if queries else ["security", "devsecops"]
492
+ for query in search_queries:
493
+ logger.info(f"Recherche de datasets pour: {query}")
494
+ search_url = f"{base_url}/datasets"
495
+ params = {"search": query, "limit": 10}
496
+ response = make_request(search_url, headers=headers, params=params)
497
+ if not response: continue
498
+
499
+ data = response.json()
500
+ for dataset in data:
501
+ dataset_id = dataset["id"].replace("/", "_")
502
+ logger.info(f"Traitement du dataset: {dataset['id']}")
503
+ dataset_url = f"{base_url}/datasets/{dataset['id']}"
504
+ dataset_response = make_request(dataset_url, headers=headers)
505
+
506
+ if dataset_response:
507
+ dataset_data = dataset_response.json()
508
+ description = clean_html(dataset_data.get("description", ""))
509
+ if not description or len(description) < 100: continue
510
+ tags = dataset_data.get("tags", [])
511
+ tags_text = ", ".join(tags) if tags else "No tags"
512
+ answer = f"Dataset: {dataset_data.get('id', '')}\nDownloads: {dataset_data.get('downloads', 0)}\nTags: {tags_text}\n\n{description}"
513
+
514
+ save_qa_pair(
515
+ question=f"What is the {dataset_data.get('id', '')} dataset about?", answer=answer,
516
+ category="security", subcategory="dataset", source=f"huggingface_{dataset_id}", tags=tags
517
+ )
518
+ time.sleep(random.uniform(1, 3))
519
+ logger.info("Collecte des données Hugging Face terminée.")
520
+
521
+ def collect_nvd_data():
522
+ logger.info("Début de la collecte des données NVD...")
523
  base_url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
524
+ headers = {"Accept": "application/json"}
525
  if config.USE_API_KEYS:
526
+ key = os.getenv('NVD_API_KEY')
527
+ headers["apiKey"] = key
528
 
529
+ params = {"resultsPerPage": 50}
530
  response = make_request(base_url, headers=headers, params=params)
531
+ if not response:
532
+ logger.warning("Impossible de récupérer les données du NVD.")
533
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ data = response.json()
536
+ vulnerabilities = data.get("vulnerabilities", [])
537
+ logger.info(f"Traitement de {len(vulnerabilities)} vulnérabilités...")
538
+
539
+ for vuln in vulnerabilities:
540
+ cve_data = vuln.get("cve", {})
541
+ cve_id = cve_data.get("id", "")
542
+ descriptions = cve_data.get("descriptions", [])
543
+ description = next((desc.get("value", "") for desc in descriptions if desc.get("lang") == "en"), "")
544
+ if not description or len(description) < 50: continue
545
+
546
+ cvss_v3 = cve_data.get("metrics", {}).get("cvssMetricV31", [{}])[0].get("cvssData", {})
547
+ severity = cvss_v3.get("baseSeverity", "UNKNOWN")
548
+ score = cvss_v3.get("baseScore", 0)
549
+ references = [ref.get("url", "") for ref in cve_data.get("references", [])]
550
+
551
+ answer = f"CVE ID: {cve_id}\nSeverity: {severity}\nCVSS Score: {score}\nReferences: {', '.join(references[:5])}\n\nDescription: {description}"
552
+
553
+ save_qa_pair(
554
+ question=f"What is the vulnerability {cve_id}?", answer=answer,
555
+ category="security", subcategory="vulnerability", source=f"nvd_{cve_id}"
556
+ )
557
+ logger.info("Collecte des données NVD terminée.")
558
+
559
+ def collect_stack_exchange_data(queries):
560
+ logger.info("Début de la collecte des données Stack Exchange...")
561
+ base_url = "https://api.stackexchange.com/2.3"
562
+ params_base = {"pagesize": 10, "order": "desc", "sort": "votes", "filter": "withbody"}
563
+ if config.USE_API_KEYS:
564
+ key = os.getenv('STACK_EXCHANGE_API_KEY')
565
+ params_base["key"] = key
566
+
567
+ sites = [
568
+ {"site": "security", "category": "security", "subcategory": "security"},
569
+ {"site": "devops", "category": "devsecops", "subcategory": "devops"}
570
+ ]
571
+
572
+ tags_by_site = {
573
+ "security": ["security", "vulnerability"],
574
+ "devops": ["devops", "ci-cd"]
575
+ }
576
+
577
+ for site_config in sites:
578
+ site = site_config["site"]
579
+ category = site_config["category"]
580
+ subcategory = site_config["subcategory"]
581
+ logger.info(f"Collecte des données du site: {site}")
582
+ tags = tags_by_site.get(site, []) + (queries.split('\n') if queries else [])
583
+
584
+ for tag in list(set(tags)):
585
+ logger.info(f"Recherche de questions avec le tag: {tag}")
586
+ questions_url = f"{base_url}/questions"
587
+ params = {**params_base, "site": site, "tagged": tag}
588
+
589
+ response = make_request(questions_url, params=params)
590
+ if not response: continue
591
+
592
+ questions_data = response.json()
593
+ for question in questions_data.get("items", []):
594
+ question_id = question.get("question_id")
595
+ title = question.get("title", "")
596
+ body = clean_html(question.get("body", ""))
597
+ if not body or len(body) < 50: continue
598
+
599
+ answers_url = f"{base_url}/questions/{question_id}/answers"
600
+ answers_params = {**params_base, "site": site}
601
+ answers_response = make_request(answers_url, params=answers_params)
602
+ answer_body = ""
603
+ if answers_response and answers_response.json().get("items"):
604
+ answer_body = clean_html(answers_response.json()["items"][0].get("body", ""))
605
+
606
+ if answer_body:
607
+ save_qa_pair(
608
+ question=title, answer=answer_body, category=category,
609
+ subcategory=subcategory, source=f"{site}_{question_id}", tags=question.get("tags", [])
610
+ )
611
+ time.sleep(random.uniform(1, 3))
612
+ logger.info("Collecte des données Stack Exchange terminée.")
613
+
614
+ def run_data_collection(sources, queries):
615
+ st.session_state.bot_status = "En cours d'exécution"
616
+ st.session_state.logs = []
617
 
618
  check_api_keys()
619
 
620
+ progress_bar = st.progress(0)
621
+ status_text = st.empty()
622
+
623
  enabled_sources = [s for s, enabled in sources.items() if enabled]
624
+ total_sources = len(enabled_sources)
625
+ completed_sources = 0
626
+
627
+ for source_name in enabled_sources:
628
+ status_text.text(f"Collecte des données de {source_name}...")
 
629
  try:
630
+ if source_name == "Kaggle":
631
+ collect_kaggle_data(queries.get("Kaggle", ""))
632
+ elif source_name == "GitHub":
633
+ collect_github_data(queries.get("GitHub", ""))
634
+ elif source_name == "Hugging Face":
635
+ collect_huggingface_data(queries.get("Hugging Face", ""))
636
  elif source_name == "NVD":
637
+ collect_nvd_data()
638
+ elif source_name == "Stack Exchange":
639
+ collect_stack_exchange_data(queries.get("Stack Exchange", ""))
640
  except Exception as e:
641
+ logger.error(f"Erreur fatale lors de la collecte de {source_name}: {str(e)}")
642
+
643
+ completed_sources += 1
644
+ progress_bar.progress(completed_sources / total_sources)
645
+
646
  st.session_state.bot_status = "Arrêté"
647
+ st.info("Collecte des données terminée!")
648
+ progress_bar.empty()
649
+ status_text.empty()
650
 
651
+ # CORRECTION 2: Forcer le rafraîchissement de l'UI pour afficher les résultats
 
652
  st.rerun()
653
 
654
  def main():
655
+ st.title("DevSecOps Data Bot")
656
+ st.markdown("""
657
+ Ce bot est conçu pour collecter des données de diverses sources (GitHub, Kaggle, Hugging Face, NVD, Stack Exchange)
658
+ afin de construire un jeu de données de questions/réponses DevSecOps.
659
+ """)
 
660
 
661
+ tabs = st.tabs(["Bot", "Statistiques & Données", "Configuration"])
662
 
663
  with tabs[0]:
664
+ st.header("État du bot")
665
  col1, col2, col3 = st.columns(3)
666
+ with col1:
667
+ st.metric("Statut", st.session_state.bot_status)
668
+ with col2:
669
+ st.metric("Paires Q/R", st.session_state.total_qa_pairs)
670
+ with col3:
671
+ st.metric("Statut du serveur LLM", st.session_state.server_status)
672
+
673
+ st.markdown("---")
674
+
675
+ st.header("Lancer la collecte")
676
+
677
+ st.subheader("Sources de données")
678
+ sources_columns = st.columns(5)
679
+ sources = {
680
+ "GitHub": sources_columns[0].checkbox("GitHub", value=True),
681
+ "Kaggle": sources_columns[1].checkbox("Kaggle", value=True),
682
+ "Hugging Face": sources_columns[2].checkbox("Hugging Face", value=True),
683
+ "NVD": sources_columns[3].checkbox("NVD", value=True),
684
+ "Stack Exchange": sources_columns[4].checkbox("Stack Exchange", value=True),
685
+ }
686
+
687
+ st.subheader("Requêtes de recherche")
688
+ queries = {}
689
+ queries["GitHub"] = st.text_area("Requêtes GitHub (une par ligne)", "topic:devsecops\ntopic:security\nvulnerability")
690
+ queries["Kaggle"] = st.text_area("Requêtes Kaggle (une par ligne)", "cybersecurity\nvulnerability dataset\npenetration testing")
691
+ queries["Hugging Face"] = st.text_area("Requêtes Hugging Face (une par ligne)", "security dataset\nvulnerability\nlanguage model security")
692
+ queries["Stack Exchange"] = st.text_area("Tags Stack Exchange (un par ligne)", "devsecops\nsecurity\nvulnerability")
693
+
694
+ st.markdown("---")
695
+
696
+ if st.session_state.bot_status == "Arrêté":
697
+ if st.button("Lancer la collecte", use_container_width=True, type="primary"):
698
+ st.session_state.logs = []
699
+ st.session_state.qa_data = []
700
+ st.session_state.total_qa_pairs = 0
701
+ run_data_collection(sources, queries)
702
+ else:
703
+ st.warning("La collecte est en cours. Veuillez attendre qu'elle se termine.")
704
+ if st.button("Forcer l'arrêt", use_container_width=True, type="secondary"):
705
+ st.session_state.bot_status = "Arrêté"
706
+ st.info("La collecte a été arrêtée manuellement.")
707
+
708
+ st.markdown("---")
709
+ st.subheader("Logs d'exécution")
710
+ log_container = st.container(border=True)
711
+ with log_container:
712
+ for log in st.session_state.logs:
713
+ st.text(log)
714
+
715
  with tabs[1]:
716
+ st.header("Statistiques")
717
  if st.session_state.qa_data:
718
  df = pd.DataFrame(st.session_state.qa_data)
719
 
720
  st.subheader("Aperçu des données")
721
+ st.dataframe(df, use_container_width=True)
722
 
723
  st.subheader("Répartition par source")
724
+ source_counts = df['source'].apply(lambda x: x.split('_')[0]).value_counts().reset_index()
725
+ source_counts.columns = ['Source', 'Nombre']
726
+ fig_source = px.bar(source_counts, x='Source', y='Nombre', title="Nombre de paires Q/R par source")
727
  st.plotly_chart(fig_source, use_container_width=True)
728
 
729
+ st.subheader("Répartition par catégorie")
730
+ category_counts = df['category'].value_counts().reset_index()
731
+ category_counts.columns = ['Catégorie', 'Nombre']
732
+ fig_cat = px.pie(category_counts, names='Catégorie', values='Nombre', title="Répartition par catégorie")
733
+ st.plotly_chart(fig_cat, use_container_width=True)
734
+
735
+ st.subheader("Téléchargement des données")
736
+ col1, col2 = st.columns(2)
737
+ with col1:
738
+ json_data = json.dumps(st.session_state.qa_data, indent=2)
739
+ st.download_button(
740
+ label="Télécharger JSON",
741
+ data=json_data,
742
+ file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
743
+ mime="application/json",
744
+ use_container_width=True
745
+ )
746
+ with col2:
747
+ csv_data = df.to_csv(index=False)
748
+ st.download_button(
749
+ label="Télécharger CSV",
750
+ data=csv_data,
751
+ file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
752
+ mime="text/csv",
753
+ use_container_width=True
754
+ )
755
  else:
756
+ st.info("Aucune donnée à afficher. Lancez d'abord la collecte de données.")
757
+
758
  with tabs[2]:
759
  st.header("Configuration Avancée")
760
+ st.subheader("Paramètres du serveur LLM")
 
761
 
762
  llm_col1, llm_col2 = st.columns(2)
763
+ with llm_col1:
764
+ if st.button("Démarrer le serveur LLM", type="primary", use_container_width=True):
765
+ start_llm_server()
766
+ if st.button("Vérifier le statut du serveur", use_container_width=True):
767
+ check_server_status()
768
+ st.rerun()
769
+ with llm_col2:
770
+ if st.button("Arrêter le serveur LLM", type="secondary", use_container_width=True):
771
+ stop_llm_server()
772
+
773
+ st.markdown("---")
774
+
775
  st.subheader("Paramètres d'enrichissement IA")
776
+ st.session_state.enable_enrichment = st.checkbox("Activer l'enrichissement IA", value=st.session_state.enable_enrichment, help="Utilise le LLM pour améliorer les paires Q/R.")
777
+ st.session_state.min_relevance = st.slider("Score de pertinence minimum", 0, 100, st.session_state.min_relevance, help="Les contenus en dessous de ce score ne seront pas traités.")
778
+ st.session_state.temperature = st.slider("Température de l'IA", 0.0, 1.0, st.session_state.temperature, help="Contrôle la créativité de l'IA. 0.0 = plus déterministe, 1.0 = plus créatif.")
779
+ st.session_state.n_predict = st.slider("Nombre de tokens", 128, 1024, st.session_state.n_predict, help="Nombre maximum de tokens à générer par l'IA.")
780
 
781
  if __name__ == "__main__":
782
  main()