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"Connecteur MCP
Section intitulée « Connecteur MCP »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 } } ]}Signature d’un webhook
Section intitulée « Signature d’un webhook »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 surhorodatage + "." + 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 dusecret sur ``X-TWA-Timestamp + "." + corps``, préfixe ``sha256=``. Le corpsest celui reçu, octets pour octets. Un horodatage écarté de 300 secondes ouplus est refusé. L'envoi ne mémorise pas les signatures déjà parties : unecopie renvoyée dans cette fenêtre se vérifie encore.
Usage : python3 verifier_signature.py SECRET HORODATAGE ENTETE < corpsCode 0 si la signature est bonne, 1 si elle est refusée, 2 si l'usage est faux."""from __future__ import annotations
import hashlibimport hmacimport sysimport 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())Action GitHub
Section intitulée « Action GitHub »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.