Compare commits

...

24 Commits

Author SHA1 Message Date
Arkadiusz Wieczorek ea70d26a38 Update 'README.md' 2023-09-26 23:05:42 +02:00
Kuba Orlik 1680026bc4 Typos 2023-06-21 17:14:40 +02:00
Kuba Orlik d5a8172759 Fix typos 2023-06-21 17:14:07 +02:00
Kuba Orlik 039698264a Bardziej podejrzane domeny dawaj na początku w toolbarze 2023-03-22 15:51:07 +01:00
Arkadiusz Wieczorek 7cf1b95461 Update 'README.md' 2023-02-17 10:37:40 +01:00
Kuba Orlik 799f17eac8 Add mozilla addons link 2023-02-14 20:34:21 +01:00
Kuba Orlik 5c96a7f4cb Bump version 2022-09-25 14:04:51 +02:00
Kuba Orlik 32107f0ebc Dodanie explainerów dot. cookies na podstawie komentarzy, od których
odwołał się WSA w uzasadnieniu wyroku w sprawie z iSecure
2022-09-25 14:03:06 +02:00
Kuba Orlik 3910b5c67e Update version 2022-09-12 11:44:29 +02:00
Arkadiusz Wieczorek b2487c0511 Merge pull request 'Fix sometimes not being able to uncheck a domain - Fixes #92' (#101) from fix-checkbox into develop
Reviewed-on: #101
2022-09-12 09:48:04 +02:00
Arkadiusz Wieczorek 1106e86b41 Merge pull request 'Problem: nieznany cel. Dodanie narzędzi diagnostycznych' (#99) from unknown-purpose into develop
Reviewed-on: #99
2022-09-12 09:47:52 +02:00
Arkadiusz Wieczorek c16e8f3d7c Merge pull request 'Poprawa zaznaczania kawałków danych - teraz nie trzeba klikać na' (#95) from #64 into develop
Reviewed-on: #95
2022-09-12 09:47:11 +02:00
Kuba Orlik aae1a7e970 HOTFIX: nie działały URL-e, które mają '#' 2022-09-02 22:07:04 +02:00
Kuba Orlik 3a32456534 Fix $ typo 2022-09-01 07:47:16 +02:00
Kuba Orlik 67544a4bff Finish broken sentence 2022-08-31 21:23:35 +02:00
Kuba Orlik e3f4f8858d Fix rentgen being fooled with a "=" in cookie value (e.g. the NID cookie on google.com) 2022-08-31 09:50:37 +02:00
Kuba Orlik e3d6e6b83c Ikonka cookiesów przy tych domenach, które mają entriesy z cookiesami 2022-08-16 14:08:13 +02:00
Kuba Orlik cc713004b0 Fix sometimes not being able to uncheck a domain - Fixes #92 2022-08-14 12:55:21 +02:00
Kuba Orlik ea59a506f8 Problem: nieznany cel. Fixes #68 #97 2022-08-13 22:42:50 +02:00
Kuba Orlik 65e1ab2ecc Poprawa zaznaczania kawałków danych - teraz nie trzeba klikać na
checkbox, wystarczy klikać na labelkę
2022-07-31 16:05:48 +02:00
Kuba Orlik 5f29730613 Zmiana wordingu w problemie z administratorem. Wyświetlanie pytania o
konieczność w większej ilości przypadków
2022-07-31 16:01:01 +02:00
Arkadiusz Wieczorek 8fe2083d8d Update 'README.md' 2022-07-16 23:59:21 +02:00
Arkadiusz Wieczorek 45d87bc2a7 Bump version 2022-07-15 14:15:19 +02:00
Kuba Orlik 09dfe8d63a Change domain of screenshot service 2022-07-15 14:14:42 +02:00
27 changed files with 546 additions and 90 deletions

106
README.md
View File

@ -1,52 +1,5 @@
<h1 style="display: flex; align-items: center;"><img src="./assets/icon-addon-2048.png" alt="Rentgen logo" style="margin-right: 1rem;" width="48"/> Rentgen</h1> <h1 style="display: flex; align-items: center;"><img src="./assets/icon-addon-2048.png" alt="Rentgen logo" style="margin-right: 1rem;" width="48"/> Rentgen</h1>
<strong>Rentgen</strong> to wtyczka dla przeglądarek opartych o Firefoxa, która automatycznie wizualizuje, jakie dane zostały ~~wykradzione~~ wysłane do podmiotów trzecich przez odwiedzane strony.
**Funkcje Rentgena:**
- analiza ruchu sieciowego generowanego przez stronę internetową;
- wizualizacja danych przekazanych do podmiotów trzecich przez odwiedzaną stronę (historia przeglądania użytkownika oraz jego ciasteczka);
- przygotowywanie zrzutów ekranów narzędzi deweloperskich będących dowodem przekazanych danych do podmiotów trzecich;
- pomoc w oszacowaniu potencjalnych obszarów roboczych względem zgodności z RODO;
- generowanie raportu lub treści maila, którą można wysłać do administratora oraz Urzędu Ochrony Danych Osobowych.
## Jak zbudować i uruchomić Rentgena ze źródeł
### Wymagania wstępne
- System operacyjny: Linux x86_64
- Node.js: 16.x
- npm: 7.x lub wyższy
### Proces budowy
1. Pobierz repozytorium przez `git pull https://git.internet-czas-dzialac.pl/icd/rentgen.git` lub pobierz archwium zip
2. Przejdź do głównego katalogu pobranego repozytorium
3. Uruchom komendę: `npm install`
4. Uruchom komendę: `npm run build`
5. Uruchom komendę: `npm run create-package`
6. Przejdź do katalogu `web-ext-artifacts`
7. Znajdziesz tam archiwum zip: `rentgen-x-x-x.zip` (`x-x-x` oznaczają wersję wtyczki)
### Kroki do uruchomienia
1. Uruchom Firefoxa i przejdź do strony `about:debugging`
2. Kliknij zakładkę _This Firefox_
3. Kliknij przycisk _Load Temporary Add-on..._
4. Wybierz archiwum, które zbudowałeś w ostatnim kroku procesu budowy
## Zgłaszanie błędów
Link do issue trackera na naszej instancji Discourse: https://forum.internet-czas-dzialac.pl/c/rentgen-issue-tracker
Nie przyjmujemy zgłoszeń na platformie Microsoft Github.
Każdy problem zostanie sprawdzony i przeniesiony na wewnętrzną listę problemów na naszej instancji Gitea: https://git.internet-czas-dzialac.pl/icd/rentgen/issues. Korzystamy z Gitea i najprawdopodobniej w przyszłości dzięki federalizacji Gitea będziemy w stanie wpuścić użytkowników do zgłaszania błędów bezpośrednio ze strony Gitea.
---
## English description 🇬🇧
<strong>Rentgen</strong> is an add-on prepared for Firefox-based browsers. This extension will automatically visualize all the data that a given website ~~steals~~ sends to third parties. <strong>Rentgen</strong> is an add-on prepared for Firefox-based browsers. This extension will automatically visualize all the data that a given website ~~steals~~ sends to third parties.
Note: At the moment, we support Polish language because this extension generates mail content that is dedicated to Polish website owners. In further versions of this add-on, we will add other languages as well. Note: At the moment, we support Polish language because this extension generates mail content that is dedicated to Polish website owners. In further versions of this add-on, we will add other languages as well.
@ -59,6 +12,10 @@ Note: At the moment, we support Polish language because this extension generates
- assisting in the evaluation of potential work areas for compliance with GDPR; - assisting in the evaluation of potential work areas for compliance with GDPR;
- generating a report or email content that can be sent to an administrator and Personal Data Protection Office in Poland. - generating a report or email content that can be sent to an administrator and Personal Data Protection Office in Poland.
## Installation
Firefox: https://addons.mozilla.org/en-US/firefox/addon/rentgen/
## How to build and run Rentgen on your own ## How to build and run Rentgen on your own
### Pre-requirements ### Pre-requirements
@ -86,7 +43,9 @@ Note: At the moment, we support Polish language because this extension generates
## Issue tracker ## Issue tracker
Link to issue tracker on our Discourse instance: https://forum.internet-czas-dzialac.pl/c/rentgen-issue-tracker If you find a problem, please send us an email: kontakt@internet-czas-dzialac.pl
We don't receive issues on Microsoft Github.
Each issue will be reviewed and moved to an internal issues list of our Gitea instance: https://git.internet-czas-dzialac.pl/icd/rentgen/issues. We use Gitea and most likely in the future with the federalization of Gitea, we will be able to let users in to report issues directly from the Gitea site. Each issue will be reviewed and moved to an internal issues list of our Gitea instance: https://git.internet-czas-dzialac.pl/icd/rentgen/issues. We use Gitea and most likely in the future with the federalization of Gitea, we will be able to let users in to report issues directly from the Gitea site.
@ -102,3 +61,54 @@ Each issue will be reviewed and moved to an internal issues list of our Gitea in
<img src="./assets/screenshots/5b.png" /> <img src="./assets/screenshots/5b.png" />
<img src="./assets/screenshots/2022-07-14_21-04.png" /> <img src="./assets/screenshots/2022-07-14_21-04.png" />
---
<strong>Rentgen</strong> to wtyczka dla przeglądarek opartych o Firefoxa, która automatycznie wizualizuje, jakie dane zostały ~~wykradzione~~ wysłane do podmiotów trzecich przez odwiedzane strony. Wtyczka obrazuje ilość skryptów śledzących na stronie internetowej i pomaga w sformułowaniu maila do administratora strony, który może być podstawą do skargi RODO w Urzędzie Ochrony Danych Osobowych.
**Funkcje Rentgena:**
- analiza ruchu sieciowego generowanego przez stronę internetową;
- wizualizacja danych przekazanych do podmiotów trzecich przez odwiedzaną stronę (historia przeglądania użytkownika oraz jego ciasteczka);
- przygotowywanie zrzutów ekranów narzędzi deweloperskich będących dowodem przekazanych danych do podmiotów trzecich;
- pomoc w oszacowaniu potencjalnych obszarów roboczych względem zgodności z RODO;
- generowanie raportu lub treści maila, którą można wysłać do administratora oraz Urzędu Ochrony Danych Osobowych.
## Instalacja
Firefox: https://addons.mozilla.org/pl/firefox/addon/rentgen/
## Jak zbudować i uruchomić Rentgena ze źródeł
### Wymagania wstępne
- System operacyjny: Linux x86_64
- Node.js: 16.x
- npm: 7.x lub wyższy
### Proces budowy
1. Pobierz repozytorium przez `git pull https://git.internet-czas-dzialac.pl/icd/rentgen.git` lub pobierz archwium zip
2. Przejdź do głównego katalogu pobranego repozytorium
3. Uruchom komendę: `npm install`
4. Uruchom komendę: `npm run build`
5. Uruchom komendę: `npm run create-package`
6. Przejdź do katalogu `web-ext-artifacts`
7. Znajdziesz tam archiwum zip: `rentgen-x-x-x.zip` (`x-x-x` oznaczają wersję wtyczki)
### Kroki do uruchomienia
1. Uruchom Firefoxa i przejdź do strony `about:debugging`
2. Kliknij zakładkę _This Firefox_
3. Kliknij przycisk _Load Temporary Add-on..._
4. Wybierz archiwum, które zbudowałeś w ostatnim kroku procesu budowy
## Zgłaszanie błędów
Jeżeli znajdziesz jakieś problem, napisz do nas maila: kontakt@internet-czas-dzialac.pl
Nie przyjmujemy zgłoszeń na platformie Microsoft Github.
Każdy problem zostanie sprawdzony i przeniesiony na wewnętrzną listę problemów na naszej instancji Gitea: https://git.internet-czas-dzialac.pl/icd/rentgen/issues. Korzystamy z Gitea i najprawdopodobniej w przyszłości dzięki federalizacji Gitea będziemy w stanie wpuścić użytkowników do zgłaszania błędów bezpośrednio ze strony Gitea.
---

View File

@ -5,6 +5,7 @@ import { Problem } from './problems/problem';
import { TransferOutsideEU } from './problems/transfer-outside-eu'; import { TransferOutsideEU } from './problems/transfer-outside-eu';
import { UnknownIdentity } from './problems/unknown-identity'; import { UnknownIdentity } from './problems/unknown-identity';
import { UnknownLegalBasis } from './problems/unknown-legal-basis'; import { UnknownLegalBasis } from './problems/unknown-legal-basis';
import { UnknownPurposes } from './problems/unknown-purpose';
import { UnlawfulCookieAccess } from './problems/unlawful-cookies'; import { UnlawfulCookieAccess } from './problems/unlawful-cookies';
export default function deduceProblems( export default function deduceProblems(
@ -13,6 +14,7 @@ export default function deduceProblems(
): Problem[] { ): Problem[] {
return [ return [
NoInformationAtAllProblem, NoInformationAtAllProblem,
UnknownPurposes,
UnlawfulCookieAccess, UnlawfulCookieAccess,
UnknownLegalBasis, UnknownLegalBasis,
UnknownIdentity, UnknownIdentity,

View File

@ -7,6 +7,8 @@ import './email-content.scss';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import emailIntro from './email-intro'; import emailIntro from './email-intro';
import { reportIntro } from './report-intro'; import { reportIntro } from './report-intro';
import { downloadText } from '../../util';
import { getFakeClusterData } from './fake-clusters';
const SS_URL = 'http://65.108.60.135:3000'; const SS_URL = 'http://65.108.60.135:3000';
@ -136,6 +138,23 @@ export default function EmailContent({
</p> </p>
</section> </section>
) : null} ) : null}
<div className="diag-toolbox">
<a
href="#"
onClick={() =>
downloadText(
'diag.json',
JSON.stringify({
answers,
fake_clusters_data: getFakeClusterData(clusters),
visited_url,
})
)
}
>
Pobierz plik diagnostyczny
</a>
</div>
</div> </div>
</Fragment> </Fragment>
); );

View File

@ -9,7 +9,49 @@ export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => J
<p> <p>
Sztucznie wygenerowane identyfikatory przechowywane w plikach Cookies stanowią dane Sztucznie wygenerowane identyfikatory przechowywane w plikach Cookies stanowią dane
osobowe. Wskazuje na to wprost Art. 4. pkt 1. RODO, wymieniając identyfikator osobowe. Wskazuje na to wprost Art. 4. pkt 1. RODO, wymieniając identyfikator
internetowy i numer identyfikacyjny jako przykłady danych osobowych. internetowy i numer identyfikacyjny jako przykłady danych osobowych. Losowe
przypisane identyfikatory mogą nie zawierać imienia i nazwiska osoby, której
dotyczą, ani nie prowadzić wprost do ich ustalenia, ale pozwalają odróżnić jedną,
daną konkretną osobę, od innych.
</p>
<p>
Por. komentarz z D. Lubasz [w:] Ochrona Danych Osobowych [red.] D. Lubasz, Warszawa
2020 r., str. 81:
</p>
<p>
<em>
Zidentyfikowaną osobą fizyczną jest osoba, której tożsamość jest ustalona -
bezpośrednio i natychmiast, czyli taka, którą bezpośrednio można wskazać,
wyodrębnić lub wyróżnić z określonej zbiorowości.{' '}
<strong>Nie musi to natomiast polegać na podaniu jej imienia nazwiska</strong>.
Konstatacja ta jest zwłaszcza istotna w środowisku cyfrowym, w którym
identyfikacja sprowadza się do oznaczenia danego użytkownika w celu wywierania
na niego określonego wpływu. (...) Możliwą do zidentyfikowania jest osoba,
której tożsamość dopiero administrator może ustalić -{' '}
<strong>niezależnie od tego, czy to zrobi, czy nie</strong>.
</em>
</p>
<p>
Podobnie za{' '}
<em>
P. Litwiński [w:] Rozporządzenie UE w sprawie ochrony osób fizycznych w związku
z przetwarzaniem danych osobowych i w sprawie swobodnego przepływu takich
danych, Komentarz [red.] P. Litwiński, Warszawa 2018 r.
</em>
:
</p>
<p>
<em>
Jak zwrócono uwagę w nauce prawa, identyfikacja osoby powinna być rozumiana jako
możliwość fizycznego wskazania tejże osoby, nie zaś jako ustalenie
podstawowych danych tej osoby (...). Analogicznie,{' '}
<em>identyfikacja osoby nie wymaga znajomości jej imienia lub nazwiska</em>,
wymaga natomiast znajomości pewnych unikalnych cech tej osoby, które odróżniają
od innych osób (...). W ten sam sposób należy więc rozumieć zwrot można
zidentyfikować - nie tylko jako możliwość odniesienia konkretnej informacji do
konkretnej osoby, lecz także jako możliwość wskazania tej osoby, rozumianego
jako faktyczne wyodrębnienie jej spośród innych osób.
</em>
</p> </p>
</> </>
), ),

View File

@ -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<string, RequestCluster>
): Record<string, FakeRequestClusterData> {
return Object.fromEntries(
Object.entries(clusters).map(([key, cluster]) => [key, cluster.makeDataForFake()])
);
}
export function makeFakeClusters(
fake_clusters_data: Record<string, FakeRequestClusterData>
): Record<string, FakeCluster> {
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;
}
}

View File

@ -150,7 +150,9 @@ function generateHostPage(
...defaultValue('was_processing_necessary'), ...defaultValue('was_processing_necessary'),
visibleIf: `{${f('legal_basis_type')}} = "legitimate_interest" or {${f( visibleIf: `{${f('legal_basis_type')}} = "legitimate_interest" or {${f(
'present' 'present'
)}} = "not_mentioned" or {popup_type} = "none"`, )}} = "not_mentioned" or {${f(
'present'
)}} = "not_before_making_a_choice" or {popup_type} = "none"`,
choices: [ choices: [
{ value: 'yes', text: 'Tak, było konieczne' }, { value: 'yes', text: 'Tak, było konieczne' },
{ value: 'no', text: 'Nie, nie było konieczne' }, { value: 'no', text: 'Nie, nie było konieczne' },

View File

@ -44,7 +44,7 @@ export default class NoInformationAtAllProblem extends Problem {
<li>Jaki jest cel takiego przetwarzania danych przez Państwa stronę?</li> <li>Jaki jest cel takiego przetwarzania danych przez Państwa stronę?</li>
<li> <li>
Jaka jest podstawa prawna takiego przetwarzania{' '} Jaka jest podstawa prawna takiego przetwarzania{' '}
{mode == 'email' ? _('moich') : ''} danych osobowych $ {mode == 'email' ? _('moich') : ''} danych osobowych
{mode == 'report' ? 'użytkowników końcowych' : ''} przez Państwa stronę? {mode == 'report' ? 'użytkowników końcowych' : ''} przez Państwa stronę?
</li> </li>
</ul> </ul>

View File

@ -5,7 +5,8 @@ import { Problem } from './problem';
export class TransferOutsideEU extends Problem { export class TransferOutsideEU extends Problem {
getNecessaryExplainers(): ExplainerKey[] { getNecessaryExplainers(): ExplainerKey[] {
return []; const has_cookies = this.getRelatedClusters().some((cluster) => cluster.hasCookies());
return has_cookies ? ['cookies_are_pii'] : [];
} }
qualifies(): boolean { qualifies(): boolean {

View File

@ -18,15 +18,12 @@ export class UnknownIdentity extends Problem {
<h2>Tożsamość administratora</h2> <h2>Tożsamość administratora</h2>
{mode == 'email' ? ( {mode == 'email' ? (
<p> <p>
Na Państwa stronie nie {_('znalazłem')} informacji o tym, kto jest Na Państwa stronie nie {_('znalazłem')} sposobu na poznanie tożsamości
administratorem moich danych osobowych przetwarzanych przez Państwa stronę w administratora strony <strong>przed</strong> podjęciem wyboru dotyczącego
trakcie {_('moich')} odwiedzin. przetwarzania danych mnie dotyczących.
</p> </p>
) : ( ) : (
<p> <p>Na stronie brakuje sposobu na poznanie tożsamości administratora strony.</p>
Na stronie brakuje informacji o tym, kto jest administratorem danych
osobowych osób odwiedzających.
</p>
)} )}
<p> <p>
Zgodnie z treścią Art. 13 RODO, jeżeli dane osobowe osoby, której dane dotyczą, Zgodnie z treścią Art. 13 RODO, jeżeli dane osobowe osoby, której dane dotyczą,

View File

@ -21,7 +21,11 @@ const testCluster: (cluster: RequestCluster, answers: ParsedHostAnswers | undefi
export class UnknownLegalBasis extends Problem { export class UnknownLegalBasis extends Problem {
getNecessaryExplainers(): ExplainerKey[] { getNecessaryExplainers(): ExplainerKey[] {
return ['responsibility_for_third_parties']; const has_cookies = this.getRelatedClusters().some((cluster) => cluster.hasCookies());
return [
'responsibility_for_third_parties',
...(has_cookies ? ['cookies_are_pii' as ExplainerKey] : []),
];
} }
qualifies(): boolean { qualifies(): boolean {
@ -70,8 +74,8 @@ export class UnknownLegalBasis extends Problem {
Na stronie nie znajdują się informacje o tym, jaka jest podstawa prawna Na stronie nie znajdują się informacje o tym, jaka jest podstawa prawna
takiego przetwarzania danych osobowych, jakimi jest część historii takiego przetwarzania danych osobowych, jakimi jest część historii
przeglądania. Zgodnie z treścią Artykułu 13. p. 1 lit. c) RODO, aby przeglądania. Zgodnie z treścią Artykułu 13. p. 1 lit. c) RODO, aby
przetwarzać dane osobowe, trzeba poinformować osobę, któ©ej dane dotyczą, o przetwarzać dane osobowe, trzeba poinformować osobę, której dane dotyczą, o
tym, jak ajest podstaw aprawna takiego przetwarzania danych. tym, jaka jest podstawa prawna takiego przetwarzania danych.
</p> </p>
)} )}
{mode == 'email' ? ( {mode == 'email' ? (

View File

@ -0,0 +1,114 @@
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[] {
const has_cookies = this.getAffectedClusters().some((cluster) => cluster.hasCookies());
if (has_cookies) {
return ['cookies_are_pii'];
} else {
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 (
<>
<h2>Cele przetwarzania danych</h2>
<p>
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 właścicielami nastepujących domen:
</p>
<ul>
{affected_clusters.map((cluster, index) => {
const locations = cluster.exposesOriginWhere();
return (
<li>
<strong>{cluster.id}</strong>:{' '}
{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 ? '.' : ';'}
</li>
);
})}
</ul>
{mode === 'email' ? (
tone === 'official' ? (
<p>
Proszę o wskazanie, jakie cele takiego przetwarzania danych, które
mnie dotyczą.
</p>
) : (
<p>
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.
</p>
)
) : (
<p>
<strong>Zalecenie</strong>: warto dodać informacje o tym, jakie cele
ujawniania wyżej opisanych danych wyżej opisanym podmiotom trzecim.
</p>
)}
</>
);
}
}

View File

@ -5,6 +5,17 @@
font-family: 'OpenSans' !important; 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 { html {
font-size: 1rem; font-size: 1rem;
} }
@ -289,3 +300,9 @@ h1 {
color: $ultra-black-color; color: $ultra-black-color;
} }
} }
.diag-toolbox {
position: fixed;
bottom: 10px;
left: 10px;
}

View File

@ -110,7 +110,7 @@ function Report() {
)} )}
</div> </div>
</header> </header>
<section>{result}</section> <section id="main-section">{result}</section>
</Fragment> </Fragment>
); );
} catch (e) { } catch (e) {

View File

@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import { RequestCluster } from '../../request-cluster'; import { RequestCluster } from '../../request-cluster';
import './screenshot-generator.scss'; import './screenshot-generator.scss';
const SS_URL = 'http://65.108.60.135:3000'; const SS_URL = 'https://screenshot-service.internet-czas-dzialac.pl';
enum taskState { enum taskState {
WAITING = 'waiting', WAITING = 'waiting',
@ -38,7 +38,7 @@ interface screenshotTask {
} }
function createTaskEndpoint(visited_url: string, domains: string[]) { function createTaskEndpoint(visited_url: string, domains: string[]) {
return `${SS_URL}/api/requests?url=${visited_url}${domains.reduce( return `${SS_URL}/api/requests?url=${encodeURIComponent(visited_url)}${domains.reduce(
(prev: string, curr: string) => prev + '&domains[]=' + curr, (prev: string, curr: string) => prev + '&domains[]=' + curr,
'' ''
)}`; )}`;

View File

@ -7,6 +7,15 @@
flex-flow: column; flex-flow: column;
border-bottom: none; border-bottom: none;
&__header {
display: flex;
align-items: center;
.icon.cookie-data {
margin-left: 0.25rem;
}
}
.domain-checkbox { .domain-checkbox {
margin-right: 0.5rem; margin-right: 0.5rem;
width: 0.875rem; width: 0.875rem;

View File

@ -2,12 +2,10 @@ import React from 'react';
import { getMemory } from '../../memory'; import { getMemory } from '../../memory';
import { StolenDataEntry } from '../../stolen-data-entry'; import { StolenDataEntry } from '../../stolen-data-entry';
import { maskString, useEmitter } from '../../util'; import { useEmitter } from '../../util';
import './stolen-data-cluster.scss'; import './stolen-data-cluster.scss';
const MAX_STRING_VALUE_LENGTH = 100;
function StolenDataValue({ entry }: { entry: StolenDataEntry; prefixKey?: string }) { function StolenDataValue({ entry }: { entry: StolenDataEntry; prefixKey?: string }) {
const [version] = useEmitter(entry); const [version] = useEmitter(entry);
let body = null; let body = null;
@ -43,13 +41,16 @@ function StolenDataRow({ entry }: { entry: StolenDataEntry }) {
<input <input
type="checkbox" type="checkbox"
checked={entry.isMarked} checked={entry.isMarked}
id={entry.id.toString()}
onChange={() => { onChange={() => {
entry.toggleMark(); entry.toggleMark();
getMemory().emit('change', entry.request.shorthost); getMemory().emit('change', entry.request.shorthost);
}} }}
/> />
</td> </td>
<th title={`Nazwa: ${entry.name}\nŹródło: ${entry.source}`}>{entry.name}</th> <th title={`Nazwa: ${entry.name}\nŹródło: ${entry.source}`}>
<label htmlFor={entry.id.toString()}>{entry.name}</label>
</th>
<td className="icons"> <td className="icons">
{entry.source === 'cookie' ? ( {entry.source === 'cookie' ? (
<span title="Dane przechowywane w Cookies"> <span title="Dane przechowywane w Cookies">
@ -103,13 +104,11 @@ export default function StolenDataCluster({
shorthost, shorthost,
minValueLength, minValueLength,
cookiesOnly, cookiesOnly,
refreshToken,
cookiesOrOriginOnly, cookiesOrOriginOnly,
detailsVisibility, detailsVisibility,
}: { }: {
origin: string; origin: string;
shorthost: string; shorthost: string;
refreshToken: number;
minValueLength: number; minValueLength: number;
cookiesOnly: boolean; cookiesOnly: boolean;
cookiesOrOriginOnly: boolean; cookiesOrOriginOnly: boolean;
@ -122,20 +121,34 @@ export default function StolenDataCluster({
return ( return (
<div className="stolen-data-cluster-container"> <div className="stolen-data-cluster-container">
<header className="domains-container"> <header className="domains-container">
<div> <div className="domains-container__header">
<input <input
type="checkbox" type="checkbox"
className="domain-checkbox" className="domain-checkbox"
data-version={version} data-version={version}
checked={cluster.hasMarks()} checked={cluster.hasMarks()}
onChange={() => { onChange={() => {
console.log('Clicked checkbox!', {
cluster_id: cluster.id,
has_marks: cluster.hasMarks(),
});
cluster.hasMarks() ? cluster.undoMark() : cluster.autoMark(); cluster.hasMarks() ? cluster.undoMark() : cluster.autoMark();
getMemory().emit('change', cluster.id); getMemory().emit('change', cluster.id);
}} }}
/> />
<a className="domain" href={'https://' + cluster.id} target="_blank"> <a className="domain" href={'https://' + cluster.id} target="_blank">
{cluster.id} {cluster.id}
</a> </a>{' '}
{cluster.hasCookies() ? (
<img
src="/assets/icons/cookie.svg"
height={16}
width={16}
className="icon cookie-data"
/>
) : (
''
)}
</div> </div>
<div className="subdomains-container"> <div className="subdomains-container">
{fullHosts.map((host, index) => ( {fullHosts.map((host, index) => (

View File

@ -14,6 +14,15 @@ async function getCurrentTab() {
import './../../styles/global.scss'; import './../../styles/global.scss';
import './toolbar.scss'; import './toolbar.scss';
function isDomainHighlySuspicious(domain: string): boolean {
return (
domain.includes('facebook') ||
domain.includes('twitter') ||
domain.includes('linkedin') ||
false
);
}
const Toolbar = () => { const Toolbar = () => {
const [origin, setOrigin] = React.useState<string | null>(null); const [origin, setOrigin] = React.useState<string | null>(null);
const [eventCounts] = useEmitter(getMemory()); const [eventCounts] = useEmitter(getMemory());
@ -53,6 +62,13 @@ const Toolbar = () => {
if (!origin) return; if (!origin) return;
const exposedOriginDomains = Object.values(getMemory().getClustersForOrigin(origin)) const exposedOriginDomains = Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.exposesOrigin()) .filter((cluster) => cluster.exposesOrigin())
.sort((cluster1, cluster2) =>
isDomainHighlySuspicious(cluster1.id)
? -1
: isDomainHighlySuspicious(cluster2.id)
? 1
: 0
)
.map((cluster) => cluster.id); .map((cluster) => cluster.id);
setExposedOriginDomainCopy(''); setExposedOriginDomainCopy('');
@ -86,6 +102,13 @@ const Toolbar = () => {
if (!origin) return; if (!origin) return;
const cookieDomains = Object.values(getMemory().getClustersForOrigin(origin)) const cookieDomains = Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.hasCookies()) .filter((cluster) => cluster.hasCookies())
.sort((cluster1, cluster2) =>
isDomainHighlySuspicious(cluster1.id)
? -1
: isDomainHighlySuspicious(cluster2.id)
? 1
: 0
)
.map((cluster) => cluster.id); .map((cluster) => cluster.id);
setCookieDomainCopy(''); setCookieDomainCopy('');

13
diag.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>RENTGEN DIAG</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="/node_modules/react/umd/react.production.min.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.production.min.js"></script>
<script src="/node_modules/survey-react/survey.react.min.js"></script>
<script src="./lib/diag.js"></script>
</html>

65
diag.tsx Normal file
View File

@ -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<any, { hasError: boolean; error: any }> {
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 <h1>Something went wrong.</h1>;
}
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 (
<div style={{ display: 'grid', gridTemplateColumns: '50% 50%', minHeight: '100vh' }}>
<div>
<textarea
style={{ width: 'calc(100% - 50px)', height: '100%' }}
value={json}
onChange={(e) => {
setjson(e.target.value);
}}
></textarea>
</div>
<div>
<EmailContent
{...{
answers,
visited_url,
clusters: fake_clusters,
scrRequestPath: '/screenshots',
downloadFiles: () => {
alert('download!');
},
user_role: 'user',
}}
/>
</div>
</div>
);
}
ReactDOM.render(
<ErrorBoundary>
<Diag />
</ErrorBoundary>,
document.getElementById('app')
);

View File

@ -48,6 +48,7 @@ esbuild
'components/sidebar/sidebar.tsx', 'components/sidebar/sidebar.tsx',
'components/report-window/report-window.tsx', 'components/report-window/report-window.tsx',
'background.ts', 'background.ts',
'diag.tsx',
'styles/global.scss', 'styles/global.scss',
'styles/fonts.scss', 'styles/fonts.scss',
], ],

View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { StolenDataEntry } from './stolen-data-entry'; import { DataLocation, StolenDataEntry } from './stolen-data-entry';
import { import {
flattenObjectEntries, flattenObjectEntries,
getshorthost, getshorthost,
@ -164,12 +164,12 @@ export default class ExtendedRequest {
); );
} }
exposesOrigin() { exposesOriginWhere(): null | DataLocation {
const host = this.originalHost; const host = this.originalHost;
const path = this.originalPathname || '/'; const path = this.originalPathname || '/';
const shorthost = getshorthost(host); const shorthost = getshorthost(host);
if (this.getReferer().includes(shorthost)) { if (this.getReferer().includes(shorthost)) {
return true; return { path: this.url, source: 'header', key: 'Referer' };
} }
for (const entry of this.stolenData) { for (const entry of this.stolenData) {
if ( if (
@ -177,10 +177,14 @@ export default class ExtendedRequest {
entry.value.includes(path) || entry.value.includes(path) ||
entry.value.includes(shorthost) entry.value.includes(shorthost)
) { ) {
return true; return entry.toDataLocation();
} }
} }
return false; return null;
}
exposesOrigin() {
return this.exposesOriginWhere() !== null;
} }
private getAllStolenData(): StolenDataEntry[] { private getAllStolenData(): StolenDataEntry[] {
@ -301,6 +305,10 @@ export default class ExtendedRequest {
return this.stolenData.filter((data) => data.isMarked); return this.stolenData.filter((data) => data.isMarked);
} }
unmarkAllEntries() {
this.stolenData.forEach((entry) => entry.unmark());
}
getHost() { getHost() {
return new URL(this.url).host; return new URL(this.url).host;
} }

View File

@ -3,7 +3,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "Rentgen", "name": "Rentgen",
"short_name": "Rentgen", "short_name": "Rentgen",
"version": "0.1.7", "version": "0.1.10",
"author": "Kuba Orlik, Arkadiusz Wieczorek (Internet. Czas działać!)", "author": "Kuba Orlik, Arkadiusz Wieczorek (Internet. Czas działać!)",
"homepage_url": "https://git.internet-czas-dzialac.pl/icd/rentgen", "homepage_url": "https://git.internet-czas-dzialac.pl/icd/rentgen",
"background": { "background": {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "rentgen", "name": "rentgen",
"version": "0.1.7", "version": "0.1.10",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "rentgen", "name": "rentgen",
"version": "0.1.7", "version": "0.1.10",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@iabtcf/core": "^1.3.1", "@iabtcf/core": "^1.3.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "rentgen", "name": "rentgen",
"version": "0.1.7", "version": "0.1.10",
"description": "Rentgen is an add-on prepared for Firefox-based browsers. This extension will automatically visualize all the data that a given website sends to third parties.", "description": "Rentgen is an add-on prepared for Firefox-based browsers. This extension will automatically visualize all the data that a given website sends to third parties.",
"main": "esbuild.config.js", "main": "esbuild.config.js",
"type": "module", "type": "module",

View File

@ -1,6 +1,7 @@
import { FakeRequestClusterData } from './components/report-window/fake-clusters';
import ExtendedRequest from './extended-request'; import ExtendedRequest from './extended-request';
import { SaferEmitter } from './safer-emitter'; 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'; 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, []); 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()); return this.requests.some((request) => request.exposesOrigin());
} }
@ -184,9 +191,7 @@ export class RequestCluster extends SaferEmitter {
undoMark() { undoMark() {
this.calculateRepresentativeStolenData(); this.calculateRepresentativeStolenData();
this.representativeStolenData.forEach((entry) => { this.requests.forEach((request) => request.unmarkAllEntries());
entry.unmark();
});
} }
getDataTypeDescription(noun = 'Twojej') { getDataTypeDescription(noun = 'Twojej') {
@ -203,4 +208,15 @@ export class RequestCluster extends SaferEmitter {
} }
return types_of_data.join(', '); 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(),
};
}
} }

View File

@ -33,6 +33,12 @@ const id = (function* id() {
export type DecodingSchema = 'base64' | 'raw'; export type DecodingSchema = 'base64' | 'raw';
export type DataLocation = {
path: string;
source: Sources;
key: string;
};
export class StolenDataEntry extends SaferEmitter { export class StolenDataEntry extends SaferEmitter {
public isIAB = false; public isIAB = false;
public id: number; public id: number;
@ -253,4 +259,12 @@ export class StolenDataEntry extends SaferEmitter {
haystack.includes(getshorthost(this.request.origin)) haystack.includes(getshorthost(this.request.origin))
); );
} }
toDataLocation(): DataLocation {
return {
path: this.request.url,
source: this.source,
key: this.name,
};
}
} }

36
util.ts
View File

@ -1,5 +1,6 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import React from 'react'; import React from 'react';
import { DataLocation, Sources } from './stolen-data-entry';
export type Unpromisify<T> = T extends Promise<infer R> ? R : T; export type Unpromisify<T> = T extends Promise<infer R> ? R : T;
export type Unarray<T> = T extends Array<infer R> ? R : T; export type Unarray<T> = T extends Array<infer R> ? R : T;
@ -77,7 +78,7 @@ export function useEmitter(
export function parseCookie(cookie: string): Record<string, string> { export function parseCookie(cookie: string): Record<string, string> {
return cookie return cookie
.split(';') .split(';')
.map((l) => l.split('=')) .map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)])
.reduce( .reduce(
(acc, [key, value]) => ({ (acc, [key, value]) => ({
...acc, ...acc,
@ -283,8 +284,37 @@ export function normalizeForClassname(string: string) {
} }
export function wordlist(words: string[]) { export function wordlist(words: string[]) {
return words.reduce( return Array.from(new Set(words)).reduce(
(acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, (acc, word, i) =>
`${acc}${
i > 0 ? (i < words.length - 1 ? ', ' : Math.random() > 0.5 ? ' i ' : ' oraz ') : ''
}${word}`,
'' ''
); );
} }
const source_to_word: Record<Sources, string> = {
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);
}