Ferdlance commited on
Commit
62a0596
·
0 Parent(s):

Initial commit for the DevSecOps bot

Browse files
.app.py.swp ADDED
Binary file (1.02 kB). View file
 
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignorer le dossier de l'environnement virtuel Python
2
+ venv/
3
+
4
+ # Ignorer les fichiers de l'environnement virtuel Python créés par Poetry
5
+ .venv/
6
+
7
+ # Ignorer les fichiers des secrets de l'environnement
8
+ .env
9
+
10
+ # Ignorer le dossier build de llama.cpp car il est recréé par le script
11
+ llama.cpp/build/
12
+
13
+ # Ignorer les logs générés
14
+ logs/
15
+
16
+ # Ignorer le modèle GGUF qui est téléchargé au démarrage
17
+ models/*.gguf
18
+
19
+ # Si d'autres fichiers doivent être exclus, ajoutez-les ici
app.py ADDED
@@ -0,0 +1,824 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sys
4
+ import time
5
+ import random
6
+ import logging
7
+ import requests
8
+ import json
9
+ import re
10
+ import subprocess
11
+ import shutil
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ # Importation et chargement des variables d'environnement depuis le fichier .env
16
+ from dotenv import load_dotenv
17
+ load_dotenv()
18
+
19
+ import streamlit as st
20
+ import pandas as pd
21
+ import plotly.express as px
22
+ import plotly.graph_objects as go
23
+ from bs4 import BeautifulSoup
24
+ import html2text
25
+ import kaggle
26
+
27
+ # Importation du module de configuration
28
+ from config import app_config as config
29
+
30
+ # Configuration de la page Streamlit
31
+ st.set_page_config(
32
+ page_title="DevSecOps Data Bot",
33
+ layout="wide",
34
+ initial_sidebar_state="expanded"
35
+ )
36
+
37
+ # Initialisation des variables de session
38
+ config.init_session_state()
39
+
40
+ # Configuration du logging
41
+ def setup_logging():
42
+ log_dir = Path("logs")
43
+ log_dir.mkdir(exist_ok=True)
44
+ log_file = log_dir / f"data_collector_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
45
+ logging.basicConfig(
46
+ level=logging.INFO,
47
+ format='%(asctime)s - %(levelname)s - %(message)s',
48
+ handlers=[
49
+ logging.FileHandler(log_file),
50
+ logging.StreamHandler(sys.stdout)
51
+ ]
52
+ )
53
+ return logging.getLogger(__name__)
54
+
55
+ logger = setup_logging()
56
+
57
+ # Création des répertoires et scripts nécessaires
58
+ def create_initial_setup():
59
+ dirs = [
60
+ "data/devsecops/qa", "data/security/qa", "data/development/qa",
61
+ "data/data_analysis/qa", "logs", "config", "server", "scripts",
62
+ "models", "llama.cpp", ".kaggle"
63
+ ]
64
+ for dir_path in dirs:
65
+ Path(dir_path).mkdir(parents=True, exist_ok=True)
66
+
67
+ download_script = Path("scripts/download_with_aria2c.sh")
68
+ if not download_script.exists():
69
+ with open(download_script, 'w') as f:
70
+ f.write("""#!/bin/bash
71
+ URL=$1
72
+ OUTPUT=$2
73
+ MAX_RETRIES=5
74
+ for i in $(seq 1 $MAX_RETRIES); do
75
+ echo "Tentative $i/$MAX_RETRIES: $URL"
76
+ aria2c -x 16 -s 16 -d "$(dirname "$OUTPUT")" -o "$(basename "$OUTPUT")" "$URL" && break
77
+ sleep 10
78
+ done
79
+ """)
80
+ os.chmod(download_script, 0o755)
81
+
82
+ llama_dir = Path("llama.cpp")
83
+ if not llama_dir.exists():
84
+ st.info("Installation de llama.cpp...")
85
+ subprocess.run(["git", "clone", "https://github.com/ggerganov/llama.cpp.git", str(llama_dir)])
86
+ os.chdir(str(llama_dir))
87
+ subprocess.run(["mkdir", "-p", "build"])
88
+ os.chdir("build")
89
+ subprocess.run(["cmake", "..", "-DLLAMA_CURL=1"])
90
+ subprocess.run(["cmake", "--build", ".", "--config", "Release"])
91
+ os.chdir(Path(__file__).parent)
92
+
93
+ # Convertisseur HTML vers texte
94
+ h = html2text.HTML2Text()
95
+ h.ignore_links = False
96
+ h.ignore_images = True
97
+ h.ignore_emphasis = False
98
+ h.body_width = 0
99
+
100
+ # Fonctions pour le serveur LLM (llama.cpp)
101
+ def check_server_status():
102
+ try:
103
+ response = requests.get(config.LLM_SERVER_URL.replace("/completion", "/health"), timeout=2)
104
+ if response.status_code == 200:
105
+ st.session_state.server_status = "Actif"
106
+ return True
107
+ else:
108
+ st.session_state.server_status = "Inactif"
109
+ return False
110
+ except requests.exceptions.RequestException:
111
+ st.session_state.server_status = "Inactif"
112
+ return False
113
+
114
+ def start_llm_server():
115
+ if check_server_status():
116
+ st.info("Le serveur llama.cpp est déjà en cours d'exécution.")
117
+ return
118
+
119
+ model_path = Path("models/qwen2.5-coder-1.5b-q8_0.gguf")
120
+ if not model_path.exists():
121
+ st.error("Le modèle GGUF n'existe pas. Veuillez le placer dans le dossier models/.")
122
+ return
123
+
124
+ llama_server = Path("llama.cpp/build/bin/llama-server")
125
+ if not llama_server.exists():
126
+ st.error("llama.cpp n'est pas compilé. Veuillez compiler llama.cpp d'abord.")
127
+ return
128
+
129
+ start_script = Path("server/start_server.sh")
130
+ if not start_script.exists():
131
+ with open(start_script, 'w') as f:
132
+ f.write(f"""#!/bin/bash
133
+ MODEL_PATH="{str(model_path)}"
134
+ if [ ! -f "$MODEL_PATH" ]; then
135
+ echo "Le modèle GGUF est introuvable à: $MODEL_PATH"
136
+ exit 1
137
+ fi
138
+ "{str(llama_server)}" \\
139
+ -m "$MODEL_PATH" \\
140
+ --port 8080 \\
141
+ --host 0.0.0.0 \\
142
+ -c 4096 \\
143
+ -ngl 999 \\
144
+ --threads 8 \\
145
+ > "logs/llama_server.log" 2>&1 &
146
+ echo $! > "server/server.pid"
147
+ """)
148
+ os.chmod(start_script, 0o755)
149
+
150
+ try:
151
+ subprocess.Popen(["bash", str(start_script)])
152
+ st.success("Le serveur llama.cpp est en cours de démarrage...")
153
+ time.sleep(5)
154
+ if check_server_status():
155
+ st.success("Serveur llama.cpp démarré avec succès!")
156
+ else:
157
+ st.error("Le serveur n'a pas pu démarrer. Vérifiez les logs dans le dossier logs/.")
158
+ except Exception as e:
159
+ st.error(f"Erreur lors du démarrage du serveur: {str(e)}")
160
+
161
+ def stop_llm_server():
162
+ stop_script = Path("server/stop_server.sh")
163
+ if not stop_script.exists():
164
+ with open(stop_script, 'w') as f:
165
+ f.write("""#!/bin/bash
166
+ PID_FILE="server/server.pid"
167
+ if [ -f "$PID_FILE" ]; then
168
+ PID=$(cat "$PID_FILE")
169
+ kill $PID
170
+ rm "$PID_FILE"
171
+ echo "Serveur llama.cpp arrêté."
172
+ else
173
+ echo "Aucun PID de serveur trouvé."
174
+ fi
175
+ """)
176
+ os.chmod(stop_script, 0o755)
177
+
178
+ try:
179
+ subprocess.run(["bash", str(stop_script)])
180
+ st.success("Le serveur llama.cpp est en cours d'arrêt...")
181
+ time.sleep(2)
182
+ if not check_server_status():
183
+ st.success("Serveur llama.cpp arrêté avec succès!")
184
+ else:
185
+ st.warning("Le serveur n'a pas pu être arrêté correctement.")
186
+ except Exception as e:
187
+ st.error(f"Erreur lors de l'arrêt du serveur: {str(e)}")
188
+
189
+ def load_prompts():
190
+ prompts_file = Path("config/prompts.json")
191
+ if not prompts_file.exists():
192
+ default_prompts = {
193
+ "enrich_qa": {
194
+ "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.",
195
+ "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}}"
196
+ },
197
+ "analyze_relevance": {
198
+ "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.",
199
+ "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}}"
200
+ },
201
+ "generate_queries": {
202
+ "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.",
203
+ "prompt_template": "Données actuelles: {current_data}...\n\nRéponds sous forme de JSON:\n{{\n \"queries\": [\"query1\", \"query2\", \"query3\", \"query4\", \"query5\"]\n}}"
204
+ }
205
+ }
206
+ with open(prompts_file, 'w') as f:
207
+ json.dump(default_prompts, f, indent=2)
208
+
209
+ with open(prompts_file, 'r', encoding='utf-8') as f:
210
+ return json.load(f)
211
+
212
+ PROMPTS = load_prompts()
213
+
214
+ class IAEnricher:
215
+ def __init__(self):
216
+ self.server_url = config.LLM_SERVER_URL
217
+ self.available = check_server_status()
218
+ if self.available:
219
+ logger.info("Serveur llama.cpp détecté et prêt.")
220
+ else:
221
+ logger.warning("Serveur llama.cpp non disponible. Les fonctionnalités d'IA seront désactivées.")
222
+
223
+ def _get_qwen_response(self, prompt, **kwargs):
224
+ if not self.available:
225
+ return None
226
+
227
+ payload = {
228
+ "prompt": prompt,
229
+ "n_predict": kwargs.get('n_predict', 512),
230
+ "temperature": kwargs.get('temperature', 0.7),
231
+ "stop": ["<|im_end|>", "</s>"]
232
+ }
233
+
234
+ try:
235
+ response = requests.post(self.server_url, json=payload, timeout=60)
236
+ if response.status_code == 200:
237
+ return response.json().get('content', '')
238
+ else:
239
+ logger.error(f"Erreur du serveur LLM: {response.status_code} - {response.text}")
240
+ return None
241
+ except requests.exceptions.RequestException as e:
242
+ logger.error(f"Erreur de connexion au serveur LLM: {str(e)}")
243
+ return None
244
+
245
+ def enrich_qa_pair(self, question, answer, context=""):
246
+ if not self.available or not st.session_state.enable_enrichment:
247
+ return question, answer, [], []
248
+
249
+ prompt_template = PROMPTS.get("enrich_qa", {}).get("prompt_template", "")
250
+ system_prompt = PROMPTS.get("enrich_qa", {}).get("system", "")
251
+
252
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(question=question, answer=answer, context=context[:500])}"
253
+ response_text = self._get_qwen_response(full_prompt, n_predict=1024)
254
+
255
+ if response_text:
256
+ try:
257
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
258
+ if json_match:
259
+ enriched_data = json.loads(json_match.group())
260
+ return (
261
+ enriched_data.get('question', question),
262
+ enriched_data.get('answer', answer),
263
+ enriched_data.get('tags', []),
264
+ enriched_data.get('attack_signatures', [])
265
+ )
266
+ except json.JSONDecodeError as e:
267
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
268
+
269
+ return question, answer, [], []
270
+
271
+ def analyze_content_relevance(self, content):
272
+ if not self.available or not st.session_state.enable_enrichment:
273
+ return True, [], [], 50
274
+
275
+ prompt_template = PROMPTS.get("analyze_relevance", {}).get("prompt_template", "")
276
+ system_prompt = PROMPTS.get("analyze_relevance", {}).get("system", "")
277
+
278
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(content=content[:1500])}"
279
+ response_text = self._get_qwen_response(full_prompt, n_predict=256, temperature=st.session_state.temperature)
280
+
281
+ if response_text:
282
+ try:
283
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
284
+ if json_match:
285
+ analysis = json.loads(json_match.group())
286
+ return (
287
+ analysis.get('relevant', True),
288
+ analysis.get('attack_signatures', []),
289
+ analysis.get('security_tags', []),
290
+ analysis.get('it_relevance_score', 50)
291
+ )
292
+ except json.JSONDecodeError as e:
293
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
294
+ return True, [], [], 50
295
+
296
+ def generate_adaptive_queries(self, current_data):
297
+ if not self.available or not st.session_state.enable_enrichment:
298
+ return []
299
+
300
+ prompt_template = PROMPTS.get("generate_queries", {}).get("prompt_template", "")
301
+ system_prompt = PROMPTS.get("generate_queries", {}).get("system", "")
302
+
303
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(current_data=current_data[:1000])}"
304
+ response_text = self._get_qwen_response(full_prompt, n_predict=st.session_state.n_predict)
305
+
306
+ if response_text:
307
+ try:
308
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
309
+ if json_match:
310
+ queries_data = json.loads(json_match.group())
311
+ return queries_data.get('queries', [])
312
+ except json.JSONDecodeError as e:
313
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
314
+ return []
315
+
316
+ ia_enricher = IAEnricher()
317
+
318
+ def check_api_keys():
319
+ keys = {
320
+ 'GITHUB_API_TOKEN': os.getenv('GITHUB_API_TOKEN'),
321
+ 'HUGGINGFACE_API_TOKEN': os.getenv('HUGGINGFACE_API_TOKEN'),
322
+ 'NVD_API_KEY': os.getenv('NVD_API_KEY'),
323
+ 'STACK_EXCHANGE_API_KEY': os.getenv('STACK_EXCHANGE_API_KEY')
324
+ }
325
+
326
+ valid_keys = {k: v for k, v in keys.items() if v and v != f'your_{k.lower()}_here'}
327
+
328
+ config.USE_API_KEYS = len(valid_keys) == len(keys)
329
+ if not config.USE_API_KEYS:
330
+ missing = set(keys.keys()) - set(valid_keys.keys())
331
+ logger.warning(f"Clés d'API manquantes ou non configurées: {', '.join(missing)}")
332
+ logger.warning("Le bot fonctionnera en mode dégradé avec des pauses plus longues.")
333
+ else:
334
+ logger.info("Toutes les clés d'API sont configurées.")
335
+ return config.USE_API_KEYS
336
+
337
+ def make_request(url, headers=None, params=None, is_api_call=True):
338
+ config.REQUEST_COUNT += 1
339
+
340
+ pause_factor = 1 if config.USE_API_KEYS else 2
341
+
342
+ if config.REQUEST_COUNT >= config.MAX_REQUESTS_BEFORE_PAUSE:
343
+ pause_time = random.uniform(config.MIN_PAUSE * pause_factor, config.MAX_PAUSE * pause_factor)
344
+ logger.info(f"Pause de {pause_time:.2f} secondes après {config.REQUEST_COUNT} requêtes...")
345
+ time.sleep(pause_time)
346
+ config.REQUEST_COUNT = 0
347
+
348
+ try:
349
+ response = requests.get(url, headers=headers, params=params, timeout=30)
350
+
351
+ if response.status_code == 200:
352
+ return response
353
+ elif response.status_code in [401, 403]:
354
+ logger.warning(f"Accès non autorisé à {url}. Vérifiez vos clés d'API.")
355
+ return None
356
+ elif response.status_code == 429:
357
+ retry_after = int(response.headers.get('Retry-After', 10))
358
+ logger.warning(f"Limite de débit atteinte. Pause de {retry_after} secondes...")
359
+ time.sleep(retry_after)
360
+ return make_request(url, headers, params, is_api_call)
361
+ else:
362
+ logger.warning(f"Statut HTTP {response.status_code} pour {url}")
363
+ return None
364
+ except requests.exceptions.RequestException as e:
365
+ logger.error(f"Erreur lors de la requête à {url}: {str(e)}")
366
+ return None
367
+
368
+ def clean_html(html_content):
369
+ if not html_content:
370
+ return ""
371
+ text = h.handle(html_content)
372
+ text = re.sub(r'\s+', ' ', text).strip()
373
+ return text
374
+
375
+ def save_qa_pair(question, answer, category, subcategory, source, attack_signatures=None, tags=None):
376
+ if ia_enricher.available and st.session_state.enable_enrichment:
377
+ enriched_question, enriched_answer, enriched_tags, enriched_signatures = ia_enricher.enrich_qa_pair(
378
+ question, answer, f"{category}/{subcategory}"
379
+ )
380
+
381
+ question = enriched_question
382
+ answer = enriched_answer
383
+ tags = list(set((tags or []) + enriched_tags))
384
+ attack_signatures = list(set((attack_signatures or []) + enriched_signatures))
385
+
386
+ save_dir = Path("data") / category / "qa"
387
+ save_dir.mkdir(parents=True, exist_ok=True)
388
+
389
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
390
+ filename = f"{subcategory}_{source}_{st.session_state.total_qa_pairs}_{timestamp}.json"
391
+ filename = re.sub(r'[^\w\s-]', '', filename).replace(' ', '_')
392
+
393
+ qa_data = {
394
+ "question": question,
395
+ "answer": answer,
396
+ "category": category,
397
+ "subcategory": subcategory,
398
+ "source": source,
399
+ "timestamp": timestamp,
400
+ "attack_signatures": attack_signatures or [],
401
+ "tags": tags or []
402
+ }
403
+
404
+ try:
405
+ with open(save_dir / filename, "w", encoding="utf-8") as f:
406
+ json.dump(qa_data, f, indent=2, ensure_ascii=False)
407
+
408
+ st.session_state.total_qa_pairs += 1
409
+ st.session_state.qa_data.append(qa_data)
410
+
411
+ logger.info(f"Paire Q/R sauvegardée: {filename} (Total: {st.session_state.total_qa_pairs})")
412
+ st.session_state.logs.append(f"Sauvegardé: {filename}")
413
+ except Exception as e:
414
+ logger.error(f"Erreur lors de la sauvegarde du fichier {filename}: {str(e)}")
415
+
416
+ def collect_kaggle_data(queries):
417
+ logger.info("Début de la collecte des données Kaggle...")
418
+ kaggle_dir = Path(".kaggle")
419
+ kaggle_json = kaggle_dir / "kaggle.json"
420
+ if not kaggle_json.exists():
421
+ logger.warning("Fichier kaggle.json non trouvé. Veuillez le placer dans le dossier .kaggle/")
422
+ return
423
+
424
+ os.environ['KAGGLE_CONFIG_DIR'] = str(kaggle_dir.absolute())
425
+
426
+ try:
427
+ kaggle.api.authenticate()
428
+ except Exception as e:
429
+ logger.error(f"Erreur d'authentification Kaggle: {str(e)}")
430
+ return
431
+
432
+ search_queries = queries.split('\n') if queries else ["cybersecurity", "vulnerability"]
433
+
434
+ if ia_enricher.available and st.session_state.enable_enrichment:
435
+ adaptive_queries = ia_enricher.generate_adaptive_queries("Initial data keywords: " + ", ".join(search_queries))
436
+ search_queries.extend(adaptive_queries)
437
+
438
+ for query in list(set(search_queries)):
439
+ logger.info(f"Recherche de datasets Kaggle pour: {query}")
440
+ try:
441
+ datasets = kaggle.api.dataset_list(search=query, max_results=5)
442
+ for dataset in datasets:
443
+ dataset_ref = dataset.ref
444
+ if ia_enricher.available and st.session_state.enable_enrichment:
445
+ is_relevant, _, _, relevance_score = ia_enricher.analyze_content_relevance(dataset.title + " " + dataset.subtitle)
446
+ if not is_relevant or relevance_score < st.session_state.min_relevance:
447
+ logger.info(f"Dataset non pertinent ({relevance_score}%): {dataset_ref}. Ignoré.")
448
+ continue
449
+
450
+ logger.info(f"Traitement du dataset: {dataset_ref}")
451
+ download_dir = Path("data") / "security" / "kaggle" / dataset_ref.replace('/', '_')
452
+ download_dir.mkdir(parents=True, exist_ok=True)
453
+ kaggle.api.dataset_download_files(dataset_ref, path=download_dir, unzip=True)
454
+
455
+ for file_path in download_dir.glob('*'):
456
+ if file_path.is_file() and file_path.suffix.lower() in ['.json', '.csv', '.txt']:
457
+ try:
458
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
459
+ file_content = f.read()[:5000]
460
+ is_relevant, signatures, security_tags, _ = ia_enricher.analyze_content_relevance(file_content)
461
+ if is_relevant:
462
+ save_qa_pair(
463
+ question=f"Quelles informations de sécurité contient le fichier {file_path.name} du dataset '{dataset.title}'?",
464
+ answer=file_content, category="security", subcategory="vulnerability",
465
+ source=f"kaggle_{dataset_ref}", attack_signatures=signatures, tags=security_tags
466
+ )
467
+ except Exception as e:
468
+ logger.error(f"Erreur lors du traitement du fichier {file_path}: {str(e)}")
469
+ time.sleep(random.uniform(2, 4))
470
+ except Exception as e:
471
+ logger.error(f"Erreur lors de la collecte des données Kaggle pour {query}: {str(e)}")
472
+ logger.info("Collecte des données Kaggle terminée.")
473
+
474
+ def collect_github_data(queries):
475
+ logger.info("Début de la collecte des données GitHub...")
476
+ base_url = "https://api.github.com"
477
+ headers = {"Accept": "application/vnd.github.v3+json"}
478
+ if config.USE_API_KEYS:
479
+ token = os.getenv('GITHUB_API_TOKEN')
480
+ headers["Authorization"] = f"token {token}"
481
+
482
+ search_queries = queries.split('\n') if queries else ["topic:devsecops", "topic:security"]
483
+
484
+ for query in search_queries:
485
+ logger.info(f"Recherche de repositories pour: {query}")
486
+ search_url = f"{base_url}/search/repositories"
487
+ params = {"q": query, "sort": "stars", "per_page": 10}
488
+ response = make_request(search_url, headers=headers, params=params)
489
+ if not response:
490
+ continue
491
+
492
+ data = response.json()
493
+ for repo in data.get("items", []):
494
+ repo_name = repo["full_name"].replace("/", "_")
495
+ logger.info(f"Traitement du repository: {repo['full_name']}")
496
+
497
+ issues_url = f"{base_url}/repos/{repo['full_name']}/issues"
498
+ issues_params = {"state": "closed", "labels": "security,bug,vulnerability", "per_page": 10}
499
+ issues_response = make_request(issues_url, headers=headers, params=issues_params)
500
+
501
+ if issues_response:
502
+ issues_data = issues_response.json()
503
+ for issue in issues_data:
504
+ if "pull_request" in issue: continue
505
+ question = issue.get("title", "")
506
+ body = clean_html(issue.get("body", ""))
507
+ if not question or not body or len(body) < 50: continue
508
+
509
+ comments_url = issue.get("comments_url")
510
+ comments_response = make_request(comments_url, headers=headers)
511
+ answer_parts = []
512
+ if comments_response:
513
+ comments_data = comments_response.json()
514
+ for comment in comments_data:
515
+ comment_body = clean_html(comment.get("body", ""))
516
+ if comment_body: answer_parts.append(comment_body)
517
+
518
+ if answer_parts:
519
+ answer = "\n\n".join(answer_parts)
520
+ save_qa_pair(
521
+ question=f"{question}: {body}", answer=answer, category="devsecops",
522
+ subcategory="github", source=f"github_{repo_name}"
523
+ )
524
+ time.sleep(random.uniform(1, 3))
525
+ logger.info("Collecte des données GitHub terminée.")
526
+
527
+ def collect_huggingface_data(queries):
528
+ logger.info("Début de la collecte des données Hugging Face...")
529
+ base_url = "https://huggingface.co/api"
530
+ headers = {"Accept": "application/json"}
531
+ if config.USE_API_KEYS:
532
+ token = os.getenv('HUGGINGFACE_API_TOKEN')
533
+ headers["Authorization"] = f"Bearer {token}"
534
+
535
+ search_queries = queries.split('\n') if queries else ["security", "devsecops"]
536
+ for query in search_queries:
537
+ logger.info(f"Recherche de datasets pour: {query}")
538
+ search_url = f"{base_url}/datasets"
539
+ params = {"search": query, "limit": 10}
540
+ response = make_request(search_url, headers=headers, params=params)
541
+ if not response: continue
542
+
543
+ data = response.json()
544
+ for dataset in data:
545
+ dataset_id = dataset["id"].replace("/", "_")
546
+ logger.info(f"Traitement du dataset: {dataset['id']}")
547
+ dataset_url = f"{base_url}/datasets/{dataset['id']}"
548
+ dataset_response = make_request(dataset_url, headers=headers)
549
+
550
+ if dataset_response:
551
+ dataset_data = dataset_response.json()
552
+ description = clean_html(dataset_data.get("description", ""))
553
+ if not description or len(description) < 100: continue
554
+ tags = dataset_data.get("tags", [])
555
+ tags_text = ", ".join(tags) if tags else "No tags"
556
+ answer = f"Dataset: {dataset_data.get('id', '')}\nDownloads: {dataset_data.get('downloads', 0)}\nTags: {tags_text}\n\n{description}"
557
+
558
+ save_qa_pair(
559
+ question=f"What is the {dataset_data.get('id', '')} dataset about?", answer=answer,
560
+ category="security", subcategory="dataset", source=f"huggingface_{dataset_id}", tags=tags
561
+ )
562
+ time.sleep(random.uniform(1, 3))
563
+ logger.info("Collecte des données Hugging Face terminée.")
564
+
565
+ def collect_nvd_data():
566
+ logger.info("Début de la collecte des données NVD...")
567
+ base_url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
568
+ headers = {"Accept": "application/json"}
569
+ if config.USE_API_KEYS:
570
+ key = os.getenv('NVD_API_KEY')
571
+ headers["apiKey"] = key
572
+
573
+ params = {"resultsPerPage": 50}
574
+ response = make_request(base_url, headers=headers, params=params)
575
+ if not response:
576
+ logger.warning("Impossible de récupérer les données du NVD.")
577
+ return
578
+
579
+ data = response.json()
580
+ vulnerabilities = data.get("vulnerabilities", [])
581
+ logger.info(f"Traitement de {len(vulnerabilities)} vulnérabilités...")
582
+
583
+ for vuln in vulnerabilities:
584
+ cve_data = vuln.get("cve", {})
585
+ cve_id = cve_data.get("id", "")
586
+ descriptions = cve_data.get("descriptions", [])
587
+ description = next((desc.get("value", "") for desc in descriptions if desc.get("lang") == "en"), "")
588
+ if not description or len(description) < 50: continue
589
+
590
+ cvss_v3 = cve_data.get("metrics", {}).get("cvssMetricV31", [{}])[0].get("cvssData", {})
591
+ severity = cvss_v3.get("baseSeverity", "UNKNOWN")
592
+ score = cvss_v3.get("baseScore", 0)
593
+ references = [ref.get("url", "") for ref in cve_data.get("references", [])]
594
+
595
+ answer = f"CVE ID: {cve_id}\nSeverity: {severity}\nCVSS Score: {score}\nReferences: {', '.join(references[:5])}\n\nDescription: {description}"
596
+
597
+ save_qa_pair(
598
+ question=f"What is the vulnerability {cve_id}?", answer=answer,
599
+ category="security", subcategory="vulnerability", source=f"nvd_{cve_id}"
600
+ )
601
+ logger.info("Collecte des données NVD terminée.")
602
+
603
+ def collect_stack_exchange_data(queries):
604
+ logger.info("Début de la collecte des données Stack Exchange...")
605
+ base_url = "https://api.stackexchange.com/2.3"
606
+ params_base = {"pagesize": 10, "order": "desc", "sort": "votes", "filter": "withbody"}
607
+ if config.USE_API_KEYS:
608
+ key = os.getenv('STACK_EXCHANGE_API_KEY')
609
+ params_base["key"] = key
610
+
611
+ sites = [
612
+ {"site": "security", "category": "security", "subcategory": "security"},
613
+ {"site": "devops", "category": "devsecops", "subcategory": "devops"}
614
+ ]
615
+
616
+ tags_by_site = {
617
+ "security": ["security", "vulnerability"],
618
+ "devops": ["devops", "ci-cd"]
619
+ }
620
+
621
+ for site_config in sites:
622
+ site = site_config["site"]
623
+ category = site_config["category"]
624
+ subcategory = site_config["subcategory"]
625
+ logger.info(f"Collecte des données du site: {site}")
626
+ tags = tags_by_site.get(site, []) + (queries.split('\n') if queries else [])
627
+
628
+ for tag in list(set(tags)):
629
+ logger.info(f"Recherche de questions avec le tag: {tag}")
630
+ questions_url = f"{base_url}/questions"
631
+ params = {**params_base, "site": site, "tagged": tag}
632
+
633
+ response = make_request(questions_url, params=params)
634
+ if not response: continue
635
+
636
+ questions_data = response.json()
637
+ for question in questions_data.get("items", []):
638
+ question_id = question.get("question_id")
639
+ title = question.get("title", "")
640
+ body = clean_html(question.get("body", ""))
641
+ if not body or len(body) < 50: continue
642
+
643
+ answers_url = f"{base_url}/questions/{question_id}/answers"
644
+ answers_params = {**params_base, "site": site}
645
+ answers_response = make_request(answers_url, params=answers_params)
646
+ answer_body = ""
647
+ if answers_response and answers_response.json().get("items"):
648
+ answer_body = clean_html(answers_response.json()["items"][0].get("body", ""))
649
+
650
+ if answer_body:
651
+ save_qa_pair(
652
+ question=title, answer=answer_body, category=category,
653
+ subcategory=subcategory, source=f"{site}_{question_id}", tags=question.get("tags", [])
654
+ )
655
+ time.sleep(random.uniform(1, 3))
656
+ logger.info("Collecte des données Stack Exchange terminée.")
657
+
658
+ def run_data_collection(sources, queries):
659
+ st.session_state.bot_status = "En cours d'exécution"
660
+ st.session_state.logs = []
661
+
662
+ check_api_keys()
663
+
664
+ progress_bar = st.progress(0)
665
+ status_text = st.empty()
666
+
667
+ enabled_sources = [s for s, enabled in sources.items() if enabled]
668
+ total_sources = len(enabled_sources)
669
+ completed_sources = 0
670
+
671
+ for source_name in enabled_sources:
672
+ status_text.text(f"Collecte des données de {source_name}...")
673
+ try:
674
+ if source_name == "Kaggle":
675
+ collect_kaggle_data(queries.get("Kaggle", ""))
676
+ elif source_name == "GitHub":
677
+ collect_github_data(queries.get("GitHub", ""))
678
+ elif source_name == "Hugging Face":
679
+ collect_huggingface_data(queries.get("Hugging Face", ""))
680
+ elif source_name == "NVD":
681
+ collect_nvd_data()
682
+ elif source_name == "Stack Exchange":
683
+ collect_stack_exchange_data(queries.get("Stack Exchange", ""))
684
+ except Exception as e:
685
+ logger.error(f"Erreur fatale lors de la collecte de {source_name}: {str(e)}")
686
+
687
+ completed_sources += 1
688
+ progress_bar.progress(completed_sources / total_sources)
689
+
690
+ st.session_state.bot_status = "Arrêté"
691
+ st.info("Collecte des données terminée!")
692
+ progress_bar.empty()
693
+ status_text.empty()
694
+
695
+ def main():
696
+ st.title("DevSecOps Data Bot")
697
+ st.markdown("""
698
+ Ce bot est conçu pour collecter des données de diverses sources (GitHub, Kaggle, Hugging Face, NVD, Stack Exchange)
699
+ afin de construire un jeu de données de questions/réponses DevSecOps.
700
+ """)
701
+
702
+ tabs = st.tabs(["Bot", "Statistiques & Données", "Configuration"])
703
+
704
+ with tabs[0]:
705
+ st.header("État du bot")
706
+ col1, col2, col3 = st.columns(3)
707
+ with col1:
708
+ st.metric("Statut", st.session_state.bot_status)
709
+ with col2:
710
+ st.metric("Paires Q/R", st.session_state.total_qa_pairs)
711
+ with col3:
712
+ st.metric("Statut du serveur LLM", st.session_state.server_status)
713
+
714
+ st.markdown("---")
715
+
716
+ st.header("Lancer la collecte")
717
+
718
+ st.subheader("Sources de données")
719
+ sources_columns = st.columns(5)
720
+ sources = {
721
+ "GitHub": sources_columns[0].checkbox("GitHub", value=True),
722
+ "Kaggle": sources_columns[1].checkbox("Kaggle", value=True),
723
+ "Hugging Face": sources_columns[2].checkbox("Hugging Face", value=True),
724
+ "NVD": sources_columns[3].checkbox("NVD", value=True),
725
+ "Stack Exchange": sources_columns[4].checkbox("Stack Exchange", value=True),
726
+ }
727
+
728
+ st.subheader("Requêtes de recherche")
729
+ queries = {}
730
+ queries["GitHub"] = st.text_area("Requêtes GitHub (une par ligne)", "topic:devsecops\ntopic:security\nvulnerability")
731
+ queries["Kaggle"] = st.text_area("Requêtes Kaggle (une par ligne)", "cybersecurity\nvulnerability dataset\npenetration testing")
732
+ queries["Hugging Face"] = st.text_area("Requêtes Hugging Face (une par ligne)", "security dataset\nvulnerability\nlanguage model security")
733
+ queries["Stack Exchange"] = st.text_area("Tags Stack Exchange (un par ligne)", "devsecops\nsecurity\nvulnerability")
734
+
735
+ st.markdown("---")
736
+
737
+ if st.session_state.bot_status == "Arrêté":
738
+ if st.button("Lancer la collecte", use_container_width=True, type="primary"):
739
+ st.session_state.logs = []
740
+ st.session_state.qa_data = []
741
+ st.session_state.total_qa_pairs = 0
742
+ run_data_collection(sources, queries)
743
+ else:
744
+ st.warning("La collecte est en cours. Veuillez attendre qu'elle se termine.")
745
+ if st.button("Forcer l'arrêt", use_container_width=True, type="secondary"):
746
+ st.session_state.bot_status = "Arrêté"
747
+ st.info("La collecte a été arrêtée manuellement.")
748
+
749
+ st.markdown("---")
750
+ st.subheader("Logs d'exécution")
751
+ log_container = st.container(border=True)
752
+ with log_container:
753
+ for log in st.session_state.logs:
754
+ st.text(log)
755
+
756
+ with tabs[1]:
757
+ st.header("Statistiques")
758
+ if st.session_state.qa_data:
759
+ df = pd.DataFrame(st.session_state.qa_data)
760
+
761
+ st.subheader("Aperçu des données")
762
+ st.dataframe(df, use_container_width=True)
763
+
764
+ st.subheader("Répartition par source")
765
+ source_counts = df['source'].apply(lambda x: x.split('_')[0]).value_counts().reset_index()
766
+ source_counts.columns = ['Source', 'Nombre']
767
+ fig_source = px.bar(source_counts, x='Source', y='Nombre', title="Nombre de paires Q/R par source")
768
+ st.plotly_chart(fig_source, use_container_width=True)
769
+
770
+ st.subheader("Répartition par catégorie")
771
+ category_counts = df['category'].value_counts().reset_index()
772
+ category_counts.columns = ['Catégorie', 'Nombre']
773
+ fig_cat = px.pie(category_counts, names='Catégorie', values='Nombre', title="Répartition par catégorie")
774
+ st.plotly_chart(fig_cat, use_container_width=True)
775
+
776
+ st.subheader("Téléchargement des données")
777
+ col1, col2 = st.columns(2)
778
+ with col1:
779
+ json_data = json.dumps(st.session_state.qa_data, indent=2)
780
+ st.download_button(
781
+ label="Télécharger JSON",
782
+ data=json_data,
783
+ file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
784
+ mime="application/json",
785
+ use_container_width=True
786
+ )
787
+ with col2:
788
+ csv_data = df.to_csv(index=False)
789
+ st.download_button(
790
+ label="Télécharger CSV",
791
+ data=csv_data,
792
+ file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
793
+ mime="text/csv",
794
+ use_container_width=True
795
+ )
796
+ else:
797
+ st.info("Aucune donnée à afficher. Lancez d'abord la collecte de données.")
798
+
799
+ with tabs[2]:
800
+ st.header("Configuration Avancée")
801
+ st.subheader("Paramètres du serveur LLM")
802
+
803
+ llm_col1, llm_col2 = st.columns(2)
804
+ with llm_col1:
805
+ if st.button("Démarrer le serveur LLM", type="primary", use_container_width=True):
806
+ start_llm_server()
807
+ if st.button("Vérifier le statut du serveur", use_container_width=True):
808
+ check_server_status()
809
+ st.rerun()
810
+ with llm_col2:
811
+ if st.button("Arrêter le serveur LLM", type="secondary", use_container_width=True):
812
+ stop_llm_server()
813
+
814
+ st.markdown("---")
815
+
816
+ st.subheader("Paramètres d'enrichissement IA")
817
+ 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.")
818
+ 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.")
819
+ 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.")
820
+ 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.")
821
+
822
+ if __name__ == "__main__":
823
+ create_initial_setup()
824
+ main()
app_old.py ADDED
@@ -0,0 +1,901 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sys
4
+ import time
5
+ import random
6
+ import logging
7
+ import requests
8
+ import json
9
+ import re
10
+ import subprocess
11
+ import shutil
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ import streamlit as st
16
+ import pandas as pd
17
+ import plotly.express as px
18
+ import plotly.graph_objects as go
19
+ from bs4 import BeautifulSoup
20
+ import html2text
21
+ import kaggle
22
+
23
+ # Importation des configurations
24
+ from config import app_config as config
25
+ # Configuration de la page Streamlit
26
+ st.set_page_config(
27
+ page_title="DevSecOps Data Bot",
28
+ layout="wide",
29
+ initial_sidebar_state="expanded"
30
+ )
31
+
32
+ # Configuration du logging
33
+ def setup_logging():
34
+ log_dir = Path("logs")
35
+ log_dir.mkdir(exist_ok=True)
36
+
37
+ log_file = log_dir / f"data_collector_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
38
+
39
+ logging.basicConfig(
40
+ level=logging.INFO,
41
+ format='%(asctime)s - %(levelname)s - %(message)s',
42
+ handlers=[
43
+ logging.FileHandler(log_file),
44
+ logging.StreamHandler(sys.stdout)
45
+ ]
46
+ )
47
+ return logging.getLogger(__name__)
48
+
49
+ logger = setup_logging()
50
+
51
+ # Création des répertoires et scripts nécessaires
52
+ def create_initial_setup():
53
+ dirs = [
54
+ "data/devsecops/qa", "data/security/qa", "data/development/qa",
55
+ "data/data_analysis/qa", "logs", "config", "server", "scripts",
56
+ "models", "llama.cpp", ".kaggle"
57
+ ]
58
+ for dir_path in dirs:
59
+ Path(dir_path).mkdir(parents=True, exist_ok=True)
60
+
61
+ download_script = Path("scripts/download_with_aria2c.sh")
62
+ if not download_script.exists():
63
+ with open(download_script, 'w') as f:
64
+ f.write("""#!/bin/bash
65
+ URL=$1
66
+ OUTPUT=$2
67
+ MAX_RETRIES=5
68
+ for i in $(seq 1 $MAX_RETRIES); do
69
+ echo "Tentative $i/$MAX_RETRIES: $URL"
70
+ aria2c -x 16 -s 16 -d "$(dirname "$OUTPUT")" -o "$(basename "$OUTPUT")" "$URL" && break
71
+ sleep 10
72
+ done
73
+ """)
74
+ os.chmod(download_script, 0o755)
75
+
76
+ llama_dir = Path("llama.cpp")
77
+ if not llama_dir.exists():
78
+ st.info("Installation de llama.cpp...")
79
+ subprocess.run(["git", "clone", "https://github.com/ggerganov/llama.cpp.git", str(llama_dir)])
80
+ os.chdir(str(llama_dir))
81
+ subprocess.run(["mkdir", "-p", "build"])
82
+ os.chdir("build")
83
+ subprocess.run(["cmake", "..", "-DLLAMA_CURL=1"])
84
+ subprocess.run(["cmake", "--build", ".", "--config", "Release"])
85
+ os.chdir(Path(__file__).parent)
86
+
87
+ model_path = Path("models/qwen2.5-1.5b-instruct-q8_0.gguf")
88
+ if not model_path.exists():
89
+ st.warning("Le modèle GGUF n'existe pas. Téléchargement en cours...")
90
+ Path("models").mkdir(exist_ok=True)
91
+ model_url = "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q8_0.gguf"
92
+ try:
93
+ subprocess.run(["bash", str(download_script), model_url, str(model_path)])
94
+ if model_path.exists():
95
+ st.success("Modèle GGUF téléchargé avec succès!")
96
+ else:
97
+ st.error("Échec du téléchargement du modèle GGUF. Veuillez le placer manuellement dans le dossier models/")
98
+ except Exception as e:
99
+ st.error(f"Erreur lors du téléchargement du modèle: {str(e)}")
100
+
101
+ # Convertisseur HTML vers texte
102
+ h = html2text.HTML2Text()
103
+ h.ignore_links = False
104
+ h.ignore_images = True
105
+ h.ignore_emphasis = False
106
+ h.body_width = 0
107
+
108
+ # Fonctions pour le serveur LLM (llama.cpp)
109
+ def check_server_status():
110
+ try:
111
+ response = requests.get("http://localhost:8080/health", timeout=2)
112
+ if response.status_code == 200:
113
+ st.session_state.server_status = "Actif"
114
+ return True
115
+ else:
116
+ st.session_state.server_status = "Inactif"
117
+ return False
118
+ except requests.exceptions.RequestException:
119
+ st.session_state.server_status = "Inactif"
120
+ return False
121
+
122
+ def start_llm_server():
123
+ if check_server_status():
124
+ st.info("Le serveur llama.cpp est déjà en cours d'exécution.")
125
+ return
126
+
127
+ model_path = Path("models/qwen2.5-1.5b-instruct-q8_0.gguf")
128
+ if not model_path.exists():
129
+ st.error("Le modèle GGUF n'existe pas. Veuillez le placer dans le dossier models/.")
130
+ return
131
+
132
+ llama_server = Path("llama.cpp/build/bin/llama-server")
133
+ if not llama_server.exists():
134
+ st.error("llama.cpp n'est pas compilé. Veuillez compiler llama.cpp d'abord.")
135
+ return
136
+
137
+ start_script = Path("server/start_server.sh")
138
+ if not start_script.exists():
139
+ with open(start_script, 'w') as f:
140
+ f.write(f"""#!/bin/bash
141
+ MODEL_PATH="{str(model_path)}"
142
+ if [ ! -f "$MODEL_PATH" ]; then
143
+ echo "Le modèle GGUF est introuvable à: $MODEL_PATH"
144
+ exit 1
145
+ fi
146
+ "{str(llama_server)}" \\
147
+ -m "$MODEL_PATH" \\
148
+ --port 8080 \\
149
+ --host 0.0.0.0 \\
150
+ -c 4096 \\
151
+ -ngl 999 \\
152
+ --threads 8 \\
153
+ > "logs/llama_server.log" 2>&1 &
154
+ echo $! > "server/server.pid"
155
+ """)
156
+ os.chmod(start_script, 0o755)
157
+
158
+ try:
159
+ subprocess.Popen(["bash", str(start_script)])
160
+ st.success("Le serveur llama.cpp est en cours de démarrage...")
161
+ time.sleep(5)
162
+ if check_server_status():
163
+ st.success("Serveur llama.cpp démarré avec succès!")
164
+ else:
165
+ st.error("Le serveur n'a pas pu démarrer. Vérifiez les logs dans le dossier logs/.")
166
+ except Exception as e:
167
+ st.error(f"Erreur lors du démarrage du serveur: {str(e)}")
168
+
169
+ def stop_llm_server():
170
+ stop_script = Path("server/stop_server.sh")
171
+ if not stop_script.exists():
172
+ with open(stop_script, 'w') as f:
173
+ f.write("""#!/bin/bash
174
+ PID_FILE="server/server.pid"
175
+ if [ -f "$PID_FILE" ]; then
176
+ PID=$(cat "$PID_FILE")
177
+ kill $PID
178
+ rm "$PID_FILE"
179
+ echo "Serveur llama.cpp arrêté."
180
+ else
181
+ echo "Aucun PID de serveur trouvé."
182
+ fi
183
+ """)
184
+ os.chmod(stop_script, 0o755)
185
+
186
+ try:
187
+ subprocess.run(["bash", str(stop_script)])
188
+ st.success("Le serveur llama.cpp est en cours d'arrêt...")
189
+ time.sleep(2)
190
+ if not check_server_status():
191
+ st.success("Serveur llama.cpp arrêté avec succès!")
192
+ else:
193
+ st.warning("Le serveur n'a pas pu être arrêté correctement.")
194
+ except Exception as e:
195
+ st.error(f"Erreur lors de l'arrêt du serveur: {str(e)}")
196
+
197
+ def load_prompts():
198
+ prompts_file = Path("config/prompts.json")
199
+ if not prompts_file.exists():
200
+ default_prompts = {
201
+ "enrich_qa": {
202
+ "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.",
203
+ "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}}"
204
+ },
205
+ "analyze_relevance": {
206
+ "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.",
207
+ "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}}"
208
+ },
209
+ "generate_queries": {
210
+ "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.",
211
+ "prompt_template": "Données actuelles: {current_data}...\n\nRéponds sous forme de JSON:\n{{\n \"queries\": [\"query1\", \"query2\", \"query3\", \"query4\", \"query5\"]\n}}"
212
+ }
213
+ }
214
+ with open(prompts_file, 'w') as f:
215
+ json.dump(default_prompts, f, indent=2)
216
+
217
+ with open(prompts_file, 'r', encoding='utf-8') as f:
218
+ return json.load(f)
219
+
220
+ PROMPTS = load_prompts()
221
+
222
+ class IAEnricher:
223
+ def __init__(self):
224
+ self.server_url = config.LLM_SERVER_URL
225
+ self.available = check_server_status()
226
+ if self.available:
227
+ logger.info("Serveur llama.cpp détecté et prêt.")
228
+ else:
229
+ logger.warning("Serveur llama.cpp non disponible. Les fonctionnalités d'IA seront désactivées.")
230
+
231
+ def _get_qwen_response(self, prompt, **kwargs):
232
+ if not self.available:
233
+ return None
234
+
235
+ payload = {
236
+ "prompt": prompt,
237
+ "n_predict": kwargs.get('n_predict', 512),
238
+ "temperature": kwargs.get('temperature', 0.7),
239
+ "stop": ["<|im_end|>", "</s>"]
240
+ }
241
+
242
+ try:
243
+ response = requests.post(self.server_url, json=payload, timeout=60)
244
+ if response.status_code == 200:
245
+ return response.json().get('content', '')
246
+ else:
247
+ logger.error(f"Erreur du serveur LLM: {response.status_code} - {response.text}")
248
+ return None
249
+ except requests.exceptions.RequestException as e:
250
+ logger.error(f"Erreur de connexion au serveur LLM: {str(e)}")
251
+ return None
252
+
253
+ def enrich_qa_pair(self, question, answer, context=""):
254
+ if not self.available or not st.session_state.enable_enrichment:
255
+ return question, answer, [], []
256
+
257
+ prompt_template = PROMPTS.get("enrich_qa", {}).get("prompt_template", "")
258
+ system_prompt = PROMPTS.get("enrich_qa", {}).get("system", "")
259
+
260
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(question=question, answer=answer, context=context[:500])}"
261
+ response_text = self._get_qwen_response(full_prompt, n_predict=1024)
262
+
263
+ if response_text:
264
+ try:
265
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
266
+ if json_match:
267
+ enriched_data = json.loads(json_match.group())
268
+ return (
269
+ enriched_data.get('question', question),
270
+ enriched_data.get('answer', answer),
271
+ enriched_data.get('tags', []),
272
+ enriched_data.get('attack_signatures', [])
273
+ )
274
+ except json.JSONDecodeError as e:
275
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
276
+
277
+ return question, answer, [], []
278
+
279
+ def analyze_content_relevance(self, content):
280
+ if not self.available or not st.session_state.enable_enrichment:
281
+ return True, [], [], 50
282
+
283
+ prompt_template = PROMPTS.get("analyze_relevance", {}).get("prompt_template", "")
284
+ system_prompt = PROMPTS.get("analyze_relevance", {}).get("system", "")
285
+
286
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(content=content[:1500])}"
287
+ response_text = self._get_qwen_response(full_prompt, n_predict=256, temperature=st.session_state.temperature)
288
+
289
+ if response_text:
290
+ try:
291
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
292
+ if json_match:
293
+ analysis = json.loads(json_match.group())
294
+ return (
295
+ analysis.get('relevant', True),
296
+ analysis.get('attack_signatures', []),
297
+ analysis.get('security_tags', []),
298
+ analysis.get('it_relevance_score', 50)
299
+ )
300
+ except json.JSONDecodeError as e:
301
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
302
+ return True, [], [], 50
303
+
304
+ def generate_adaptive_queries(self, current_data):
305
+ if not self.available or not st.session_state.enable_enrichment:
306
+ return []
307
+
308
+ prompt_template = PROMPTS.get("generate_queries", {}).get("prompt_template", "")
309
+ system_prompt = PROMPTS.get("generate_queries", {}).get("system", "")
310
+
311
+ full_prompt = f"{system_prompt}\n\n{prompt_template.format(current_data=current_data[:1000])}"
312
+ response_text = self._get_qwen_response(full_prompt, n_predict=st.session_state.n_predict)
313
+
314
+ if response_text:
315
+ try:
316
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
317
+ if json_match:
318
+ queries_data = json.loads(json_match.group())
319
+ return queries_data.get('queries', [])
320
+ except json.JSONDecodeError as e:
321
+ logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
322
+ return []
323
+
324
+ ia_enricher = IAEnricher()
325
+
326
+ def check_api_keys():
327
+ keys = {
328
+ 'GITHUB_API_TOKEN': os.getenv('GITHUB_API_TOKEN'),
329
+ 'HUGGINGFACE_API_TOKEN': os.getenv('HUGGINGFACE_API_TOKEN'),
330
+ 'NVD_API_KEY': os.getenv('NVD_API_KEY'),
331
+ 'STACK_EXCHANGE_API_KEY': os.getenv('STACK_EXCHANGE_API_KEY')
332
+ }
333
+
334
+ valid_keys = {k: v for k, v in keys.items() if v and v != f'your_{k.lower()}_here'}
335
+
336
+ config.USE_API_KEYS = len(valid_keys) == len(keys)
337
+ if not config.USE_API_KEYS:
338
+ missing = set(keys.keys()) - set(valid_keys.keys())
339
+ logger.warning(f"Clés d'API manquantes ou non configurées: {', '.join(missing)}")
340
+ logger.warning("Le bot fonctionnera en mode dégradé avec des pauses plus longues.")
341
+ else:
342
+ logger.info("Toutes les clés d'API sont configurées.")
343
+ return config.USE_API_KEYS
344
+
345
+ def make_request(url, headers=None, params=None, is_api_call=True):
346
+ global REQUEST_COUNT
347
+
348
+ pause_factor = 1 if config.USE_API_KEYS else 2
349
+
350
+ if config.REQUEST_COUNT >= config.MAX_REQUESTS_BEFORE_PAUSE:
351
+ pause_time = random.uniform(config.MIN_PAUSE * pause_factor, config.MAX_PAUSE * pause_factor)
352
+ logger.info(f"Pause de {pause_time:.2f} secondes après {config.REQUEST_COUNT} requêtes...")
353
+ time.sleep(pause_time)
354
+ config.REQUEST_COUNT = 0
355
+
356
+ try:
357
+ response = requests.get(url, headers=headers, params=params, timeout=30)
358
+ config.REQUEST_COUNT += 1
359
+
360
+ if response.status_code == 200:
361
+ return response
362
+ elif response.status_code in [401, 403]:
363
+ logger.warning(f"Accès non autorisé à {url}. Vérifiez vos clés d'API.")
364
+ return None
365
+ elif response.status_code == 429:
366
+ retry_after = int(response.headers.get('Retry-After', 10))
367
+ logger.warning(f"Limite de débit atteinte. Pause de {retry_after} secondes...")
368
+ time.sleep(retry_after)
369
+ return make_request(url, headers, params, is_api_call)
370
+ else:
371
+ logger.warning(f"Statut HTTP {response.status_code} pour {url}")
372
+ return None
373
+ except requests.exceptions.RequestException as e:
374
+ logger.error(f"Erreur lors de la requête à {url}: {str(e)}")
375
+ return None
376
+
377
+ def clean_html(html_content):
378
+ if not html_content:
379
+ return ""
380
+ text = h.handle(html_content)
381
+ text = re.sub(r'\s+', ' ', text).strip()
382
+ return text
383
+
384
+ def save_qa_pair(question, answer, category, subcategory, source, attack_signatures=None, tags=None):
385
+ global TOTAL_QA_PAIRS
386
+
387
+ if ia_enricher.available and st.session_state.enable_enrichment:
388
+ enriched_question, enriched_answer, enriched_tags, enriched_signatures = ia_enricher.enrich_qa_pair(
389
+ question, answer, f"{category}/{subcategory}"
390
+ )
391
+
392
+ question = enriched_question
393
+ answer = enriched_answer
394
+ tags = list(set((tags or []) + enriched_tags))
395
+ attack_signatures = list(set((attack_signatures or []) + enriched_signatures))
396
+
397
+ save_dir = Path("data") / category / "qa"
398
+ save_dir.mkdir(parents=True, exist_ok=True)
399
+
400
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
401
+ filename = f"{subcategory}_{source}_{TOTAL_QA_PAIRS}_{timestamp}.json"
402
+ filename = re.sub(r'[^\w\s-]', '', filename).replace(' ', '_')
403
+
404
+ qa_data = {
405
+ "question": question,
406
+ "answer": answer,
407
+ "category": category,
408
+ "subcategory": subcategory,
409
+ "source": source,
410
+ "timestamp": timestamp,
411
+ "attack_signatures": attack_signatures or [],
412
+ "tags": tags or []
413
+ }
414
+
415
+ try:
416
+ with open(save_dir / filename, "w", encoding="utf-8") as f:
417
+ json.dump(qa_data, f, indent=2, ensure_ascii=False)
418
+
419
+ config.TOTAL_QA_PAIRS += 1
420
+ st.session_state.total_qa_pairs = config.TOTAL_QA_PAIRS
421
+ st.session_state.qa_data.append(qa_data)
422
+
423
+ logger.info(f"Paire Q/R sauvegardée: {filename} (Total: {config.TOTAL_QA_PAIRS})")
424
+ st.session_state.logs.append(f"Sauvegardé: {filename}")
425
+ except Exception as e:
426
+ logger.error(f"Erreur lors de la sauvegarde du fichier {filename}: {str(e)}")
427
+
428
+ def collect_kaggle_data(queries):
429
+ logger.info("Début de la collecte des données Kaggle...")
430
+ kaggle_dir = Path(".kaggle")
431
+ kaggle_json = kaggle_dir / "kaggle.json"
432
+ if not kaggle_json.exists():
433
+ logger.warning("Fichier kaggle.json non trouvé. Veuillez le placer dans le dossier .kaggle/")
434
+ return
435
+
436
+ os.environ['KAGGLE_CONFIG_DIR'] = str(kaggle_dir.absolute())
437
+
438
+ try:
439
+ kaggle.api.authenticate()
440
+ except Exception as e:
441
+ logger.error(f"Erreur d'authentification Kaggle: {str(e)}")
442
+ return
443
+
444
+ search_queries = queries.split('\n') if queries else ["cybersecurity", "vulnerability"]
445
+
446
+ if ia_enricher.available and st.session_state.enable_enrichment:
447
+ adaptive_queries = ia_enricher.generate_adaptive_queries("Initial data keywords: " + ", ".join(search_queries))
448
+ search_queries.extend(adaptive_queries)
449
+
450
+ for query in list(set(search_queries)):
451
+ logger.info(f"Recherche de datasets Kaggle pour: {query}")
452
+ try:
453
+ datasets = kaggle.api.dataset_list(search=query, max_results=5)
454
+ for dataset in datasets:
455
+ dataset_ref = dataset.ref
456
+ if ia_enricher.available and st.session_state.enable_enrichment:
457
+ is_relevant, _, _, relevance_score = ia_enricher.analyze_content_relevance(dataset.title + " " + dataset.subtitle)
458
+ if not is_relevant or relevance_score < st.session_state.min_relevance:
459
+ logger.info(f"Dataset non pertinent ({relevance_score}%): {dataset_ref}. Ignoré.")
460
+ continue
461
+
462
+ logger.info(f"Traitement du dataset: {dataset_ref}")
463
+ download_dir = Path("data") / "security" / "kaggle" / dataset_ref.replace('/', '_')
464
+ download_dir.mkdir(parents=True, exist_ok=True)
465
+ kaggle.api.dataset_download_files(dataset_ref, path=download_dir, unzip=True)
466
+
467
+ for file_path in download_dir.glob('*'):
468
+ if file_path.is_file() and file_path.suffix.lower() in ['.json', '.csv', '.txt']:
469
+ try:
470
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
471
+ file_content = f.read()[:5000]
472
+ is_relevant, signatures, security_tags, _ = ia_enricher.analyze_content_relevance(file_content)
473
+ if is_relevant:
474
+ save_qa_pair(
475
+ question=f"Quelles informations de sécurité contient le fichier {file_path.name} du dataset '{dataset.title}'?",
476
+ answer=file_content, category="security", subcategory="vulnerability",
477
+ source=f"kaggle_{dataset_ref}", attack_signatures=signatures, tags=security_tags
478
+ )
479
+ except Exception as e:
480
+ logger.error(f"Erreur lors du traitement du fichier {file_path}: {str(e)}")
481
+ time.sleep(random.uniform(2, 4))
482
+ except Exception as e:
483
+ logger.error(f"Erreur lors de la collecte des données Kaggle pour {query}: {str(e)}")
484
+ logger.info("Collecte des données Kaggle terminée.")
485
+
486
+ def collect_github_data(queries):
487
+ logger.info("Début de la collecte des données GitHub...")
488
+ base_url = "https://api.github.com"
489
+ headers = {"Accept": "application/vnd.github.v3+json"}
490
+ if config.USE_API_KEYS:
491
+ token = os.getenv('GITHUB_API_TOKEN')
492
+ headers["Authorization"] = f"token {token}"
493
+
494
+ search_queries = queries.split('\n') if queries else ["topic:devsecops", "topic:security"]
495
+
496
+ for query in search_queries:
497
+ logger.info(f"Recherche de repositories pour: {query}")
498
+ search_url = f"{base_url}/search/repositories"
499
+ params = {"q": query, "sort": "stars", "per_page": 10}
500
+ response = make_request(search_url, headers=headers, params=params)
501
+ if not response:
502
+ continue
503
+
504
+ data = response.json()
505
+ for repo in data.get("items", []):
506
+ repo_name = repo["full_name"].replace("/", "_")
507
+ logger.info(f"Traitement du repository: {repo['full_name']}")
508
+
509
+ issues_url = f"{base_url}/repos/{repo['full_name']}/issues"
510
+ issues_params = {"state": "closed", "labels": "security,bug,vulnerability", "per_page": 10}
511
+ issues_response = make_request(issues_url, headers=headers, params=issues_params)
512
+
513
+ if issues_response:
514
+ issues_data = issues_response.json()
515
+ for issue in issues_data:
516
+ if "pull_request" in issue: continue
517
+ question = issue.get("title", "")
518
+ body = clean_html(issue.get("body", ""))
519
+ if not question or not body or len(body) < 50: continue
520
+
521
+ comments_url = issue.get("comments_url")
522
+ comments_response = make_request(comments_url, headers=headers)
523
+ answer_parts = []
524
+ if comments_response:
525
+ comments_data = comments_response.json()
526
+ for comment in comments_data:
527
+ comment_body = clean_html(comment.get("body", ""))
528
+ if comment_body: answer_parts.append(comment_body)
529
+
530
+ if answer_parts:
531
+ answer = "\n\n".join(answer_parts)
532
+ save_qa_pair(
533
+ question=f"{question}: {body}", answer=answer, category="devsecops",
534
+ subcategory="github", source=f"github_{repo_name}"
535
+ )
536
+ time.sleep(random.uniform(1, 3))
537
+ logger.info("Collecte des données GitHub terminée.")
538
+
539
+ def collect_huggingface_data(queries):
540
+ logger.info("Début de la collecte des données Hugging Face...")
541
+ base_url = "https://huggingface.co/api"
542
+ headers = {"Accept": "application/json"}
543
+ if config.USE_API_KEYS:
544
+ token = os.getenv('HUGGINGFACE_API_TOKEN')
545
+ headers["Authorization"] = f"Bearer {token}"
546
+
547
+ search_queries = queries.split('\n') if queries else ["security", "devsecops"]
548
+ for query in search_queries:
549
+ logger.info(f"Recherche de datasets pour: {query}")
550
+ search_url = f"{base_url}/datasets"
551
+ params = {"search": query, "limit": 10}
552
+ response = make_request(search_url, headers=headers, params=params)
553
+ if not response: continue
554
+
555
+ data = response.json()
556
+ for dataset in data:
557
+ dataset_id = dataset["id"].replace("/", "_")
558
+ logger.info(f"Traitement du dataset: {dataset['id']}")
559
+ dataset_url = f"{base_url}/datasets/{dataset['id']}"
560
+ dataset_response = make_request(dataset_url, headers=headers)
561
+
562
+ if dataset_response:
563
+ dataset_data = dataset_response.json()
564
+ description = clean_html(dataset_data.get("description", ""))
565
+ if not description or len(description) < 100: continue
566
+ tags = dataset_data.get("tags", [])
567
+ tags_text = ", ".join(tags) if tags else "No tags"
568
+ answer = f"Dataset: {dataset_data.get('id', '')}\nDownloads: {dataset_data.get('downloads', 0)}\nTags: {tags_text}\n\n{description}"
569
+
570
+ save_qa_pair(
571
+ question=f"What is the {dataset_data.get('id', '')} dataset about?", answer=answer,
572
+ category="security", subcategory="dataset", source=f"huggingface_{dataset_id}", tags=tags
573
+ )
574
+ time.sleep(random.uniform(1, 3))
575
+ logger.info("Collecte des données Hugging Face terminée.")
576
+
577
+ def collect_nvd_data():
578
+ logger.info("Début de la collecte des données NVD...")
579
+ base_url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
580
+ headers = {"Accept": "application/json"}
581
+ if config.USE_API_KEYS:
582
+ key = os.getenv('NVD_API_KEY')
583
+ headers["apiKey"] = key
584
+
585
+ params = {"resultsPerPage": 50}
586
+ response = make_request(base_url, headers=headers, params=params)
587
+ if not response:
588
+ logger.warning("Impossible de récupérer les données du NVD.")
589
+ return
590
+
591
+ data = response.json()
592
+ vulnerabilities = data.get("vulnerabilities", [])
593
+ logger.info(f"Traitement de {len(vulnerabilities)} vulnérabilités...")
594
+
595
+ for vuln in vulnerabilities:
596
+ cve_data = vuln.get("cve", {})
597
+ cve_id = cve_data.get("id", "")
598
+ descriptions = cve_data.get("descriptions", [])
599
+ description = next((desc.get("value", "") for desc in descriptions if desc.get("lang") == "en"), "")
600
+ if not description or len(description) < 50: continue
601
+
602
+ cvss_v3 = cve_data.get("metrics", {}).get("cvssMetricV31", [{}])[0].get("cvssData", {})
603
+ severity = cvss_v3.get("baseSeverity", "UNKNOWN")
604
+ score = cvss_v3.get("baseScore", 0)
605
+ references = [ref.get("url", "") for ref in cve_data.get("references", [])]
606
+
607
+ answer = f"CVE ID: {cve_id}\nSeverity: {severity}\nCVSS Score: {score}\nReferences: {', '.join(references[:5])}\n\nDescription: {description}"
608
+
609
+ save_qa_pair(
610
+ question=f"What is the vulnerability {cve_id}?", answer=answer,
611
+ category="security", subcategory="vulnerability", source=f"nvd_{cve_id}"
612
+ )
613
+ logger.info("Collecte des données NVD terminée.")
614
+
615
+ def collect_stack_exchange_data(queries):
616
+ logger.info("Début de la collecte des données Stack Exchange...")
617
+ base_url = "https://api.stackexchange.com/2.3"
618
+ params_base = {"pagesize": 10, "order": "desc", "sort": "votes", "filter": "withbody"}
619
+ if config.USE_API_KEYS:
620
+ key = os.getenv('STACK_EXCHANGE_API_KEY')
621
+ params_base["key"] = key
622
+
623
+ sites = [
624
+ {"site": "security", "category": "security", "subcategory": "security"},
625
+ {"site": "devops", "category": "devsecops", "subcategory": "devops"}
626
+ ]
627
+
628
+ tags_by_site = {
629
+ "security": ["security", "vulnerability"],
630
+ "devops": ["devops", "ci-cd"]
631
+ }
632
+
633
+ for site_config in sites:
634
+ site = site_config["site"]
635
+ category = site_config["category"]
636
+ subcategory = site_config["subcategory"]
637
+ logger.info(f"Collecte des données du site: {site}")
638
+ tags = tags_by_site.get(site, []) + (queries.split('\n') if queries else [])
639
+
640
+ for tag in list(set(tags)):
641
+ logger.info(f"Recherche de questions avec le tag: {tag}")
642
+ questions_url = f"{base_url}/questions"
643
+ params = {**params_base, "site": site, "tagged": tag}
644
+
645
+ response = make_request(questions_url, params=params)
646
+ if not response: continue
647
+
648
+ questions_data = response.json()
649
+ for question in questions_data.get("items", []):
650
+ question_id = question.get("question_id")
651
+ title = question.get("title", "")
652
+ body = clean_html(question.get("body", ""))
653
+ if not body or len(body) < 50: continue
654
+
655
+ answers_url = f"{base_url}/questions/{question_id}/answers"
656
+ answers_params = {**params_base, "site": site}
657
+ answers_response = make_request(answers_url, params=answers_params)
658
+ answer_body = ""
659
+ if answers_response and answers_response.json().get("items"):
660
+ answer_body = clean_html(answers_response.json()["items"][0].get("body", ""))
661
+
662
+ if answer_body:
663
+ save_qa_pair(
664
+ question=title, answer=answer_body, category=category,
665
+ subcategory=subcategory, source=f"{site}_{question_id}", tags=question.get("tags", [])
666
+ )
667
+ time.sleep(random.uniform(1, 3))
668
+ logger.info("Collecte des données Stack Exchange terminée.")
669
+
670
+ def run_data_collection(sources, queries):
671
+ st.session_state.bot_status = "En cours d'exécution"
672
+ st.session_state.logs = []
673
+
674
+ check_api_keys()
675
+
676
+ progress_bar = st.progress(0)
677
+ status_text = st.empty()
678
+
679
+ enabled_sources = [s for s, enabled in sources.items() if enabled]
680
+ total_sources = len(enabled_sources)
681
+ completed_sources = 0
682
+
683
+ for source_name in enabled_sources:
684
+ status_text.text(f"Collecte des données de {source_name}...")
685
+ try:
686
+ if source_name == "Kaggle":
687
+ collect_kaggle_data(queries.get("Kaggle", ""))
688
+ elif source_name == "GitHub":
689
+ collect_github_data(queries.get("GitHub", ""))
690
+ elif source_name == "Hugging Face":
691
+ collect_huggingface_data(queries.get("Hugging Face", ""))
692
+ elif source_name == "NVD":
693
+ collect_nvd_data()
694
+ elif source_name == "Stack Exchange":
695
+ collect_stack_exchange_data(queries.get("Stack Exchange", ""))
696
+ except Exception as e:
697
+ logger.error(f"Erreur fatale lors de la collecte de {source_name}: {str(e)}")
698
+
699
+ completed_sources += 1
700
+ progress_bar.progress(completed_sources / total_sources)
701
+
702
+ st.session_state.bot_status = "Terminé"
703
+ status_text.text(f"Collecte terminée. Total de paires Q/R sauvegardées: {st.session_state.total_qa_pairs}")
704
+
705
+ def create_visualizations():
706
+ if not st.session_state.qa_data:
707
+ st.info("Aucune donnée à visualiser. Lancez d'abord la collecte de données.")
708
+ return
709
+
710
+ df = pd.DataFrame(st.session_state.qa_data)
711
+
712
+ st.subheader("Répartition des données par catégorie")
713
+ category_counts = df['category'].value_counts()
714
+ fig1 = px.pie(values=category_counts.values, names=category_counts.index, title="Répartition par catégorie")
715
+ st.plotly_chart(fig1, use_container_width=True)
716
+
717
+ st.subheader("Répartition des données par sous-catégorie")
718
+ subcategory_counts = df['subcategory'].value_counts().head(10)
719
+ fig2 = px.bar(x=subcategory_counts.values, y=subcategory_counts.index, orientation='h',
720
+ title="Top 10 des sous-catégories", labels={'x': 'Nombre de paires Q/R', 'y': 'Sous-catégorie'})
721
+ st.plotly_chart(fig2, use_container_width=True)
722
+
723
+ st.subheader("Répartition des données par source")
724
+ source_counts = df['source'].value_counts().head(10)
725
+ fig3 = px.bar(x=source_counts.values, y=source_counts.index, orientation='h',
726
+ title="Top 10 des sources", labels={'x': 'Nombre de paires Q/R', 'y': 'Source'})
727
+ st.plotly_chart(fig3, use_container_width=True)
728
+
729
+ st.subheader("Tags les plus fréquents")
730
+ all_tags = [tag for tags_list in df['tags'] for tag in tags_list]
731
+ if all_tags:
732
+ tag_counts = pd.Series(all_tags).value_counts().head(15)
733
+ fig5 = px.bar(x=tag_counts.values, y=tag_counts.index, orientation='h',
734
+ title="Top 15 des tags", labels={'x': 'Fréquence', 'y': 'Tag'})
735
+ st.plotly_chart(fig5, use_container_width=True)
736
+
737
+ st.subheader("Signatures d'attaques les plus fréquentes")
738
+ all_signatures = [sig for sigs_list in df['attack_signatures'] for sig in sigs_list]
739
+ if all_signatures:
740
+ signature_counts = pd.Series(all_signatures).value_counts().head(15)
741
+ fig6 = px.bar(x=signature_counts.values, y=signature_counts.index, orientation='h',
742
+ title="Top 15 des signatures d'attaques", labels={'x': 'Fréquence', 'y': 'Signature'})
743
+ st.plotly_chart(fig6, use_container_width=True)
744
+
745
+ def main():
746
+ create_initial_setup()
747
+
748
+ st.title("🤖 DevSecOps Data Bot")
749
+ st.markdown("---")
750
+
751
+ col1, col2, col3 = st.columns(3)
752
+ with col1:
753
+ st.metric("Statut du bot", st.session_state.bot_status)
754
+ with col2:
755
+ check_server_status() # Met à jour le statut
756
+ st.metric("Statut du serveur llama.cpp", st.session_state.server_status)
757
+ with col3:
758
+ st.metric("Paires Q/R collectées", st.session_state.total_qa_pairs)
759
+
760
+ st.markdown("---")
761
+
762
+ tab1, tab2, tab3, tab4 = st.tabs(["Réglages", "Collecte", "Traitement IA", "Résultats"])
763
+
764
+ with tab1:
765
+ st.header("Réglages")
766
+ subtab1, subtab2, subtab3 = st.tabs(["Clés d'API", "Serveur llama.cpp", "Performance"])
767
+
768
+ with subtab1:
769
+ st.subheader("Clés d'API")
770
+ github_token = st.text_input("GitHub API Token", value=os.getenv('GITHUB_API_TOKEN', ''), type="password")
771
+ huggingface_token = st.text_input("Hugging Face API Token", value=os.getenv('HUGGINGFACE_API_TOKEN', ''), type="password")
772
+ nvd_api_key = st.text_input("NVD API Key", value=os.getenv('NVD_API_KEY', ''), type="password")
773
+ stack_exchange_key = st.text_input("Stack Exchange API Key", value=os.getenv('STACK_EXCHANGE_API_KEY', ''), type="password")
774
+
775
+ if st.button("Sauvegarder les clés d'API"):
776
+ with open(config.env_path, 'w') as f:
777
+ f.write(f"GITHUB_API_TOKEN={github_token}\n")
778
+ f.write(f"HUGGINGFACE_API_TOKEN={huggingface_token}\n")
779
+ f.write(f"NVD_API_KEY={nvd_api_key}\n")
780
+ f.write(f"STACK_EXCHANGE_API_KEY={stack_exchange_key}\n")
781
+ f.write(f"LLM_SERVER_URL={os.getenv('LLM_SERVER_URL', 'http://localhost:8080/completion')}\n")
782
+
783
+ config.load_dotenv(dotenv_path=config.env_path)
784
+ check_api_keys()
785
+ st.success("Clés d'API sauvegardées!")
786
+
787
+ with subtab2:
788
+ st.subheader("Serveur llama.cpp")
789
+
790
+ col1, col2 = st.columns(2)
791
+ with col1:
792
+ if st.button("Démarrer le serveur"):
793
+ start_llm_server()
794
+ with col2:
795
+ if st.button("Arrêter le serveur"):
796
+ stop_llm_server()
797
+
798
+ st.markdown(f"**Statut actuel:** `{st.session_state.server_status}`")
799
+
800
+ llm_url = st.text_input("URL du serveur LLM", value=config.LLM_SERVER_URL)
801
+ if st.button("Mettre à jour l'URL"):
802
+ config.LLM_SERVER_URL = llm_url
803
+ os.environ['LLM_SERVER_URL'] = llm_url
804
+ st.success("URL mise à jour!")
805
+
806
+ with subtab3:
807
+ st.subheader("Paramètres de performance")
808
+ max_requests = st.number_input("Nombre de requêtes avant pause", value=config.MAX_REQUESTS_BEFORE_PAUSE)
809
+ min_pause = st.number_input("Pause minimum (secondes)", value=config.MIN_PAUSE)
810
+ max_pause = st.number_input("Pause maximum (secondes)", value=config.MAX_PAUSE)
811
+
812
+ if st.button("Sauvegarder les paramètres"):
813
+ config.MAX_REQUESTS_BEFORE_PAUSE = max_requests
814
+ config.MIN_PAUSE = min_pause
815
+ config.MAX_PAUSE = max_pause
816
+ st.success("Paramètres sauvegardés!")
817
+
818
+ with tab2:
819
+ st.header("Collecte de données")
820
+
821
+ sources = {
822
+ "Kaggle": st.checkbox("Kaggle", value=True),
823
+ "GitHub": st.checkbox("GitHub", value=True),
824
+ "Hugging Face": st.checkbox("Hugging Face", value=True),
825
+ "NVD": st.checkbox("NVD", value=True),
826
+ "Stack Exchange": st.checkbox("Stack Exchange", value=True)
827
+ }
828
+
829
+ st.subheader("Requêtes de recherche")
830
+ queries = {
831
+ "Kaggle": st.text_area("Requêtes Kaggle (une par ligne)", value="cybersecurity\nvulnerability"),
832
+ "GitHub": st.text_area("Requêtes GitHub (une par ligne)", value="topic:devsecops\ntopic:security"),
833
+ "Hugging Face": st.text_area("Requêtes Hugging Face (une par ligne)", value="security\ndevsecops"),
834
+ "Stack Exchange": st.text_area("Requêtes Stack Exchange (une par ligne)", value="security\nvulnerability")
835
+ }
836
+
837
+ if st.button("Lancer la collecte"):
838
+ run_data_collection(sources, queries)
839
+
840
+ with tab3:
841
+ st.header("Traitement IA")
842
+ enable_enrichment = st.checkbox("Activer l'enrichissement IA", value=st.session_state.enable_enrichment)
843
+ st.session_state.enable_enrichment = enable_enrichment
844
+
845
+ if enable_enrichment:
846
+ st.session_state.min_relevance = st.slider("Score de pertinence minimum", 0, 100, st.session_state.min_relevance)
847
+ st.session_state.num_queries = st.number_input("Nombre de nouvelles requêtes", value=st.session_state.num_queries)
848
+
849
+ st.subheader("Paramètres du LLM")
850
+ st.session_state.temperature = st.slider("Température", 0.0, 1.0, 0.7)
851
+ st.session_state.n_predict = st.number_input("Nombre de tokens de prédiction", value=512)
852
+
853
+ prompts = load_prompts()
854
+ st.subheader("Prompts")
855
+ for task, task_data in prompts.items():
856
+ st.write(f"**{task}**")
857
+ st.text_area(f"System Prompt - {task}", value=task_data.get("system", ""), height=100, key=f"system_{task}")
858
+ st.text_area(f"Prompt Template - {task}", value=task_data.get("prompt_template", ""), height=150, key=f"template_{task}")
859
+
860
+ if st.button("Sauvegarder les prompts"):
861
+ updated_prompts = {
862
+ task: {
863
+ "system": st.session_state[f"system_{task}"],
864
+ "prompt_template": st.session_state[f"template_{task}"]
865
+ } for task in prompts
866
+ }
867
+ with open("config/prompts.json", 'w') as f:
868
+ json.dump(updated_prompts, f, indent=2)
869
+ global PROMPTS
870
+ PROMPTS = load_prompts()
871
+ st.success("Prompts sauvegardés!")
872
+
873
+ with tab4:
874
+ st.header("Résultats")
875
+ subtab1, subtab2, subtab3 = st.tabs(["Visualisations", "Données", "Logs"])
876
+
877
+ with subtab1:
878
+ create_visualizations()
879
+ with subtab2:
880
+ st.subheader("Aperçu des données")
881
+ if st.session_state.qa_data:
882
+ df = pd.DataFrame(st.session_state.qa_data)
883
+ st.dataframe(df.tail(10))
884
+
885
+ st.subheader("Téléchargement des données")
886
+ col1, col2 = st.columns(2)
887
+ with col1:
888
+ json_data = json.dumps(st.session_state.qa_data, indent=2)
889
+ st.download_button(label="Télécharger JSON", data=json_data, file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", mime="application/json")
890
+ with col2:
891
+ csv_data = df.to_csv(index=False)
892
+ st.download_button(label="Télécharger CSV", data=csv_data, file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", mime="text/csv")
893
+ else:
894
+ st.info("Aucune donnée à afficher. Lancez d'abord la collecte.")
895
+ with subtab3:
896
+ st.subheader("Logs d'exécution")
897
+ for log in st.session_state.logs:
898
+ st.text(log)
899
+
900
+ if __name__ == "__main__":
901
+ main()
build.sh ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Fonction pour afficher des messages informatifs en vert
4
+ info() {
5
+ echo -e "\e[32m[INFO]\e[0m $1"
6
+ }
7
+
8
+ # Fonction pour afficher des messages d'erreur en rouge
9
+ error() {
10
+ echo -e "\e[31m[ERREUR]\e[0m $1"
11
+ }
12
+
13
+ # --- Début du script ---
14
+
15
+ # Étape 1: Vérification et compilation de llama.cpp
16
+ info "Étape 1: Vérification et compilation de llama.cpp..."
17
+ if [ ! -d "llama.cpp/build/bin" ]; then
18
+ info "Dossier 'llama.cpp/build/bin' introuvable. Démarrage de la compilation."
19
+
20
+ if [ ! -d "llama.cpp" ]; then
21
+ info "Clonage du dépôt llama.cpp..."
22
+ git clone https://github.com/ggerganov/llama.cpp.git || { error "Échec du clonage."; exit 1; }
23
+ fi
24
+
25
+ cd llama.cpp
26
+ info "Lancement de la compilation..."
27
+ mkdir -p build && cd build
28
+ cmake .. -DLLAMA_CURL=1 || { error "Échec de la configuration CMake."; cd ../..; exit 1; }
29
+ cmake --build . --config Release || { error "Échec de la compilation."; cd ../..; exit 1; }
30
+ cd ../..
31
+ info "Compilation de llama.cpp terminée avec succès."
32
+ else
33
+ info "llama.cpp est déjà compilé. Ignoré."
34
+ fi
35
+
36
+ # Étape 2: Vérification et téléchargement du modèle GGUF
37
+ info "Étape 2: Vérification et téléchargement du modèle GGUF..."
38
+ MODEL_PATH="models/qwen2.5-coder-1.5b-q8_0.gguf"
39
+ if [ ! -f "$MODEL_PATH" ]; then
40
+ info "Le modèle $MODEL_PATH est introuvable. Début du téléchargement..."
41
+ mkdir -p models
42
+ wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q8_0.gguf -O "$MODEL_PATH" || { error "Échec du téléchargement du modèle."; exit 1; }
43
+ info "Téléchargement du modèle terminé."
44
+ else
45
+ info "Le modèle GGUF est déjà présent. Ignoré."
46
+ fi
47
+
48
+ # Étape 3: Création du script de démarrage du serveur
49
+ info "Étape 3: Création du script de démarrage du serveur..."
50
+ mkdir -p server
51
+ START_SCRIPT="server/start_server.sh"
52
+ echo '#!/bin/bash' > "$START_SCRIPT"
53
+ echo "MODEL_PATH=\"../$MODEL_PATH\"" >> "$START_SCRIPT"
54
+ echo 'if [ ! -f "$MODEL_PATH" ]; then' >> "$START_SCRIPT"
55
+ echo ' echo "Le modèle GGUF est introuvable à: $MODEL_PATH"' >> "$START_SCRIPT"
56
+ echo ' exit 1' >> "$START_SCRIPT"
57
+ echo 'fi' >> "$START_SCRIPT"
58
+ echo '"../llama.cpp/build/bin/llama-server" \\' >> "$START_SCRIPT"
59
+ echo " -m \"\$MODEL_PATH\" \\" >> "$START_SCRIPT"
60
+ echo " --port 8080 \\" >> "$START_SCRIPT"
61
+ echo " --host 0.0.0.0 \\" >> "$START_SCRIPT"
62
+ echo " -c 4096 \\" >> "$START_SCRIPT"
63
+ echo " -ngl 999 \\" >> "$START_SCRIPT"
64
+ echo " --threads 8 \\" >> "$START_SCRIPT"
65
+ echo ' > "logs/llama_server.log" 2>&1 &' >> "$START_SCRIPT"
66
+ echo 'echo $! > "server/server.pid"' >> "$START_SCRIPT"
67
+ chmod +x "$START_SCRIPT"
68
+ info "Script de démarrage du serveur créé."
69
+
70
+ # Étape 4: Création du script d'arrêt du serveur
71
+ info "Étape 4: Création du script d'arrêt du serveur..."
72
+ STOP_SCRIPT="server/stop_server.sh"
73
+ echo '#!/bin/bash' > "$STOP_SCRIPT"
74
+ echo 'PID_FILE="server/server.pid"' >> "$STOP_SCRIPT"
75
+ echo 'if [ -f "$PID_FILE" ]; then' >> "$STOP_SCRIPT"
76
+ echo ' PID=$(cat "$PID_FILE")' >> "$STOP_SCRIPT"
77
+ echo ' kill $PID' >> "$STOP_SCRIPT"
78
+ echo ' rm "$PID_FILE"' >> "$STOP_SCRIPT"
79
+ echo ' echo "Serveur llama.cpp arrêté."' >> "$STOP_SCRIPT"
80
+ echo 'else' >> "$STOP_SCRIPT"
81
+ echo ' echo "Aucun PID de serveur trouvé."' >> "$STOP_SCRIPT"
82
+ echo 'fi' >> "$STOP_SCRIPT"
83
+ chmod +x "$STOP_SCRIPT"
84
+ info "Script d'arrêt du serveur créé."
85
+
86
+ # Étape 5: Lancement de l'application Streamlit
87
+ info "Étape 5: Lancement de l'application Streamlit..."
88
+ streamlit run app.py
build_old.sh ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Fonction pour afficher des messages informatifs en vert
4
+ info() {
5
+ echo -e "\e[32m[INFO]\e[0m $1"
6
+ }
7
+
8
+ # Fonction pour afficher des messages d'erreur en rouge
9
+ error() {
10
+ echo -e "\e[31m[ERREUR]\e[0m $1"
11
+ }
12
+
13
+ # --- Début du script ---
14
+
15
+ info "Vérification de l'existence du dossier 'llama.cpp'..."
16
+
17
+ # Vérifie si le dossier 'llama.cpp' n'existe pas
18
+ if [ ! -d "llama.cpp" ]; then
19
+ info "Le dossier 'llama.cpp' n'existe pas. Téléchargement en cours..."
20
+ git clone https://github.com/ggerganov/llama.cpp.git
21
+
22
+ # Vérifie si le clonage a réussi
23
+ if [ $? -ne 0 ]; then
24
+ error "Le clonage a échoué. Veuillez vérifier votre connexion Internet ou l'URL du dépôt."
25
+ exit 1
26
+ fi
27
+
28
+ # Entre dans le dossier cloné
29
+ cd llama.cpp
30
+ info "Démarrage de la compilation de llama.cpp..."
31
+
32
+ # Crée le dossier de compilation et s'y déplace
33
+ mkdir -p build && cd build
34
+
35
+ # Exécute la configuration avec CMake, en activant CURL
36
+ info "Configuration avec CMake..."
37
+ cmake .. -DLLAMA_CURL=1
38
+
39
+ # Vérifie si la configuration a réussi
40
+ if [ $? -ne 0 ]; then
41
+ error "La configuration CMake a échoué. Assurez-vous que CMake est installé."
42
+ cd ../..
43
+ exit 1
44
+ fi
45
+
46
+ # Lance la compilation
47
+ info "Lancement de la compilation..."
48
+ cmake --build . --config Release
49
+
50
+ # Vérifie si la compilation a réussi
51
+ if [ $? -ne 0 ]; then
52
+ error "La compilation a échoué. Veuillez vérifier les dépendances (make, g++, etc.)."
53
+ cd ../..
54
+ exit 1
55
+ fi
56
+
57
+ # Retourne au répertoire initial
58
+ cd ../..
59
+ info "Compilation terminée avec succès !"
60
+ else
61
+ info "Le dossier 'llama.cpp' existe déjà. Ignorons le téléchargement et la compilation."
62
+ fi
63
+
64
+ info "Script terminé."
config/__pycache__/app_config.cpython-312.pyc ADDED
Binary file (2.95 kB). View file
 
config/app_config.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_config.py
2
+ import os
3
+ from pathlib import Path
4
+ import streamlit as st
5
+ from dotenv import load_dotenv
6
+
7
+ def setup_dotenv():
8
+ env_dir = Path("config")
9
+ env_dir.mkdir(exist_ok=True)
10
+ env_path = env_dir / ".env"
11
+ if not env_path.exists():
12
+ with open(env_path, 'w') as f:
13
+ f.write("# Configuration du serveur LLM et des clés API\n")
14
+ f.write("LLM_SERVER_URL=http://localhost:8080/completion\n")
15
+ f.write("GITHUB_API_TOKEN=\n")
16
+ f.write("HUGGINGFACE_API_TOKEN=\n")
17
+ f.write("NVD_API_KEY=\n")
18
+ f.write("STACK_EXCHANGE_API_KEY=\n")
19
+ load_dotenv(dotenv_path=env_path)
20
+
21
+ setup_dotenv()
22
+
23
+ LLM_SERVER_URL = os.getenv('LLM_SERVER_URL', 'http://localhost:8080/completion')
24
+
25
+ def init_session_state():
26
+ if 'bot_status' not in st.session_state:
27
+ st.session_state.bot_status = "Arrêté"
28
+ if 'server_status' not in st.session_state:
29
+ st.session_state.server_status = "Inactif"
30
+ if 'total_qa_pairs' not in st.session_state:
31
+ st.session_state.total_qa_pairs = 0
32
+ if 'logs' not in st.session_state:
33
+ st.session_state.logs = []
34
+ if 'qa_data' not in st.session_state:
35
+ st.session_state.qa_data = []
36
+ if 'enable_enrichment' not in st.session_state:
37
+ st.session_state.enable_enrichment = True
38
+ if 'min_relevance' not in st.session_state:
39
+ st.session_state.min_relevance = 70
40
+ if 'num_queries' not in st.session_state:
41
+ st.session_state.num_queries = 5
42
+ if 'temperature' not in st.session_state:
43
+ st.session_state.temperature = 0.7
44
+ if 'n_predict' not in st.session_state:
45
+ st.session_state.n_predict = 512
46
+
47
+ REQUEST_COUNT = 0
48
+ MAX_REQUESTS_BEFORE_PAUSE = 15
49
+ MIN_PAUSE = 2
50
+ MAX_PAUSE = 5
51
+ USE_API_KEYS = True
config/prompts.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "enrich_qa": {
3
+ "system": "Tu es un expert DevSecOps. Am\u00e9liore cette paire question/r\u00e9ponse en y ajoutant des tags, des signatures d'attaques potentielles, et en structurant les informations. R\u00e9ponds uniquement avec un objet JSON.",
4
+ "prompt_template": "Question originale: {question}\nR\u00e9ponse originale: {answer}\nContexte: {context}\n\nFournis une version am\u00e9lior\u00e9e sous forme de JSON:\n{{\n \"question\": \"question am\u00e9lior\u00e9e\",\n \"answer\": \"r\u00e9ponse am\u00e9lior\u00e9e\",\n \"tags\": [\"tag1\", \"tag2\"],\n \"attack_signatures\": [\"signature1\", \"signature2\"]\n}}"
5
+ },
6
+ "analyze_relevance": {
7
+ "system": "Analyse ce contenu et d\u00e9termine s'il est pertinent pour DevSecOps. Si pertinent, extrais les signatures d'attaques connues. R\u00e9ponds uniquement avec un objet JSON.",
8
+ "prompt_template": "Contenu: {content}...\n\nR\u00e9ponds 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}}"
9
+ },
10
+ "generate_queries": {
11
+ "system": "Analyse les donn\u00e9es actuelles et g\u00e9n\u00e8re 5 nouvelles requ\u00eates de recherche pour trouver plus de contenu DevSecOps pertinent, en particulier des signatures d'attaques et vuln\u00e9rabilit\u00e9s. R\u00e9ponds uniquement avec un objet JSON.",
12
+ "prompt_template": "Donn\u00e9es actuelles: {current_data}...\n\nR\u00e9ponds sous forme de JSON:\n{{\n \"queries\": [\"query1\", \"query2\", \"query3\", \"query4\", \"query5\"]\n}}"
13
+ }
14
+ }
llama.cpp ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 19f4decae0ead52debe56095ba8d693b4f14e4df
monitor.sh ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Nom du script de build
4
+ BUILD_SCRIPT="./build.sh"
5
+
6
+ # Dossier de base à vérifier
7
+ BASE_FOLDER="llama.cpp"
8
+
9
+ # Dossier des binaires à vérifier pour s'assurer que la compilation a réussi
10
+ BIN_FOLDER="$BASE_FOLDER/build"
11
+
12
+ # Fonction pour afficher des messages informatifs en vert
13
+ info() {
14
+ echo -e "\e[32m[INFO]\e[0m $1"
15
+ }
16
+
17
+ # Fonction pour afficher des messages d'erreur en rouge
18
+ error() {
19
+ echo -e "\e[31m[ERREUR]\e[0m $1"
20
+ }
21
+
22
+ # Fonction pour afficher des messages d'avertissement en jaune
23
+ warning() {
24
+ echo -e "\e[33m[ATTENTION]\e[0m $1"
25
+ }
26
+
27
+ # Fonction pour nettoyer et relancer le processus
28
+ clean_and_retry() {
29
+ error "Le dossier de compilation est manquant. Nettoyage et relance en cours..."
30
+ rm -rf "$BASE_FOLDER"
31
+ sleep 5 # Attendre quelques secondes avant de relancer
32
+ }
33
+
34
+ # Boucle principale de vérification
35
+ info "Démarrage de la surveillance. Vérification de la présence des binaires de llama.cpp..."
36
+ while [ ! -d "$BIN_FOLDER" ]; do
37
+
38
+ # Vérifie si le dossier de base existe, mais n'a pas été compilé
39
+ if [ -d "$BASE_FOLDER" ]; then
40
+ warning "Le dossier '$BASE_FOLDER' existe, mais le dossier de compilation '$BIN_FOLDER' est manquant."
41
+ warning "Cela peut indiquer un échec de la compilation. Nous allons le supprimer et relancer."
42
+ clean_and_retry
43
+ fi
44
+
45
+ warning "Les binaires sont introuvables. Lancement du script de build ('$BUILD_SCRIPT')..."
46
+
47
+ # Exécute le script de build
48
+ "$BUILD_SCRIPT"
49
+
50
+ # Donne un peu de temps à la commande pour s'exécuter
51
+ sleep 5
52
+ done
53
+
54
+ info "Félicitations ! Les binaires de llama.cpp ont été trouvés avec succès !"
55
+ info "Le processus de téléchargement et de compilation est terminé."
requirements.txt ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ altair==5.5.0
2
+ attrs==25.3.0
3
+ beautifulsoup4==4.13.4
4
+ bleach==6.2.0
5
+ blinker==1.9.0
6
+ cachetools==6.1.0
7
+ certifi==2025.8.3
8
+ charset-normalizer==3.4.3
9
+ click==8.2.1
10
+ contourpy==1.3.3
11
+ cycler==0.12.1
12
+ fonttools==4.59.1
13
+ gitdb==4.0.12
14
+ GitPython==3.1.45
15
+ html2text==2025.4.15
16
+ idna==3.10
17
+ Jinja2==3.1.6
18
+ jsonschema==4.25.0
19
+ jsonschema-specifications==2025.4.1
20
+ kaggle==1.7.4.5
21
+ kiwisolver==1.4.9
22
+ lxml==6.0.0
23
+ MarkupSafe==3.0.2
24
+ matplotlib==3.10.5
25
+ narwhals==2.1.2
26
+ numpy==2.3.2
27
+ packaging==25.0
28
+ pandas==2.3.1
29
+ pillow==11.3.0
30
+ plotly==6.3.0
31
+ protobuf==6.32.0
32
+ pyarrow==21.0.0
33
+ pydeck==0.9.1
34
+ pyparsing==3.2.3
35
+ python-dateutil==2.9.0.post0
36
+ python-dotenv==1.1.1
37
+ python-slugify==8.0.4
38
+ pytz==2025.2
39
+ referencing==0.36.2
40
+ requests==2.32.4
41
+ rpds-py==0.27.0
42
+ setuptools==80.9.0
43
+ six==1.17.0
44
+ smmap==5.0.2
45
+ soupsieve==2.7
46
+ streamlit==1.48.1
47
+ tenacity==9.1.2
48
+ text-unidecode==1.3
49
+ toml==0.10.2
50
+ tornado==6.5.2
51
+ tqdm==4.67.1
52
+ typing_extensions==4.14.1
53
+ tzdata==2025.2
54
+ urllib3==2.5.0
55
+ watchdog==6.0.0
56
+ webencodings==0.5.1
run.sh ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # --- Couleurs pour l'affichage ---
4
+ RED='\033[0;31m'
5
+ GREEN='\033[0;32m'
6
+ YELLOW='\033[1;33m'
7
+ BLUE='\033[0;34m'
8
+ NC='\033[0m'
9
+
10
+ # --- Fonctions d'affichage ---
11
+ step() { echo -e "${GREEN}[ÉTAPE]${NC} $1"; }
12
+ info() { echo -e "${BLUE}[INFO]${NC} $1"; }
13
+ warning() { echo -e "${YELLOW}[AVERTISSEMENT]${NC} $1"; }
14
+ error() { echo -e "${RED}[ERREUR]${NC} $1"; }
15
+
16
+ # Vérifier si on est root
17
+ if [[ $EUID -eq 0 ]]; then
18
+ error "Ne pas exécuter ce script en tant que root. Veuillez utiliser un utilisateur normal."
19
+ exit 1
20
+ fi
21
+
22
+ # Supprimer l'arborescence et l'environnement existants pour une installation propre
23
+ step "Suppression de l'environnement existant et de l'arborescence..."
24
+ rm -rf data logs config server scripts models .kaggle venv
25
+ info "Nettoyage terminé."
26
+
27
+ # --- Étape 1 : Création de l'arborescence du projet ---
28
+ step "Création de l'arborescence du projet..."
29
+ mkdir -p data/devsecops/qa
30
+ mkdir -p data/security/qa
31
+ mkdir -p data/development/qa
32
+ mkdir -p data/data_analysis/qa
33
+ mkdir -p logs
34
+ mkdir -p config
35
+ mkdir -p server
36
+ mkdir -p scripts
37
+ mkdir -p models
38
+ mkdir -p llama.cpp # Ajout du dossier pour llama.cpp
39
+ mkdir -p .kaggle
40
+ info "Arborescence créée."
41
+
42
+ # --- Étape 2 : Installation des dépendances système ---
43
+ step "Installation des dépendances système (peut nécessiter votre mot de passe)..."
44
+ sudo apt update
45
+ sudo apt install -y python3 python3-pip python3-venv cmake build-essential git aria2
46
+ info "Dépendances système installées."
47
+
48
+ # --- Étape 3 : Création et activation de l'environnement virtuel ---
49
+ step "Création de l'environnement virtuel..."
50
+ python3 -m venv venv
51
+ info "Environnement virtuel créé."
52
+ source venv/bin/activate
53
+ info "Environnement virtuel activé."
54
+
55
+ # --- Étape 4 : Création du fichier requirements.txt et installation des dépendances ---
56
+ step "Génération et installation des dépendances Python..."
57
+ # Note : j'ai ajouté 'python-dotenv' pour gérer les variables d'environnement
58
+ cat << EOF > requirements.txt
59
+ streamlit
60
+ pandas
61
+ plotly
62
+ beautifulsoup4
63
+ html2text
64
+ requests
65
+ kaggle
66
+ python-dotenv
67
+ lxml
68
+ matplotlib
69
+ EOF
70
+ pip install -r requirements.txt
71
+ info "Dépendances Python installées."
72
+
73
+ # --- Étape 5 : Création des fichiers de configuration ---
74
+ step "Création du fichier de configuration 'config/app_config.py'..."
75
+ cat << EOF > config/app_config.py
76
+ # app_config.py
77
+ import os
78
+ from pathlib import Path
79
+ import streamlit as st
80
+ from dotenv import load_dotenv
81
+
82
+ def setup_dotenv():
83
+ env_dir = Path("config")
84
+ env_dir.mkdir(exist_ok=True)
85
+ env_path = env_dir / ".env"
86
+ if not env_path.exists():
87
+ with open(env_path, 'w') as f:
88
+ f.write("# Configuration du serveur LLM et des clés API\n")
89
+ f.write("LLM_SERVER_URL=http://localhost:8080/completion\n")
90
+ f.write("GITHUB_API_TOKEN=\n")
91
+ f.write("HUGGINGFACE_API_TOKEN=\n")
92
+ f.write("NVD_API_KEY=\n")
93
+ f.write("STACK_EXCHANGE_API_KEY=\n")
94
+ load_dotenv(dotenv_path=env_path)
95
+
96
+ setup_dotenv()
97
+
98
+ LLM_SERVER_URL = os.getenv('LLM_SERVER_URL', 'http://localhost:8080/completion')
99
+
100
+ def init_session_state():
101
+ if 'bot_status' not in st.session_state:
102
+ st.session_state.bot_status = "Arrêté"
103
+ if 'server_status' not in st.session_state:
104
+ st.session_state.server_status = "Inactif"
105
+ if 'total_qa_pairs' not in st.session_state:
106
+ st.session_state.total_qa_pairs = 0
107
+ if 'logs' not in st.session_state:
108
+ st.session_state.logs = []
109
+ if 'qa_data' not in st.session_state:
110
+ st.session_state.qa_data = []
111
+ if 'enable_enrichment' not in st.session_state:
112
+ st.session_state.enable_enrichment = True
113
+ if 'min_relevance' not in st.session_state:
114
+ st.session_state.min_relevance = 70
115
+ if 'num_queries' not in st.session_state:
116
+ st.session_state.num_queries = 5
117
+ if 'temperature' not in st.session_state:
118
+ st.session_state.temperature = 0.7
119
+ if 'n_predict' not in st.session_state:
120
+ st.session_state.n_predict = 512
121
+
122
+ REQUEST_COUNT = 0
123
+ MAX_REQUESTS_BEFORE_PAUSE = 15
124
+ MIN_PAUSE = 2
125
+ MAX_PAUSE = 5
126
+ USE_API_KEYS = True
127
+ EOF
128
+ info "Fichier de configuration créé."
129
+
130
+ # --- Étape 6 : Lancement de l'application Streamlit ---
131
+ step "Lancement de l'application app.py..."
132
+ ./venv/bin/streamlit run app.py
scripts/download_with_aria2c.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ URL=$1
3
+ OUTPUT=$2
4
+ MAX_RETRIES=5
5
+ for i in $(seq 1 $MAX_RETRIES); do
6
+ echo "Tentative $i/$MAX_RETRIES: $URL"
7
+ aria2c -x 16 -s 16 -d "$(dirname "$OUTPUT")" -o "$(basename "$OUTPUT")" "$URL" && break
8
+ sleep 10
9
+ done
server/server.pid ADDED
@@ -0,0 +1 @@
 
 
1
+ 12412
server/start_server.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ MODEL_PATH="../models/qwen2.5-coder-1.5b-q8_0.gguf"
3
+ if [ ! -f "$MODEL_PATH" ]; then
4
+ echo "Le modèle GGUF est introuvable à: $MODEL_PATH"
5
+ exit 1
6
+ fi
7
+ "../llama.cpp/build/bin/llama-server" \\
8
+ -m "$MODEL_PATH" \
9
+ --port 8080 \
10
+ --host 0.0.0.0 \
11
+ -c 4096 \
12
+ -ngl 999 \
13
+ --threads 8 \
14
+ > "logs/llama_server.log" 2>&1 &
15
+ echo $! > "server/server.pid"
server/stop_server.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ PID_FILE="server/server.pid"
3
+ if [ -f "$PID_FILE" ]; then
4
+ PID=$(cat "$PID_FILE")
5
+ kill $PID
6
+ rm "$PID_FILE"
7
+ echo "Serveur llama.cpp arrêté."
8
+ else
9
+ echo "Aucun PID de serveur trouvé."
10
+ fi