From ea59a506f800611b45d695e50f2780b75b53809d Mon Sep 17 00:00:00 2001 From: Kuba Orlik Date: Sat, 13 Aug 2022 22:42:50 +0200 Subject: [PATCH] Problem: nieznany cel. Fixes #68 #97 --- components/report-window/deduce-problems.tsx | 2 + components/report-window/email-content.tsx | 19 +++ components/report-window/fake-clusters.ts | 56 +++++++++ .../problems/unknown-purpose.tsx | 109 ++++++++++++++++++ components/report-window/report-window.scss | 17 +++ components/report-window/report-window.tsx | 2 +- components/sidebar/stolen-data-cluster.tsx | 6 +- diag.html | 13 +++ diag.tsx | 65 +++++++++++ esbuild.config.js | 1 + extended-request.ts | 14 ++- request-cluster.ts | 22 +++- stolen-data-entry.ts | 14 +++ util.ts | 34 +++++- 14 files changed, 362 insertions(+), 12 deletions(-) create mode 100644 components/report-window/fake-clusters.ts create mode 100644 components/report-window/problems/unknown-purpose.tsx create mode 100644 diag.html create mode 100644 diag.tsx diff --git a/components/report-window/deduce-problems.tsx b/components/report-window/deduce-problems.tsx index 5512aa3..7a26e95 100644 --- a/components/report-window/deduce-problems.tsx +++ b/components/report-window/deduce-problems.tsx @@ -5,6 +5,7 @@ import { Problem } from './problems/problem'; import { TransferOutsideEU } from './problems/transfer-outside-eu'; import { UnknownIdentity } from './problems/unknown-identity'; import { UnknownLegalBasis } from './problems/unknown-legal-basis'; +import { UnknownPurposes } from './problems/unknown-purpose'; import { UnlawfulCookieAccess } from './problems/unlawful-cookies'; export default function deduceProblems( @@ -13,6 +14,7 @@ export default function deduceProblems( ): Problem[] { return [ NoInformationAtAllProblem, + UnknownPurposes, UnlawfulCookieAccess, UnknownLegalBasis, UnknownIdentity, diff --git a/components/report-window/email-content.tsx b/components/report-window/email-content.tsx index 5115c32..c0f0109 100644 --- a/components/report-window/email-content.tsx +++ b/components/report-window/email-content.tsx @@ -7,6 +7,8 @@ import './email-content.scss'; import { Fragment, useState } from 'react'; import emailIntro from './email-intro'; import { reportIntro } from './report-intro'; +import { downloadText } from '../../util'; +import { getFakeClusterData } from './fake-clusters'; const SS_URL = 'http://65.108.60.135:3000'; @@ -136,6 +138,23 @@ export default function EmailContent({

) : null} +
+ + downloadText( + 'diag.json', + JSON.stringify({ + answers, + fake_clusters_data: getFakeClusterData(clusters), + visited_url, + }) + ) + } + > + Pobierz plik diagnostyczny + +
); diff --git a/components/report-window/fake-clusters.ts b/components/report-window/fake-clusters.ts new file mode 100644 index 0000000..14da1e7 --- /dev/null +++ b/components/report-window/fake-clusters.ts @@ -0,0 +1,56 @@ +// good for diagnostic purposes + +import { RequestCluster } from '../../request-cluster'; +import { DataLocation } from '../../stolen-data-entry'; + +export type FakeRequestClusterData = { + id: string; + hasCookies: boolean; + hasMarkedCookies: boolean; + hasMarks: boolean; + exposesOriginWhere: DataLocation[]; + exposesOrigin: boolean; +}; + +export function getFakeClusterData( + clusters: Record +): Record { + return Object.fromEntries( + Object.entries(clusters).map(([key, cluster]) => [key, cluster.makeDataForFake()]) + ); +} + +export function makeFakeClusters( + fake_clusters_data: Record +): Record { + return Object.fromEntries( + Object.entries(fake_clusters_data).map(([key, cluster_data]) => [ + key, + new FakeCluster(cluster_data), + ]) + ); +} + +export class FakeCluster extends RequestCluster { + constructor(public data: FakeRequestClusterData) { + super(data.id); + + for (const key of [ + 'hasCookies', + 'hasMarkedCookies', + 'hasMarks', + 'exposesOriginWhere', + 'exposesOrigin', + ]) { + //@ts-ignore + this[key] = () => { + //@ts-ignore + return this.data[key]; + }; + } + } + + hasCookies() { + return this.data.hasCookies; + } +} diff --git a/components/report-window/problems/unknown-purpose.tsx b/components/report-window/problems/unknown-purpose.tsx new file mode 100644 index 0000000..31afade --- /dev/null +++ b/components/report-window/problems/unknown-purpose.tsx @@ -0,0 +1,109 @@ +import { RequestCluster } from '../../../request-cluster'; +import { dataLocationToText, wordlist } from '../../../util'; +import { ExplainerKey } from '../explainers'; +import { v } from '../verbs'; +import { Problem } from './problem'; + +export class UnknownPurposes extends Problem { + getNecessaryExplainers(): ExplainerKey[] { + return []; + } + + isHostAffected(host: string) { + const answers = this.answers.hosts[host]; + if (!answers) { + return false; + } + return ( + ['not_mentioned', 'not_before_making_a_choice'].includes(answers.present) && + ['no', 'not_sure'].includes(answers.was_processing_necessary) && + (this.clusters[host].hasCookies() || this.clusters[host].exposesOrigin()) + ); + } + + qualifies(): boolean { + return Object.keys(this.answers.hosts).some((host) => this.isHostAffected(host)); + } + + getAffectedClusters(): RequestCluster[] { + return Object.keys(this.answers.hosts) + .filter((host) => this.isHostAffected(host)) + .map((host) => this.clusters[host]); + } + + getEmailContent({ mode, tone }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) { + const _ = (key: string) => v(key, this.answers.zaimek); + const affected_clusters = this.getAffectedClusters(); + const has_history = affected_clusters.some((cluster) => cluster.exposesOrigin()); + const has_cookies = affected_clusters.some((cluster) => cluster.hasCookies()); + + return ( + <> +

Cele przetwarzania danych

+

+ Państwa strona{' '} + {mode == 'email' + ? `ujawniła dane ${_('mnie')} dotyczące` + : 'ujawnia dane dotyczące użytkowników'}{' '} + w zakresie{' '} + {wordlist([ + ...(has_cookies ? ['treści plików cookies'] : []), + ...(has_history + ? [ + mode === 'email' + ? `części ${_('mojej')} historii przeglądania` + : `części historii przeglądania`, + ] + : []), + ])}{' '} + podmiotom, które są właścicielami nastepujących domen: +

+
    + {affected_clusters.map((cluster, index) => { + const locations = cluster.exposesOriginWhere(); + return ( +
  • + {cluster.id}:{' '} + {wordlist([ + ...(cluster.hasCookies() ? ['treść plików cookies'] : []), + ...(cluster.exposesOrigin() + ? [ + (mode === 'email' + ? `część ${_('mojej')} historii przeglądania` + : `część historii przeglądania użytkownika`) + + ' (' + + wordlist( + locations.map((l) => dataLocationToText(l)) + ) + + ')', + ] + : []), + ])} + {index === affected_clusters.length - 1 ? '.' : ';'} +
  • + ); + })} +
+ {mode === 'email' ? ( + tone === 'official' ? ( +

+ Proszę o wskazanie, jakie są cele takiego przetwarzania danych, które + mnie dotyczą. +

+ ) : ( +

+ Apeluję o umieszczenie informacji na temat na Państwa stronie, aby jej + użytkownicy mogli podejmować w pełni świadome wybory dotyczące + przetwarzania danych ich dotyczących. +

+ ) + ) : ( +

+ Zalecenie: warto dodać informacje o tym, jakie są cele + ujawniania wyżej opisanych danych wyżej opisanym podmiotom trzecim. +

+ )} + + ); + } +} diff --git a/components/report-window/report-window.scss b/components/report-window/report-window.scss index f612dbc..1724d29 100644 --- a/components/report-window/report-window.scss +++ b/components/report-window/report-window.scss @@ -5,6 +5,17 @@ font-family: 'OpenSans' !important; } +#app { + min-height: 100vh; + display: flex; + flex-flow: column; +} + +#main-section { + flex-grow: 1; + margin-bottom: 20px; // to contain diag section +} + html { font-size: 1rem; } @@ -289,3 +300,9 @@ h1 { color: $ultra-black-color; } } + +.diag-toolbox { + position: fixed; + bottom: 10px; + left: 10px; +} diff --git a/components/report-window/report-window.tsx b/components/report-window/report-window.tsx index c51a7ff..5dc58e8 100644 --- a/components/report-window/report-window.tsx +++ b/components/report-window/report-window.tsx @@ -110,7 +110,7 @@ function Report() { )} -
{result}
+
{result}
); } catch (e) { diff --git a/components/sidebar/stolen-data-cluster.tsx b/components/sidebar/stolen-data-cluster.tsx index 65d0514..5f41074 100644 --- a/components/sidebar/stolen-data-cluster.tsx +++ b/components/sidebar/stolen-data-cluster.tsx @@ -103,13 +103,11 @@ export default function StolenDataCluster({ shorthost, minValueLength, cookiesOnly, - refreshToken, cookiesOrOriginOnly, detailsVisibility, }: { origin: string; shorthost: string; - refreshToken: number; minValueLength: number; cookiesOnly: boolean; cookiesOrOriginOnly: boolean; @@ -129,6 +127,10 @@ export default function StolenDataCluster({ data-version={version} checked={cluster.hasMarks()} onChange={() => { + console.log('Clicked checkbox!', { + cluster_id: cluster.id, + has_marks: cluster.hasMarks(), + }); cluster.hasMarks() ? cluster.undoMark() : cluster.autoMark(); getMemory().emit('change', cluster.id); }} diff --git a/diag.html b/diag.html new file mode 100644 index 0000000..7d3794d --- /dev/null +++ b/diag.html @@ -0,0 +1,13 @@ + + + + RENTGEN DIAG + + +
+ + + + + + diff --git a/diag.tsx b/diag.tsx new file mode 100644 index 0000000..5239130 --- /dev/null +++ b/diag.tsx @@ -0,0 +1,65 @@ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import EmailContent from './components/report-window/email-content'; + +import { makeFakeClusters } from './components/report-window/fake-clusters'; + +class ErrorBoundary extends React.Component { + constructor(props: any) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: any) { + return { hasError: true, error }; + } + + render() { + if (this.state.hasError) { + return

Something went wrong.

; + } + return this.props.children; + } +} + +function Diag() { + const [json, setjson] = React.useState( + JSON.stringify({ answers: { hosts: {} }, visited_url: '', fake_clusters_data: {} }) + ); + const { answers, visited_url, fake_clusters_data } = JSON.parse(json); + const fake_clusters = makeFakeClusters(fake_clusters_data); + return ( +
+
+ +
+
+ { + alert('download!'); + }, + user_role: 'user', + }} + /> +
+
+ ); +} + +ReactDOM.render( + + + , + document.getElementById('app') +); diff --git a/esbuild.config.js b/esbuild.config.js index f747889..873010c 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -48,6 +48,7 @@ esbuild 'components/sidebar/sidebar.tsx', 'components/report-window/report-window.tsx', 'background.ts', + 'diag.tsx', 'styles/global.scss', 'styles/fonts.scss', ], diff --git a/extended-request.ts b/extended-request.ts index 4c82f8c..462e990 100644 --- a/extended-request.ts +++ b/extended-request.ts @@ -1,5 +1,5 @@ 'use strict'; -import { StolenDataEntry } from './stolen-data-entry'; +import { DataLocation, StolenDataEntry } from './stolen-data-entry'; import { flattenObjectEntries, getshorthost, @@ -164,12 +164,12 @@ export default class ExtendedRequest { ); } - exposesOrigin() { + exposesOriginWhere(): null | DataLocation { const host = this.originalHost; const path = this.originalPathname || '/'; const shorthost = getshorthost(host); if (this.getReferer().includes(shorthost)) { - return true; + return { path: this.url, source: 'header', key: 'Referer' }; } for (const entry of this.stolenData) { if ( @@ -177,10 +177,14 @@ export default class ExtendedRequest { entry.value.includes(path) || entry.value.includes(shorthost) ) { - return true; + return entry.toDataLocation(); } } - return false; + return null; + } + + exposesOrigin() { + return this.exposesOriginWhere() !== null; } private getAllStolenData(): StolenDataEntry[] { diff --git a/request-cluster.ts b/request-cluster.ts index 0e4b9a8..70c6288 100644 --- a/request-cluster.ts +++ b/request-cluster.ts @@ -1,6 +1,7 @@ +import { FakeRequestClusterData } from './components/report-window/fake-clusters'; import ExtendedRequest from './extended-request'; import { SaferEmitter } from './safer-emitter'; -import { Sources, StolenDataEntry } from './stolen-data-entry'; +import { DataLocation, Sources, StolenDataEntry } from './stolen-data-entry'; import { allSubhosts, isSameURL, reduceConcat, unique } from './util'; @@ -171,7 +172,13 @@ export class RequestCluster extends SaferEmitter { return this.requests.map((request) => request.getMarkedEntries()).reduce(reduceConcat, []); } - exposesOrigin() { + exposesOriginWhere(): DataLocation[] { + return this.requests + .map((request) => request.exposesOriginWhere()) + .filter((l) => l !== null) as DataLocation[]; + } + + exposesOrigin(): boolean { return this.requests.some((request) => request.exposesOrigin()); } @@ -203,4 +210,15 @@ export class RequestCluster extends SaferEmitter { } return types_of_data.join(', '); } + + makeDataForFake(): FakeRequestClusterData { + return { + id: this.id, + hasCookies: this.hasCookies(), + hasMarkedCookies: this.hasMarkedCookies(), + hasMarks: this.hasMarks(), + exposesOriginWhere: this.exposesOriginWhere(), + exposesOrigin: this.exposesOrigin(), + }; + } } diff --git a/stolen-data-entry.ts b/stolen-data-entry.ts index 6c110a2..9be09f2 100644 --- a/stolen-data-entry.ts +++ b/stolen-data-entry.ts @@ -33,6 +33,12 @@ const id = (function* id() { export type DecodingSchema = 'base64' | 'raw'; +export type DataLocation = { + path: string; + source: Sources; + key: string; +}; + export class StolenDataEntry extends SaferEmitter { public isIAB = false; public id: number; @@ -253,4 +259,12 @@ export class StolenDataEntry extends SaferEmitter { haystack.includes(getshorthost(this.request.origin)) ); } + + toDataLocation(): DataLocation { + return { + path: this.request.url, + source: this.source, + key: this.name, + }; + } } diff --git a/util.ts b/util.ts index 516c11d..13bdcc4 100644 --- a/util.ts +++ b/util.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; import React from 'react'; +import { DataLocation, Sources } from './stolen-data-entry'; export type Unpromisify = T extends Promise ? R : T; export type Unarray = T extends Array ? R : T; @@ -283,8 +284,37 @@ export function normalizeForClassname(string: string) { } export function wordlist(words: string[]) { - return words.reduce( - (acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, + return Array.from(new Set(words)).reduce( + (acc, word, i) => + `${acc}${ + i > 0 ? (i < words.length - 1 ? ', ' : Math.random() > 0.5 ? ' i ' : ' oraz ') : '' + }${word}`, '' ); } + +const source_to_word: Record = { + cookie: 'plik cookie o nazwie', + pathname: 'fragment ścieżki w URL', + queryparams: 'query params w URL o nazwie', + header: 'nagłówek HTTP', + request_body: 'body zapytania HTTP, pod kluczem', +}; + +export function dataLocationToText(l: DataLocation) { + return `${source_to_word[l.source]} ${l.key}`; +} + +export function downloadText(filename: string, text: string) { + // https://stackoverflow.com/questions/45831191/generate-and-download-file-from-js + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +}