Aller au contenu

Exemples

Trois lectures de la même chose : la clé, les organisations, les projets, la stack, et les alertes déclenchées depuis hier. Aucune n’écrit.

La spécification OpenAPI 3.1 de l’API publique se télécharge ici : openapi-v1.json. Elle suit toujours la version en service. La clé d’organisation se crée dans les réglages, onglet API, et s’envoie dans Authorization: Bearer twa_…. Voir Authentification.

curl.sh enchaîne cinq GET. TWA_ORG_ID et TWA_PROJECT_ID sont les identifiants renvoyés par les deux premiers appels. TWA_SINCE, s’il est posé, est un instant ISO 8601 ; sinon le script prend les dernières 24 heures.

#!/usr/bin/env bash
# Lecture seule de l'API publique. Aucune écriture.
# Variables : TWA_API_KEY, TWA_ORG_ID, TWA_PROJECT_ID.
# TWA_SINCE (optionnel) : instant ISO 8601, sinon les dernières 24 heures.
set -euo pipefail
BASE="${TWA_BASE_URL:-https://app.techwatchalert.com}"
auth="Authorization: Bearer ${TWA_API_KEY:?}"
if [[ -z "${TWA_SINCE:-}" ]]; then
TWA_SINCE="$(python3 -c 'from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ"))')"
fi
curl -fsS -H "$auth" "$BASE/api/v1/me"
curl -fsS -H "$auth" "$BASE/api/v1/organizations"
curl -fsS -H "$auth" "$BASE/api/v1/organizations/${TWA_ORG_ID:?}/projects"
curl -fsS -H "$auth" "$BASE/api/v1/organizations/${TWA_ORG_ID}/projects/${TWA_PROJECT_ID:?}/stack"
curl -fsS -G -H "$auth" \
--data-urlencode "since=${TWA_SINCE}" \
--data-urlencode "limit=50" \
"$BASE/api/v1/organizations/${TWA_ORG_ID}/projects/${TWA_PROJECT_ID}/alerts"

Le serveur est https://mcp.techwatchalert.com/mcp. Les appels de mcp.json sont des outils de lecture : organisations, projets, stack, alertes du projet, recherche dans la base CVE. project accepte l’identifiant du projet ou son nom exact.

{
"server": "https://mcp.techwatchalert.com/mcp",
"appels": [
{
"name": "list_organizations",
"arguments": {}
},
{
"name": "list_projects",
"arguments": {}
},
{
"name": "get_project_stack",
"arguments": {
"project": "production"
}
},
{
"name": "list_project_alerts",
"arguments": {
"project": "production",
"sort": "date",
"limit": 20
}
},
{
"name": "search_cves",
"arguments": {
"query": "openssl",
"severities": ["CRITICAL"],
"limit": 5
}
}
]
}

Chaque appel porte deux en-têtes :

  • X-TWA-Timestamp : l’heure d’envoi, en secondes Unix ;
  • X-TWA-Signature : sha256= suivi du HMAC-SHA256 du secret sur horodatage + "." + corps brut.

Le corps est celui reçu, sans le reformater. Un horodatage écarté de cinq minutes (300 secondes) ou plus se refuse. L’envoi ne mémorise pas les signatures déjà livrées : une copie renvoyée dans cette fenêtre se vérifie encore. Le secret est montré une fois, à la création du webhook, et à chaque régénération.

verifier_signature.py recalcule la même valeur que l’envoi. Voir aussi Signature des webhooks.

L’événement cve.exploitee part une seule fois par CVE et par projet. L’appel est signé avec X-TWA-Signature. cvss_score et epss_score valent null quand le score n’est pas publié : ce n’est pas un zéro.

{
"event": "cve.exploitee",
"project_id": "…",
"triggered_at": "2026-10-13T18:00:00+00:00",
"cve_id": "CVE-2026-0001",
"severity": "CRITICAL",
"cvss_score": null,
"epss_score": null,
"has_poc": false,
"is_kev": true,
"rancongiciel": false,
"description": "…",
"matched_type": "product",
"matched_value": "openssl"
}
"""Vérifie X-TWA-Signature sur le corps brut d'un webhook TechWatchAlert.
Même calcul que l'envoi (app.core.outbox.signer_webhook) : HMAC-SHA256 du
secret sur ``X-TWA-Timestamp + "." + corps``, préfixe ``sha256=``. Le corps
est celui reçu, octets pour octets. Un horodatage écarté de 300 secondes ou
plus est refusé. L'envoi ne mémorise pas les signatures déjà parties : une
copie renvoyée dans cette fenêtre se vérifie encore.
Usage : python3 verifier_signature.py SECRET HORODATAGE ENTETE < corps
Code 0 si la signature est bonne, 1 si elle est refusée, 2 si l'usage est faux.
"""
from __future__ import annotations
import hashlib
import hmac
import sys
import time
FENETRE_SECONDES = 300
def signature(secret: str, horodatage: str, corps: bytes) -> str:
mac = hmac.new(secret.encode(), horodatage.encode() + b"." + corps, hashlib.sha256)
return "sha256=" + mac.hexdigest()
def verifier(
secret: str,
horodatage: str,
corps: bytes,
entete: str,
*,
maintenant: float | None = None,
) -> bool:
if not horodatage.isdigit():
return False
instant = time.time() if maintenant is None else maintenant
if abs(instant - int(horodatage)) >= FENETRE_SECONDES:
return False
return hmac.compare_digest(signature(secret, horodatage, corps), entete)
def main() -> int:
if len(sys.argv) != 4:
print(
"usage: verifier_signature.py SECRET HORODATAGE ENTETE < corps",
file=sys.stderr,
)
return 2
secret, horodatage, entete = sys.argv[1:]
corps = sys.stdin.buffer.read()
ok = verifier(secret, horodatage, corps, entete)
print("ok" if ok else "refus")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())

nouvelles-cve-stack.yml est un exemple à copier dans .github/workflows/ de votre dépôt, avec nouvelles_cve_stack.py dans .github/scripts/ et le secret TWA_API_KEY. Il tourne chaque jour, ou à la demande. Le droit du dépôt est contents: read. Le travail n’appelle que les GET des organisations, des projets et des alertes. Il échoue s’il y a au moins une alerte dans la fenêtre, pour que l’exécution planifiée le signale. Rien n’est écrit dans le dépôt, et l’API n’est pas appelée en écriture.

TWA_SINCE borne la fenêtre. TWA_ORG_ID et TWA_PROJECT_ID limitent le parcours ; vides, la clé voit tout ce qu’elle a le droit de lire.

La suite rejoue ces exemples : une route renommée, un outil retiré ou une signature qui ne suit plus l’envoi la fait échouer.