cuonztech-privacy-chaos-gate 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/action.yml CHANGED
@@ -188,7 +188,7 @@ runs:
188
188
  shell: bash
189
189
  env:
190
190
  ACTION_PATH: ${{ github.action_path }}
191
- CPCG_PINNED_VERSION: "0.3.0" # = package.json version; bei jedem Release mit anheben (tests/action-pin.test.ts prüft das)
191
+ CPCG_PINNED_VERSION: "0.3.2" # = package.json version; bei jedem Release mit anheben (tests/action-pin.test.ts prüft das)
192
192
  run: |
193
193
  set -euo pipefail
194
194
 
@@ -517,6 +517,7 @@ function hintViolations(violations, strict = false, jurisdictions = resolveJuris
517
517
  const blocking = new Set(blockingViolations(violations, strict, jurisdictions));
518
518
  return violations.filter((v) => !blocking.has(v));
519
519
  }
520
+ var UNLISTED_EMBED_PREFIX = "Fremd-Inhalt (";
520
521
  function hintReasonOf(v, jurisdictions) {
521
522
  if (isAdvisory(v) && v.trackerCategory === "cookieless-analytics") {
522
523
  return "Cookielose Reichweitenmessung (keine Cookies, kein Zugriff auf das Ger\xE4t): nach verbreiteter Aufsichtspraxis einwilligungsfrei, wenn sie nur der Reichweitenmessung dient; dokumentiert, mit --strict blockierend.";
@@ -524,6 +525,9 @@ function hintReasonOf(v, jurisdictions) {
524
525
  if (isAdvisory(v) && v.tcf === "denied") {
525
526
  return "Request nach dem Ablehnen mit IAB-TCF-String, der die Ablehnung tr\xE4gt (kein Zweck erteilt): der Anbieter muss selbst verzichten; der Request selbst \xFCbermittelt trotzdem Daten (IP, Seite). Dokumentiert, mit --strict blockierend.";
526
527
  }
528
+ if (isAdvisory(v) && v.trackerName.startsWith(UNLISTED_EMBED_PREFIX)) {
529
+ return "Eingebetteter Fremd-Inhalt, der vor der Einwilligung l\xE4dt und nicht in der Tracker-Liste steht: Inhalt und Datenschutzerkl\xE4rung pr\xFCfen; dokumentiert, mit --strict blockierend.";
530
+ }
527
531
  if (isAdvisory(v)) return "Rein funktionaler Drittanbieter (z. B. Schriften): kein Verstoss, dokumentiert.";
528
532
  return hintReasonFor(jurisdictions, v.ruleId, v.trackerCategory);
529
533
  }
@@ -1406,6 +1410,7 @@ var CURATED_VENDOR_DOMAINS = [
1406
1410
  ["gc.zgo.at", "GoatCounter", "cookieless-analytics"],
1407
1411
  ["withcabin.com", "Cabin Analytics", "cookieless-analytics"],
1408
1412
  ["counter.dev", "counter.dev", "cookieless-analytics"],
1413
+ ["cloudflareinsights.com", "Cloudflare Web Analytics", "cookieless-analytics"],
1409
1414
  // --- Werbung, Ad-Tech, Datenplattformen ---
1410
1415
  ["googletagservices.com", "Google Ad Manager", "marketing"],
1411
1416
  ["amazon-adsystem.com", "Amazon Ads", "marketing"],
@@ -1546,7 +1551,8 @@ var KNOWN_TRACKERS = [
1546
1551
  name: "Google Analytics / Google Tag (gtag.js)",
1547
1552
  category: "analytics",
1548
1553
  consentModeLoader: true,
1549
- patterns: [at(www("googletagmanager.com"), "/gtag/js$")]
1554
+ // `/a?id=G-…` ist der Beacon des Google-Tags (Konfiguration/Diagnose) derselben Messkennung.
1555
+ patterns: [at(www("googletagmanager.com"), "/gtag/js$"), at(www("googletagmanager.com"), "/a$")]
1550
1556
  },
1551
1557
  {
1552
1558
  name: "Meta (Facebook) Pixel",
@@ -3646,6 +3652,9 @@ function ruleIdFor(phase) {
3646
3652
  if (phase === "gpc") return "gpc-ignored";
3647
3653
  return phase === "post-wipe" ? "tracking-without-consent" : phase === "post-reject" || phase === "post-reject-restart" || phase === "post-tamper" ? "tracking-after-deny" : phase === "post-revoke" ? "tracking-after-revoke" : "pre-consent-tracking";
3648
3654
  }
3655
+ function isFirstPartyGoogleMeasurement(t) {
3656
+ return !!t.trackerName?.startsWith("Google Analytics (First-Party-Endpoint");
3657
+ }
3649
3658
  function asScope(s) {
3650
3659
  return typeof s === "string" ? new TargetScope(s) : s;
3651
3660
  }
@@ -5254,6 +5263,62 @@ var ChaosScanner = class {
5254
5263
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
5255
5264
  });
5256
5265
  }
5266
+ /** Hinweise (nie blockierend) auf Fremd-Iframes ohne Eintrag in der Tracker-Liste. */
5267
+ unlistedEmbedHints(scenario, unlisted) {
5268
+ return unlisted.map((u) => ({
5269
+ id: `${scenario}-unlisted-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
5270
+ ruleId: "pre-consent-embed",
5271
+ scenario,
5272
+ trackerName: `${UNLISTED_EMBED_PREFIX}${u.host})`,
5273
+ trackerCategory: "functional",
5274
+ requestUrl: u.sample,
5275
+ method: "GET",
5276
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5277
+ description: `Eingebetteter Inhalt von ${u.host} l\xE4dt vor Consent-Freigabe ${u.count} Request(s); der Anbieter steht nicht in der Tracker-Liste.`,
5278
+ advisory: true
5279
+ }));
5280
+ }
5281
+ /**
5282
+ * Hosts eingebetteter Fremd-Frames, die vor der Freigabe Requests auslösen, ohne dass die Tracker-Liste sie kennt.
5283
+ * Eigene Hosts, Loopback/IP und bereits erkannte Tracker zählen nicht.
5284
+ */
5285
+ unlistedEmbedHosts(embeds, targetUrl, timeline) {
5286
+ let targetHost;
5287
+ try {
5288
+ targetHost = new URL(targetUrl).hostname;
5289
+ } catch {
5290
+ return [];
5291
+ }
5292
+ const hosts = /* @__PURE__ */ new Set();
5293
+ for (const e of embeds) {
5294
+ try {
5295
+ const u = new URL(e.src, targetUrl);
5296
+ if (/^https?:$/.test(u.protocol) && !/^[\d.]+$|^\[|^localhost$/i.test(u.hostname) && !isFirstPartyHost(u.hostname, targetHost)) hosts.add(u.hostname);
5297
+ } catch {
5298
+ }
5299
+ }
5300
+ const out = [];
5301
+ for (const host of hosts) {
5302
+ const own = timeline.filter((t) => {
5303
+ try {
5304
+ const h = new URL(t.url).hostname;
5305
+ return !t.trackerName && (h === host || h.endsWith(`.${host}`));
5306
+ } catch {
5307
+ return false;
5308
+ }
5309
+ });
5310
+ const known = timeline.some((t) => {
5311
+ try {
5312
+ const h = new URL(t.url).hostname;
5313
+ return !!t.trackerName && (h === host || h.endsWith(`.${host}`));
5314
+ } catch {
5315
+ return false;
5316
+ }
5317
+ });
5318
+ if (own.length && !known) out.push({ host, count: own.length, sample: own[0].url });
5319
+ }
5320
+ return out;
5321
+ }
5257
5322
  /**
5258
5323
  * CNAME-Cloaking nachtragen: Hosts der eigenen Seite aus der Zeitlinie, die kein Tracker sind, per DNS prüfen. Zeigt die
5259
5324
  * CNAME-Kette auf einen Tracker-Host, wird der Request mit seiner ursprünglichen Phase als Tracker-Befund eingetragen.
@@ -6293,7 +6358,7 @@ var ChaosScanner = class {
6293
6358
  googleRequests.push(t.url);
6294
6359
  }
6295
6360
  }
6296
- const hits = timeline.filter((t) => isGoogleMeasurementRequest(t.url) || isGoogleMeasurementRequest(viaCname(t.url) ?? "")).map((t) => ({ ...t, signal: summarizeSignals(parseConsentSignals(t.url)) }));
6361
+ const hits = timeline.filter((t) => isGoogleMeasurementRequest(t.url) || isGoogleMeasurementRequest(viaCname(t.url) ?? "") || isFirstPartyGoogleMeasurement(t)).map((t) => ({ ...t, signal: summarizeSignals(parseConsentSignals(t.url)) }));
6297
6362
  const signalled = hits.some((h) => h.signal !== "none");
6298
6363
  const hasConsentMode = decided && (Object.keys(defaults).length > 0 || Object.keys(after).length > 0 || signalled);
6299
6364
  const gaCookies = storageAfter.cookies.filter((c) => /^_ga|_gid|_gat|_gcl/i.test(c));
@@ -6453,6 +6518,8 @@ var ChaosScanner = class {
6453
6518
  await page.waitForTimeout(1500);
6454
6519
  await this.snap(page, "embed-leaks", "01-embeds", screenshots, actionTimeline, actionSequence);
6455
6520
  const storageAfter = await this.snapshotStorage(page);
6521
+ const unlistedEmbeds = this.unlistedEmbedHosts(embeds, config.targetUrl, timeline);
6522
+ violations.push(...this.unlistedEmbedHints("embed-leaks", unlistedEmbeds));
6456
6523
  if (embeds.length === 0 && embedLeakCount === 0 && embedAdvisoryCount === 0) {
6457
6524
  const status2 = embedsEvalOk ? "not-applicable" : "not-checkable";
6458
6525
  evidence.push({
@@ -6476,7 +6543,7 @@ var ChaosScanner = class {
6476
6543
  evidence.push({
6477
6544
  scenario: "embed-leaks",
6478
6545
  status,
6479
- note: (status === "passed" ? embedAdvisoryCount ? `${embeds.length} Embed(s) gefunden, keine marketing-/analytics-relevanten Drittanbieter-Requests vor Freigabe.` : `${embeds.length} Embed(s) gefunden, keine Drittanbieter-Requests vor Freigabe.` : `${embedLeakCount} Embed-/Widget-Request(s) vor Consent-Freigabe.`) + (embedAdvisoryCount ? ` ${embedAdvisoryCount} Hinweis(e) auf rein funktionale Einbettungen (z. B. Karten) vor Freigabe \u2014 im Report sichtbar, ohne Gate-Wirkung.` : ""),
6546
+ note: (status === "passed" ? embedAdvisoryCount ? `${embeds.length} Embed(s) gefunden, keine marketing-/analytics-relevanten Drittanbieter-Requests vor Freigabe.` : `${embeds.length} Embed(s) gefunden, keine Drittanbieter-Requests vor Freigabe.` : `${embedLeakCount} Embed-/Widget-Request(s) vor Consent-Freigabe.`) + (embedAdvisoryCount ? ` ${embedAdvisoryCount} Hinweis(e) auf rein funktionale Einbettungen (z. B. Karten) vor Freigabe \u2014 im Report sichtbar, ohne Gate-Wirkung.` : "") + (unlistedEmbeds.length ? ` Fremd-Inhalt ohne Eintrag in der Tracker-Liste l\xE4dt vor Freigabe: ${unlistedEmbeds.map((u) => `${u.host} (${u.count} Request(s))`).join(", ")} \u2014 Hinweis, ohne Gate-Wirkung.` : ""),
6480
6547
  actionSequence,
6481
6548
  actionTimeline,
6482
6549
  requestTimeline: timeline,
@@ -6644,8 +6711,13 @@ var ChaosScanner = class {
6644
6711
  const actionTimeline = [];
6645
6712
  const screenshots = [];
6646
6713
  let armed = false;
6714
+ const frameSrcs = [];
6647
6715
  onTargetRequest(page, scope, (req) => {
6648
6716
  if (!armed) return;
6717
+ try {
6718
+ if (req.isNavigationRequest() && req.frame().parentFrame() === page.mainFrame()) frameSrcs.push({ src: req.url() });
6719
+ } catch {
6720
+ }
6649
6721
  const hit = identifyTracker(req.url());
6650
6722
  timeline.push({ at: (/* @__PURE__ */ new Date()).toISOString(), url: req.url(), trackerName: hit?.name, trackerCategory: hit?.category, phase: "pre-consent" });
6651
6723
  if (hit && this.trackerVerdict(hit) !== "skip") {
@@ -6750,6 +6822,9 @@ var ChaosScanner = class {
6750
6822
  await this.snap(page, "subpage-spa", "02-after-spa", screenshots, actionTimeline, actionSequence);
6751
6823
  const storageAfter = await this.snapshotStorage(page);
6752
6824
  await this.applyCnameCloaking("subpage-spa", config, timeline, violations, /* @__PURE__ */ new Map());
6825
+ const unlistedFrames = this.unlistedEmbedHosts(frameSrcs, config.targetUrl, timeline);
6826
+ violations.push(...this.unlistedEmbedHints("subpage-spa", unlistedFrames));
6827
+ const frameNote = unlistedFrames.length ? ` Fremd-Inhalt ohne Eintrag in der Tracker-Liste l\xE4dt auf Unterseiten vor Freigabe: ${unlistedFrames.map((u) => `${u.host} (${u.count} Request(s))`).join(", ")} \u2014 Hinweis, ohne Gate-Wirkung.` : "";
6753
6828
  const leaks = timeline.filter((e) => this.isBlocking(e) && e.phase === "pre-consent");
6754
6829
  const status = leaks.length > 0 ? "suspicious" : reached.length === 0 ? "not-checkable" : "passed";
6755
6830
  const sampling = deep ? deepSamplingText(deep, reached.length, visited.length, deepSaved, budgetExhausted) : sample.total > reached.length ? `Stichprobe: ${reached.length} von ${sample.total} verlinkten Unterseiten gepr\xFCft \u2014 keine vollst\xE4ndige Pr\xFCfung der Website; nicht gepr\xFCfte Routen k\xF6nnen Verst\xF6sse enthalten (--max-subpages erh\xF6hen).` : `${reached.length} von ${sample.total} verlinkten Unterseiten der Startseite gepr\xFCft (Unterseiten ohne Link auf der Startseite bleiben ungepr\xFCft).`;
@@ -6769,7 +6844,7 @@ var ChaosScanner = class {
6769
6844
  evidence.push({
6770
6845
  scenario: "subpage-spa",
6771
6846
  status,
6772
- note: status === "passed" ? `Kein Tracking ohne Consent auf ${reached.length} verlinkter Unterseite(n)${spaNavigations > 0 ? ` und bei ${spaNavigations} clientseitiger SPA-Navigation(en)` : " (keine clientseitige SPA-Navigation beobachtet \u2014 Links laden die Seite neu)"}.${spaSkipped ? ` ${spaSkipped} SPA-Durchgang/-g\xE4nge ausgelassen (Seite nicht stabil ladbar).` : ""} ${sampling}` : status === "suspicious" ? `${leaks.length} Tracker auf Unterseite/SPA ohne Consent. ${sampling}` : links.length === 0 ? deep ? `Weder Sitemap noch Startseite lieferten eine Unterseite im Zielbereich \u2014 Unterseiten/SPA nicht pr\xFCfbar. ${sampling}` : "Keine verlinkte Same-Origin-Unterseite auf der Startseite gefunden \u2014 Unterseiten/SPA nicht pr\xFCfbar." : `Keine der verlinkten Unterseiten antwortete mit HTTP-Status < 400 \u2014 Unterseiten/SPA nicht pr\xFCfbar.${deep ? ` ${sampling}` : ""}`,
6847
+ note: status === "passed" ? `Kein Tracking ohne Consent auf ${reached.length} verlinkter Unterseite(n)${spaNavigations > 0 ? ` und bei ${spaNavigations} clientseitiger SPA-Navigation(en)` : " (keine clientseitige SPA-Navigation beobachtet \u2014 Links laden die Seite neu)"}.${spaSkipped ? ` ${spaSkipped} SPA-Durchgang/-g\xE4nge ausgelassen (Seite nicht stabil ladbar).` : ""} ${sampling}${frameNote}` : status === "suspicious" ? `${leaks.length} Tracker auf Unterseite/SPA ohne Consent. ${sampling}${frameNote}` : links.length === 0 ? deep ? `Weder Sitemap noch Startseite lieferten eine Unterseite im Zielbereich \u2014 Unterseiten/SPA nicht pr\xFCfbar. ${sampling}` : "Keine verlinkte Same-Origin-Unterseite auf der Startseite gefunden \u2014 Unterseiten/SPA nicht pr\xFCfbar." : `Keine der verlinkten Unterseiten antwortete mit HTTP-Status < 400 \u2014 Unterseiten/SPA nicht pr\xFCfbar.${deep ? ` ${sampling}` : ""}`,
6773
6848
  actionSequence,
6774
6849
  actionTimeline,
6775
6850
  requestTimeline: timeline,
@@ -10931,7 +11006,7 @@ import pc from "picocolors";
10931
11006
  import { createHash } from "crypto";
10932
11007
  import fs10 from "fs/promises";
10933
11008
  import path10 from "path";
10934
- var CPCG_VERSION = true ? "0.3.0" : "dev";
11009
+ var CPCG_VERSION = true ? "0.3.2" : "dev";
10935
11010
  function scanConfigOf(config) {
10936
11011
  return {
10937
11012
  targetUrl: config.targetUrl,
package/dist/cli.js CHANGED
@@ -11,12 +11,17 @@ import {
11
11
  resolveEntitlement,
12
12
  resolveGateConfig,
13
13
  resolveJurisdictions
14
- } from "./chunk-YOWEZW3W.js";
14
+ } from "./chunk-6MBUQ5EL.js";
15
15
 
16
16
  // src/cli.ts
17
17
  import { Command, CommanderError } from "commander";
18
18
  import pc from "picocolors";
19
- var VERSION = true ? "0.3.0" : "dev";
19
+ var VERSION = true ? "0.3.2" : "dev";
20
+ for (const stream of [process.stdout, process.stderr]) {
21
+ stream.on("error", (err) => {
22
+ if (err.code !== "EPIPE") throw err;
23
+ });
24
+ }
20
25
  loadProjectEnv();
21
26
  function finish(code) {
22
27
  process.exitCode = code;
package/dist/index.d.ts CHANGED
@@ -2439,6 +2439,13 @@ declare class ChaosScanner implements ChaosScannerInterface {
2439
2439
  * Ergebnis: passed nur bei ausgeführter Aktion und null Illegal-Funden.
2440
2440
  */
2441
2441
  private runScenario;
2442
+ /** Hinweise (nie blockierend) auf Fremd-Iframes ohne Eintrag in der Tracker-Liste. */
2443
+ private unlistedEmbedHints;
2444
+ /**
2445
+ * Hosts eingebetteter Fremd-Frames, die vor der Freigabe Requests auslösen, ohne dass die Tracker-Liste sie kennt.
2446
+ * Eigene Hosts, Loopback/IP und bereits erkannte Tracker zählen nicht.
2447
+ */
2448
+ private unlistedEmbedHosts;
2442
2449
  /**
2443
2450
  * CNAME-Cloaking nachtragen: Hosts der eigenen Seite aus der Zeitlinie, die kein Tracker sind, per DNS prüfen. Zeigt die
2444
2451
  * CNAME-Kette auf einen Tracker-Host, wird der Request mit seiner ursprünglichen Phase als Tracker-Befund eingetragen.
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ import {
56
56
  uncheckableReasons,
57
57
  uncheckableReasonsForReport,
58
58
  waitForVerifyUrl
59
- } from "./chunk-YOWEZW3W.js";
59
+ } from "./chunk-6MBUQ5EL.js";
60
60
 
61
61
  // src/index.ts
62
62
  async function runGate(optionsOrConfig) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cuonztech-privacy-chaos-gate",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CuonzTech Privacy Chaos Gate — CI/CD Consent-Scanner, Chaos-Szenarien und Auto-Fix-PR",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",