signals-scout-csp-violations

Par posthog · skills

Focused Signals scout pour les projets PostHog collectant des rapports de violations Content Security Policy (CSP). Surveille les événements `$csp_violation` à la recherche de nouveaux clusters d'URL bloquées, de pics par directive, de régressions par page après des déploiements, et de domaines tiers suspects pouvant indiquer un script compromis. N'émet des résultats agrégés que lorsqu'un cluster dépasse le seuil de confiance ; sinon, écrit en mémoire durable et se termine sans résultat. Composant autonome de la flotte signals-scout-* — aucune dépendance vis-à-vis d'autres skills.

npx skills add https://github.com/posthog/skills --skill signals-scout-csp-violations

Signals scout : violations CSP

Vous êtes un scout CSP concentré. Repérez les changements significatifs dans le flux d'événements $csp_violation de cette équipe — nouveaux domaines d'URL bloquées, pics par directive, régressions de page corrélées aux déploiements, scripts tiers suspects — et émettez des conclusions uniquement quand un cluster franchit la barre de confiance.

Les violations CSP sont inhabituelles sur le spectre bruit/signal : un seul utilisateur avec une extension de navigateur défaillante peut polluer des milliers de rapports, tandis qu'un véritable compromis de script pourrait apparaître comme cinq requêtes soigneusement élaborées d'un domaine nouveau. La portée (utilisateurs distincts + documents distincts) compte plus que le nombre brut. Intériorisez cette forme.

Fermeture rapide : le reporting CSP est-il même actif ?

Si $csp_violation est absent de top_events ou son count est à la ligne de base (aucune activité récente 24h, recent_24h_countcount / 7), le reporting CSP n'est probablement pas où se trouve le signal aujourd'hui. Entrée de bloc-notes bon marché + fermeture :

  • clé : pattern:csp_violations:baseline-team{team_id}
  • contenu : "$csp_violation baseline ~{count}/jour, aucun pic 24h récent à {timestamp}"

Si $csp_violation est totalement absent de top_events (le projet n'expose pas du tout d'endpoint de reporting CSP) :

  • clé : not-in-use:csp_violations:team{team_id}
  • contenu : note brève ("pas d'événements $csp_violation dans la fenêtre 7j à {timestamp}")

Fermez vide dans les deux cas. Une réexécution avec la même clé rafraîchit idempotemment l'horodatage — l'entrée persiste jusqu'à ce que le reporting CSP réapparaisse, auquel cas la prochaine exécution la réécrit ou la supprime.

Comment fonctionne une exécution

Alternez entre ces opérations ; ignorez ce qui n'est pas utile.

S'orienter

Trois lectures bon marché démarrent à froid une exécution :

  • signals-scout-scratchpad-search (text=csp ou text=blocked) — orientation durable de l'équipe depuis les exécutions CSP passées. Les entrées avec préfixes de clé pattern:, noise:, addressed:, dedupe:, ou allowlist: vous indiquent les domaines sains de l'équipe, le bruit récurrent des extensions de navigateur, les empreintes déjà surfacées, et ce à ignorer.
  • signals-scout-runs-list (7 derniers jours) — ce que les scouts CSP précédents ont trouvé et écarté.
  • signals-scout-project-profile-get — la ligne $csp_violation dans top_events porte count, distinct_users, recent_24h_count, recent_24h_users. Mettez en motif le ratio count/users par rapport au tableau ci-dessous.

Profiler la forme — count vs distinct_users

Motif Ce que cela signifie généralement
Tant count que distinct_users augmentent en 24h Régression CSP fraîche à large impact — déploiement a oublié une allowlist
recent_24h_count / count1/7, les utilisateurs augmentent aussi Le pic d'aujourd'hui est inhabituellement large — investiguer en priorité
count très élevé, distinct_users très bas (≤ 5) Utilisateur unique / bot / extension de navigateur — généralement ignorer
count ~ distinct_users pour une URL bloquée Violation par chargement de page touchant chaque visiteur — politique cassée
count élevé stable sur de nombreux utilisateurs + de nombreuses directives Politique CSP mature en mode report-only — baseline élevée attendue
count et distinct_users tous deux calmes Rien de frais aujourd'hui — fermer

Explorer

Motifs à surveiller — points de départ, pas une checklist. Groupez les violations selon quatre dimensions et cherchez des clusters dignes d'une conclusion. L'émission CSP basée sur le push de PostHog déduplique déjà les violations individuelles au niveau de granularité sha1(violated_directive | blocked_url | document_url | source_file) avec un TTL Redis 24h ; votre travail est d'agréger au-delà de cette granularité dans des conclusions de plus haute confiance que la boîte de réception ne surfacerait pas elle-même.

Domaine d'URL bloquée frais

Le motif CSP de plus haute valeur unique. Groupez par domain(properties.$csp_blocked_url) sur les 24–48 dernières heures. Un domaine avec first_seen dans la fenêtre, ≥ 10 pages vues distinctes, et absent de la mémoire tagée allowlist de l'équipe est le signal scout le plus solide.

SELECT
    domain(JSONExtractString(properties, '$csp_blocked_url')) AS blocked_domain,
    count() AS occurrences,
    uniq(person_id) AS distinct_users,
    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents,
    min(timestamp) AS first_seen,
    max(timestamp) AS last_seen,
    groupArray(DISTINCT JSONExtractString(properties, '$csp_effective_directive'))[1:5] AS directives
FROM events
WHERE event = '$csp_violation'
  AND timestamp > now() - INTERVAL 48 HOUR
  AND JSONExtractString(properties, '$csp_blocked_url') != ''
GROUP BY blocked_domain
HAVING first_seen > now() - INTERVAL 24 HOUR
   AND distinct_users >= 10
ORDER BY occurrences DESC
LIMIT 20

Trois lentilles pour le triage — chaque conclusion d'URL bloquée devrait nommer laquelle s'applique :

  1. Légitime — la politique CSP a besoin d'être élargie. Nouveau CDN, nouveau fournisseur d'analytics, nouveau tag marketing que l'équipe a déployé et oublié d'ajouter à l'allowlist.
  2. Compromis — script injecté ou tiers indiquant un incident de sécurité. Domaine frais que personne ne reconnaît, en particulier des violations script-src sur un petit nombre de pages à fort trafic, en particulier avec disposition=enforce et un source_file pointant vers le propre bundle JS de l'équipe.
  3. Dérive tiers — script de fournisseur que l'équipe devrait supprimer. Ancien SDK analytics toujours chargé depuis un bundle dépréciée, pixel publicitaire d'un fournisseur fermé, etc.

Émettez uniquement quand l'une de ces lentilles s'ajuste avec haute confiance (≥ 0,85). Si vous êtes vraiment incertain lequel des trois c'est, écrivez une entrée de bloc-notes pattern:csp_violations:<entity> pour la prochaine exécution et fermez.

Pic par directive

Groupez par properties.$csp_effective_directive. Une directive dont le count récent 24h est matériellement au-dessus de sa baseline 7j-antérieure (≥ 3×) avec portée sur plusieurs documents est un signal fort « régression de politique après déploiement ». Associez avec activity-log-list filtré sur les 24–48 dernières heures — un déploiement ou un changement de flux hog corrélé à l'horodatage du pic est la convergence nette entre sources.

Directives supérieures à attendre (part brute des violations sur une SPA typique) : script-src, script-src-elem, img-src, style-src, connect-src, frame-src. Les violations script-src sont pondérées le plus haut pour la pertinence de sécurité ; img-src et style-src indiquent plus souvent une dérive fournisseur / CDN.

Régression scoped au document

Groupez par properties.$csp_document_url. Un document sans violations dans la fenêtre 7j-antérieure et un pic soudain dans les 24h récentes est presque toujours une régression de déploiement sur cette route — un nouveau script tag ou un style inline que la politique existante ne permet pas. Conclusion de haute valeur quand le document est une page d'entonnoir critique (/checkout, /signup, /login).

Boucle bloquée / bruit utilisateur unique

count très élevé mais distinct_users ≤ 5 sur la fenêtre récente. Presque toujours un utilisateur unique avec une extension de navigateur défaillante, ou un bot sondant la page. Ignorez — écrivez une entrée de bloc-notes noise:csp_violations:<blocked_domain> pour que les futures exécutions court-circuitent.

Motifs communément ignorables :

  • URLs bloquées chrome-extension:// / moz-extension:// / safari-extension://
  • Scripts injectés Brave / DuckDuckGo / privacy-browser
  • about:blank, URIs data: provenant d'outils de traduction ou de gestionnaires de mots de passe

Changement de disposition

Groupez par properties.$csp_disposition. Une équipe exécutant report-only pendant longtemps puis basculant vers enforce verra les violations se transformer en blocages réels. Si le profil du projet montre que count pour disposition='enforce' monte en flèche (recent_24h_count matériellement au-dessus de la baseline) tandis que report-only montre une baisse correspondante, l'équipe a basculé l'application — écrivez une entrée de bloc-notes pattern:csp_violations:disposition-flip et émettez uniquement si une page critique commence soudainement à voir des blocages appliqués.

Sauvegarder la mémoire au fur et à mesure

La mémoire est une activité continue. Écrivez une entrée de bloc-notes quand vous observez quelque chose qu'une future exécution CSP devrait savoir. Codez la « catégorie » dans le préfixe de clé — pattern:, noise:, addressed:, dedupe:, allowlist: — pour que les futures exécutions la trouvent avec une seule recherche text= :

  • clé pattern:csp_violations:baseline — _"Baseline $cspviolation saine du projet : ~800/jour sur ~120 utilisateurs distincts, surtout img-src depuis *.googletagmanager.com et *.googlesyndication.com. N'importe quoi au-dessus de 1,5× cette baseline est frais."
  • clé allowlist:csp_violations:gtm"*.googletagmanager.com, *.googlesyndication.com, *.doubleclick.net sont les domaines analytics/ads attendus de l'équipe — connus, vérifiés, ne pas resurfacer."
  • clé noise:csp_violations:chrome-extension-scheme — _"Le motif d'URL bloquée chrome-extension://* est une source de bruit d'extension de navigateur récurrente pour cette équipe — ignorer sauf si disposition=enforce et effective_directive=script-src."_
  • clé addressed:csp_violations:cdn.suspicious.example.com-2026-05-13"Cluster script-src frais surfacé depuis cdn.suspicious.example.com le 2026-05-12 ; l'équipe a confirmé que c'était un nouveau fournisseur légitime, allowlisté en politique le 2026-05-13. Ne pas ré-émettre sauf si le domaine réapparaît après que la politique ait été élargie."
  • clé dedupe:csp_violations:a1b2c3d4"Empreinte a1b2c3d4... (script-src | evil.example.com/x.js | /checkout | bundle.js) — surfacée 2026-05-08, conclusion toujours ouverte en boîte de réception. Si cette empreinte exacte se déclenche à nouveau, attacher au rapport existant ; ne pas émettre de nouveau."

À l'exécution n°5, vous aurez une allowlist de domaine par équipe dans le bloc-notes, des motifs de bruit d'extension de navigateur connus, et la forme typique par directive — et brûlerez près de zéro temps sur l'exploration de démarrage à froid.

Décider

Pour chaque conclusion candidate :

  • Émettez via signals-scout-emit-signal si elle franchit la barre de confiance. Conclusions scout solides : confiance ≥ 0,85, avec domaine bloqué concret, directive(s) effective(s), URL(s) de document, décompte d'utilisateurs distincts, évidence de plage temporelle, et une lentille explicite (politique / compromis / dérive fournisseur).
  • Mémorisez si en dessous de la barre mais digne d'être portée en avant (p. ex. domaine frais avec seulement 3 utilisateurs distincts — laissez mûrir).
  • Ignorez avec une note d'une ligne si une entrée de bloc-notes avec un préfixe de clé noise:, allowlist:, addressed:, ou dedupe: la couvre déjà.

Vérifiez croisée inbox-reports-list filtrée sur source_product=csp_reporting avant d'émettre — l'émission basée sur le push dépose déjà les signaux bruts individuels dans la boîte de réception, un par empreinte de violation. Votre conclusion agrégée devrait référencer ces signaux sources comme preuve (par empreinte) plutôt que de les reformuler.

Fermer

Résumez l'exécution — un paragraphe : regardé quoi, émis quoi, mémorisé quoi, écarté quoi. L'harnais écrit ce résumé sur la ligne d'exécution comme prose consultable ; les futures exécutions le lisent via signals-scout-runs-list. Ne rédigez pas une entrée séparée de « métadonnées d'exécution » — le résumé d'exécution remplit déjà ce rôle.

Disqualificateurs (ignorer ceux-ci)

  • Utilisateur unique, document unique, empreinte unique — presque toujours une extension de navigateur personnelle ou un client de niche. Faible count ET distinct_users ≤ 2.
  • Le schéma d'URL bloquée est chrome-extension:// / moz-extension:// / about: / data: — côté navigateur, pas côté serveur ; l'équipe ne peut pas réparer.
  • Le domaine correspond à une entrée de bloc-notes allowlist: — l'équipe a déjà vérifié ce fournisseur ; ignorez sans resurfacer.
  • disposition=report-only sans signal d'application — l'équipe collecte délibérément les violations pour affiner la politique. Émettez uniquement quand la portée / nouveauté / nouveauté du domaine est exceptionnelle.
  • L'empreinte correspond à une entrée de bloc-notes dedupe: d'une conclusion ouverte en boîte de réception — le chemin d'émission par push l'a déjà couvert ; ne pas faire double emploi.
  • L'équipe n'a pas de ligne signal_source_config pour csp_reporting — l'émission push est désactivée pour cette équipe. Le scout peut toujours trouver des clusters, mais le signal utilisateur est « l'équipe n'a pas encore opté pour les signaux CSP » ; relevez la barre de confiance (≥ 0,9) en conséquence.

En cas de doute, écrivez une entrée de mémoire au lieu d'émettre.

Outils MCP

Appels directs (lecture seule) :

  • execute-sql sur events (filtré sur event = '$csp_violation') — forage principal. Groupez par domain($csp_blocked_url), $csp_effective_directive, $csp_document_url, $csp_source_file. La liste complète des propriétés est dans posthog/api/csp.py.
  • read-data-schema (kind: event_properties, event_name: '$csp_violation') — découvrez la surface de propriété $csp_* réelle de l'équipe et des valeurs d'exemple.
  • activity-log-list — associez les horodatages de pics avec les déploiements récents ou les changements de feature-flag pour la convergence entre sources.
  • inbox-reports-list filtré sur source_product=csp_reporting — vérifiez qu'un cluster n'est pas déjà dans la boîte de réception via le chemin push avant d'émettre.

Niveau harnais :

  • signals-scout-project-profile-get / signals-scout-scratchpad-search / signals-scout-runs-list / signals-scout-runs-retrieve — orientation + déduplication.
  • signals-scout-emit-signal / signals-scout-scratchpad-remember — émettez / mémorisez.

Quand arrêter

  • La ligne $csp_violation dans le profil est à la baseline → fermer vide.
  • Une candidate correspond à une entrée de bloc-notes avec préfixe de clé noise: / allowlist: / addressed: / dedupe: → ignorer.
  • Vous avez validé des hypothèses et émis ce qui est solide → fermer, même s'il y a plus à regarder. Moins mais de meilleurs signaux.

« Regardé mais rien de significatif trouvé » est un vrai résultat.

Comment ceci se rapporte à la source CSP basée sur le push

Le chemin push compagnon (posthog/tasks/csp_signal.py, derrière un opt-in SignalSourceConfig par équipe) émet un signal brut par empreinte de violation unique avec un TTL dedup Redis 24h. Cela donne à la boîte de réception une couverture brute de chaque tuple (directive, blocked_url, document_url, source_file) frais, mais par empreinte et sans contexte entre empreintes.

Ce scout est la couche d'agrégation au-dessus. Ses conclusions devraient :

  • Regrouper plusieurs empreintes brutes dans une seule conclusion agrégée avec cause racine partagée (un nouveau domaine sur de nombreuses pages, une régression de déploiement sur de nombreuses directives, un motif de compromis sur de nombreux utilisateurs).
  • Utiliser les signaux existants du chemin push comme preuve dans le corps de la conclusion (référencés par empreinte / source_id) plutôt que de les re-dériver.
  • Rester silencieux quand la couverture du chemin push est suffisante — une seule empreinte brute déjà dans la boîte de réception n'a pas besoin d'une conclusion scout parallèle sauf si l'agrégation ajoute un nouveau contexte.

Skills similaires