Construction d’un agent IA au service de la cybersécurité défensive
L’évolution de l’intelligence artificielle et son intégration dans la cybersécurité ouvrent la voie à de nouveaux usages pouvant impacter positivement la posture de sécurité des entreprises tout en limitant l’impact sur les équipes. Très plébiscitée dans le cadre d’activités offensives (pentest, bug bounty par exemple), l’IA trouve aussi sa place sur le plan défensif en favorisant une détection au plus tôt des vulnérabilités ainsi qu’une contextualisation aidant à la priorisation.
Cet article propose d’illustrer la création d’un agent IA mis à disposition des développeurs pour évaluer le niveau de confiance d’une dépendance disponible sur GitHub avant son intégration.
LLM, IA agentique et MCP
Les LLM (Large Language Model), piliers de l’IA générative, correspondent aux modèles entrainés avec un très grand nombre de données (le contenu public d’internet par exemple) pouvant répondre, en langage naturel, à n’importe quelle demande avec la génération d’un enchainement de mots cohérents. Ces modèles sont intéressants pour une utilisation dans la vie courante mais se trouvent assez vite limités lorsqu’ils doivent interagir avec l’environnement de l’utilisateur. C’est ici que l’IA agentique entre en jeu en mettant à disposition des LLM un ensemble d’agents spécifiques pouvant interagir avec leur environnement (un projet de développement par exemple) et exécuter des actions.
Pour exécuter ces actions, les agents IA vont s’appuyer sur des serveurs MCP (Model Context Protocol) proposant des outils spécifiques (outils d’analyse de code par exemple).
Objectif et fonctionnement de l’agent IA
Dans cet article, l’agent IA a pour mission d’évaluer un dépôt GitHub pour déterminer, à partir de critères définis, s’il a un niveau de sécurité suffisant pour être intégré dans le projet d’une entreprise.
Pour obtenir ces informations, l’agent va pouvoir interroger GitHub pour récupérer:
- La popularité du dépôt : plus un dépôt est populaire (nombre d’étoiles et de forks) plus il est considéré comme utilisé et éprouvé ;
- Le niveau de maintenance : avec des commits et releases réguliers, la réactivité face à la découverte d’une vulnérabilité est positivement impactée ;
- Les contributeurs actifs : un dépôt géré par une seule personne est plus fragile que si une équipe de plusieurs développeurs est active.
Ces informations aident à établir un premier bilan de santé du dépôt candidat mais ne suffisent pas à définir si le niveau de sécurité est acceptable.
Si le dépôt est renommé et populaire, des CVE (Common Vulnerabilities and Exposures) existent peut-être pour certaines versions. Si l’utilisateur a spécifié la version de la dépendance à analyser, l’agent IA peut interroger les bases de données de CVE à la recherche de vulnérabilités connues.
Tous ces outils sont à disposition de l’IA générative pour établir, en fonction du prompt système fourni (les instructions) et du prompt utilisateur (le dépôt à évaluer et sa version en option), une synthèse du niveau de sécurité de la dépendance spécifiée.
L’utilisateur peut solliciter l’agent via une ligne de commande ou directement depuis son IDE (Cursor par exemple).
Architecture
La mise à disposition des outils permettant de réaliser les actions spécifiques passe par l’utilisation de serveurs MCP faisant l’interface entre l’agent IA et les services locaux ou distants à interroger (API GitHub et API CVE ici). L’appel de ces différents outils par l’agent IA est réalisé sur demande du LLM (Gemini ici) chargé de répondre à la demande de l’utilisateur.
GitHub met à disposition un serveur MCP officiel qui se présente sous la forme d’une image Docker exposant de multiples outils. Pour les CVE, plusieurs implémentations sont disponibles sur GitHub. Dans le cadre de cet article, le serveur roadwy/cve-search_mcp est utilisé.
L’architecture de l’agent IA développé peut être représentée comme suit :

Architecture de l’agent IA
Pour être exploité dans un IDE, l’agent IA doit aussi être encapsulé sous forme de serveur MCP.
La dynamique entre les différents composants est la suivante :

Fonctionnement de l’agent IA
Implémentation
Le code qui suit est une proposition d’implémentation en python de l’agent IA. Par souci de lisibilité, l’entièreté du code n’est pas affichée mais reste disponible sur GitHub.
Prérequis
Pour mettre en place cet agent, l’environnement utilisé dispose des outils suivants :
- Python3 ;
- uv ;
- Docker (pour l’exécution du serveur MCP GitHub).
Afin d’appeler les API GitHub, un token est nécessaire. Pour échanger avec Gemini, une clé d’API doit également être récupérée.
Le serveur MCP communiquant avec les bases de données de CVE, s’exécutant en python, peut être téléchargé sur son espace GitHub et placé dans le dossier mcp_servers, situé à la racine du projet.
Les principales dépendances python utilisées dans le projet sont :
- google.genai pour s’interfacer avec Gemini ;
- python-sdk MCP pour communiquer avec les serveurs MCP consommés et exposer l’agent sous forme de serveur MCP.
Mise en place de l’agent
Configuration
La clé d’API Gemini et la liste des serveurs MCP à disposition de l’agent sont les éléments de configuration nécessaires au fonctionnement de l’agent IA. Ils sont stockés dans un fichier .env se présentant comme suit :
1GEMINI_API_KEY=XXXXXXX
2MCP_SERVERS_CONFIG='{
3 "github": {
4 "command": "docker",
5 "args": [
6 "run",
7 "-i", "--rm",
8 "-e", "GITHUB_PERSONAL_ACCESS_TOKEN=XXXXXXX",
9 "-e", "GITHUB_TOOLSETS=all",
10 "ghcr.io/github/github-mcp-server:latest"
11 ]
12 },
13 "cve-search_mcp": {
14 "command": "uv",
15 "args": [
16 "--directory",
17 "<chemin_du_projet>/mcp_servers/cve-search_mcp-main",
18 "run",
19 "main.py"
20 ]
21 }
22}'
Connexions aux serveurs MCP
Avant de communiquer avec le LLM et répondre aux requêtes de l’utilisateur, l’agent doit établir une connexion avec chacun des serveurs MCP déclarés et récupérer la liste des outils qu’ils proposent. Pour échanger avec ces serveurs, MCP propose 2 protocoles dont le plus répandu est stdio (documentation). Avec ce moyen d’échange, chaque serveur MCP est lancé dans un sous processus et communique avec l’agent IA en JSON-RPC via les flux standards stdin et stdout.
Bien que pratique, ce mode de fonctionnement peut générer des erreurs si du contenu textuel (appel à print() par exemple) ou des erreurs se retrouvent affichées dans stdout.
Le code décrivant les connexions et les échanges avec les serveurs MCP sera consigné dans la classe MCPClientManager.
Pour se connecter aux agents, la méthode suivante est utilisée pour récupérer les serveurs déclarés et s’y connecter avec le protocole stdio :
1async def connect(self):
2 for name, config in self.servers_config.items():
3 try:
4 server_params = StdioServerParameters(
5 command=config['command'],
6 args=config['args'],
7 env={**config.get('env', {})}
8 )
9
10 transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
11 session = await self.exit_stack.enter_async_context(ClientSession(transport[0], transport[1]))
12 await session.initialize()
13
14 self.sessions[name] = session
15 logger.info(f"Connecté au serveur MCP: {name}")
16
17 except Exception as e:
18 logger.error(f"Erreur de connexion au serveur {name}: {e}")
Une fois la connexion établie, l’agent récupère la liste des outils de chaque agent pour les communiquer au LLM.
1async def get_available_tools(self) -> List[Dict[str, Any]]:
2 all_tools = []
3 for server_name, session in self.sessions.items():
4 result = await session.list_tools()
5 for tool in result.tools:
6 all_tools.append({
7 "server": server_name,
8 "name": tool.name,
9 "description": tool.description,
10 "input_schema": tool.inputSchema
11 })
12 return all_tools
Enfin, lorsque le LLM donnera l’instruction d’appeler un outil, il faudra contacter le serveur MCP correspondant et récupérer le résultat une fois l’action réalisée.
1async def call_tool(self, server: str, tool_name: str, arguments: Dict[str, Any]):
2 if server not in self.sessions:
3 raise ValueError(f"Serveur {server} inconnu.")
4
5 result = await self.sessions[server].call_tool(tool_name, arguments)
6 return result.content
Fonctionnement de l’agent
Maintenant que la connexion et la communication sont possibles avec les serveurs MCP via la classe MCPClientManager, le coeur de l’agent peut être développé au sein de la classe GeminiAgent.
Dans la méthode run(user_prompt) démarrant l’agent, la première étape consiste à définir le modèle du LLM à utiliser et d’initialiser la connexion vers ce même LLM.
1self.client = genai.Client(api_key=settings.gemini_api_key)
2self.model_id = 'gemini-2.5-flash'
La logique principale de l’agent, décrite dans la méthode suivante, se compose de 3 étapes :
- Récupérer les outils disponibles
L’agent commence par récupérer la liste des outils avec get_available_tools().
1tools = await self.mcp_client.get_available_tools()
- Construire le prompt système
Pour répondre aux demandes des utilisateurs, l’agent IA a besoin d’instructions initiales lui décrivant son contexte, son objectif et la démarche pour y arriver.
Le prompt système suivant donne la directive au LLM dé vérifier la popularité, maintenabilité et la dernière version de la dépendance à l’aide du serveur MCP GitHub puis de faire une recherche de CVE avec les outils du MCP CVE.
1system_instruction = (
2 "Tu es un expert en sécurité informatique.\n"
3 "Ton objectif est d'analyser la sécurité des dépôts GitHub.\n"
4 "Tu as accès à des outils externes via MCP (Model Context Protocol).\n"
5 "Prends bien en compte la branche ou la version spécifiée par l'utilisateur si elle est présente (paramètre 'branch' ou 'ref' dans les outils GitHub).\n"
6 "Si tu ne trouves pas avec 'branch' ou 'ref', regarde dans les releases"
7 "Voici les éléments à analyser :\n"
8 " - popularité : nombre d'étoiles et de forks\n"
9 " - maintenance : nombre de `contributors` actifs et réguliers\n"
10 " - dernière version : `latest release`\n"
11 " - présence de CVE : recherche les CVE sur le dépôt et sa version si elle est spécifiée avec les outils `vul_vendor_products` et `vul_vendor_product_cve`\n"
12 f"Outils disponibles :\n{tools_desc}\n\n"
13 "Si tu dois utiliser un outil, réponds UNIQUEMENT avec un JSON au format suivant :\n"
14 '{"server": "nom_serveur", "tool": "nom_outil", "arguments": {"arg1": "valeur"}}\n'
15 'Si un outil retourne une erreur, essaie un autre outil ou passe à la suite.\n'
16 "Sinon, uniquement si tu ne prévois plus d'utiliser d'outils, fournis ta réponse finale en texte clair en faisant une synthèse en moins de 10 lignes de ton analyse."
17)
- Démarrage de l’échange avec le LLM
Après avoir formatté le message initial intégrant le prompt de l’utilisateur, une boucle infinie peut démarrer pour envoyer au LLM la requête initiale puis attendre ses instructions pour appeler un outil spécifique mis à disposition par un des serveurs MCP configuré qu’il juge pertinent. En fonction des résultats de l’appel du 1er outil, le LLM peut décider d’appeler d’autres outils jusqu’à ce qu’il obtienne les informations souhaitées.
Comme indiqué dans le prompt système, si le LLM souhaite appeler un outil, il doit répondre avec un message au format JSON. S’il a terminé son analyse, il répond avec une synthèse au format texte mettant fin à la boucle infinie.
1messages = [f"{system_instruction}\n\nQuestion: {user_prompt}"]
2
3while True:
4 try:
5 response = self.client.models.generate_content(
6 model=self.model_id,
7 contents=messages
8 )
9
10 if not response.text:
11 return "Erreur: Réponse vide de Gemini."
12
13 # Vérifier si c'est un appel d'outil
14 if '{"server":' in response.text:
15 tool_result_msg = await self._execute_tool(response.text)
16 messages.append(response.text)
17 messages.append(tool_result_msg)
18 # Sinon, c'est la réponse finale
19 else:
20 return response.text
21
22 except Exception as e:
23 return f"Erreur dans la boucle de réflexion : {e}"
La méthode _execute_tool() utilisée ci-dessus n’est pas développée dans l’article mais son objectif est de réaliser un appel à la méthode call_tool() de la classe MCPClientManager. Le code de cette méthode est disponible dans le dépôt GitHub.
Exposition sous forme de serveur MCP
La logique de fonctionnement de l’agent est en place. Pour l’utiliser dans Cursor ou un autre IDE IA, l’agent doit être exposé via un serveur MCP.
Le code suivant définit le serveur MCP n’exposant qu’un seul outil correspondant à l’agent IA.
1async def serve():
2 app = Server("ai-agent-server")
3
4 @app.list_tools()
5 async def list_tools():
6 return [
7 Tool(
8 name="ask_security_agent",
9 description="Pose une question à l'agent expert en sécurité capable d'utiliser GitHub et d'autres outils pour analyser la sécurité d'un dépôt.",
10 inputSchema={
11 "type": "object",
12 "properties": {
13 "question": {"type": "string", "description": "La question à poser à l'agent"}
14 },
15 "required": ["question"]
16 }
17 )
18 ]
19
20 @app.call_tool()
21 async def call_tool(name: str, arguments: dict) -> list[TextContent | ImageContent | EmbeddedResource]:
22 if name != "ask_security_agent":
23 raise ValueError(f"Outil inconnu: {name}")
24
25 question = arguments.get("question")
26
27 mcp_client = MCPClientManager(settings.mcp_servers_config)
28 agent = GeminiAgent(mcp_client)
29
30 try:
31 await mcp_client.connect()
32 response = await agent.run(question)
33 return [TextContent(type="text", text=response)]
34 finally:
35 await mcp_client.cleanup()
36
37 async with stdio_server() as (read_stream, write_stream):
38 await app.run(
39 read_stream,
40 write_stream,
41 app.create_initialization_options()
42 )
Point d’entrée
A ce stade, l’agent IA est presque opérationnel. Il reste à définir ses 2 modes d’exécution, le mode serveur MCP ou CLI.
1app = typer.Typer()
2
3@app.command()
4def mcp():
5 # Configuration des logs (utilisation de stdio)
6 logging.getLogger().setLevel(logging.ERROR)
7 asyncio.run(serve())
8
9@app.command()
10def ask(question: str):
11 async def _run():
12 client = MCPClientManager(settings.mcp_servers_config)
13 agent = GeminiAgent(client)
14 try:
15 await client.connect()
16 print(f"\n[bold green]Réponse:[/bold green]\n{await agent.run(question)}")
17 finally:
18 await client.cleanup()
19
20 try:
21 asyncio.run(_run())
22 except KeyboardInterrupt:
23 print("\n[bold red]Interruption utilisateur (Ctrl+C).[/bold red]")
24 except Exception as e:
25 print(f"\n[bold red]Erreur fatale:[/bold red] {e}")
26
27def main():
28 app()
29
30if __name__ == "__main__":
31 main()
L’utilisation de @app_command() permet un lancement des 2 modes comme suit :
- CLI :
uv run agent ask "<prompt_utilisateur>" - MCP :
uv run agent map
Exécution
En ligne de commande
Intégration dans Cursor
Cursor et ses concurrents autorisent l’utilisateur à ajouter des serveurs MCP pour mettre à disposition du LLM davantage d’outils.
L’ajout de serveurs MCP se fait dans les paramètres de Cursor, rubrique « Tools & MCP », avec le JSON décrivant le moyen de lancer le serveur MCP souhaité.

Déclaration du nouveau serveur MCP

Paramètres MCP Cursor
Lorsque l’agent est connu de Cursor, il peut être interrogé directement dans le chat.
Et la sécurité dans tout ça ?
Être assisté par un agent IA pour sécuriser ses produits est de plus en plus simple et intégré dans les outils du quotidien. Il est cependant crucial de ne pas se reposer à 100% sur l’IA qui peut commettre des erreurs et passer à côté d’évidences.
Dans les tests effectués pour la rédaction de cet article, il est arrivé qu’en demandant à l’agent IA d’analyser le dépôt apache/logging-log4j2 en version 2.14.1 (version vulnérable à la CVE critique Log4Shell), la réponse ne remonte que des CVE de sévérités moyennes.
Le risque d’hallucinations reste également possible avec un LLM qui hallucine en confondant ou inventant des versions ou des vulnérabilités. Le résultat fourni par l’IA doit servir de base de réflexion mais doit toujours être challengé.
En complément des vulnérabilités associées aux LLM TOP 10 OWASP LLM 2025, les serveurs MCP et la logique de l’agent IA restent du code pouvant souffrir de vulnérabilités plus classiques (injection de commande et manque de contrôle d’accès par exemple) mais aussi de vulnérabilités plus spécifiques au contexte de l’IA agentique (communication inter-agent non sécurisée et détournement de la finalité de l’agent par exemple). Ces vulnérabilités spécifiques sont regroupées dans le TOP 10 OWASP pour les applications agentiques.
Enfin, internet regorge de listes de serveurs MCP, officiels ou non, plus ou moins maintenus et développés dans les règles de l’art. Intégrer ces serveurs MCP revient à exécuter du code tiers sur son environnement et partager des données avec d’éventuels services en ligne (en plus du LLM). Avant toute intégration dans un environnement de production, une étape de due diligence est nécessaire pour choisir les bons serveurs MCP et éviter d’introduire de nouvelles vulnérabilités dans le système.
Comment améliorer l’agent IA ?
L’agent IA proposé ici n’est pas destiné à être utilisé en production. Il ne propose qu’une couverture trop partielle des vérifications de sécurité préalables à l’intégration d’une dépendance dans son application.
Pour améliorer sa pertinence, l’évaluation pourrait s’enrichir des éléments suivants :
- Vérifier que la librairie est légitime et ne souffre pas de typosquatting ;
- Evaluer la pertinence de la documentation ;
- Effectuer une analyse SAST et SCA pour identifier des vulnérabilités dans le code source.
D’autres services comme NPM, PyPi ou autres peuvent s’ajouter à GitHub pour couvrir un nombre plus important de dépôts/dépendances et s’adapter au contexte des développeurs.
Enfin, le prompt système peut être renforcé pour ajouter ces nouvelles vérifications et décrire plus précisément la manière dont les outils doivent être utilisés et éviter ainsi l’hétérogénéité des réponses pour une même demande.
Conclusion
L’IA agentique est un outil intéressant pour industrialiser et augmenter la posture de sécurité des équipes produits en s’intégrant dans les outils déjà utilisés. Rien n’est cependant magique, l’IA agentique reste du code qui peut aussi souffrir de vulnérabilités parfois complexes à débusquer.