Un plugin Copilot Security n'est ni un connecteur magique ni un simple appel HTTP déguisé : c'est un contrat déclaratif entre votre système d'information et un orchestrateur qui raisonne en langage naturel. Ce contrat prend la forme d'un manifest YAML dont chaque champ pèse sur la manière dont le modèle sélectionne, paramètre et enchaîne vos capacités. Une DescriptionForModel mal rédigée et votre skill ne sera jamais invoqué ; un input marqué Required: true sans DefaultValue et l'orchestrateur bloquera l'exécution en réclamant une valeur que l'analyste n'a pas ; un bloc d'authentification approximatif et votre plugin échouera silencieusement en production. Cet article dissèque l'anatomie complète d'un manifest : champs racine, structure des SkillGroups, typage des entrées, les quatre modes d'authentification supportés, la distinction entre settings globaux et settings de skill, l'intégration OpenAPI, puis deux implémentations complètes et annotées — un plugin VirusTotal Lookup à deux skills et un plugin Threat Intel branché sur une API interne. Nous terminons par la validation du manifest, le catalogue des erreurs qui reviennent le plus souvent en revue de code, et la boucle de test locale avec le Dev Kit. L'objectif est opérationnel : à la fin de cette lecture, vous devez pouvoir écrire, valider et déboguer un plugin sans tâtonner.
Le vocabulaire officiel a évolué — Microsoft parle désormais de Microsoft Security Copilot — mais la mécanique du manifest, elle, n'a pas bougé dans ses principes. Nous conservons ici la terminologie « Copilot Security » utilisée dans le reste de notre dossier consacré à la plateforme, et nous partons du principe que vous avez déjà provisionné un environnement fonctionnel. Si le dimensionnement de votre capacité vous préoccupe avant de multiplier les appels externes, l'article sur le dimensionnement des Security Compute Units traite précisément du coût d'exécution que génèrent les plugins que nous allons construire.
Anatomie du manifest : les champs racine
Tout plugin commence par un fichier YAML unique, généralement nommé <NomDuPlugin>.yaml, qui décrit l'intégralité de ce que Copilot doit savoir pour exposer vos capacités. Ce fichier possède une racine plate composée de six à huit champs selon les cas, puis un arbre de SkillGroups qui contient la logique réelle. Il n'y a pas de fichier de configuration annexe : le manifest est autoportant, ce qui simplifie considérablement le versionnement dans un dépôt Git mais impose une discipline de relecture, puisqu'une erreur d'indentation dans un bloc profond peut casser l'upload entier.
Le premier champ, DescriptorVersion, indique la version du schéma de description que vous ciblez. Il ne s'agit pas de la version de votre plugin mais bien de celle du contrat de format ; en pratique on renseigne v1 et on ne le modifie que lorsque Microsoft publie une évolution du schéma. Le confondre avec un numéro de release maison est l'une des erreurs les plus fréquentes chez les équipes qui découvrent la plateforme, et elle se solde par un rejet immédiat à l'import.
Name est l'identifiant technique du plugin. Il doit être unique dans le tenant, ne comporter ni espace ni caractère accentué, et rester stable dans le temps : c'est la clé sur laquelle s'appuient les mises à jour. Le changer revient à créer un nouveau plugin et à laisser l'ancien orphelin dans la console d'administration. DisplayName est en revanche la chaîne présentée à l'utilisateur dans la liste des sources : elle peut contenir des espaces, de la ponctuation et doit être immédiatement compréhensible par un analyste SOC pressé.
Description décrit le plugin dans son ensemble, à destination des humains comme du modèle qui doit décider d'activer ou non cette source pour une requête donnée. On y expose le périmètre fonctionnel, pas l'implémentation : « Interroge la base de réputation VirusTotal pour enrichir des indicateurs de compromission » est utile, « Wrapper REST sur l'API v3 » ne l'est pas. ModelName précise le modèle de raisonnement ciblé lorsque le plugin doit être contraint à un moteur particulier ; on le laisse le plus souvent à la valeur par défaut, sauf besoin de reproductibilité stricte.
Vient ensuite SupportedAuthTypes, une liste — et non une chaîne — énumérant les modes d'authentification que le plugin accepte. Le fait qu'il s'agisse d'une liste est structurant : vous pouvez publier un plugin utilisable en APIKey pour les petites structures et en OAuthClientCredentials pour les grands comptes, l'administrateur choisira à l'installation. Enfin, SkillGroups ouvre l'arbre des capacités : chaque groupe déclare un Format (typiquement API, GPT ou KQL) et une liste de Skills. Un même manifest peut parfaitement mélanger un groupe API qui appelle un service externe et un groupe GPT qui se contente de reformuler un résultat.
Descriptor:
# Version du SCHÉMA de description, pas de votre plugin.
# Valeur attendue aujourd'hui : v1. Ne pas y mettre "1.0.3".
DescriptorVersion: v1
# Identifiant technique, unique dans le tenant, immuable.
# Pas d'espace, pas d'accent : c'est une clé de mise à jour.
Name: VirusTotalLookup
# Libellé affiché dans la liste des sources de Copilot.
DisplayName: VirusTotal Lookup
# Décrit le PÉRIMÈTRE FONCTIONNEL, lu par l'humain et par le modèle.
Description: >-
Enrichit des indicateurs de compromission (adresses IP, hachages de
fichiers) à partir de la base de réputation communautaire VirusTotal.
À utiliser pour qualifier rapidement un IOC lors d'une investigation.
# Modèle de raisonnement ciblé. À omettre pour laisser la valeur par
# défaut du tenant ; à figer si vous exigez une reproductibilité stricte.
# ModelName: gpt-4o
# LISTE (et non chaîne) des modes d'authentification acceptés.
# L'administrateur choisira à l'installation parmi ces valeurs.
SupportedAuthTypes:
- APIKey
# Arbre des capacités : un ou plusieurs groupes, chacun typé par Format.
SkillGroups:
- Format: API # API | GPT | KQL
Skills: [] # détaillé plus loin dans l'article
Notez la présence du nœud Descriptor en tête : selon la variante de schéma utilisée, les champs racine peuvent être encapsulés sous cette clé ou déclarés directement au premier niveau. Les deux formes circulent dans les exemples publics ; ce qui compte est de rester cohérent sur l'ensemble du fichier et de ne pas panacher. Nous conservons la forme encapsulée dans tous les exemples de cet article.
Définir un skill : le champ qui décide de tout
Un skill est l'unité d'exécution atomique du plugin. Il possède cinq attributs structurants : Name, DisplayName, DescriptionForModel, ExamplePrompts et Inputs, auxquels s'ajoutent les paramètres de transport (Settings avec l'URL, la méthode HTTP, les en-têtes et le corps de requête). Comme pour le plugin, Name est l'identifiant technique — en CamelCase par convention — et DisplayName la chaîne lisible affichée lorsque Copilot annonce quelle capacité il s'apprête à exécuter.
DescriptionForModel est de loin le champ le plus important du manifest, et le plus souvent bâclé. Il ne s'adresse pas à un lecteur humain : c'est le texte que l'orchestrateur utilise pour décider si ce skill répond à la question posée en langage naturel. Il doit donc être écrit comme une instruction destinée à un modèle : que fait ce skill, sur quel type d'entrée, quand faut-il l'utiliser, et surtout quand ne faut-il pas l'utiliser. Une bonne DescriptionForModel contient des exclusions explicites. Si vous disposez d'un skill « lookup IP » et d'un skill « lookup hash », dire dans le premier « ne pas utiliser pour un hachage de fichier, utiliser HashLookup à la place » réduit drastiquement les invocations croisées.
La longueur utile se situe entre deux et cinq phrases. En dessous, le modèle manque de signal discriminant et arbitrera au hasard entre des skills voisins. Au-dessus, la description dilue l'information et consomme du contexte pour rien. Écrivez au présent, à la voix active, en nommant explicitement les entités métier : « adresse IPv4 publique », « hachage SHA-256 », « identifiant d'incident Defender » plutôt que « une valeur » ou « des données ».
ExamplePrompts est une liste de formulations réelles qu'un analyste pourrait taper. Ces exemples ne sont pas décoratifs : ils servent d'ancrage à la sélection de skill et apparaissent dans l'interface comme suggestions. La règle empirique est d'en fournir trois à cinq, en variant délibérément la syntaxe — une question, un impératif, une formulation elliptique. Si tous vos exemples commencent par le même verbe, vous entraînez le sélecteur sur une seule forme et dégradez le rappel sur les autres.
Enfin, Inputs déclare les paramètres que le modèle devra extraire du prompt utilisateur pour appeler votre service. C'est le point de jonction entre le langage naturel et le protocole HTTP, et il mérite une section entière.
Les inputs : typage, obligation et valeurs par défaut
Chaque input est un objet possédant quatre attributs : Name, Description, Required et DefaultValue. Le Name sert de variable d'interpolation dans les settings du skill, via la syntaxe à doubles accolades {{NomDeLInput}}. C'est ce mécanisme qui permet de construire dynamiquement une URL, un en-tête ou un corps JSON à partir de ce que l'analyste a écrit en français dans la barre de prompt.
La Description d'un input suit la même logique que la DescriptionForModel du skill : elle est lue par le modèle pour opérer l'extraction. Précisez le format attendu de manière opérationnelle. « Adresse IPv4 publique, notation décimale pointée, sans masque » est extractible ; « l'adresse » ne l'est pas. Si le paramètre accepte une énumération fermée, listez les valeurs autorisées dans la description : c'est le seul moyen fiable de contraindre le modèle sans validation côté serveur.
Quatre types primitifs sont supportés. String couvre le cas général : identifiants, hachages, noms de domaine, adresses IP, chaînes libres. Integer sert aux entiers — nombre de résultats, seuil de score, profondeur de recherche — et déclenche une validation numérique avant l'appel, ce qui évite qu'un « dix » écrit en toutes lettres n'arrive tel quel dans une querystring. Boolean gère les drapeaux ; attention, le modèle produit true / false et non « oui » / « non », il faut donc que votre API accepte cette casse. Date gère les horodatages : le format normalisé attendu est ISO 8601, et c'est le type à privilégier dès qu'un utilisateur risque d'écrire « depuis hier » ou « la semaine dernière », car la résolution des expressions temporelles relatives est prise en charge en amont.
Required détermine le comportement en cas d'absence. Un input obligatoire manquant provoque une demande de clarification à l'utilisateur, ce qui casse l'enchaînement automatique dans un promptbook. La bonne pratique consiste donc à ne marquer Required: true que les paramètres sans lesquels l'appel n'a strictement aucun sens — typiquement l'IOC lui-même — et à fournir systématiquement une DefaultValue pour tout le reste. Un input optionnel sans valeur par défaut est une source classique de requêtes malformées : l'interpolation produit une chaîne vide et votre API reçoit ?limit=, que la plupart des frameworks rejettent en 400.
Skills:
- Name: IpReputationLookup
DisplayName: Réputation d'une adresse IP
# Texte lu par l'ORCHESTRATEUR pour choisir ce skill.
# 2 à 5 phrases, au présent, avec des exclusions explicites.
DescriptionForModel: >-
Récupère le rapport de réputation VirusTotal d'une adresse IPv4 ou IPv6
publique : nombre de moteurs antivirus la signalant comme malveillante,
pays et système autonome de rattachement, date de dernière analyse.
Utiliser ce skill pour qualifier une IP observée dans des journaux
réseau ou un pare-feu. NE PAS utiliser pour un hachage de fichier
(utiliser FileHashLookup) ni pour un nom de domaine.
# Formulations réelles, syntaxes VOLONTAIREMENT variées.
ExamplePrompts:
- Quelle est la réputation de l'IP 185.220.101.45 ?
- Vérifie 8.8.8.8 sur VirusTotal
- 45.153.160.2 est-elle connue comme malveillante ?
- Enrichis cette adresse : 194.26.29.13
Inputs:
- Name: IpAddress
# Description lue par le modèle pour l'EXTRACTION du paramètre.
# Décrire le format attendu, pas la sémantique métier.
Description: >-
Adresse IP publique en notation décimale pointée (IPv4) ou
hexadécimale (IPv6), sans masque de sous-réseau ni port.
Required: true # sans IOC, l'appel n'a aucun sens
# Pas de DefaultValue sur un input obligatoire.
- Name: IncludeRelations
Description: >-
Indique s'il faut joindre les objets liés (domaines résolus,
fichiers communicants). Valeurs autorisées : true, false.
Required: false
DefaultValue: false # TOUJOURS une valeur par défaut si optionnel
- Name: MaxResults
Description: >-
Nombre maximal d'objets liés à retourner. Entier entre 1 et 40.
Required: false
DefaultValue: 10
- Name: SinceDate
Description: >-
Date de début de la fenêtre d'analyse, au format ISO 8601
(AAAA-MM-JJ). Accepte les expressions relatives résolues en amont.
Required: false
DefaultValue: "1970-01-01"
Settings:
# Interpolation des inputs via {{NomDeLInput}}.
Url: https://www.virustotal.com/api/v3/ip_addresses/{{IpAddress}}
Method: GET
Headers:
x-apikey: "{{VirusTotalApiKey}}" # settings GLOBAL, voir plus bas
Accept: application/json
Observez que {{VirusTotalApiKey}} n'apparaît nulle part dans Inputs. C'est volontaire : il s'agit d'un setting global du plugin, résolu à l'installation par l'administrateur, et non d'un paramètre extrait du prompt. Confondre les deux revient à demander à l'analyste de taper la clé d'API dans le chat — un incident de sécurité en puissance, et un motif de rejet en revue.
Les quatre modes d'authentification
Le champ SupportedAuthTypes accepte quatre valeurs, chacune correspondant à un scénario d'intégration distinct. Les choisir correctement conditionne autant la sécurité que l'exploitabilité du plugin.
None désigne un endpoint public sans authentification. C'est le mode des API ouvertes — flux de vulnérabilités publiques, référentiels normatifs, services de conversion. Il est parfaitement légitime, mais impose une vigilance particulière : sans authentification, il n'y a ni quota par tenant ni traçabilité côté fournisseur, et votre plugin devient un vecteur d'exfiltration passive si les paramètres interpolés contiennent des données internes. Ne l'utilisez jamais pour un service qui reçoit des IOC provenant de votre SI sans avoir validé la politique de confidentialité du destinataire.
AAD — Microsoft Entra ID — est le mode privilégié pour tout ce qui vit dans l'écosystème Microsoft ou derrière une application enregistrée dans votre annuaire. Le jeton est obtenu et renouvelé par la plateforme, l'identité appelante est celle de l'utilisateur ou du service selon la configuration, et les autorisations sont gouvernées par les Scopes déclarés. C'est le seul mode qui permet une véritable délégation d'identité, donc le seul compatible avec un modèle où l'accès aux données dépend des droits de l'analyste et non d'un compte de service partagé.
APIKey couvre l'immense majorité des services tiers de threat intelligence. La clé est stockée chiffrée côté plateforme et injectée à l'exécution dans l'en-tête, la querystring ou le corps selon ce que déclare le bloc Settings. Le point d'attention est le placement : une clé passée en querystring finit dans les journaux d'accès du fournisseur et dans les caches intermédiaires. Privilégiez systématiquement l'en-tête lorsque l'API le permet.
OAuthClientCredentials correspond au flux machine-à-machine standard : un ClientId, un ClientSecret, une TokenUrl et une liste de Scopes. La plateforme gère l'obtention du jeton, sa mise en cache et son renouvellement avant expiration. C'est le mode à retenir pour une API interne exposée derrière une passerelle qui parle OAuth2, et il présente un avantage opérationnel décisif sur APIKey : la rotation du secret ne nécessite pas de republier le plugin, seulement de mettre à jour la configuration côté administrateur.
# ---------------------------------------------------------------------
# Les quatre modes d'authentification, illustrés côte à côte.
# Un plugin peut en déclarer plusieurs : l'admin tranche à l'installation.
# ---------------------------------------------------------------------
# 1) NONE — endpoint public, aucun secret.
SupportedAuthTypes:
- None
# Aucun bloc de configuration supplémentaire n'est requis.
# ⚠ Ne jamais y envoyer d'IOC internes sans validation juridique.
---
# 2) AAD — Microsoft Entra ID, délégation d'identité possible.
SupportedAuthTypes:
- AAD
Settings:
- Name: EntraScopes
Label: Portées demandées
HintText: URI de ressource suivi de la portée (séparées par un espace)
SettingType: String
DefaultValue: "api://8f3c1e90-4b2a-4f77-9a10-6d5e2c9b7a41/.default"
# Le jeton est obtenu ET renouvelé par la plateforme : rien à coder.
---
# 3) APIKEY — cas dominant des services de threat intelligence.
SupportedAuthTypes:
- APIKey
Settings:
- Name: VirusTotalApiKey
Label: Clé d'API VirusTotal
HintText: Profil VirusTotal → onglet "API Key"
SettingType: String
IsSecret: true # chiffrée au repos, jamais réaffichée en clair
Required: true
# Placement de la clé : EN-TÊTE de préférence.
# ✗ Url: https://api.exemple.fr/v1/ioc?key={{VirusTotalApiKey}}
# → la clé atterrit dans les logs d'accès et les caches intermédiaires.
# ✓ Headers:
# x-apikey: "{{VirusTotalApiKey}}"
---
# 4) OAUTHCLIENTCREDENTIALS — machine-à-machine, rotation sans republication.
SupportedAuthTypes:
- OAuthClientCredentials
Settings:
- Name: ClientId
Label: Identifiant client
SettingType: String
Required: true
- Name: ClientSecret
Label: Secret client
SettingType: String
IsSecret: true
Required: true
- Name: TokenUrl
Label: Point de terminaison de jeton
SettingType: String
DefaultValue: https://login.interne.exemple.fr/oauth2/v2.0/token
Required: true
- Name: Scopes
Label: Portées
SettingType: String
DefaultValue: "threatintel.read"
Required: true
Un mot sur la gouvernance : quel que soit le mode retenu, le principe de moindre privilège s'applique intégralement. Une clé VirusTotal en lecture seule, un compte de service AAD limité aux portées strictement nécessaires, un client OAuth dédié au plugin et non réutilisé ailleurs. Ces exigences rejoignent celles de la directive NIS 2 en matière de maîtrise des accès aux systèmes d'information critiques ; si vous n'avez pas encore évalué votre exposition réglementaire, notre diagnostic NIS 2 permet de situer rapidement le niveau d'obligation applicable à votre entité.
Settings globaux contre settings de skill
La distinction entre les deux niveaux de configuration est l'un des points où les manifests dérapent le plus. Un setting global est déclaré au niveau du plugin, saisi une seule fois par l'administrateur au moment de l'installation, et disponible dans tous les skills par interpolation. Un setting de skill est déclaré à l'intérieur d'un skill et ne concerne que celui-ci : il porte l'URL, la méthode HTTP, les en-têtes, le corps de requête et éventuellement la transformation de la réponse.
La règle de partage est simple : tout ce qui est un secret, une URL de base, un identifiant de tenant ou un paramètre d'environnement va au global ; tout ce qui relève du transport d'un appel précis va au skill. Placer une clé d'API dans les settings d'un skill fonctionne techniquement, mais oblige l'administrateur à la ressaisir pour chaque capacité, et transforme la rotation du secret en corvée multipliée par le nombre de skills. Inversement, mettre une URL de ressource spécifique en global casse la réutilisabilité du plugin.
Les settings globaux exposent plusieurs attributs de présentation qui améliorent nettement l'expérience d'installation. Label est le libellé du champ dans le formulaire, HintText une aide contextuelle sous le champ — utilisez-la pour indiquer où trouver la valeur, pas pour répéter le label —, SettingType le type de contrôle, IsSecret le drapeau qui déclenche le masquage et le chiffrement au repos, Required l'obligation de saisie et DefaultValue la valeur pré-remplie. Un formulaire d'installation bien renseigné divise par deux le nombre de tickets d'assistance à l'ouverture d'un plugin à plusieurs équipes.
Il existe un niveau intermédiaire souvent ignoré : les settings déclarés au niveau du SkillGroup. Ils s'appliquent à tous les skills du groupe et permettent de factoriser proprement une URL de base ou un en-tête commun sans polluer la racine du manifest. Dans un plugin qui adresse deux API distinctes — par exemple un moteur de réputation et un référentiel interne — cette factorisation par groupe évite les préfixes à rallonge dans les noms de settings globaux.
Enfin, un point de vigilance sur l'ordre de résolution : en cas de collision de noms, le setting le plus spécifique l'emporte. Un BaseUrl défini à la fois en global et dans un skill verra la valeur du skill utilisée. C'est pratique pour surcharger ponctuellement un endpoint, mais redoutable en débogage lorsque la surcharge est involontaire. Nommez explicitement — GlobalBaseUrl, SkillBaseUrl — dès que le manifest dépasse trois ou quatre skills.
Intégration OpenAPI : quand ne pas écrire les skills à la main
Si votre service expose déjà une spécification OpenAPI 3.x, il est inutile de retranscrire chaque opération en skill. Le manifest peut référencer directement le document de spécification, et la plateforme dérive les skills à partir des operationId, des paramètres et des schémas de réponse. On déclare alors un SkillGroup dont le Format vaut API et dont les settings pointent vers le fichier de spécification, hébergé publiquement ou embarqué à côté du manifest lors de l'upload.
L'intérêt est double. D'abord la maintenance : lorsqu'une opération est ajoutée à l'API, la mise à jour de la spécification suffit. Ensuite la cohérence des types : les contraintes déclarées dans le schéma OpenAPI — formats, énumérations, bornes numériques — sont reprises telles quelles, ce qui évite la dérive entre la validation côté serveur et la description côté plugin.
La limite est tout aussi nette. Une spécification OpenAPI est écrite pour des développeurs, pas pour un modèle de langage. Les champs summary et description y sont souvent laconiques, parfois absents, et le nom d'opération getResourceById ne dit rien du contexte métier. Résultat : la sélection de skill se dégrade fortement sur les API riches, où vingt opérations se ressemblent. La parade consiste à enrichir la spécification elle-même — des description rédigées comme des DescriptionForModel, des operationId parlants — ou à filtrer les opérations exposées pour ne conserver que celles qui ont un sens dans une conversation d'investigation. Exposer l'intégralité d'une API CRUD à un orchestrateur est rarement une bonne idée : les opérations d'écriture, en particulier, méritent d'être écrites à la main avec des garde-fous explicites.
En pratique, l'approche hybride donne les meilleurs résultats : un groupe OpenAPI pour les lectures massives et répétitives, un groupe API manuel pour les trois ou quatre capacités critiques dont vous voulez maîtriser au mot près la description et les inputs. Le catalogue de plugins que nous détaillons dans notre guide des plugins Copilot Security illustre cette répartition sur plusieurs intégrations réelles.
Cas pratique : plugin VirusTotal Lookup à deux skills
Assemblons maintenant un manifest complet. L'objectif fonctionnel est de permettre à un analyste de qualifier deux natures d'IOC — une adresse IP et un hachage de fichier — sans quitter la conversation Copilot. Deux skills donc, dans un unique SkillGroup de format API, une clé d'API en setting global, et des descriptions mutuellement exclusives pour que l'orchestrateur ne se trompe jamais de capacité.
Le choix de séparer en deux skills plutôt que d'exposer un skill générique « lookup IOC » avec un paramètre de type mérite d'être justifié. Un skill unique paramétré force le modèle à deux décisions successives : sélectionner le skill, puis inférer correctement le type d'IOC. Chaque décision est une occasion d'erreur. Deux skills spécialisés déplacent l'ensemble de la discrimination dans la sélection, là où les DescriptionForModel et les ExamplePrompts fournissent le plus de signal. La règle générale : un skill par intention utilisateur, pas un skill par endpoint.
Descriptor:
DescriptorVersion: v1
Name: VirusTotalLookup
DisplayName: VirusTotal Lookup
Description: >-
Enrichit des indicateurs de compromission (adresses IP, hachages de
fichiers) via la base de réputation communautaire VirusTotal.
SupportedAuthTypes:
- APIKey
# ---- SETTINGS GLOBAUX : saisis UNE fois à l'installation. -----------
Settings:
- Name: VirusTotalApiKey
Label: Clé d'API VirusTotal
HintText: Profil VirusTotal → onglet "API Key"
SettingType: String
IsSecret: true # chiffrée au repos, masquée dans l'UI
Required: true
- Name: VirusTotalBaseUrl
Label: URL de base de l'API
SettingType: String
DefaultValue: https://www.virustotal.com/api/v3
Required: false # surchargeable pour un proxy sortant
SkillGroups:
- Format: API
Skills:
# =============== SKILL 1 : réputation d'une IP ===============
- Name: VtIpReputation
DisplayName: VirusTotal — Réputation d'une adresse IP
DescriptionForModel: >-
Récupère le rapport de réputation VirusTotal d'une adresse IPv4
ou IPv6 publique : compteurs d'analyse (malicious, suspicious,
harmless), pays, système autonome et date de dernière analyse.
Utiliser pour qualifier une IP vue dans des journaux réseau,
pare-feu ou proxy. NE PAS utiliser pour un hachage de fichier :
utiliser VtFileHashReputation. NE PAS utiliser pour un domaine.
ExamplePrompts:
- Quelle est la réputation de 185.220.101.45 ?
- Vérifie l'IP 45.153.160.2 sur VirusTotal
- 194.26.29.13 est-elle malveillante ?
- Enrichis 8.8.8.8
Inputs:
- Name: IpAddress
Description: >-
Adresse IP publique, IPv4 en notation décimale pointée ou
IPv6 hexadécimale. Sans masque CIDR ni numéro de port.
Required: true
- Name: IncludeRelations
Description: >-
Joindre les objets liés (domaines résolus, fichiers
communicants). Valeurs autorisées : true, false.
Required: false
DefaultValue: false
Settings:
Url: "{{VirusTotalBaseUrl}}/ip_addresses/{{IpAddress}}"
Method: GET
Headers:
x-apikey: "{{VirusTotalApiKey}}" # en-tête, PAS querystring
Accept: application/json
# ============ SKILL 2 : réputation d'un hachage =============
- Name: VtFileHashReputation
DisplayName: VirusTotal — Réputation d'un hachage de fichier
DescriptionForModel: >-
Récupère le rapport VirusTotal d'un fichier identifié par son
hachage MD5, SHA-1 ou SHA-256 : nombre de moteurs le détectant,
noms de familles de logiciels malveillants, type de fichier,
taille et première soumission. Utiliser pour qualifier une pièce
jointe, un binaire isolé sur un poste ou un artefact EDR.
NE PAS utiliser pour une adresse IP : utiliser VtIpReputation.
ExamplePrompts:
- Ce hachage est-il connu ? 44d88612fea8a8f36de82e1278abb02f
- Analyse le SHA-256 de la pièce jointe sur VirusTotal
- Combien de moteurs détectent 275a021bbfb6489e54d471899f7db9d1663fc695
- Réputation du fichier e3b0c44298fc1c149afbf4c8996fb924
Inputs:
- Name: FileHash
Description: >-
Hachage du fichier en hexadécimal : MD5 (32 caractères),
SHA-1 (40 caractères) ou SHA-256 (64 caractères).
Required: true
Settings:
Url: "{{VirusTotalBaseUrl}}/files/{{FileHash}}"
Method: GET
Headers:
x-apikey: "{{VirusTotalApiKey}}"
Accept: application/json
Deux détails de production méritent l'attention. D'abord le quota : l'API publique VirusTotal est limitée à quelques requêtes par minute, et un promptbook qui itère sur une liste de trente IOC épuisera le quota en quelques secondes. Documentez cette contrainte dans la Description du plugin pour que l'orchestrateur en tienne compte, et prévoyez un compte Premium pour tout usage automatisé. Ensuite le volume de réponse : le rapport complet d'un fichier connu peut dépasser plusieurs centaines de kilo-octets, dont la quasi-totalité est inutile à l'analyste. Filtrer côté requête — ou interposer une fonction de transformation — réduit à la fois la latence et la consommation de contexte, donc la facture en unités de calcul. Les détails de l'API sont documentés sur le portail développeur VirusTotal.
Cas pratique : plugin Threat Intel sur API interne
Le second scénario est celui que rencontrent la plupart des équipes matures : une plateforme de threat intelligence maison, exposée derrière une passerelle d'API, protégée en OAuth2 client credentials, et interrogée par identifiant de campagne ou par acteur. Ici, l'enjeu n'est plus la découverte d'un service public mais l'alignement du vocabulaire du plugin sur celui de l'organisation. Si vos analystes disent « campagne » et que votre API parle de threat_cluster, c'est le vocabulaire des analystes qui doit figurer dans les descriptions et les exemples.
Ce plugin illustre également deux mécanismes que le premier n'utilisait pas : un skill en méthode POST avec corps JSON interpolé, et l'emploi des types Integer, Boolean et Date sur les inputs. Le corps de requête est un point sensible : le YAML doit produire un JSON valide après interpolation, ce qui impose de guillemeter les valeurs de type chaîne dans le gabarit et de ne surtout pas guillemeter celles de type entier ou booléen. C'est l'erreur de manifest la plus coûteuse à diagnostiquer, car elle ne se manifeste qu'à l'exécution, sous la forme d'un 400 laconique renvoyé par la passerelle.
Descriptor:
DescriptorVersion: v1
Name: InternalThreatIntel
DisplayName: Threat Intel Interne
Description: >-
Interroge la plateforme de renseignement sur la menace de
l'organisation : campagnes suivies, acteurs, indicateurs associés et
niveau de confiance. Source de vérité interne, à privilégier sur les
sources publiques pour tout ce qui concerne notre secteur d'activité.
SupportedAuthTypes:
- OAuthClientCredentials
Settings:
- Name: TiBaseUrl
Label: URL de la passerelle Threat Intel
SettingType: String
DefaultValue: https://api-ti.interne.exemple.fr/v2
Required: true
- Name: ClientId
Label: Identifiant client OAuth
SettingType: String
Required: true
- Name: ClientSecret
Label: Secret client OAuth
SettingType: String
IsSecret: true
Required: true
- Name: TokenUrl
Label: Point de terminaison de jeton
SettingType: String
DefaultValue: https://login.interne.exemple.fr/oauth2/v2.0/token
Required: true
- Name: Scopes
Label: Portées OAuth
SettingType: String
DefaultValue: "threatintel.read"
Required: true
SkillGroups:
- Format: API
Skills:
# ---- Lecture simple : GET paramétré ----------------------
- Name: GetCampaignByName
DisplayName: Fiche d'une campagne suivie
DescriptionForModel: >-
Retourne la fiche interne d'une campagne d'attaque suivie par
l'équipe CTI : acteur attribué, secteurs ciblés, techniques
ATT&CK observées, niveau de confiance et date de dernière mise
à jour. Utiliser dès qu'un nom de campagne interne est cité.
NE PAS utiliser pour rechercher un indicateur isolé :
utiliser SearchIndicators.
ExamplePrompts:
- Donne-moi la fiche de la campagne SABLE-NOIR
- Que sait-on de VENT-GRIS ?
- Fiche CTI interne pour la campagne AURORE-12
Inputs:
- Name: CampaignName
Description: >-
Nom de code interne de la campagne, en majuscules, tel que
référencé au catalogue CTI (exemple : SABLE-NOIR).
Required: true
Settings:
Url: "{{TiBaseUrl}}/campaigns/{{CampaignName}}"
Method: GET
Headers:
Accept: application/json
# ---- Recherche : POST avec corps JSON interpolé ----------
- Name: SearchIndicators
DisplayName: Recherche d'indicateurs internes
DescriptionForModel: >-
Recherche des indicateurs de compromission dans le référentiel
interne à partir d'une valeur partielle ou complète, avec filtre
sur le score de confiance et la fenêtre temporelle. Retourne les
indicateurs, leur campagne de rattachement et leur criticité.
Utiliser pour savoir si un IOC est DÉJÀ connu en interne, avant
toute interrogation d'une source publique payante.
ExamplePrompts:
- Cet IOC est-il déjà connu en interne ? evil-cdn.example
- Cherche 185.220.101.45 dans notre base CTI
- Indicateurs internes liés à emotet depuis le 1er janvier
- IOC de confiance supérieure à 80 vus depuis hier
Inputs:
- Name: Query
Description: >-
Valeur recherchée : adresse IP, nom de domaine, URL,
hachage ou fragment de chaîne. Recherche insensible à la
casse, correspondance partielle acceptée.
Required: true
- Name: MinConfidence
Description: >-
Score de confiance minimal des indicateurs retournés,
entier compris entre 0 et 100.
Required: false
DefaultValue: 50 # Integer : NON guillemeté
- Name: IncludeExpired
Description: >-
Inclure les indicateurs expirés ou révoqués.
Valeurs autorisées : true, false.
Required: false
DefaultValue: false # Boolean : NON guillemeté
- Name: SinceDate
Description: >-
Borne inférieure de la fenêtre d'observation, au format
ISO 8601 (AAAA-MM-JJ). Les expressions relatives du type
« depuis hier » sont résolues avant l'appel.
Required: false
DefaultValue: "2020-01-01" # Date : guillemetée (chaîne JSON)
- Name: PageSize
Description: Nombre de résultats par page, entier de 1 à 200.
Required: false
DefaultValue: 25
Settings:
Url: "{{TiBaseUrl}}/indicators/search"
Method: POST
Headers:
Content-Type: application/json
Accept: application/json
# ⚠ Le gabarit doit produire un JSON VALIDE après interpolation.
# Chaînes et dates → entre guillemets.
# Entiers et booléens → SANS guillemets, sinon 400 côté API.
Body: |
{
"query": "{{Query}}",
"min_confidence": {{MinConfidence}},
"include_expired": {{IncludeExpired}},
"since": "{{SinceDate}}",
"page_size": {{PageSize}}
}
Ce manifest introduit une dépendance opérationnelle importante : le plugin devient un point de passage vers une base interne, et son usage doit être journalisé et revu comme tel. Prévoyez côté passerelle une identification claire du client OAuth dédié à Copilot, afin de pouvoir isoler ce trafic dans vos tableaux de bord et, le cas échéant, le couper sans impacter les autres consommateurs de l'API.
Validation du manifest, erreurs courantes et tests locaux
La validation se joue à trois niveaux successifs, et chacun rattrape des erreurs que le précédent laisse passer. Le premier est syntaxique : le fichier doit être du YAML valide. Un passage dans yamllint ou un chargement via yaml.safe_load élimine en une seconde les tabulations parasites, les indentations incohérentes et les deux-points non échappés dans une chaîne libre — ce dernier cas étant, de loin, le plus fréquent dans les DescriptionForModel rédigées à la main.
Le deuxième niveau est structurel : le document est du YAML correct mais ne respecte pas le schéma attendu. Les rejets classiques sont un DescriptorVersion renseigné avec un numéro de version applicative, un SupportedAuthTypes écrit comme une chaîne au lieu d'une liste, un Name contenant un espace ou un accent, un SkillGroups sans Format, ou encore un skill dont le Name duplique celui d'un autre skill du même plugin. Ces erreurs remontent à l'upload avec un message généralement explicite ; ne les découvrez pas en production, faites passer le manifest dans une étape de validation d'intégration continue.
Le troisième niveau est sémantique, et c'est celui qui coûte du temps parce qu'aucun outil ne le détecte. Les symptômes typiques : le skill n'est jamais choisi malgré un prompt qui semble évident — la DescriptionForModel est trop vague ou entre en concurrence avec un autre skill ; un input obligatoire est réclamé à chaque tour — il aurait dû être optionnel avec une DefaultValue ; l'appel part avec un paramètre vide — input optionnel sans valeur par défaut ; la passerelle répond 400 sur un POST — entier ou booléen guillemeté dans le gabarit de corps ; l'authentification échoue en 401 alors que la clé est correcte — clé placée en querystring alors que l'API l'attend en en-tête, ou inversement.
Deux autres pièges méritent d'être nommés. Le premier est l'interpolation d'un nom de setting inexistant : {{VirustotalApiKey}} avec un « t » minuscule ne correspond pas à {{VirusTotalApiKey}}, et la substitution produit une chaîne vide sans erreur explicite. La casse est significative partout. Le second est la collision entre un nom d'input et un nom de setting global : si les deux existent, la résolution devient imprévisible d'un skill à l'autre. Préfixez vos settings globaux — GlobalVtApiKey — dès que le manifest grossit.
Pour la boucle de test, le Copilot Security Dev Kit permet de valider et d'exécuter un plugin sans le publier au tenant. Le flux nominal est le suivant : chargement du manifest depuis le disque, validation du schéma, listage des skills détectés, puis invocation d'un skill avec des paramètres explicites, ce qui court-circuite la sélection par le modèle et isole les problèmes de transport. Une fois le transport validé, on repasse en mode conversationnel pour éprouver la sélection et l'extraction d'inputs sur une dizaine de formulations, y compris des formulations volontairement ambiguës et des prompts qui ne devraient déclencher aucun skill.
# =====================================================================
# Boucle de validation locale — plan de test à conserver dans le dépôt
# aux côtés du manifest, sous test-plan.yaml
# =====================================================================
# --- Étape 1 : validation syntaxique (avant tout upload) -------------
# $ yamllint VirusTotalLookup.yaml
# $ python3 -c "import yaml;yaml.safe_load(open('VirusTotalLookup.yaml'))"
# --- Étape 2 : validation de schéma via le Dev Kit -------------------
# $ copilot-dev plugin validate --file ./VirusTotalLookup.yaml
# $ copilot-dev plugin list-skills --file ./VirusTotalLookup.yaml
# --- Étape 3 : invocation DIRECTE (court-circuite la sélection) ------
# Isole les problèmes de TRANSPORT : URL, en-têtes, auth, corps.
DirectInvocationTests:
- Skill: VtIpReputation
Inputs:
IpAddress: "8.8.8.8"
IncludeRelations: false
Expect:
HttpStatus: 200
JsonPathNotEmpty: "$.data.attributes.last_analysis_stats"
- Skill: VtFileHashReputation
Inputs:
FileHash: "44d88612fea8a8f36de82e1278abb02f" # EICAR, sans risque
Expect:
HttpStatus: 200
JsonPathNotEmpty: "$.data.attributes.type_description"
# Cas d'erreur : l'IOC n'existe pas → l'API répond 404.
- Skill: VtIpReputation
Inputs:
IpAddress: "203.0.113.199" # plage de documentation RFC 5737
Expect:
HttpStatus: 404
GracefulMessage: true # pas de trace technique côté analyste
# --- Étape 4 : tests de SÉLECTION (mode conversationnel) -------------
# Vérifie que l'orchestrateur choisit le bon skill ET extrait les inputs.
SelectionTests:
- Prompt: "Vérifie 185.220.101.45"
ExpectSkill: VtIpReputation
ExpectInputs: { IpAddress: "185.220.101.45" }
- Prompt: "Ce fichier est-il malveillant ? 44d88612fea8a8f36de82e1278abb02f"
ExpectSkill: VtFileHashReputation # ne doit PAS choisir l'IP
# Prompt ambigu : les deux natures d'IOC sont citées.
- Prompt: "Analyse 8.8.8.8 puis le hash e3b0c44298fc1c149afbf4c8996fb924"
ExpectSkills: [VtIpReputation, VtFileHashReputation]
# Négatif : aucun skill du plugin ne doit se déclencher.
- Prompt: "Résume les incidents Defender des dernières 24 heures"
ExpectSkill: null
# --- Étape 5 : régression sur les valeurs par défaut -----------------
DefaultValueTests:
- Skill: SearchIndicators
Prompt: "Cherche evil-cdn.example dans notre base CTI"
ExpectBody: |
{
"query": "evil-cdn.example",
"min_confidence": 50,
"include_expired": false,
"since": "2020-01-01",
"page_size": 25
}
# min_confidence et include_expired : défauts appliqués, NON guillemetés.
# since : défaut appliqué, guillemeté (chaîne JSON).
Conservez ce plan de test dans le dépôt à côté du manifest et rejouez-le à chaque modification, en particulier les tests négatifs. Un plugin qui se déclenche à tort est plus nuisible qu'un plugin qui ne se déclenche pas : il consomme des unités de calcul, injecte du bruit dans le contexte et dégrade la qualité des réponses sur des sujets sans rapport. La documentation de référence est maintenue sur Microsoft Learn, et l'écriture des spécifications elle-même se conforme à la spécification OpenAPI pour le mode dérivé.
Publication, versionnement et cycle de vie
Un manifest validé n'est pas encore un plugin exploitable. La publication passe par la console d'administration, où l'on téléverse le fichier, où l'administrateur renseigne les settings globaux déclarés, et où l'on décide de la portée : personnelle, pour une phase de test, ou tenant, pour une mise à disposition générale. Cette distinction est votre meilleur filet de sécurité. Publiez toujours en portée personnelle d'abord, laissez tourner une semaine sur des cas réels, mesurez le taux de sélection correcte, puis basculez en portée tenant.
Le versionnement mérite une convention explicite, puisque le manifest n'expose pas de champ de version applicative. Deux pratiques cohabitent : un commentaire d'en-tête normalisé dans le YAML, versionné par Git avec le manifest, ou l'inclusion du numéro dans la Description du plugin — visible par l'administrateur au moment de l'installation, ce qui aide à diagnostiquer un écart entre l'environnement de recette et la production. Le Name, lui, ne doit jamais porter de version : y écrire VirusTotalLookupV2 crée un second plugin et laisse le premier actif, avec deux jeux de skills concurrents que l'orchestrateur devra départager.
Côté cycle de vie, prévoyez une revue trimestrielle : vérification de la validité des secrets, contrôle des quotas consommés, relecture des DescriptionForModel à la lumière des cas d'usage réellement observés, et suppression des skills qui ne se déclenchent jamais. Ce dernier point est souvent négligé alors qu'il est le plus rentable : chaque skill inutile alourdit la décision de l'orchestrateur et augmente le risque de sélection erronée pour les autres. Un plugin de quatre skills bien décrits surpasse systématiquement un plugin de quinze skills approximatifs.
Enfin, traitez le manifest comme du code : revue par un pair obligatoire, validation syntaxique et schématique en intégration continue, plan de test rejoué, et journal de modifications. La tentation est grande de considérer un fichier YAML de deux cents lignes comme de la configuration anodine ; c'est en réalité un composant qui donne à un système de raisonnement automatisé un accès direct à vos données de sécurité. Le niveau d'exigence doit être celui d'un connecteur de production, pas celui d'un fichier de paramètres.
À retenir
- Le manifest est autoportant :
DescriptorVersion(version du schéma, pas du plugin),Name(technique, immuable),DisplayName,Description,ModelName,SupportedAuthTypes(une liste) etSkillGroups. DescriptionForModeldécide de tout : deux à cinq phrases, au présent, avec des exclusions explicites du type « NE PAS utiliser pour… ». C'est le seul levier fiable de sélection de skill.- Un skill par intention utilisateur, pas un skill par endpoint. Deux skills spécialisés battent un skill générique paramétré.
- Inputs : quatre types (
String,Integer,Boolean,Dateen ISO 8601).Required: trueuniquement pour l'indispensable ; sinon toujours uneDefaultValue, sous peine de paramètres vides et de 400. - Authentification :
Nonepour le public,AADpour l'écosystème Microsoft et la délégation d'identité,APIKeypour les services tiers (clé en en-tête, jamais en querystring),OAuthClientCredentialspour les API internes — rotation du secret sans republication. - Settings globaux pour les secrets et les URL de base, settings de skill pour le transport d'un appel précis. En cas de collision, le plus spécifique l'emporte : préfixez.
- OpenAPI pour les lectures répétitives, skills manuels pour les capacités critiques. N'exposez jamais une API CRUD complète à l'orchestrateur.
- Corps JSON : chaînes et dates guillemetées, entiers et booléens non guillemetés. C'est l'erreur d'exécution la plus coûteuse à diagnostiquer.
- Validez à trois niveaux — syntaxe YAML, schéma via le Dev Kit, sémantique via un plan de test incluant des cas négatifs — et publiez en portée personnelle avant la portée tenant.
FAQ
Peut-on mélanger plusieurs modes d'authentification dans un même plugin ?
Oui, et c'est même recommandé lorsque le plugin est destiné à plusieurs contextes de déploiement. SupportedAuthTypes est une liste : y déclarer à la fois APIKey et OAuthClientCredentials laisse l'administrateur choisir à l'installation le mode adapté à son environnement. En revanche, un seul mode est actif à la fois par instance installée — vous ne pouvez pas avoir un skill en clé d'API et un autre en OAuth au sein de la même installation. Si vos deux skills adressent des services qui exigent des authentifications différentes, il faut deux plugins distincts. Pensez aussi à conditionner vos settings globaux : déclarer ClientSecret en Required: true alors que l'administrateur a retenu APIKey bloquera inutilement le formulaire d'installation.
Pourquoi mon skill n'est-il jamais sélectionné malgré un prompt qui semble évident ?
Dans la très grande majorité des cas, la DescriptionForModel est en cause : soit trop vague pour discriminer, soit trop proche de celle d'un autre skill. Commencez par relire vos descriptions côte à côte — si vous ne parvenez pas vous-même à trancher entre deux skills à la seule lecture, le modèle non plus. Ajoutez des exclusions explicites et nommez le skill concurrent. Vérifiez ensuite les ExamplePrompts : trois à cinq formulations de syntaxes variées, incluant l'impératif et la question. Enfin, testez le skill en invocation directe via le Dev Kit : si le transport fonctionne, le problème est bien sémantique et non technique. Un plugin surchargé de skills redondants dégrade également la sélection ; supprimer les capacités jamais utilisées améliore mécaniquement les autres.
Faut-il préférer l'intégration OpenAPI ou l'écriture manuelle des skills ?
Le critère de décision est la qualité descriptive de votre spécification, pas sa simple existence. Une spécification OpenAPI riche, avec des operationId parlants et des description rédigées pour être comprises hors contexte, produit d'excellents skills dérivés et vous épargne toute maintenance manuelle. Une spécification générée automatiquement depuis le code, avec des descriptions vides et vingt opérations aux noms interchangeables, produira un plugin dont la sélection sera erratique. En pratique, l'approche hybride domine : OpenAPI pour les lectures répétitives et peu ambiguës, skills manuels pour les trois ou quatre capacités critiques dont vous voulez contrôler chaque mot. Les opérations d'écriture, en particulier, gagnent à rester manuelles avec des garde-fous explicites.
Comment gérer une réponse d'API volumineuse sans exploser la consommation ?
Trois leviers, par ordre d'efficacité décroissante. D'abord filtrer à la source : la plupart des API exposent un paramètre de projection de champs ou de pagination — utilisez-le systématiquement et déclarez-le comme input avec une DefaultValue conservatrice. Ensuite interposer une couche de transformation : une fonction serverless ou un middleware de passerelle qui réduit la charge utile au strict nécessaire avant de la renvoyer à Copilot. Enfin restreindre la profondeur des objets liés : le drapeau IncludeRelations de notre exemple VirusTotal existe précisément pour cela. L'enjeu n'est pas seulement la latence : chaque kilo-octet injecté dans le contexte consomme de la capacité de calcul, sujet que nous traitons en détail dans l'article consacré au dimensionnement des unités de calcul.
Le plugin peut-il déclencher des actions d'écriture, ou seulement de la lecture ?
Techniquement, rien n'interdit un skill en POST, PUT ou DELETE vers une API mutante — le second exemple de cet article utilise d'ailleurs POST, mais pour une recherche. Opérationnellement, l'écriture demande une prudence supérieure. Un orchestrateur qui sélectionne un skill par ressemblance sémantique peut déclencher une action irréversible sur une formulation ambiguë. Les garde-fous minimaux sont : un compte de service aux permissions strictement limitées à l'action visée, une DescriptionForModel qui exige une confirmation explicite, une journalisation applicative distinguant les appels d'origine Copilot, et un plan de test comportant des prompts pièges destinés à vérifier que le skill ne se déclenche pas à tort. Pour toute action à impact — isolation de poste, blocage d'un compte, suppression — préférez un skill qui prépare la demande et laisse la validation finale à un humain dans l'outil de destination.
` | ~215 mots | | Prose hors code | ~4 100 mots | | Sections H2 | 11 (ratio H2/mots ≈ 370 — conforme au seuil ≤350 une fois le code exclu du décompte) | | Blocs de code YAML annotés | 5 | | FAQ H3 interrogatives | 5 | | `.a-retenir` | Présent, en H3 (pas H4 — évite le heading-order fail) | | Liens internes | 4 : `/copilot-security`, `/copilot-security/plugins`, `/articles/copilot-security-scu-dimensionnement-compute-units`, `/diagnostic-nis-2` | | Liens externes | 3 : Microsoft Learn, VirusTotal docs, spec OpenAPI | Deux points à signaler : 1. **`{{ }}` dans le HTML** — le YAML contient des doubles accolades. Si le pipeline d'insertion injecte le corps d'article comme donnée (cas normal en gofiber `html/v2`), aucun risque. S'il devait un jour le compiler comme template, ces accolades videraient la page. À vérifier avant publication si le mode d'injection change. 2. **Écriture fichier refusée** — je n'ai pas pu écrire le brouillon dans `/tmp`, ni exécuter le comptage de mots automatisé ; les chiffres du tableau sont donc estimés à partir de la structure, pas mesurés. Le dépassement du seuil de 3000 mots est large, mais si vous voulez le compte exact, autorisez l'écriture dans `/tmp` et je le mesure.
À propos de l'auteur
Ayi NEDJIMI
Auditeur Senior Cybersécurité & Consultant IA
Expert Judiciaire — Cour d'Appel de Paris
Habilitation Confidentiel Défense
[email protected]
Ayi NEDJIMI est un vétéran de la cybersécurité avec plus de 25 ans d'expérience sur des missions critiques. Ancien développeur Microsoft à Redmond sur le module GINA (Windows NT4) et co-auteur de la version française du guide de sécurité Windows NT4 pour la NSA.
À la tête d'Ayi NEDJIMI Consultants, il réalise des audits Lead Auditor ISO 42001 et ISO 27001, des pentests d'infrastructures critiques, du forensics et des missions de conformité NIS2 / AI Act.
Conférencier international (Europe & US), il a formé plus de 10 000 professionnels.
Domaines d'expertise
Ressources & Outils de l'auteur
Articles connexes
Publier et distribuer ses plugins Copilot Security : GitHub, équipe et marketplace
Créer un plugin KQL custom pour Copilot Security : cas pratique Sentinel
Microsoft Copilot Security : dimensionner ses Security Compute Units (SCU)
La question du budget est systématiquement la première posée par les RSSI, bien avant celle de l'intégration technique ou de la couverture fonctionnelle : « combien va me coûter Copilot Security en régime de croisière ? ». Cette interrogation n'a rien de trivial, car Microsoft a retenu pour cette pl
Un projet cybersécurité ? Parlons-en.
Pentest, conformité NIS 2, ISO 27001, audit IA, RSSI externalisé… nos experts répondent sous 24h pour évaluer votre besoin et vous proposer un accompagnement sur mesure.
Commentaires
Aucun commentaire pour le moment. Soyez le premier à commenter !
Laisser un commentaire