Compare commits

...

86 Commits

Author SHA1 Message Date
ea70d26a38 Update 'README.md' 2023-09-26 23:05:42 +02:00
1680026bc4 Typos 2023-06-21 17:14:40 +02:00
d5a8172759 Fix typos 2023-06-21 17:14:07 +02:00
039698264a Bardziej podejrzane domeny dawaj na początku w toolbarze 2023-03-22 15:51:07 +01:00
7cf1b95461 Update 'README.md' 2023-02-17 10:37:40 +01:00
799f17eac8 Add mozilla addons link 2023-02-14 20:34:21 +01:00
5c96a7f4cb Bump version 2022-09-25 14:04:51 +02:00
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
3910b5c67e Update version 2022-09-12 11:44:29 +02:00
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
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
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
aae1a7e970 HOTFIX: nie działały URL-e, które mają '#' 2022-09-02 22:07:04 +02:00
3a32456534 Fix $ typo 2022-09-01 07:47:16 +02:00
67544a4bff Finish broken sentence 2022-08-31 21:23:35 +02:00
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
e3d6e6b83c Ikonka cookiesów przy tych domenach, które mają entriesy z cookiesami 2022-08-16 14:08:13 +02:00
cc713004b0 Fix sometimes not being able to uncheck a domain - Fixes #92 2022-08-14 12:55:21 +02:00
ea59a506f8 Problem: nieznany cel. Fixes #68 #97 2022-08-13 22:42:50 +02:00
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
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
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
09dfe8d63a Change domain of screenshot service 2022-07-15 14:14:42 +02:00
Arkadiusz Wieczorek
6ec2722e59 Update package-lock.json 2022-07-15 14:10:31 +02:00
Arkadiusz Wieczorek
78f6dbb7a6 Bump version 2022-07-15 14:09:45 +02:00
Arkadiusz Wieczorek
3d9711b605 Fix typo 2022-07-15 14:06:28 +02:00
Arkadiusz Wieczorek
2e136a7ad5 Bump version 2022-07-15 12:34:38 +02:00
Arkadiusz Wieczorek
09cab7dee3 Bump version 2022-07-15 12:32:56 +02:00
adf82b3dd6 Send to screenshot service only the domains that are marked 2022-07-15 12:11:24 +02:00
520a26923e Reduce live preview flicker. Make the preview the first item 2022-07-15 12:11:11 +02:00
7c4de6fc5c Use thumnbails instead of full screenshots for previews 2022-07-15 12:10:10 +02:00
4163bf3cf7 Change preview width so it fits the entire screenshot 2022-07-15 12:09:29 +02:00
Arkadiusz Wieczorek
4f17c5164a Fix address bar animation 2022-07-15 10:53:26 +02:00
Arkadiusz Wieczorek
1c773becf3 Add live preview 2022-07-15 10:25:20 +02:00
8dc9953818 Delete file 2022-07-14 23:52:19 +02:00
0cebdd78f5 Update 'README.md' 2022-07-14 23:20:25 +02:00
9d711ccdbb Upload files to 'assets/screenshots' 2022-07-14 23:18:44 +02:00
Arkadiusz Wieczorek
fddf8b4de1 Adjust css styles 2022-07-14 23:05:45 +02:00
388f512a1e Podmiana linku do addona 2022-07-14 22:43:46 +02:00
10038467ae Notka informacyjna o screenshotach 2022-07-14 22:43:33 +02:00
057beb3ecd Mention github 2022-07-11 17:07:01 +02:00
d306565376 Wspomnienie o Prawie Telekomunikacyjnym w dropdownie - bo badamy nie
tylko zgodnośc z samym RODO
2022-07-11 15:29:43 +02:00
Arkadiusz Wieczorek
0d9226ad1f Survey fixes 2022-07-10 13:52:48 +02:00
Arkadiusz Wieczorek
137f58f6d1 Update README 2022-07-10 13:34:23 +02:00
Arkadiusz Wieczorek
844e00dd2e Revert commit 2022-07-10 13:32:17 +02:00
Arkadiusz Wieczorek
c53b557a89 Update 2022-07-10 13:31:30 +02:00
Arkadiusz Wieczorek
ecca9766d9 Add info about issue tracker 2022-07-10 13:30:04 +02:00
Arkadiusz Wieczorek
dc2270aade Typo 2022-07-10 13:23:46 +02:00
Arkadiusz Wieczorek
ed46e4ac63 Update README, bump version 2022-07-10 13:21:57 +02:00
94f054df16 Include deps in the zip 2022-07-10 12:32:57 +02:00
Arkadiusz Wieczorek
e31c4faa04 Bump version 2022-07-09 21:54:03 +02:00
Arkadiusz Wieczorek
6198856693 Update README 2022-07-09 21:48:57 +02:00
Arkadiusz Wieczorek
0c05df45cf Update browser style 2022-07-09 21:43:51 +02:00
61e01c5244 Display current action while polling screenshots 2022-07-09 21:28:34 +02:00
f4f287a3bb Fix report not working 2022-07-09 20:20:05 +02:00
Arkadiusz Wieczorek
50701adcf6 Remove wczytywanie... text 2022-07-09 18:05:56 +02:00
Arkadiusz Wieczorek
f6a28256e9 Update README 2022-07-09 17:53:24 +02:00
4a030e47c3 Fix react warnings
Closes #55
2022-07-09 16:52:29 +02:00
8a93cf5463 Fix mozilla linter problems 2022-07-09 16:41:22 +02:00
256d44613f Better documentURL detector. Closes #57 2022-07-09 16:04:52 +02:00
416a6aa340 Fix typechecks. Closes #54 2022-07-09 15:51:34 +02:00
e1d97f0411 typecheck fixes checkpoint 2022-07-09 15:28:37 +02:00
Arkadiusz Wieczorek
c86a3e2c23 Update README 2022-07-09 12:43:39 +02:00
Arkadiusz Wieczorek
d5fc4da1a1 Fix dimmensions 2022-07-09 12:42:28 +02:00
Arkadiusz Wieczorek
ea64bcb846 Update README 2022-07-09 12:40:40 +02:00
Arkadiusz Wieczorek
3c456a7763 Update README 2022-07-09 12:38:23 +02:00
Arkadiusz Wieczorek
4a2fd2677f Update screenshot 2022-07-09 12:37:40 +02:00
Arkadiusz Wieczorek
394886c325 Update README 2022-07-09 12:34:37 +02:00
Arkadiusz Wieczorek
f420195ceb Add screenshots 2022-07-09 12:33:57 +02:00
Arkadiusz Wieczorek
3a40f4db12 Update version and package.json 2022-07-09 10:56:30 +02:00
Arkadiusz Wieczorek
f44daf4909 Cleanup 2022-07-09 10:53:20 +02:00
Arkadiusz Wieczorek
3271af3896 Fix address url in browser on screenshots view 2022-07-08 23:13:57 +02:00
Arkadiusz Wieczorek
fae0a8d8b7 Download buttons for screenshots 2022-07-08 22:42:49 +02:00
895260b096 Add Unknown Identity problem
Closes #10
2022-07-08 21:13:50 +02:00
ebad5ab022 lowercase the verb options 2022-07-08 21:01:55 +02:00
2b763cd7e8 Closes #19 - dodanie alternatywnych templatek (raport vs email) 2022-07-08 20:53:12 +02:00
69115b9e64 Copy na intro 2022-07-08 17:07:11 +02:00
7118a597f1 remove logs 2022-07-08 12:53:34 +02:00
316e4ab50c Typo 2022-07-07 22:18:52 +02:00
3ab97b23aa Add OutsideEU problem, improve wording 2022-07-07 22:10:03 +02:00
f39bfd12b5 Add unknown-legal-basis problem 2022-07-07 21:16:48 +02:00
6e062f92d7 Attempt to fix a problem where previous page request influenced the score of the new page 2022-07-07 20:00:33 +02:00
d512e3cede Attempt to fix a problem with lots of errors being logged in the console 2022-07-07 20:00:00 +02:00
c22c8a9045 Labels for cookies/history were reversed 2022-07-07 19:59:37 +02:00
052eb5eb89 Improve wording and explainers 2022-07-07 19:59:16 +02:00
62 changed files with 5790 additions and 1600 deletions

View File

@ -1,9 +0,0 @@
{
"cSpell.words": [
"ECLI",
"EROD",
"targetowania",
"targetowaniem",
"TSUE"
]
}

117
README.md
View File

@ -1,17 +1,114 @@
<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>
## 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> 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.
<strong>Rentgen</strong> 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.
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.
## How build and run add-on **Features:**
1. Use latest node 16.x and npm 8.x - analysis of web traffic generated by the visited website;
2. `npm install` - visualization of data transmitted to third parties by the visited site (user's browsing history and cookies);
3. `npm run build` - preparation of screenshots of development tools as evidence of data transmitted to third parties;
4. The build code is located in the `lib/` directory - 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.
## Installation
Firefox: https://addons.mozilla.org/en-US/firefox/addon/rentgen/
## How to build and run Rentgen on your own
### Pre-requirements
- OS: Linux x86_64
- Node.js: 16.x version
- npm: 7.x version or higher
### Build steps
1. Pull repository or download a zip package
2. Go to the root directory of the pulled repository
3. Run command: `npm install`
4. Run command: `npm run build`
5. Run command: `npm run create-package`
6. Go to the `web-ext-artifacts` directory
7. You will find a zip archive: `rentgen-x-x-x.zip` (`x-x-x` means add-on version)
### Run steps
1. Run Firefox and go to `about:debugging`
2. Click _This Firefox_ tab
3. Click _Load Temporary Add-on..._ button
4. Pick the zip archive from last step of build process.
## 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.
## Screenshots
<img src="./assets/screenshots/image-14.png" />
<img src="./assets/screenshots/image-15.png" />
<img src="./assets/screenshots/3a.png" />
<img src="./assets/screenshots/3b.png" />
<img src="./assets/screenshots/4a.png" />
<img src="./assets/screenshots/4b.png" />
<img src="./assets/screenshots/5a.png" />
<img src="./assets/screenshots/5b.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.
---

BIN
assets/doctor_welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
assets/screenshots/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

BIN
assets/screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

BIN
assets/screenshots/3a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
assets/screenshots/3b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

BIN
assets/screenshots/4a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
assets/screenshots/4b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
assets/screenshots/5a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

BIN
assets/screenshots/5b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

View File

@ -2,18 +2,24 @@ import { RequestCluster } from '../../request-cluster';
import { ParsedAnswers } from './parse-answers'; import { ParsedAnswers } from './parse-answers';
import NoInformationAtAllProblem from './problems/no-information-at-all'; import NoInformationAtAllProblem from './problems/no-information-at-all';
import { Problem } from './problems/problem'; 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'; import { UnlawfulCookieAccess } from './problems/unlawful-cookies';
export default function deduceProblems( export default function deduceProblems(
answers: ParsedAnswers, answers: ParsedAnswers,
clusters: Record<string, RequestCluster> clusters: Record<string, RequestCluster>
): Problem[] { ): Problem[] {
const problems = []; return [
if (answers.popup_type === 'none') { NoInformationAtAllProblem,
problems.push(new NoInformationAtAllProblem(answers, clusters)); UnknownPurposes,
} UnlawfulCookieAccess,
if (UnlawfulCookieAccess.qualifies(answers, Object.values(clusters))) { UnknownLegalBasis,
problems.push(new UnlawfulCookieAccess(answers, clusters)); UnknownIdentity,
} TransferOutsideEU,
return problems; ]
.map((c) => new c(answers, clusters))
.filter((p) => p.qualifies());
} }

View File

@ -5,12 +5,6 @@ h1 {
margin-bottom: calc(24 / 16 * 1rem); margin-bottom: calc(24 / 16 * 1rem);
} }
.generator-container {
max-width: 100ex;
margin: 0 auto;
font-size: calc(14 / 16 * 1rem);
}
.mail-container { .mail-container {
box-shadow: rgba(12, 12, 13, 0.1) 0px 1px 4px 0px; box-shadow: rgba(12, 12, 13, 0.1) 0px 1px 4px 0px;
background-color: #fff; background-color: #fff;
@ -53,3 +47,28 @@ h1 {
font-weight: 700; font-weight: 700;
} }
} }
.buttons-email-container {
display: grid;
grid-gap: 1rem;
grid-template-columns: 1fr 1fr 1fr 1fr;
margin: 2rem 0;
padding: 1em 0;
&--single {
grid-template-columns: 1fr 1fr 1fr;
}
.sv_prev_btn,
.sv_next_btn {
margin: 0 !important;
}
.sv_prev_btn {
grid-column: 2/3;
}
.sv_next_btn--single {
grid-column: 2;
}
}

View File

@ -1,23 +1,31 @@
import { RequestCluster } from '../../request-cluster'; import { RequestCluster } from '../../request-cluster';
import { getDate } from '../../util';
import deduceProblems from './deduce-problems'; import deduceProblems from './deduce-problems';
import { Explainers } from './explainers'; import { Explainers } from './explainers';
import { ParsedAnswers } from './parse-answers'; import { ParsedAnswers } from './parse-answers';
import { v } from './verbs'; import { v } from './verbs';
import './email-content.scss'; import './email-content.scss';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import emailIntro from './email-intro';
import { reportIntro } from './report-intro';
import { downloadText } from '../../util';
import { getFakeClusterData } from './fake-clusters';
declare var PLUGIN_NAME: string; const SS_URL = 'http://65.108.60.135:3000';
declare var PLUGIN_URL: string;
export default function EmailContent({ export default function EmailContent({
answers, answers,
visited_url, visited_url,
clusters, clusters,
scrRequestPath,
downloadFiles,
user_role,
}: { }: {
answers: ParsedAnswers; answers: ParsedAnswers;
visited_url: string; visited_url: string;
clusters: Record<string, RequestCluster>; clusters: Record<string, RequestCluster>;
scrRequestPath: string;
downloadFiles: Function;
user_role: string;
}) { }) {
const _ = (key: string) => v(key, answers.zaimek); const _ = (key: string) => v(key, answers.zaimek);
const problems = deduceProblems(answers, clusters); const problems = deduceProblems(answers, clusters);
@ -33,56 +41,92 @@ export default function EmailContent({
function copyTextToClipboard() { function copyTextToClipboard() {
// Should be changed in the future to Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write#browser_compatibility) // Should be changed in the future to Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write#browser_compatibility)
let r = document.createRange(); let r = document.createRange();
r.selectNode(document.querySelector('.mail-container__content')); const container = document.querySelector('.mail-container__content');
window.getSelection().addRange(r); if (!container) return;
r.selectNode(container);
window.getSelection()?.addRange(r);
document.execCommand('copy'); document.execCommand('copy');
window.getSelection().removeAllRanges(); window.getSelection()?.removeAllRanges();
setCopy(true); setCopy(true);
} }
const mode = answers.user_role === 'user' ? 'email' : 'report';
const email_tone = answers.email_type === 'polite_information' ? 'polite' : 'official';
return ( return (
<Fragment> <Fragment>
<div className="generator-container"> <div className="generator-container">
<h1>Treść maila</h1> <h1>Treść {mode === 'email' ? 'maila' : 'raportu'}</h1>
<div className="mail-container"> <div className="mail-container">
<div className="mail-container__header"> <div className="mail-container__header">
<div className="mail-container__header--control"></div> <div className="mail-container__header--control"></div>
</div> </div>
<article className="mail-container__content"> <article className="mail-container__content">
<p>Dzień dobry,</p> {mode === 'email'
<p> ? emailIntro(email_tone, _, visited_url)
w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po : reportIntro(visited_url)}
podejrzeniu ruchu sieciowego generowanego przez stronę za pomocą {problems.map((problem, index) => {
wtyczki <a href={PLUGIN_URL}>{PLUGIN_NAME}</a> w przeglądarce Firefox{' '} const Component = problem.getEmailContent.bind(problem);
{_('mam')} pytania dotyczące przetwarzania {_('moich')} danych return <Component mode={mode} tone={email_tone} key={index} />;
osobowych, na które nie {_('znalazłem')} odpowiedzi nigdzie na Państwa })}
stronie.
</p>
{problems.map((problem) => problem.getEmailContent())}
{explainers.map((explainer) => explainer(answers.zaimek))} {explainers.map((explainer) => explainer(answers.zaimek))}
<h2>Państwa rola jako współadministratora danych osobowych</h2> <h2>Państwa rola jako współadministratora danych osobowych</h2>
<p> {mode == 'email' ? (
{_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '} <p>
<a href="https://curia.europa.eu/juris/document/document.jsf?text=&docid=216555&pageIndex=0&doclang=PL&mode=lst&dir=&occ=first&part=1&cid=1254905"> {_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '}
treści wyroku TSUE w sprawie C-40/17 <a href="https://curia.europa.eu/juris/document/document.jsf?text=&docid=216555&pageIndex=0&doclang=PL&mode=lst&dir=&occ=first&part=1&cid=1254905">
</a>{' '} treści wyroku TSUE w sprawie C-40/17
poprzez wysyłanie moich danych w wyżej opisanym zakresie stają się </a>{' '}
Państwo współadministratorem moich danych osobowych, dlatego ciąży na poprzez wysyłanie moich danych w wyżej opisanym zakresie stają się
Państwu obowiązek odpowiedzi na moje pytania na mocy Art. 12 i 13 Państwo współadministratorem {_('moich')} danych osobowych, nawet
Rozporządzenia 2016/679 Parlamentu Europejskiego i Rady (UE) z dnia 27 jeżeli nie Państwo bezpośrednimi autorami osadzonych na Państwa
kwietnia 2016 r. w sprawie ochrony osób fizycznych w związku z stronie skryptów czy innych zasobów ujawniających dane użytkowników
przetwarzaniem danych osobowych i w sprawie swobodnego przepływu takich Państwa strony podmiotom trzecim. Dlatego ciąży na Państwu obowiązek
danych oraz uchylenia dyrektywy 95/46/WE (ogólne rozporządzenie o odpowiedzi na {_('moje')} pytania na mocy Art. 12 i 13
ochronie danych, dalej: RODO). Rozporządzenia 2016/679 Parlamentu Europejskiego i Rady (UE) z dnia
</p> 27 kwietnia 2016 r. w sprawie ochrony osób fizycznych w związku z
przetwarzaniem danych osobowych i w sprawie swobodnego przepływu
takich danych oraz uchylenia dyrektywy 95/46/WE (ogólne
rozporządzenie o ochronie danych) RODO
</p>
) : (
<p>
W myśl{' '}
<a href="https://curia.europa.eu/juris/document/document.jsf?text=&docid=216555&pageIndex=0&doclang=PL&mode=lst&dir=&occ=first&part=1&cid=1254905">
treści wyroku TSUE w sprawie C-40/17
</a>
, ponoszą Państwo współodpowiedzialność za skrypty i inne zasoby
ujawniajace dane osobowe na Państwa stronie, nawet jeżeli nie
Państwo ich bezpośrednimi autorami.
</p>
)}
</article> </article>
</div> </div>
<div className="buttons-container"> <div
<button className="sv_next_btn" onClick={() => copyTextToClipboard()}> className={
{copied ? 'Skopiowano!' : 'Kopiuj treść wiadomości'} scrRequestPath
? 'buttons-email-container'
: 'buttons-email-container buttons-email-container--single'
}
>
{scrRequestPath ? (
<button
className="sv_prev_btn"
onClick={() => downloadFiles(`${SS_URL}${scrRequestPath}`)}
>
Pobierz zrzuty ekranów
</button>
) : null}
<button
className={
scrRequestPath ? 'sv_next_btn' : 'sv_next_btn sv_next_btn--single'
}
onClick={() => copyTextToClipboard()}
>
{copied ? 'Skopiowano!' : 'Kopiuj treść'}
</button> </button>
</div> </div>
{copied ? ( {copied && user_role === 'user' ? (
<section className="greeting-text"> <section className="greeting-text">
<strong>Przed Tobą ostatni krok! 😊</strong> <strong>Przed Tobą ostatni krok! 😊</strong>
<p> <p>
@ -94,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

@ -0,0 +1,32 @@
import { getDate } from '../../util';
declare var PLUGIN_NAME: string;
declare var PLUGIN_URL: string;
export default function emailIntro(
tone: 'polite' | 'official',
_: (verb: string) => string,
visited_url: string
) {
return (
<>
<p>{tone == 'polite' ? 'Szanowni Państwo' : 'Dzień dobry'},</p>
<p>
w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po podejrzeniu ruchu
sieciowego generowanego przez stronę za pomocą wtyczki{' '}
<a href={PLUGIN_URL}>{PLUGIN_NAME}</a> w przeglądarce Firefox{' '}
{tone == 'polite' ? (
<>
{_('chciałbym')} zwrócić Państwa uwagę na kilka potencjalnych problemów ze
zgodnością RODO na Państwa stronie.
</>
) : (
<>
{_('mam')} pytania dotyczące przetwarzania {_('moich')} danych osobowych, na
które nie {_('znalazłem')} odpowiedzi nigdzie na Państwa stronie.
</>
)}
</p>
</>
);
}

View File

@ -1,4 +1,6 @@
export type ExplainerKey = 'cookies_are_pii'; // various explainers that could be related to multiple problems. They are gathered here and added at the end of the email to avoid pasting them multiple times
export type ExplainerKey = 'cookies_are_pii' | 'responsibility_for_third_parties';
export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => JSX.Element> = { export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => JSX.Element> = {
cookies_are_pii: () => ( cookies_are_pii: () => (
@ -7,7 +9,63 @@ 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>
</>
),
responsibility_for_third_parties: () => (
<>
<h2>Administrator strony ponosi odpowiedzialność za skrypty podmiotów trzecich</h2>
<p>
W wypadku, gdy ujawnienie czy dostęp do danych osobowych zostało dokonane przez
skrypty podmiotów trzecich (np. Google, Facebook, itp), których autorem nie jest
Administrator strony, Administrator wciąż jest odpowiedzialny za procesy
przetwarzania danych osobowych, jakie realizują te skrypty - w myśl treści{' '}
<a href="https://curia.europa.eu/juris/document/document.jsf?text=&docid=216555&pageIndex=0&doclang=PL&mode=lst&dir=&occ=first&part=1&cid=1254905">
wyroku TSUE w sprawie C-40/17
</a>
</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

@ -1,26 +1,30 @@
import { RequestCluster } from '../../request-cluster';
function generateHostPage( function generateHostPage(
host: string, cluster: RequestCluster,
index: number, index: number,
all_hosts: string[] all_clusters: RequestCluster[]
): { title: string; elements: any[]; visibleIf: string } { ): { title: string; elements: any[]; visibleIf?: string } {
function f(name: string, h = host) { function f(name: string, c = cluster) {
return `${h.replace(/\./g, '_')}|${name}`; return `${c.id.replace(/\./g, '_')}|${name}`;
} }
const previous_host: string | null = index > 0 ? all_hosts[index - 1] : null; const previous_cluster: RequestCluster | null = index > 0 ? all_clusters[index - 1] : null;
function defaultValue(name: string) { function defaultValue(name: string) {
if (!previous_host) { if (!previous_cluster) {
return {}; return {};
} }
return { defaultValueExpression: `{${f(name, previous_host)}}` }; return { defaultValueExpression: `{${f(name, previous_cluster)}}` };
} }
const domain = cluster.id;
const danych = cluster.getDataTypeDescription();
return { return {
title: host, title: cluster.id,
elements: [ elements: [
{ {
type: 'radiogroup', type: 'radiogroup',
name: f('present'), name: f('present'),
isRequired: true, isRequired: true,
title: `Cel ujawnienia danych właścicielowi domeny ${host}`, title: `Strona udostępniła właścicielowi domeny ${domain} ${danych}. Cel takiego przetwarzania danych:`,
...defaultValue('present'), ...defaultValue('present'),
visibleIf: '{popup_type} != "none"', visibleIf: '{popup_type} != "none"',
choices: [ choices: [
@ -56,10 +60,10 @@ function generateHostPage(
'present' 'present'
)}} != "not_mentioned" and {${f('present')}} != "not_before_making_a_choice"`, )}} != "not_mentioned" and {${f('present')}} != "not_before_making_a_choice"`,
choices: [ choices: [
{ value: 'consent', text: 'to zgoda.' }, { value: 'consent', text: 'to zgoda (art. 6 ust. 1 lit. a RODO).' },
{ {
value: 'legitimate_interest', value: 'legitimate_interest',
text: 'to uzasadniony interes.', text: 'to uzasadniony interes (art. 6 ust. 1 lit. f RODO).',
}, },
{ value: 'not_mentioned', text: 'nie jest wskazana nigdzie na stronie.' }, { value: 'not_mentioned', text: 'nie jest wskazana nigdzie na stronie.' },
], ],
@ -76,7 +80,7 @@ function generateHostPage(
choices: [ choices: [
{ {
value: 'claims_consent_but_sends_before_consent', value: 'claims_consent_but_sends_before_consent',
text: `Strona wysłała {moje} dane do ${host} zanim {wyraziłem} na to zgodę`, text: `Strona wysłała {moje} dane do ${domain} zanim {wyraziłem} na to zgodę`,
}, },
{ {
value: 'claims_consent_but_there_was_no_easy_refuse', value: 'claims_consent_but_there_was_no_easy_refuse',
@ -111,18 +115,21 @@ function generateHostPage(
}, },
{ {
type: 'text', type: 'text',
title: `Jak administrator opisał to, na czym polega uzasadniony interes w kontekście ${host}?`, title: `Jak administrator opisał to, na czym polega uzasadniony interes w kontekście ${domain}?`,
name: f('legitimate_interest_description'), name: f('legitimate_interest_description'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = 'vague'`, visibleIf: `{${f('legitimate_interest_activity_specified')}} = 'vague'`,
placeholder: 'marketing', placeholder: 'marketing',
defaultValueExpression: defaultValueExpression:
index == 0 index == 0
? 'marketing' ? 'marketing'
: `{${f('legitimate_interest_description', previous_host)}}`, : `{${f(
'legitimate_interest_description',
previous_cluster || undefined
)}}`,
}, },
{ {
type: 'radiogroup', type: 'radiogroup',
title: `Czy domena ${host} należy do podmiotu spoza Europy (np. Google, Facebook)?`, title: `Czy domena ${domain} należy do podmiotu spoza Europy (np. Google, Facebook)?`,
name: f('outside_eu'), name: f('outside_eu'),
...defaultValue('outside_eu'), ...defaultValue('outside_eu'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = "precise" or {${f( visibleIf: `{${f('legitimate_interest_activity_specified')}} = "precise" or {${f(
@ -137,13 +144,15 @@ function generateHostPage(
}, },
{ {
type: 'radiogroup', type: 'radiogroup',
title: `Czy w {Twojej} ocenie wysłanie {Twoich} danych do właściciela domeny ${host} było konieczne do świadczenia zażądanej przez {Ciebie} usługi drogą elektroniczną?`, title: `Czy w {Twojej} ocenie ujawnienie {Twoich} danych (${danych}) właścicielowi domeny ${domain} było konieczne do świadczenia zażądanej przez {Ciebie} usługi drogą elektroniczną?`,
name: f('was_processing_necessary'), name: f('was_processing_necessary'),
isRequired: true, isRequired: true,
...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' },
@ -154,7 +163,7 @@ function generateHostPage(
}; };
} }
export default function generateSurveyQuestions(hosts: string[]) { export default function generateSurveyQuestions(clusters: RequestCluster[]) {
return { return {
showQuestionNumbers: 'off', showQuestionNumbers: 'off',
showProgressBar: 'top', showProgressBar: 'top',
@ -165,28 +174,60 @@ export default function generateSurveyQuestions(hosts: string[]) {
clearInvisibleValues: 'onHidden', clearInvisibleValues: 'onHidden',
pages: [ pages: [
{ {
title: 'Tytuł - co to za ankieta?', title: 'Dodatkowe pytania',
elements: [ elements: [
{ {
type: 'html', type: 'html',
name: 'intro', name: 'intro',
html: '<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab, odit dicta at aut esse culpa eveniet iure odio voluptates veniam sit. Libero explicabo, perspiciatis ad expedita officiis inventore impedit ducimus!</p>', html: /* HTML */ `<p>
Analiza ruchu sieciowego generowanego przez stronę została zakończona.
Teraz, aby lepiej oszacować, gdzie potencjalne obszary robocze pod
względem zgodności z RODO, możesz udzielić odpowiedzi na pytania
dotyczące funkcjonowania strony. Wtyczka wtedy wygeneruje raport lub
treść maila, którą możesz wysłać do administratora strony.
</p>`,
}, },
], ],
}, },
{ {
title: 'Zaimki', title: 'Kontekst analizy',
elements: [ elements: [
{ {
type: 'radiogroup', type: 'radiogroup',
name: 'zaimek', name: 'user_role',
title: 'Forma czasownika:', title: 'Jestem:',
isRequired: true, isRequired: true,
choices: [ choices: [
{ value: 0, text: 'Wysłałem' }, { value: 'user', text: 'użytkownikiem strony' },
{ value: 1, text: 'Wysłałam' }, { value: 'admin', text: 'administratorem strony' },
{ value: 2, text: 'Wysłałom' }, ],
{ value: 3, text: 'Wysłaliśmy' }, },
{
type: 'radiogroup',
name: 'email_type',
title: 'Chcę:',
visibleIf: "{user_role} = 'user'",
choices: [
{
value: 'polite_information',
text: 'uprzejmie poinformować administratora strony o potencjalnych problemach ze zgodnością z RODO na jego stronie',
},
{
value: 'official_request',
text: 'wysłać formalne zapytanie do administratora strony, na które ma obowiązek odpowiedzieć. Jeżeli administrator nie odpowie na takie zapytanie, może to być podstawą złożenia skargi do UODO',
},
],
},
{
type: 'radiogroup',
name: 'zaimek',
title: 'Forma czasownika, jaka będzie użyta w raporcie:',
isRequired: true,
choices: [
{ value: 0, text: 'wysłałem' },
{ value: 1, text: 'wysłałam' },
{ value: 2, text: 'wysłałom' },
{ value: 3, text: 'wysłaliśmy' },
], ],
}, },
], ],
@ -302,7 +343,7 @@ export default function generateSurveyQuestions(hosts: string[]) {
}, },
{ {
value: 'yes', value: 'yes',
text: 'Nie. {Muszę} wykonać więcej czynności aby odmówić wszystkich zgód, albo opcja niewyrażenia zgody jest mało widoczna.', text: 'Nie. {Muszę} wykonać więcej czynności (np. kliknięć) aby odmówić wszystkich zgód, albo opcja niewyrażenia zgody jest mało widoczna.',
}, },
], ],
}, },
@ -393,7 +434,7 @@ export default function generateSurveyQuestions(hosts: string[]) {
}, },
], ],
}, },
...hosts.map(generateHostPage), ...clusters.map(generateHostPage),
], ],
}; };
} }

View File

@ -16,7 +16,9 @@ function handleNewFile(
); );
setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' })); setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' }));
}); });
reader.readAsText(element.files[0]); const file = element?.files?.[0];
if (!file) throw new Error('file empty?');
reader.readAsText(file);
} }
function generateFakeHAR(entries: StolenDataEntry[]) { function generateFakeHAR(entries: StolenDataEntry[]) {
@ -80,8 +82,11 @@ export default function HARConverter({ entries }: { entries: StolenDataEntry[] }
type="file" type="file"
accept=".har" accept=".har"
onChange={(e) => { onChange={(e) => {
setFilename(e.target.files[0].name); const file = e.target?.files?.[0];
handleNewFile(e.target, entries, setFiltered); if (file) {
setFilename(file.name);
handleNewFile(e.target, entries, setFiltered);
}
}} }}
/> />
{(filtered && ( {(filtered && (
@ -100,7 +105,7 @@ export default function HARConverter({ entries }: { entries: StolenDataEntry[] }
}) })
)} )}
download={`${getshorthost( download={`${getshorthost(
entries[0].request.originalURL entries[0].request.origin
)}-${new Date().toJSON()}-trimmed.har`} )}-${new Date().toJSON()}-trimmed.har`}
> >
Pobierz "zredukowany" HAR Pobierz "zredukowany" HAR

View File

@ -42,6 +42,8 @@ function parseHostAnswers(
export function parseAnswers({ export function parseAnswers({
zaimek, zaimek,
user_role,
email_type,
is_incognito_different, is_incognito_different,
policy_readable, policy_readable,
popup_type, popup_type,
@ -50,10 +52,13 @@ export function parseAnswers({
mentions_passive_consent, mentions_passive_consent,
rejection_is_hard, rejection_is_hard,
administrator_identity_available_before_choice, administrator_identity_available_before_choice,
popup_action,
...rest ...rest
}: RawAnswers): ParsedAnswers { }: RawAnswers): ParsedAnswers {
return { return {
zaimek, zaimek,
user_role,
email_type,
is_incognito_different, is_incognito_different,
policy_readable, policy_readable,
popup_type, popup_type,
@ -62,6 +67,7 @@ export function parseAnswers({
mentions_passive_consent, mentions_passive_consent,
rejection_is_hard, rejection_is_hard,
administrator_identity_available_before_choice, administrator_identity_available_before_choice,
popup_action,
hosts: parseHostAnswers(rest), hosts: parseHostAnswers(rest),
} as ParsedAnswers; } as ParsedAnswers;
} }

View File

@ -3,27 +3,49 @@ import { v } from '../verbs';
import { Problem } from './problem'; import { Problem } from './problem';
export default class NoInformationAtAllProblem extends Problem { export default class NoInformationAtAllProblem extends Problem {
getEmailContent() { qualifies() {
return this.answers.popup_type === 'none';
}
getEmailContent({ mode, tone }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) {
const _ = (word: string) => v(word, this.answers.zaimek); const _ = (word: string) => v(word, this.answers.zaimek);
return ( return (
<> <>
<h2>Brak informacji na temat przetwarzania danych osobowych</h2> <h2>Brak informacji na temat przetwarzania danych osobowych</h2>
<p> {mode == 'email' ? (
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami tone == 'official' ? (
domen: <p>
</p> {_('Moje')} dane osobowe zostały ujawnione podmiotom, które
właścicielami domen:
</p>
) : (
<p>
Państwa strona ujawnia dane użytkowników podmiotom, które
właścicielami następujących domen:
</p>
)
) : (
<p>
Poprzez skrypty osadzone na stronie dane osobowe użytkownika końcowego
przekazywane podmiotom, którzy właścicielami następujacych domen:
</p>
)}
{this.getRangeDescription()} {this.getRangeDescription()}
<p> <p>
Na stronie brakuje jednak jakichkolwiek informacji o tym, jakie cele Na stronie brakuje jednak jakichkolwiek informacji o tym, jakie cele
przetwarzania takich danych oraz jakie podstawy prawne takiego przetwarzania. przetwarzania takich danych oraz jakie podstawy prawne takiego przetwarzania.
</p> </p>
<p>Zwracam się zatem do Państwa z następującymi pytaniami:</p> {mode == 'email' ? (
<p>Zwracam się zatem do Państwa z następującymi pytaniami:</p>
) : (
<p>Na stronie należy zawrzeć odpowiedzi na następujące pytania:</p>
)}
<ul> <ul>
<li>Jaka jest tożsamość właścicieli tych domen?</li> <li>Jaka jest tożsamość właścicieli tych domen?</li>
<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 moich danych osobowych przez Jaka jest podstawa prawna takiego przetwarzania{' '}
Państwa stronę? {mode == 'email' ? _('moich') : ''} danych osobowych
{mode == 'report' ? 'użytkowników końcowych' : ''} przez Państwa stronę?
</li> </li>
</ul> </ul>
</> </>
@ -34,11 +56,11 @@ export default class NoInformationAtAllProblem extends Problem {
if ( if (
this.getMarkedClusters().some((cluster) => { this.getMarkedClusters().some((cluster) => {
console.log(cluster);
return cluster.hasMarkedCookies(); return cluster.hasMarkedCookies();
}) })
) { ) {
explainers.push('cookies_are_pii'); explainers.push('cookies_are_pii');
explainers.push('responsibility_for_third_parties');
} }
return explainers; return explainers;
} }

View File

@ -4,7 +4,6 @@ import { ParsedAnswers } from '../parse-answers';
function formatRange(cluster: RequestCluster) { function formatRange(cluster: RequestCluster) {
const parts = [] as string[]; const parts = [] as string[];
console.log(cluster);
if (cluster.hasMarkedCookies()) { if (cluster.hasMarkedCookies()) {
parts.push('mojego identyfikatora internetowego pozyskanego z Cookie'); parts.push('mojego identyfikatora internetowego pozyskanego z Cookie');
} }
@ -17,8 +16,12 @@ function formatRange(cluster: RequestCluster) {
export abstract class Problem { export abstract class Problem {
constructor(public answers: ParsedAnswers, public clusters: Record<string, RequestCluster>) {} constructor(public answers: ParsedAnswers, public clusters: Record<string, RequestCluster>) {}
abstract getEmailContent(): JSX.Element; abstract getEmailContent(props: {
mode: 'email' | 'report';
tone: 'polite' | 'official';
}): JSX.Element;
abstract getNecessaryExplainers(): ExplainerKey[]; abstract getNecessaryExplainers(): ExplainerKey[];
abstract qualifies(): boolean;
getMarkedClusters() { getMarkedClusters() {
return Object.values(this.clusters).filter((c) => c.hasMarks()); return Object.values(this.clusters).filter((c) => c.hasMarks());

View File

@ -0,0 +1,90 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { v } from '../verbs';
import { Problem } from './problem';
export class TransferOutsideEU extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
const has_cookies = this.getRelatedClusters().some((cluster) => cluster.hasCookies());
return has_cookies ? ['cookies_are_pii'] : [];
}
qualifies(): boolean {
return Object.values(this.answers.hosts).some(
(hostAnswers) => hostAnswers.outside_eu == 'yes'
);
}
getRelatedClusters(): RequestCluster[] {
return Object.entries(this.answers.hosts)
.filter(([_, hostAnswers]) => hostAnswers.outside_eu == 'yes')
.map(([id]) => this.clusters[id]);
}
getEmailContent({ mode }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) {
const clusters = this.getRelatedClusters();
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Transfer danych osobowych poza Europejski Obszar Gospodarczy</h2>
{mode == 'email' ? (
<p>
Państwa strona przetworzyła {_('moje')} dane osobowe poprzez przesłanie
danych do:
</p>
) : (
<p>
Strona przetwarza dane osobowe użytkowników końcowych poprzez przesłanie
przekazanie ich do:
</p>
)}
<ul>
{clusters.map((cluster) => (
<li key={cluster.id}>
właściciela domeny <strong>{cluster.id}</strong>: (w zakresie:{' '}
{cluster.getDataTypeDescription(mode == 'email' ? 'mojej' : '')});
</li>
))}
</ul>
{mode == 'email' ? (
<p>
Według {_('mojej')} najlepszej wiedzy, każdy z tych podmiotów utrzymuje
swoje serwery poza Europejskim Obszarem Gospodarczym. Zatem Państwa strona
przesłała
{_('moje')} dane osobowe poza EOG. Jeżeli tak jest, to takie przetwarzanie
danych jest niezgodne z prawem, gdyż dane trafiają do krajów, które nie
gwarantują ochrony danych w stopniu, jakiego wymaga RODO, a tzw. Tarcza
Prywatności została unieważniona w 2020r. Zob.{' '}
<a href="https://panoptykon.org/noyb-skargi-schrems-ii">
artykuł Fundacji Panoptykon w tej sprawie
</a>
.
</p>
) : (
<p>
Te podmioty utrzymują swoje centra danych poza Europejskim Obszarem
Gospodarczym. Jako, że tzw. Tarcza Prywatności zostałą unieważniona w
2020r., nie można przesyłać danych osobowych obywateli Unii Europejskiej do
krajów, które nie zapewniają ochrony danych o sile odpowiadającej RODO.
Przykłądem kraju, do którego nie można przekazywać danych osobowych
obywateli UE Stany Zjednoczone.
</p>
)}
{mode == 'email' ? (
<p>
{_('Zwracam')} się zatem do Państwa z pytaniem:{' '}
<strong>
czy wyżej wymienione podmioty, którym Państwa strona ujawniła moje dane
osobowe, przechowują moje dane poza EOG?
</strong>
</p>
) : (
<p>
Zaleca się rezygnację z korzystania z usług firm, które przetwarzają dane
osobowe użytkowników, a których centra danych znajdują się poza EOG.
</p>
)}
</>
);
}
}

View File

@ -0,0 +1,55 @@
import { ExplainerKey } from '../explainers';
import { v } from '../verbs';
import { Problem } from './problem';
export class UnknownIdentity extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
return ['responsibility_for_third_parties'];
}
qualifies(): boolean {
return this.answers.administrator_identity_available_before_choice == 'no';
}
getEmailContent({ mode, tone }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) {
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Tożsamość administratora</h2>
{mode == 'email' ? (
<p>
Na Państwa stronie nie {_('znalazłem')} sposobu na poznanie tożsamości
administratora strony <strong>przed</strong> podjęciem wyboru dotyczącego
przetwarzania danych mnie dotyczących.
</p>
) : (
<p>Na stronie brakuje sposobu na poznanie tożsamości administratora strony.</p>
)}
<p>
Zgodnie z treścią Art. 13 RODO, jeżeli dane osobowe osoby, której dane dotyczą,
zbierane od tej osoby, administrator podczas pozyskiwania danych osobowych
musi podać jej swoją tożsamość i dane kontaktowe.
</p>
{mode == 'email' ? (
tone == 'official' ? (
<p>
Zwracam się zatem z pytaniem:{' '}
<strong>jaka jest tożsamość administratora tej strony?</strong>
</p>
) : (
<p>
Apeluję o dodanie do Państwa strony informacji o tym, kto (np. pełna
nazwa firmy + NIP oraz dane kontaktowe) jest administratorem danych
osobowych przetwarzanych przez stronę.
</p>
)
) : (
<p>
Zalecane jest dodanie informacji o administratorze strony (pełna nazwa firmy
+ NIP i dane kontaktowe) w łatwo dostępnym miejscu na stronie.
</p>
)}
</>
);
}
}

View File

@ -0,0 +1,121 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedHostAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
const testCluster: (cluster: RequestCluster, answers: ParsedHostAnswers | undefined) => boolean = (
cluster,
hostAnswers
) => {
if (!hostAnswers) {
return false;
}
if (cluster.hasMarkedCookies()) {
/* if it has cookies, it will be picked up by the UnlawfulCookieAccess problem, and that one
is pretty detailed, so no need to mention it here. */
return false;
}
return hostAnswers.legal_basis_type == 'not_mentioned';
};
export class UnknownLegalBasis extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
const has_cookies = this.getRelatedClusters().some((cluster) => cluster.hasCookies());
return [
'responsibility_for_third_parties',
...(has_cookies ? ['cookies_are_pii' as ExplainerKey] : []),
];
}
qualifies(): boolean {
return Object.values(this.clusters).some((cluster) =>
testCluster(cluster, this.answers.hosts[cluster.id])
);
}
getRelatedClusters() {
return Object.values(this.clusters).filter((cluster) =>
testCluster(cluster, this.answers.hosts[cluster.id])
);
}
getEmailContent({ mode, tone }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) {
const clusters = this.getRelatedClusters();
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Przetwarzanie danych osobowych bez podania podstawy prawnej</h2>
{mode == 'email' ? (
<p>Państwa strona przetworzyła {_('moje')} dane osobowe poprzez ujawnienie:</p>
) : (
<p>Państwa strona przetwarza dane osobowe użytkowników poprzez ujawnienie</p>
)}
<ul>
{clusters.map((cluster) => (
<li key={cluster.id}>
właścicielowi domeny <strong>{cluster.id}</strong>:{' '}
{cluster.getDataTypeDescription(mode == 'email' ? 'mojej' : '')}
</li>
))}
</ul>
{mode == 'email' ? (
<p>
{_('Moja')} historia przeglądania stanowi {_('moje')} dane osobowe. Zgodnie
z treścią Artykułu 13 p. 1 lit. c){' '}
<a href="https://eur-lex.europa.eu/legal-content/PL/TXT/HTML/?uri=CELEX:32016R0679&qid=1632163985520&from=PL#d1e1822-1-1">
RODO
</a>
, aby przetwarzać dane osobowe, trzeba poinformować osobę, której dane
dotyczą, o tym, jaka jest podstawa prawna takiego przetwarzania danych.
</p>
) : (
<p>
Na stronie nie znajdują się informacje o tym, jaka jest podstawa prawna
takiego przetwarzania danych osobowych, jakimi jest część historii
przeglądania. Zgodnie z treścią Artykułu 13. p. 1 lit. c) RODO, aby
przetwarzać dane osobowe, trzeba poinformować osobę, której dane dotyczą, o
tym, jaka jest podstawa prawna takiego przetwarzania danych.
</p>
)}
{mode == 'email' ? (
tone == 'official' ? (
<p>
Zwracam się zatem z pytaniem:{' '}
<strong>
jakie były podstawy prawne ujawnienia moich danych każdemu z wyżej
wymienionych podmiotów przez Państwa stronę?
</strong>
</p>
) : (
<p>
Dodanie do Państwa strony informacji o tym, jakie podstawy prawne (w
znaczeniu Art. 6 pkt. 1 RODO) dla każdego z tych procesów przetwarzania
miałoby pozytywny wpływ na przejrzystość informacji dla użytkowników
końcowych, jak i na zgodność strony z obowiązującymi przepisami.
</p>
)
) : (
<>
<p>Możliwe działania:</p>
<ul>
<li>rezygnacja z niektórych skryptów śledzących;</li>
<li>
przeniesienie assetów z CDN-a na samohostowanie (przy korzystaniu z
HTTP2 to może dać zwiększoną wydajność wzgłędem CDN);
</li>
<li>
konfiguracja nagłówka{' '}
<a href="https://developer.mozilla.org/pl/docs/Web/HTTP/Headers/Referrer-Policy">
Referrer-Policy
</a>{' '}
tak, aby nie ujawniać historii przeglądania właścicielom zasobów z
domen podmiotów trzecich.
</li>
</ul>
</>
)}
</>
);
}
}

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

@ -1,31 +1,29 @@
import { RequestCluster } from '../../../request-cluster';
import { wordlist } from '../../../util'; import { wordlist } from '../../../util';
import { ExplainerKey } from '../explainers'; import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers';
import { v } from '../verbs'; import { v } from '../verbs';
import { Problem } from './problem'; import { Problem } from './problem';
export class UnlawfulCookieAccess extends Problem { export class UnlawfulCookieAccess extends Problem {
getNecessaryExplainers(): ExplainerKey[] { getNecessaryExplainers(): ExplainerKey[] {
return []; return ['cookies_are_pii', 'responsibility_for_third_parties'];
} }
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean { qualifies(): boolean {
// są cookiesy, nie było zgody, nie są konieczne do działania strony // są cookiesy, nie było zgody, nie są konieczne do działania strony
const cookie_clusters = Object.values(clusters).filter((c) => c.hasMarkedCookies()); const cookie_clusters = Object.values(this.clusters).filter((c) => c.hasMarkedCookies());
return cookie_clusters.some((cluster) => { return cookie_clusters.some((cluster) => {
const hostAnswers = answers.hosts[cluster.id]; const hostAnswers = this.answers.hosts[cluster.id];
return ( return (
(hostAnswers.present == 'not_mentioned' || (hostAnswers.present == 'not_mentioned' ||
hostAnswers.present == 'not_before_making_a_choice' || hostAnswers.present == 'not_before_making_a_choice' ||
['none', 'closed_popup', 'deny_all'].includes(answers.popup_action) || ['none', 'closed_popup', 'deny_all'].includes(this.answers.popup_action) ||
answers.popup_type === 'none') && this.answers.popup_type === 'none') &&
hostAnswers.was_processing_necessary != 'yes' hostAnswers.was_processing_necessary != 'yes'
); );
}); });
} }
getEmailContent() { getEmailContent({ mode, tone }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) {
const cookie_clusters = Object.values(this.clusters).filter((c) => c.hasMarkedCookies()); const cookie_clusters = Object.values(this.clusters).filter((c) => c.hasMarkedCookies());
const unnecessary_hosts = Object.entries(this.answers.hosts) const unnecessary_hosts = Object.entries(this.answers.hosts)
.filter(([, answers]) => answers.was_processing_necessary === 'no') .filter(([, answers]) => answers.was_processing_necessary === 'no')
@ -38,8 +36,12 @@ export class UnlawfulCookieAccess extends Problem {
<> <>
<h2>Dostęp do cookies niezgodny z ustawą Prawo Telekomunikacyjne</h2> <h2>Dostęp do cookies niezgodny z ustawą Prawo Telekomunikacyjne</h2>
<p> <p>
Państwa strona dokonała odczytu plików Cookie zapisanych na dysku twardym mojego Państwa strona {mode == 'email' ? 'dokonała' : 'dokonuje'} odczytu plików Cookie
komputera. Dotyczy to plików cookie przypisanych do domen: zapisanych na dysku twardym{' '}
{mode === 'email'
? _('mojego') + ' komputera.'
: 'komputerach użytkowników końcowych.'}
. Dotyczy to plików cookie przypisanych do domen:
</p> </p>
<ul> <ul>
{cookie_clusters.map((cluster, index) => { {cookie_clusters.map((cluster, index) => {
@ -76,7 +78,9 @@ export class UnlawfulCookieAccess extends Problem {
<li> <li>
Użytkownik wyraził zgodę na takie przetwarzanie danych <em>po</em> tym, jak Użytkownik wyraził zgodę na takie przetwarzanie danych <em>po</em> tym, jak
został poinformowany bezpośrednio o celu uzyskania dostępu do tej został poinformowany bezpośrednio o celu uzyskania dostępu do tej
informacji; informacji. Zgodnie z Art. 174 ustawy Prawo Telekomunikacyjne, taka zgoda
musi spełniać warunki zgody ustalone przez RODO, aby mogła być jako podstawa
prawna uzyskania dostępu do cookies i podobnych technologii w przeglądarce;
</li> </li>
<li> <li>
Dostęp do treści plików cookies jest konieczny do dostarczania usługi Dostęp do treści plików cookies jest konieczny do dostarczania usługi
@ -85,25 +89,47 @@ export class UnlawfulCookieAccess extends Problem {
</ol> </ol>
{(() => { {(() => {
if (this.answers.popup_type == 'none' || this.answers.popup_type == 'page') { if (this.answers.popup_type == 'none' || this.answers.popup_type == 'page') {
return ( return mode === 'email' ? (
<p> <p>
Jako, że strona nie pytała {_('mnie')} nigdy o zgodę, nie jest Jako, że strona nie pytała {_('mnie')} nigdy o zgodę, nie jest
spełniony warunek 1. spełniony warunek 1.
</p> </p>
) : (
<p>
Strona nie ma zaimplementowanego mechanizmu pozyskiwania zgód, zatem
nie spełnia warunku opisanego w punkcie 1.
</p>
); );
} else if (this.answers.popup_type === 'passive_popup') { } else if (this.answers.popup_type === 'passive_popup') {
return ( return (
<p> <p>
Państwa strona nie dała mi nigdy faktycznego wyboru dotyczącego {mode === 'email' ? (
wyrażenia lub odmówienia zgody na takie przetwarzanie danych <>
osobowych, dlatego nie jest spełniony warunek 1.{' '} Państwa strona nie dała mi nigdy faktycznego wyboru
dotyczącego wyrażenia lub odmówienia zgody na takie
przetwarzanie danych osobowych. Aby zgoda była ważna w
świetle RODO, musi być dobrowolna. Brak możliwości
odmówienia zgody sprawia, że tak wyrażona zgoda nie jest
ważna w świetle RODO. Dlatego nie jest spełniony warunek 1.{' '}
</>
) : (
<>
Aktualnie zaimplementowane okienko o przetwarzaniu danych
osobowych nie daje użytkownikom końcowym możliwości odmowy
wyrażenia zgody, przez co tak wyrażona zgoda nie spełnia
warunku dobrowolności opisanego w motywie (32) RODO. Z tego
powodu nie jest spełniony warunek opisany w punkcie 1.
powyżej, zatem tak pozyskana "zgoda" nie może stanowić
podstawy prawnej dostępu do cookiesów użytkownika końcowego.
</>
)}{' '}
{this.answers.mentions_passive_consent ? ( {this.answers.mentions_passive_consent ? (
<> <>
Zgody wyrażonej w sposób bierny lub milczący nie można uznać Należy nadmienić także, że zgody wyrażonej w sposób bierny
za ważną w świetle obowiązujących przepisów rozporządzenia lub milczący nie można uznać za ważną w świetle
2016/679. Dlatego zaniechanie zmiany ustawień przeglądarki obowiązujących przepisów rozporządzenia 2016/679. Dlatego
lub po prostu korzystanie ze strony nie stanowi ważnej zaniechanie zmiany ustawień przeglądarki lub po prostu
zgody. Takie jest{' '} korzystanie ze strony nie stanowi ważnej zgody. Takie jest{' '}
<a href="https://assets.midline.pl/pisma/2021-12-16%20odpowiedz%20UODO%20na%20skarg%C4%99%20i(n)Secure.pdf"> <a href="https://assets.midline.pl/pisma/2021-12-16%20odpowiedz%20UODO%20na%20skarg%C4%99%20i(n)Secure.pdf">
stanowisko polskiego UODO stanowisko polskiego UODO
</a> </a>
@ -116,15 +142,26 @@ export class UnlawfulCookieAccess extends Problem {
); );
} else if (this.answers.popup_type === 'some_choice') { } else if (this.answers.popup_type === 'some_choice') {
if (this.answers.popup_action === 'none') { if (this.answers.popup_action === 'none') {
return ( return mode == 'email' ? (
<p> <p>
Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')}{' '} Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')}{' '}
danych osobowych. W okienku pytającym o zgodję nic nie{' '} danych osobowych. W okienku pytającym o zgodę nic nie{' '}
{_('kliknąłem')}. Nie jest zatem spełniony warunek 1. {_('kliknąłem')}. Nie jest zatem spełniony warunek 1.
</p> </p>
) : (
<p>
Skrypty pozyskujące dostęp do plików cookie uruchamiają się
zanim użytkownik końcowy zdąży wybrać jakąkolwiek opcję w
okienku pytającym o zgodę. Aby zgoda była ważna, musi być
pozyskana <strong>zanim</strong> nastąpi proces przetwarzania
danych, którego ta zgoda dotyczy. Z tego powodu nie jest
spełniony warunek 1. Nie można używać tak pozyskanej zgody
jako podstawy prawnej dostępu do plików cookies na urządzeniu
użytkownika końcowego.
</p>
); );
} else if (this.answers.popup_action === 'closed_popup') { } else if (this.answers.popup_action === 'closed_popup') {
return ( return mode == 'email' ? (
<p> <p>
Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')}{' '} Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')}{' '}
danych osobowych. {this.answers.popup_closed_how.trim()} danych osobowych. {this.answers.popup_closed_how.trim()}
@ -136,9 +173,19 @@ export class UnlawfulCookieAccess extends Problem {
jednoznaczności opisanego w Art. 4, pkt 11 RODO. Nie jest zatem jednoznaczności opisanego w Art. 4, pkt 11 RODO. Nie jest zatem
spełniony warunek 1. spełniony warunek 1.
</p> </p>
) : (
<p>
Gdy użytkownik końcowy strony nie wyrazi jednoznacznej zgody w
wyskakującym okienku, a zamiast tego po prostu zamknie to
okienko, strona nadal pozyskuje dostęp do plików cookies na
urządzeniu użytkownika. Zamknięcia okienka (np. przyciskiem x)
nie można uznać za ważną zgodę, gdyż taka czyność nie spełnia
warunku jednoznaczności opisanego w Art. 4. pkt 11. RODO. Nie
jest zatem spełniony warunek 1.
</p>
); );
} else if (this.answers.popup_action == 'deny_all') { } else if (this.answers.popup_action == 'deny_all') {
return ( return mode == 'email' ? (
<p> <p>
{this.answers.popup_deny_all_how.trim()} {this.answers.popup_deny_all_how.trim()}
{this.answers.popup_closed_how.trim().at(-1) != '.' {this.answers.popup_closed_how.trim().at(-1) != '.'
@ -146,28 +193,80 @@ export class UnlawfulCookieAccess extends Problem {
: ''}{' '} : ''}{' '}
Zatem nie jest spełniony warunek 1. Zatem nie jest spełniony warunek 1.
</p> </p>
) : (
<p>
Gdy użytkownik jednoznacznie odmówi zgód na wszystkie cele
przetwarzania, strona nadal pozyskuje dostęp do plików cookies
na urządzeniu użytkownika. Jeżeli uzytkownik nie odmówił zgody,
to nie powinny załączać się procesy przetwarzania powołujące się
na zgodę jako podstawę prawną.
</p>
); );
} }
} }
})()} })()}
{unnecessary_hosts.length > 0 ? ( {unnecessary_hosts.length > 0 ? (
<p> mode == 'email' ? (
W {_('mojej')} ocenie odczytywanie przez Państwa stronę treści plików <p>
cookies z {wordlist(unnecessary_hosts)} nie jest konieczne do wyświetlenia W {_('mojej')} ocenie odczytywanie przez Państwa stronę treści plików
treści Państwa strony, dlatego nie jest dla nich spełniony warunek 2. Jeżeli cookies z {wordlist(unnecessary_hosts)} nie jest konieczne do
Państwa zdaniem jest inaczej, {_('proszę')} o wskazanie, co jest źródłem tej wyświetlenia treści Państwa strony, dlatego nie jest dla nich spełniony
konieczności i co odróżnia Państwa stronę od wielu innych stron, które warunek 2. Jeżeli według Państwa oceny jest inaczej, {_('proszę')} o
realizują te same funkcjonalności <em>bez</em> korzystania z plików Cookie. wskazanie, co jest źródłem tej konieczności i co odróżnia Państwa stronę
</p> od wielu innych stron, które realizują te same funkcjonalności{' '}
<em>bez</em> korzystania z plików Cookie.
</p>
) : (
<p>
Warto, aby informacje na stronie opisywały w zrozumiały sposob, które z
podmiotów, których skrypty uruchamiają się na stronie (
{wordlist(unnecessary_hosts)}) konieczne do działania strony, jaki
zakres danych przetwarzają i w jakim celu.
</p>
)
) : ( ) : (
'' ''
)} )}
<p> {mode == 'email' ? (
{_('Proszę')} o wskazanie, czy być może stosowali Państwo inną podstawę prawną tone === 'official' ? (
do takiego przetwarzania {_('moich')} danych osobowych, czy przetwarzali je <p>
państwo bez ważnej podstawy prawnej? {_('Proszę')} o wskazanie,{' '}
</p> <strong>
{maybe_unnecessary_hosts.length > 1 ? ( czy być może stosowali Państwo inną podstawę prawną do takiego
przetwarzania {_('moich')} danych osobowych, czy być może
przetwarzali je Państwo bez ważnej podstawy prawnej?
</strong>
</p>
) : (
<p>
Apeluję o wdrożenie zmian na Państwa stronie tak, aby użytkownik miał
faktyczny wybór dotyczący procesów przetwarzania jego danych osobowych,
jakie zachodzą w trakcie odwiedzin tej strony.
</p>
)
) : (
<>
<p>
Jeżeli zgoda nadal ma być używana jako podstawa prawna do odczytu plików
cookies przez skrypty wyżej wymienionych podmiotów, to należy zmienić
mechanizm zgody tak, aby:{' '}
</p>{' '}
<ul>
<li>
dawał użytkownikowi końcowemu możliwość odmowy zgody w sposób równie
łatwy i dostępny, jak na wyrażenie zgody;
</li>
<li>
skrypty śledzące uruchamiały się dopiero po uzyskaniu ważnej zgody;
</li>
<li>
skrypty śledzące nie uruchamiały się, jeżeli użytkownik nie wyraził
na nie zgody.
</li>
</ul>
</>
)}
{maybe_unnecessary_hosts.length > 1 && mode == 'email' && tone == 'official' ? (
<p> <p>
{_('Proszę')} też o wskazanie, czy dostęp do treści plików cookie z {_('Proszę')} też o wskazanie, czy dostęp do treści plików cookie z
{wordlist(maybe_unnecessary_hosts)} jest konieczny do poprawnego działania {wordlist(maybe_unnecessary_hosts)} jest konieczny do poprawnego działania

View File

@ -1,36 +0,0 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers, ParsedHostAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
type UnlawfulDataClassification = 'no_purpose';
export function classifyUnlawfulData(
hostAnswers: ParsedHostAnswers,
cluster: RequestCluster
): UnlawfulDataClassification {
if (hostAnswers.present == 'not_mentioned' && hostAnswers.was_processing_necessary == 'no') {
return 'no_purpose';
}
}
export class UnlawfulData extends Problem {
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {}
getEmailContent() {
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Przetwarzanie danych osobowych bez ważnej podsawy prawnej</h2>
<p>
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami
domen:
</p>
{this.getRangeDescription()}
</>
);
}
getNecessaryExplainers() {
return [] as ExplainerKey[];
}
}

View File

@ -1,15 +1,16 @@
import * as Survey from 'survey-react'; import * as Survey from 'survey-react';
import { RequestCluster } from '../../request-cluster';
import RawAnswers from './raw-answers'; import RawAnswers from './raw-answers';
import useSurvey from './use-survey'; import useSurvey from './use-survey';
export default function Questions({ export default function Questions({
hosts, clusters,
onComplete, onComplete,
}: { }: {
hosts: string[]; clusters: RequestCluster[];
onComplete: (data: RawAnswers) => void; onComplete: (data: RawAnswers) => void;
}) { }) {
const survey = useSurvey(hosts, { const survey = useSurvey(clusters, {
onComplete: (sender) => onComplete(sender.data), onComplete: (sender) => onComplete(sender.data),
}); });
if (!survey) { if (!survey) {

View File

@ -16,6 +16,8 @@ export type HostRawAnswers = {
export type BasicRawAnswers = { export type BasicRawAnswers = {
zaimek: 0 | 1 | 2 | 3; zaimek: 0 | 1 | 2 | 3;
user_role: 'user' | 'admin';
email_type: 'polite_information' | 'official_request';
is_incognito_different: [] | ['incognito_is_the_same']; is_incognito_different: [] | ['incognito_is_the_same'];
policy_readable: 'yes' | 'vague' | 'cant_find'; policy_readable: 'yes' | 'vague' | 'cant_find';
popup_action: 'none' | 'closed_popup' | 'accept_all' | 'deny_all' | 'other'; popup_action: 'none' | 'closed_popup' | 'accept_all' | 'deny_all' | 'other';

View File

@ -0,0 +1,3 @@
export function reportIntro(visited_url: string) {
return <h2>Analiza skryptów śledzących na {visited_url} - raport</h2>;
}

View File

@ -10,11 +10,11 @@
> >
<link <link
rel="stylesheet" rel="stylesheet"
href="/node_modules/survey-react/survey.css" href="/node_modules/survey-react/survey.min.css"
/> />
<link <link
rel="stylesheet" rel="stylesheet"
href="/node_modules/survey-react/modern.css" href="/node_modules/survey-react/modern.min.css"
/> />
<link <link
rel="stylesheet" rel="stylesheet"
@ -29,9 +29,10 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="/node_modules/react/umd/react.development.js"></script> <script src="/node_modules/react/umd/react.production.min.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.development.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/components/report-window/report-window.js"></script> <script src="/lib/components/report-window/report-window.js"></script>
</body> </body>
</html> </html>

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;
} }
@ -105,6 +116,10 @@ h1 {
.sv_body { .sv_body {
padding: 0; padding: 0;
.sv_nav {
display: flex;
}
.sv_p_root { .sv_p_root {
& > .sv_row { & > .sv_row {
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
@ -274,3 +289,20 @@ h1 {
font-weight: 600; font-weight: 600;
font-size: calc(14 / 16 * 1rem); font-size: calc(14 / 16 * 1rem);
} }
.generator-container {
max-width: 100ex;
margin: 0 auto;
font-size: calc(14 / 16 * 1rem);
margin-top: 3rem;
a {
color: $ultra-black-color;
}
}
.diag-toolbox {
position: fixed;
bottom: 10px;
left: 10px;
}

View File

@ -9,18 +9,33 @@ import EmailContent from './email-content';
import { parseAnswers, ParsedAnswers } from './parse-answers'; import { parseAnswers, ParsedAnswers } from './parse-answers';
import ScreenshotGenerator from './screenshot-generator'; import ScreenshotGenerator from './screenshot-generator';
function downloadFiles(link: string) {
let a = document.createElement('a');
a.setAttribute('href', link);
a.setAttribute('download', '');
a.setAttribute('target', '_blank');
a.click();
}
function Report() { function Report() {
try { try {
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
const origin = url.searchParams.get('origin'); const origin = url.searchParams.get('origin');
if (!origin) {
return <div>Błąd: brak parametru "origin"</div>;
}
const [counter] = useEmitter(getMemory()); const [counter] = useEmitter(getMemory());
const rawAnswers = url.searchParams.get('answers');
const [answers, setAnswers] = React.useState<ParsedAnswers>( const [answers, setAnswers] = React.useState<ParsedAnswers>(
url.searchParams.get('answers') ? JSON.parse(url.searchParams.get('answers')) : null rawAnswers ? JSON.parse(rawAnswers) : null
); );
const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey'); const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey');
const clusters = getMemory().getClustersForOrigin(origin); const [scrRequestPath, setScrRequestPath] = React.useState('');
const clusters = getMemory().getClustersForOrigin(origin || '');
React.useEffect(() => { React.useEffect(() => {
if (!origin) return;
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
url.searchParams.set('origin', origin); url.searchParams.set('origin', origin);
url.searchParams.set('answers', JSON.stringify(answers)); url.searchParams.set('answers', JSON.stringify(answers));
@ -28,16 +43,20 @@ function Report() {
history.pushState({}, 'Rentgen', url.toString()); history.pushState({}, 'Rentgen', url.toString());
}, [mode, answers, origin]); }, [mode, answers, origin]);
const visited_url = Object.values(clusters) const visited_url = Object.values(clusters)
.find((cluster) => cluster.getMarkedRequests().length > 0) .sort((a, b) => (a.lastModified > b.lastModified ? -1 : 1))
?.getMarkedRequests()[0].originalURL; .find((cluster) => !!cluster.lastFullUrl)?.lastFullUrl;
if (!visited_url) {
return <div>Wczytywanie...</div>;
}
const result = ( const result = (
<div {...{ 'data-version': counter }}> <div {...{ 'data-version': counter }}>
{mode === 'survey' ? ( {mode === 'survey' ? (
<Questions <Questions
hosts={Object.values(clusters) clusters={Object.values(clusters).filter(
.filter((cluster) => cluster.getMarkedRequests().length > 0) (cluster) => cluster.getMarkedRequests().length > 0
.map((cluster) => cluster.id)} )}
onComplete={(answers) => { onComplete={(answers) => {
setAnswers(parseAnswers(answers)); setAnswers(parseAnswers(answers));
setMode('screenshots'); setMode('screenshots');
@ -48,13 +67,32 @@ function Report() {
)} )}
{mode === 'screenshots' ? ( {mode === 'screenshots' ? (
<ScreenshotGenerator <ScreenshotGenerator
{...{ visited_url, clusters, setReportWindowMode: setMode }} {...{
visited_url,
clusters,
setReportWindowMode: setMode,
setRequestPath: setScrRequestPath,
downloadFiles: downloadFiles,
user_role: answers.user_role,
}}
/>
) : (
''
)}
{mode === 'preview' ? (
<EmailContent
{...{
answers,
visited_url,
clusters,
scrRequestPath,
downloadFiles: downloadFiles,
user_role: answers.user_role,
}}
/> />
) : ( ) : (
'' ''
)} )}
{mode === 'preview' ? <EmailContent {...{ answers, visited_url, clusters }} /> : ''}
{/* <HARConverter {...{ entries }} /> */}
</div> </div>
); );
return ( return (
@ -72,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

@ -23,16 +23,6 @@ h2 {
position: relative; position: relative;
} }
.generator-container {
max-width: 100ex;
margin: 0 auto;
font-size: calc(14 / 16 * 1rem);
a {
color: $ultra-black-color;
}
}
.buttons-container { .buttons-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -49,20 +39,34 @@ h2 {
} }
.browser { .browser {
height: 8.667rem; height: 9.267rem;
font-weight: 800 !important; font-weight: 800 !important;
color: $disabled-grey !important; color: $disabled-grey !important;
border: 1px solid $disabled-grey; border: 1px solid $disabled-grey;
background-image: linear-gradient(to bottom, $icd-rentgen-color 20%, #fff 20%, #fff 100%); background: linear-gradient(to bottom, $icd-rentgen-color 20%, #fff 20%, #fff 100%);
animation: xray 2s cubic-bezier(0, 1.43, 0.39, 1.43) infinite; background-size: 100%;
background-position-y: 26.5px;
&--filled { &--filled {
background-size: 100%; background-size: 100%;
background-position-y: 19px; background-position-y: 26.5px;
animation: none; animation: none;
.browser__header { &--address-bar {
background-color: #fff; border: 1px solid #8a949f;
height: 1rem;
width: 10rem;
font-size: 0.667rem;
font-weight: 400;
padding: 0 0.25rem;
color: #000;
overflow: hidden;
word-break: normal;
inline-size: 10rem;
text-overflow: ellipsis;
background: linear-gradient(to left, $icd-rentgen-color 20%, #fff 20%, #fff 100%);
animation: xray-header 2s cubic-bezier(0, 1.43, 0.39, 1.43) infinite;
} }
} }
@ -72,28 +76,42 @@ h2 {
} }
} }
@keyframes xray-header {
to {
background-position-x: 11.1rem;
}
}
&__header { &__header {
height: 1.667rem; height: 1.667rem;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: #fff;
padding: 0 0.5rem; padding: 0 0.5rem;
font-size: 1.25rem; font-size: 1.25rem;
border-bottom: 1px solid $disabled-grey; border-bottom: 1px solid $disabled-grey;
&--in_progress {
.browser__header--address-bar {
background: linear-gradient(to left, $icd-rentgen-color 20%, #fff 20%, #fff 100%);
animation: xray-header 2s cubic-bezier(0, 1.43, 0.39, 1.43) infinite;
}
}
&--address-bar { &--address-bar {
border: 1px solid $disabled-grey; border: 1px solid #8a949f;
height: 1rem; height: 1rem;
width: 10rem; width: 10rem;
font-size: 0.667rem; font-size: 0.667rem;
font-weight: 400; font-weight: 400;
padding-left: 0.25rem; padding: 0 0.25rem;
color: #000; color: #000;
overflow: visible; overflow: hidden;
word-break: break-all; word-break: normal;
inline-size: 10rem; inline-size: 10rem;
text-overflow: ellipsis;
} }
&--controls { &--controls {

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',
@ -12,21 +12,33 @@ enum taskState {
type Screenshot = { type Screenshot = {
url: string; url: string;
thumb_url: string;
domain: string; domain: string;
filename: string;
found_headers: string[]; found_headers: string[];
}; };
interface screenshotTask { interface screenshotTask {
url: string;
domains: string[]; domains: string[];
elapsed_time_s: number;
current_action: string;
finished_time: number;
id: string; id: string;
status: taskState;
output: string;
images: Screenshot[]; images: Screenshot[];
jobs_ahead: number;
output: string;
processing_took: number;
request_time: number;
started_time: number;
status: taskState;
url: string;
waiting_took: number;
zip_url: string;
preview: string;
} }
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,
'' ''
)}`; )}`;
@ -44,23 +56,38 @@ export default function ScreenshotGenerator({
visited_url, visited_url,
clusters, clusters,
setReportWindowMode, setReportWindowMode,
setRequestPath,
downloadFiles,
user_role,
}: { }: {
visited_url: string; visited_url: string;
clusters: Record<string, RequestCluster>; clusters: Record<string, RequestCluster>;
setReportWindowMode: Function; setReportWindowMode: Function;
setRequestPath: Function;
downloadFiles: Function;
user_role: string;
}) { }) {
const [mode, setMode] = React.useState<string>('idle'); const [mode, setMode] = React.useState<string>('idle');
const [images, setImages] = React.useState<Screenshot[]>([]); const [images, setImages] = React.useState<Screenshot[]>([]);
const [taskId, setTaskId] = React.useState<string>(null); const [taskId, setTaskId] = React.useState<string | null>(null);
const [output, setOutput] = React.useState<any>({});
const [currentAction, setCurrentAction] = React.useState<string>('');
const [preview, setPreview] = React.useState<string>('');
const [lastPreview, setLastPreview] = React.useState<string>('');
async function subscribeTask(path: string): Promise<screenshotTask> { async function subscribeTask(path: string): Promise<screenshotTask> {
let response = { status: taskState.WAITING }; let response = { status: taskState.WAITING };
let last_preview = '';
while (response.status === taskState.WAITING || response.status === taskState.RUNNING) { while (response.status === taskState.WAITING || response.status === taskState.RUNNING) {
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
response = await (await pollTask(path)).json(); response = await (await pollTask(path)).json();
setImages((response as screenshotTask)?.images); setImages((response as screenshotTask)?.images);
document.querySelector('.images').scrollTo({ setCurrentAction((response as screenshotTask)?.current_action);
top: document.querySelector('.images').scrollHeight, setLastPreview(last_preview);
setPreview((response as screenshotTask)?.preview);
last_preview = (response as screenshotTask)?.preview;
document.querySelector('.images')?.scrollTo({
top: document.querySelector('.images')?.scrollHeight,
behavior: 'smooth', behavior: 'smooth',
}); });
} }
@ -71,19 +98,6 @@ export default function ScreenshotGenerator({
return response as screenshotTask; return response as screenshotTask;
} }
function downloadFiles() {
const urls = images.map((el) => `${SS_URL}${el}`);
for (const url of urls) {
let a = document.createElement('a');
a.setAttribute('href', url);
a.setAttribute('download', '');
a.setAttribute('target', '_blank');
a.click();
}
setReportWindowMode('preview');
}
return ( return (
<div className="generator-container"> <div className="generator-container">
{mode === 'idle' ? ( {mode === 'idle' ? (
@ -91,16 +105,42 @@ export default function ScreenshotGenerator({
<h1>Przygotowanie zrzutów ekranów</h1> <h1>Przygotowanie zrzutów ekranów</h1>
<div className="container"> <div className="container">
<h2>Notka informacyjna</h2> <h2>Notka informacyjna</h2>
<p> <img
Dla potwierdzenia przechwyconych danych, warto załączyć zrzuty ekranów src="/assets/doctor_welcome.png"
narzędzi deweloperskich do maila dla administratora oraz Urzędu Ochrony style={{
Danych Osobowych. width: '100%',
</p> maxWidth: '360px',
<p> float: 'right',
Jeżeli nie wiesz jak wykonać zrzuty ekranów, skorzystaj z{' '} position: 'relative',
<a href="">naszej instrukcji</a> lub wtyczka Rentgen może wygenerować je top: '-10px',
za Ciebie. }}
</p> />
<Fragment>
<p>
W celu udokumentowania procesów przetwarzania danych, jakie wykryła
nasza wtyczka na tej stronie, warto wykonać zrzuty ekranu, na
których widać przeglądarkę z otwartymi narzędziami deweloperskimi,
ukazując wybrane elementy ruchu sieciowego generowanego przez
stronę.
</p>
<p>Jeżeli chcesz, wtyczka Rentgen może wygenerować je za Ciebie.</p>
<p>
Uwaga: aby to zrobić, adres aktualnie odwiedzonej podstrony
analizowanej witryny będzie wysłany na nasz serwer, aby na nim
odwiedzić podstronę i wykonać zrzuty ekranu.
</p>
<p>
Serwer, na którym jest wykonywana analiza należy do inicjatywy{' '}
<a href="https://www.internet-czas-dzialac.pl/contact/">
<i>Internet. Czas działać!</i>
</a>
. Zebrane dane nie wysyłane do żadnych podmiotów trzecich i
usuwane z serwera po 24 godzinach. Wysłanie na serwer informacji o
adresie przeglądanej strony jest konieczne, aby wykonać te zrzuty
ekranu w sposób automatyczny. Jeżeli nie chcesz korzystać z opcji
automatycznej, zachęcamy do wykonania zrzutów ekranu samodzielnie.
</p>
</Fragment>
</div> </div>
<div className="buttons-container"> <div className="buttons-container">
@ -108,6 +148,7 @@ export default function ScreenshotGenerator({
className="sv_prev_btn" className="sv_prev_btn"
onClick={() => { onClick={() => {
setReportWindowMode('preview'); setReportWindowMode('preview');
setRequestPath(null);
}} }}
> >
Pomiń Pomiń
@ -116,13 +157,20 @@ export default function ScreenshotGenerator({
className="sv_next_btn" className="sv_next_btn"
onClick={async () => { onClick={async () => {
setMode('in_progress'); setMode('in_progress');
const task = await createTask(visited_url, Object.keys(clusters)); const task = await createTask(
visited_url,
Object.values(clusters)
.filter((cluster) => cluster.hasMarks())
.map((cluster) => cluster.id)
);
const urlArr = task.url.split('/'); const urlArr = task.url.split('/');
setTaskId(urlArr[urlArr.length - 1]); setTaskId(urlArr[urlArr.length - 1]);
const response = await subscribeTask(task.url); const response = await subscribeTask(task.url);
setImages(response.images); setImages(response.images);
console.log('response.images', response.images); setLastPreview(preview);
console.log('output', response); setPreview(response.preview);
setOutput(response);
setRequestPath(response.zip_url);
}} }}
> >
Wygeneruj Wygeneruj
@ -142,6 +190,7 @@ export default function ScreenshotGenerator({
Nasz serwer właśnie odwiedza wskazaną przez Ciebie stronę Nasz serwer właśnie odwiedza wskazaną przez Ciebie stronę
i przygotowuje zrzuty ekranów narzędzi deweloperskich. i przygotowuje zrzuty ekranów narzędzi deweloperskich.
</p> </p>
<div>{currentAction}</div>
</Fragment> </Fragment>
) : null} ) : null}
{mode === 'finished' ? ( {mode === 'finished' ? (
@ -152,18 +201,34 @@ export default function ScreenshotGenerator({
) : null} ) : null}
<div className="images"> <div className="images">
{mode === 'in_progress' ? (
<div
className="browser"
style={{
backgroundImage: `url(${SS_URL}${preview})${
lastPreview ? `, url(${SS_URL}${lastPreview})` : ''
}`,
}}
>
<div className="browser__header browser__header--in_progress">
<div className="browser__header--address-bar"></div>
<div className="browser__header--controls">···</div>
</div>
<div className="browser__content"></div>
</div>
) : null}
{images.map((screenshot) => { {images.map((screenshot) => {
return ( return (
<div <div
key={`${taskId}_${screenshot.url}`} key={`${taskId}_${screenshot.url}`}
className="browser browser--filled" className="browser browser--filled"
style={{ style={{
backgroundImage: `url(${SS_URL}${screenshot.url})`, backgroundImage: `url(${SS_URL}${screenshot.thumb_url})`,
}} }}
> >
<div className="browser__header"> <div className="browser__header">
<div className="browser__header--address-bar"> <div className="browser__header--address-bar">
{screenshot.url.split('-').slice(-2).join('-')} 🕸 {screenshot.domain}
</div> </div>
<div className="browser__header--controls">···</div> <div className="browser__header--controls">···</div>
</div> </div>
@ -171,22 +236,18 @@ export default function ScreenshotGenerator({
</div> </div>
); );
})} })}
{mode === 'in_progress' ? (
<div className="browser">
<div className="browser__header">
<div className="browser__header--address-bar"></div>
<div className="browser__header--controls">···</div>
</div>
<div className="browser__content"></div>
</div>
) : null}
</div> </div>
</div> </div>
<div className="buttons-container"> <div className="buttons-container">
{mode === 'finished' ? ( {mode === 'finished' ? (
<Fragment> <Fragment>
<button className="sv_next_btn" onClick={() => downloadFiles()}> <button
className="sv_next_btn"
onClick={() => {
downloadFiles(`${SS_URL}${output.zip_url}`);
setReportWindowMode('preview');
}}
>
Pobierz zrzuty ekranów i przejdź dalej Pobierz zrzuty ekranów i przejdź dalej
</button> </button>
</Fragment> </Fragment>

View File

@ -1,17 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import * as Survey from 'survey-react'; import * as Survey from 'survey-react';
import { RequestCluster } from '../../request-cluster';
import generateSurveyQuestions from './generate-survey-questions'; import generateSurveyQuestions from './generate-survey-questions';
import RawAnswers from './raw-answers'; import RawAnswers from './raw-answers';
import verbs, { v } from './verbs'; import verbs, { v } from './verbs';
export default function useSurvey( export default function useSurvey(
hosts: string[], clusters: RequestCluster[],
{ onComplete }: { onComplete: (sender: { data: RawAnswers }) => void } { onComplete }: { onComplete: (sender: { data: RawAnswers }) => void }
): Survey.ReactSurveyModel { ): Survey.ReactSurveyModel | null {
const [survey, setSurvey] = React.useState<Survey.Model>(null); const [survey, setSurvey] = React.useState<Survey.Model | null>(null);
React.useEffect(() => { React.useEffect(() => {
const model = generateSurveyQuestions(hosts); const model = generateSurveyQuestions(clusters);
console.log(model);
const survey = new Survey.Model(model); const survey = new Survey.Model(model);
survey.onProcessTextValue.add(function ( survey.onProcessTextValue.add(function (
sender: Survey.SurveyModel, sender: Survey.SurveyModel,

View File

@ -1,5 +1,6 @@
const words = { const words = {
ciebie: ['ciebie', 'ciebie', 'ciebie', 'was'], ciebie: ['ciebie', 'ciebie', 'ciebie', 'was'],
chciałbym: ['chciałbym', 'chciałabym', 'chciałobym', 'chcielibyśmy'],
dokonałeś: ['dokonałeś', 'dokonałaś', 'dokonałoś', 'dokonaliście'], dokonałeś: ['dokonałeś', 'dokonałaś', 'dokonałoś', 'dokonaliście'],
jesteś: ['jesteś', 'jesteś', 'jesteś', 'jesteście'], jesteś: ['jesteś', 'jesteś', 'jesteś', 'jesteście'],
kliknąłem: ['kliknąłem', 'kliknęłam', 'klinkęłom', 'kliknęliśmy'], kliknąłem: ['kliknąłem', 'kliknęłam', 'klinkęłom', 'kliknęliśmy'],
@ -8,6 +9,8 @@ const words = {
mnie: ['mnie', 'mnie', 'mnie', 'nas'], mnie: ['mnie', 'mnie', 'mnie', 'nas'],
moich: ['moich', 'moich', 'moich', 'naszych'], moich: ['moich', 'moich', 'moich', 'naszych'],
moje: ['moje', 'moje', 'moje', 'nasze'], moje: ['moje', 'moje', 'moje', 'nasze'],
mojego: ['mojego', 'mojego', 'mojego', 'naszego'],
moja: ['moja', 'moja', 'moja', 'nasza'],
mojej: ['mojej', 'mojej', 'mojej', 'naszej'], mojej: ['mojej', 'mojej', 'mojej', 'naszej'],
muszę: ['muszę', 'muszę', 'muszę', 'musimy'], muszę: ['muszę', 'muszę', 'muszę', 'musimy'],
odkliknąłeś: ['odkliknąłeś', 'odkliknęłaś', 'odklikęłoś', 'odkliknęliście'], odkliknąłeś: ['odkliknąłeś', 'odkliknęłaś', 'odklikęłoś', 'odkliknęliście'],

View File

@ -1,20 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Options from '../../options';
import { StolenData } from './stolen-data';
import { getshorthost, useEmitter } from '../../util';
import { getMemory } from '../../memory'; import { getMemory } from '../../memory';
import Options from '../../options';
async function getCurrentTab() { import { useEmitter } from '../../util';
const [tab] = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
return tab;
}
import './../../styles/global.scss'; import './../../styles/global.scss';
import './sidebar.scss'; import './sidebar.scss';
import { StolenData } from './stolen-data';
const Sidebar = () => { const Sidebar = () => {
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
@ -28,8 +20,8 @@ const Sidebar = () => {
const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false); const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false);
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true); const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false); const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false);
const [eventCounts, setEventCounts] = useEmitter(getMemory()); const [eventCounts] = useEmitter(getMemory());
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false); const [_, setMarksOccurrence] = React.useState<boolean>(false);
const [infoDataDialogAck, setInfoDataDialogAck] = React.useState<boolean>( const [infoDataDialogAck, setInfoDataDialogAck] = React.useState<boolean>(
localStorage.getItem('infoDataDialogAck') === null localStorage.getItem('infoDataDialogAck') === null
? true ? true
@ -53,6 +45,7 @@ const Sidebar = () => {
); );
React.useEffect(() => { React.useEffect(() => {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) { if (cluster.hasMarks()) {
return setMarksOccurrence(true); return setMarksOccurrence(true);
@ -62,6 +55,7 @@ const Sidebar = () => {
return setMarksOccurrence(false); return setMarksOccurrence(false);
}, [eventCounts['*']]); }, [eventCounts['*']]);
if (!origin) return <div>Błąd: Brak parametru "origin"</div>;
return ( return (
<div className="sidebar"> <div className="sidebar">
<header className="header"> <header className="header">
@ -204,7 +198,7 @@ const Sidebar = () => {
<StolenData <StolenData
origin={origin} origin={origin}
eventCounts={eventCounts} eventCounts={eventCounts}
minValueLength={minValueLength} minValueLength={minValueLength === null ? 7 : minValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly} cookiesOrOriginOnly={cookiesOrOriginOnly}
detailsVisibility={detailsVisibility} detailsVisibility={detailsVisibility}
@ -212,7 +206,7 @@ const Sidebar = () => {
</> </>
) : ( ) : (
<Options <Options
minValueLength={minValueLength} minValueLength={minValueLength === null ? 7 : minValueLength}
setMinValueLength={setMinValueLength} setMinValueLength={setMinValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
setCookiesOnly={setCookiesOnly} setCookiesOnly={setCookiesOnly}

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;
@ -119,27 +118,37 @@ export default function StolenDataCluster({
const fullHosts = cluster.getFullHosts(); const fullHosts = cluster.getFullHosts();
const [version] = useEmitter(cluster); const [version] = useEmitter(cluster);
/* console.log('Stolen data cluster!', shorthost, refreshToken); */
console.log(cluster.getMarkedEntries());
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,7 +14,7 @@ export function StolenData({
detailsVisibility, detailsVisibility,
}: { }: {
origin: string; origin: string;
eventCounts: Record<string, number>; eventCounts: Record<string, number | undefined>;
minValueLength: number; minValueLength: number;
cookiesOnly: boolean; cookiesOnly: boolean;
cookiesOrOriginOnly: boolean; cookiesOrOriginOnly: boolean;
@ -43,7 +43,7 @@ export function StolenData({
origin={origin} origin={origin}
shorthost={cluster.id} shorthost={cluster.id}
key={cluster.id + origin} key={cluster.id + origin}
refreshToken={eventCounts[cluster.id]} refreshToken={eventCounts[cluster.id] || 0}
minValueLength={minValueLength} minValueLength={minValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly} cookiesOrOriginOnly={cookiesOrOriginOnly}

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Tab } from '../../util';
export default function TabDropdown({ export default function TabDropdown({
setPickedTab, setPickedTab,
@ -7,7 +8,7 @@ export default function TabDropdown({
setPickedTab: (tab_id: number) => void; setPickedTab: (tab_id: number) => void;
pickedTab: number; pickedTab: number;
}) { }) {
const [tabs, setTabs] = React.useState([]); const [tabs, setTabs] = React.useState<Tab[]>([]);
React.useEffect(() => { React.useEffect(() => {
browser.tabs.query({ currentWindow: true }).then(setTabs); browser.tabs.query({ currentWindow: true }).then(setTabs);
}, []); }, []);

View File

@ -22,6 +22,10 @@ body {
z-index: 1; z-index: 1;
user-select: none; user-select: none;
&--no-page {
border-bottom: 0;
}
.webpage-metadata { .webpage-metadata {
word-break: break-all; word-break: break-all;
display: flex; display: flex;

View File

@ -14,12 +14,20 @@ 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 [stolenDataView, setStolenDataView] = React.useState<boolean>(true); const [eventCounts] = useEmitter(getMemory());
const [eventCounts, setEventCounts] = useEmitter(getMemory());
const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null); const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null);
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false); const [_, setMarksOccurrence] = React.useState<boolean>(false);
const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>( const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>(
null null
); );
@ -32,7 +40,7 @@ const Toolbar = () => {
const listener = async () => { const listener = async () => {
const tab = await getCurrentTab(); const tab = await getCurrentTab();
if (tab !== undefined) { if (tab !== undefined && tab.url) {
const url = new URL(tab.url); const url = new URL(tab.url);
if (url.origin.startsWith('moz-extension')) { if (url.origin.startsWith('moz-extension')) {
return; return;
@ -51,60 +59,85 @@ const Toolbar = () => {
}); });
React.useEffect(() => { React.useEffect(() => {
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('');
switch (exposedOriginDomains.length) { switch (exposedOriginDomains.length) {
case 0: case 0:
return null; break;
case 1: case 1:
return setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`); setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`);
break;
case 2: case 2:
return setExposedOriginDomainCopy( setExposedOriginDomainCopy(
`${exposedOriginDomains[0]} oraz ${exposedOriginDomains[1]}.` `${exposedOriginDomains[0]} oraz ${exposedOriginDomains[1]}.`
); );
break;
case 3: case 3:
return setExposedOriginDomainCopy( setExposedOriginDomainCopy(
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} oraz ${exposedOriginDomains[2]}.` `${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} oraz ${exposedOriginDomains[2]}.`
); );
break;
default: default:
return setExposedOriginDomainCopy( setExposedOriginDomainCopy(
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} (i ${ `${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} (i ${
exposedOriginDomains.length - 2 < 2 ? 2 : exposedOriginDomains.length - 2 exposedOriginDomains.length - 2 < 2 ? 2 : exposedOriginDomains.length - 2
} innych).` } innych).`
); );
break;
} }
}, [eventCounts['*'], origin]); }, [eventCounts['*'], origin]);
React.useEffect(() => { React.useEffect(() => {
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('');
switch (cookieDomains.length) { switch (cookieDomains.length) {
case 0: case 0:
return null; break;
case 1: case 1:
return setCookieDomainCopy(`${cookieDomains[0]}.`); setCookieDomainCopy(`${cookieDomains[0]}.`);
break;
case 2: case 2:
return setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`); setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`);
break;
case 3: case 3:
return setCookieDomainCopy( setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} oraz ${cookieDomains[2]}.` `${cookieDomains[0]}, ${cookieDomains[1]} oraz ${cookieDomains[2]}.`
); );
break;
default: default:
return setCookieDomainCopy( setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} (i ${ `${cookieDomains[0]}, ${cookieDomains[1]} (i ${
cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2 cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2
} innych).` } innych).`
); );
break;
} }
}, [eventCounts['*'], origin]); }, [eventCounts['*'], origin]);
React.useEffect(() => { React.useEffect(() => {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) { if (cluster.hasMarks()) {
return setMarksOccurrence(true); return setMarksOccurrence(true);
@ -115,6 +148,7 @@ const Toolbar = () => {
}, [eventCounts['*']]); }, [eventCounts['*']]);
function autoMark() { function autoMark() {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
cluster.autoMark(); cluster.autoMark();
} }
@ -123,7 +157,7 @@ const Toolbar = () => {
return ( return (
<div className="toolbar"> <div className="toolbar">
<header className="header"> <header className={origin ? 'header' : 'header header--no-page'}>
<img src="../../assets/icon-addon.svg" height={32}></img> <img src="../../assets/icon-addon.svg" height={32}></img>
<div className="webpage-metadata"> <div className="webpage-metadata">
{origin ? ( {origin ? (
@ -142,95 +176,107 @@ const Toolbar = () => {
) : null} ) : null}
</header> </header>
<section className="summary"> {origin ? (
<div className="counters-wrapper">
<div className="counters">
<div className="counter counter--browser-history">
<img src="/assets/icons/warning.svg#color" width="24" height="24" />
<span data-event={`${eventCounts['*']}`}>
{
Object.values(getMemory().getClustersForOrigin(origin)).filter(
(cluster) => cluster.exposesOrigin()
).length
}
</span>
</div>
<div className="counter counter--cookies">
<img src="/assets/icons/cookie.svg#color" width="24" height="24" />
<span data-event={`${eventCounts['*']}`}>
{
Object.values(getMemory().getClustersForOrigin(origin)).filter(
(cluster) => cluster.hasCookies()
).length
}
</span>
</div>
</div>
<div className="big-counter" data-event={`${eventCounts['*']}`}>
{Object.values(getMemory().getClustersForOrigin(origin)).length}
</div>
</div>
<span className="notice">Liczba wykrytych domen podmiotów trzecich</span>
</section>
<section className="details">
{exposedOriginDomainCopy ? (
<p
data-event={`${eventCounts['*']}`}
title={Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.exposesOrigin())
.map((domain) => domain.id)
.join(', ')}
>
{first_sentence_cookie}
<strong>{exposedOriginDomainCopy}</strong>
</p>
) : null}
{cookieDomainCopy ? (
<p
data-event={`${eventCounts['*']}`}
title={Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.hasCookies())
.map((domain) => domain.id)
.join(', ')}
>
{first_sentence_history}
<strong>{cookieDomainCopy}</strong>
</p>
) : null}
</section>
{exposedOriginDomainCopy || cookieDomainCopy ? (
<Fragment> <Fragment>
<section className="about"> {' '}
<p> <section className="summary">
Takie przetwarzanie danych może być niezgodne z prawem. Przejdź do <div className="counters-wrapper">
analizy aby pomóc ustalić, czy ta strona nie narusza RODO. <div className="counters">
</p> <div className="counter counter--cookies">
<img
src="/assets/icons/cookie.svg#color"
width="24"
height="24"
/>
<span data-event={`${eventCounts['*']}`}>
{
Object.values(
getMemory().getClustersForOrigin(origin)
).filter((cluster) => cluster.hasCookies()).length
}
</span>
</div>
<div className="counter counter--browser-history">
<img
src="/assets/icons/warning.svg#color"
width="24"
height="24"
/>
<span data-event={`${eventCounts['*']}`}>
{
Object.values(
getMemory().getClustersForOrigin(origin)
).filter((cluster) => cluster.exposesOrigin()).length
}
</span>
</div>
</div>
<div className="big-counter" data-event={`${eventCounts['*']}`}>
{Object.values(getMemory().getClustersForOrigin(origin)).length}
</div>
</div>
<span className="notice">Liczba wykrytych domen podmiotów trzecich</span>
</section> </section>
<section className="actions"> <section className="details">
<button {cookieDomainCopy ? (
className="button button--report" <p
onClick={() => { data-event={`${eventCounts['*']}`}
autoMark(); title={Object.values(getMemory().getClustersForOrigin(origin))
window.open( .filter((cluster) => cluster.hasCookies())
`/components/sidebar/sidebar.html?origin=${origin}`, .map((domain) => domain.id)
'new_tab' .join(', ')}
); >
window.close(); // close toolbar popup {first_sentence_cookie}
}} <strong>{cookieDomainCopy}</strong>
> </p>
Przejdź do analizy ) : null}
</button> {exposedOriginDomainCopy ? (
<p
data-event={`${eventCounts['*']}`}
title={Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.exposesOrigin())
.map((domain) => domain.id)
.join(', ')}
>
{first_sentence_history}
<strong>{exposedOriginDomainCopy}</strong>
</p>
) : null}
</section> </section>
{exposedOriginDomainCopy || cookieDomainCopy ? (
<Fragment>
<section className="about">
<p>
Takie przetwarzanie danych może być niezgodne z prawem. Przejdź
do analizy aby pomóc ustalić, czy ta strona nie narusza RODO lub
ustawy Prawo Telekomunikacyjne.
</p>
</section>
<section className="actions">
<button
className="button button--report"
onClick={() => {
autoMark();
window.open(
`/components/sidebar/sidebar.html?origin=${origin}`,
'new_tab'
);
window.close(); // close toolbar popup
}}
>
Przejdź do analizy
</button>
</section>
</Fragment>
) : (
<Fragment>
<section className="about about__no-errors">
<p>Nie znaleziono problemów na tej stronie.</p>
</section>
</Fragment>
)}
</Fragment> </Fragment>
) : ( ) : null}
<Fragment>
<section className="about about__no-errors">
<p>Nie znaleziono problemów na tej stronie.</p>
</section>
</Fragment>
)}
</div> </div>
); );
}; };

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

@ -12,32 +12,32 @@ const watch = process.argv.includes('--watch') && {
let skipReactImports = { let skipReactImports = {
name: 'skipReactImports', name: 'skipReactImports',
setup(build) { setup(build) {
build.onResolve({ filter: /^react(-dom)?$/ }, (args) => { build.onResolve({ filter: /^(react(-dom)?|survey-react)$/ }, (args) => {
return { return {
path: args.path, path: args.path,
namespace: `globalExternal_${args.path}`, namespace: `globalExternal_${args.path}`,
}; };
}); });
build.onLoad( build.onLoad({ filter: /.*/, namespace: 'globalExternal_react' }, () => {
{ filter: /.*/, namespace: 'globalExternal_react' }, return {
() => { contents: `module.exports = globalThis.React`,
return { loader: 'js',
contents: `module.exports = globalThis.React`, };
loader: 'js', });
};
}
);
build.onLoad( build.onLoad({ filter: /.*/, namespace: 'globalExternal_react-dom' }, () => {
{ filter: /.*/, namespace: 'globalExternal_react-dom' }, return {
() => { contents: `module.exports = globalThis.ReactDOM`,
return { loader: 'js',
contents: `module.exports = globalThis.ReactDOM`, };
loader: 'js', });
}; build.onLoad({ filter: /.*/, namespace: 'globalExternal_survey-react' }, () => {
} return {
); contents: `module.exports = globalThis.Survey`,
loader: 'js',
};
});
}, },
}; };
@ -46,11 +46,11 @@ esbuild
entryPoints: [ entryPoints: [
'components/toolbar/toolbar.tsx', 'components/toolbar/toolbar.tsx',
'components/sidebar/sidebar.tsx', 'components/sidebar/sidebar.tsx',
'test.ts',
'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',
], ],
bundle: true, bundle: true,
// minify: true, // minify: true,
@ -59,9 +59,9 @@ esbuild
plugins: [scss(), skipReactImports], plugins: [scss(), skipReactImports],
define: { define: {
PLUGIN_NAME: '"Rentgen"', PLUGIN_NAME: '"Rentgen"',
PLUGIN_URL: '"https://git.internet-czas-dzialac.pl/icd/rentgen"', PLUGIN_URL: '"https://addons.mozilla.org/pl/firefox/addon/rentgen/"',
}, },
external: ['react', 'react-dom'], external: ['react', 'react-dom', 'survey-react'],
watch, watch,
}) })
.then(() => console.log('Add-on was built')) .then(() => console.log('Add-on was built'))

View File

@ -1,4 +1,5 @@
import { StolenDataEntry } from './stolen-data-entry'; 'use strict';
import { DataLocation, StolenDataEntry } from './stolen-data-entry';
import { import {
flattenObjectEntries, flattenObjectEntries,
getshorthost, getshorthost,
@ -75,12 +76,13 @@ export default class ExtendedRequest {
public tabId: number; public tabId: number;
public url: string; public url: string;
public shorthost: string; public shorthost: string;
public requestHeaders: Request['requestHeaders'] = []; public requestHeaders: { name: string; value?: string; binaryValue?: number[] }[] = [];
public originalURL: string;
public origin: string; public origin: string;
public initialized = false; public initialized = false;
public stolenData: StolenDataEntry[]; public stolenData: StolenDataEntry[] = [];
public originalPathname: string; public originalURL: string | null = null; // sometimes we can only establish that the given request applied to a certain origin, not a full URL from the address bar - in case of service workers, for example. Hence the null
public originalPathname: string | null = null; // same as above
public originalHost: string;
public requestBody: RequestBody; public requestBody: RequestBody;
static by_id = {} as Record<string, ExtendedRequest>; static by_id = {} as Record<string, ExtendedRequest>;
@ -91,67 +93,67 @@ export default class ExtendedRequest {
this.url = data.url; this.url = data.url;
this.shorthost = getshorthost(data.url); this.shorthost = getshorthost(data.url);
this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {}; this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {};
if (this.url.includes('criteo')) {
console.log(this);
}
ExtendedRequest.by_id[data.requestId] = this; ExtendedRequest.by_id[data.requestId] = this;
this.data = Object.assign({}, data); this.data = Object.assign({}, data);
(this.data as any).frameAncestors = [ (this.data as any).frameAncestors = [
...(data as any).frameAncestors.map((e: any) => ({ url: e.url })), ...((data as any)?.frameAncestors?.map((e: any) => ({ url: e.url })) || []),
]; ]; // making a copy?
// console.log('→→→',(this.data as any).frameAncestors, (data as any).frameAncestors); // console.log('→→→',(this.data as any).frameAncestors, (data as any).frameAncestors);
}
addHeaders(headers: Request['requestHeaders']) {
this.requestHeaders = headers;
return this;
}
async init() {
await this.cacheOrigin();
this.initialized = true;
this.stolenData = this.getAllStolenData();
}
async cacheOrigin(): Promise<void> {
let url: string; let url: string;
if (this.data.tabId && this.data.tabId >= 0) { let is_full_url = true;
const tab = await browser.tabs.get(this.data.tabId); let url_comes_from: string;
url = tab.url; if (this.data.type === 'main_frame') {
url = this.data.url;
url_comes_from = 'main_frame';
} else if (this.data.frameId === 0 && this.data.documentUrl) {
url = this.data.documentUrl;
url_comes_from = 'documentUrl';
if (this.data.tabId == -1) {
//a service worker?
url_comes_from = 'documentUrl (webworker)';
is_full_url = false;
}
} else if ( } else if (
(this.data as any)?.frameAncestors && (this.data as any)?.frameAncestors &&
(this.data as any).frameAncestors[0] !== undefined (this.data as any).frameAncestors[0] !== undefined
) { ) {
url = (this.data as any).frameAncestors[0].url || ''; url = (this.data as any).frameAncestors.at(-1).url || '';
url_comes_from = 'frameAncestors';
} else { } else {
const headers = Object.fromEntries( url = this.data.documentUrl || this.data.originUrl;
this.requestHeaders.map(({ name, value }) => [name, value]) url_comes_from = 'last resort';
);
if (headers.Referer) {
url = headers.Referer;
} else {
url = this.data.url;
}
} }
this.originalURL = url; this.originalURL = is_full_url ? url : null;
this.origin = new URL(url).origin; this.origin = new URL(url).origin;
this.originalPathname = new URL(url).pathname;
this.originalHost = new URL(url).host;
this.originalPathname = is_full_url ? new URL(url).pathname : null;
}
addHeaders(headers: Request['requestHeaders']) {
this.requestHeaders = headers || [];
return this;
}
init() {
this.initialized = true;
this.stolenData = this.getAllStolenData();
} }
isThirdParty() { isThirdParty() {
const request_url = new URL(this.data.url); const request_url = new URL(this.data.url);
const origin_url = new URL(this.originalURL); if (request_url.host.includes(this.originalHost)) {
if (request_url.host.includes(origin_url.host)) {
return false; return false;
} }
if (getshorthost(request_url.host) == getshorthost(origin_url.host)) { if (getshorthost(request_url.host) == getshorthost(this.originalHost)) {
return false; return false;
} }
return ( return (
request_url.origin != origin_url.origin || request_url.origin != this.origin ||
(this.data as any).urlClassification.thirdParty.length > 0 (this.data as any).urlClassification.thirdParty.length > 0
); );
} }
@ -162,13 +164,12 @@ export default class ExtendedRequest {
); );
} }
exposesOrigin() { exposesOriginWhere(): null | DataLocation {
const url = new URL(this.originalURL); const host = this.originalHost;
const host = url.host; const path = this.originalPathname || '/';
const path = url.pathname;
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 (
@ -176,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[] {
@ -217,7 +222,10 @@ export default class ExtendedRequest {
if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) { if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) {
return ['requestBody', key]; return ['requestBody', key];
} else if (!Array.isArray(value)) { } else if (!Array.isArray(value)) {
return ['raw', String.fromCharCode.apply(null, new Uint8Array(value.bytes))]; return [
'raw',
String.fromCharCode.apply(null, Array.from(new Uint8Array(value.bytes))),
];
} else { } else {
return [key, value || '']; return [key, value || ''];
} }
@ -236,7 +244,7 @@ export default class ExtendedRequest {
} }
getCookie(): string { getCookie(): string {
return this.requestHeaders.find((h) => h.name == 'Cookie')?.value; return this.requestHeaders.find((h) => h.name == 'Cookie')?.value || '';
} }
getPathParams(): StolenDataEntry[] { getPathParams(): StolenDataEntry[] {
@ -259,8 +267,8 @@ export default class ExtendedRequest {
getQueryParams(): StolenDataEntry[] { getQueryParams(): StolenDataEntry[] {
const url = new URL(this.data.url); const url = new URL(this.data.url);
return flattenObjectEntries( return flattenObjectEntries(
Array.from((url.searchParams as any).entries()) (Array.from((url.searchParams as any).entries()) as [string, string][])
.map(([key, value]) => [key, value || '']) .map(([key, value]: [string, string]) => [key, value || ''])
.map(([key, value]) => { .map(([key, value]) => {
return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))]; return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))];
}) })
@ -283,7 +291,7 @@ export default class ExtendedRequest {
.map((header) => { .map((header) => {
return [ return [
header.name, header.name,
StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)), StolenDataEntry.parseValue(safeDecodeURIComponent(header.value || '')),
]; ];
}) })
).map(([key, value]) => new StolenDataEntry(this, 'header', key, value)); ).map(([key, value]) => new StolenDataEntry(this, 'header', key, value));
@ -297,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

@ -1,9 +1,9 @@
{ {
"description": "Rentgen 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.", "description": "Rentgen 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.",
"manifest_version": 2, "manifest_version": 2,
"name": "Rentgen", "name": "Rentgen",
"short_name": "Rentgen", "short_name": "Rentgen",
"version": "0.0.3", "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": {

View File

@ -1,9 +1,9 @@
import ExtendedRequest from './extended-request'; import ExtendedRequest from './extended-request';
import { getshorthost, makeThrottle } from './util'; import { getshorthost } from './util';
import { RequestCluster } from './request-cluster'; import { RequestCluster } from './request-cluster';
import { SaferEmitter } from './safer-emitter'; import { SaferEmitter } from './safer-emitter';
function setDomainsNumber(counter: number, tabId: number) { function setDomainsCount(counter: number, tabId: number) {
browser.browserAction.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId }); browser.browserAction.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId });
browser.browserAction.setTitle({ browser.browserAction.setTitle({
title: 'Rentgen', title: 'Rentgen',
@ -35,10 +35,12 @@ export default class Memory extends SaferEmitter {
? browser.browserAction.setBadgeBackgroundColor({ color: '#ff726b' }) ? browser.browserAction.setBadgeBackgroundColor({ color: '#ff726b' })
: browser.browserAction.setBadgeBackgroundColor({ color: '#ffb900' }); : browser.browserAction.setBadgeBackgroundColor({ color: '#ffb900' });
setDomainsNumber( if (request.tabId >= 0) {
Object.values(this.getClustersForOrigin(request.origin)).length, setDomainsCount(
request.tabId Object.values(this.getClustersForOrigin(request.origin)).length,
); request.tabId
);
}
} }
constructor() { constructor() {
@ -65,7 +67,7 @@ export default class Memory extends SaferEmitter {
emit(eventName: string, data = 'any'): boolean { emit(eventName: string, data = 'any'): boolean {
setTimeout(() => super.emit(eventName, data), 0); setTimeout(() => super.emit(eventName, data), 0);
return; return true;
} }
getClustersForOrigin(origin: string): Record<string, RequestCluster> { getClustersForOrigin(origin: string): Record<string, RequestCluster> {
@ -76,7 +78,6 @@ export default class Memory extends SaferEmitter {
if (shorthost) { if (shorthost) {
const cookies = await browser.cookies.getAll({ domain: shorthost }); const cookies = await browser.cookies.getAll({ domain: shorthost });
for (const cookie of cookies) { for (const cookie of cookies) {
console.log('removing cookie', cookie.name, 'from', cookie.domain);
await browser.cookies.remove({ await browser.cookies.remove({
name: cookie.name, name: cookie.name,
url: `https://${cookie.domain}`, url: `https://${cookie.domain}`,

5064
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,17 @@
{ {
"name": "rentgen", "name": "rentgen",
"version": "0.0.3", "version": "0.1.10",
"description": "A simple Firefox extension that visualizes 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",
"scripts": { "scripts": {
"build": "node esbuild.config.js", "build": "node esbuild.config.js",
"watch": "node esbuild.config.js --watch", "watch": "node esbuild.config.js --watch",
"ext-test": "web-ext run", "ext-test": "web-ext run",
"create-package": "web-ext build", "build-addon": "npm i && npm run build && npm run create-package",
"typecheck": "tsc --noEmit" "create-package": "web-ext build --ignore-files '!**/node_modules' '!**/node_modules/**/react-dom' '!**/node_modules/**/react-dom/umd' '!**/node_modules/**/*/react-dom.production.min.js' '!**/node_modules/**/react' '!**/node_modules/**/react/umd' '!**/node_modules/**/*/react.production.min.js' '!**/node_modules/**/survey-react' '!**/node_modules/**/survey-react/*.min.js' '!**/node_modules/**/survey-react/*.min.css' --overwrite-dest",
"typecheck": "tsc --noEmit",
"lint": "web-ext lint"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,7 +1,7 @@
import { EventEmitter } from 'events'; 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';
@ -10,13 +10,20 @@ const source_priority: Array<Sources> = ['cookie', 'pathname', 'queryparams', 'h
export class RequestCluster extends SaferEmitter { export class RequestCluster extends SaferEmitter {
public requests: ExtendedRequest[] = []; public requests: ExtendedRequest[] = [];
public representativeStolenData: StolenDataEntry[] = []; public representativeStolenData: StolenDataEntry[] = [];
public expanded: boolean; public expanded: boolean = false;
public lastModified: number = 0;
public lastFullUrl: string | null = null;
constructor(public id: string) { constructor(public id: string) {
super(); super();
} }
add(request: ExtendedRequest) { add(request: ExtendedRequest) {
this.requests.push(request); this.requests.push(request);
this.emit('change'); this.emit('change');
this.lastModified = Date.now();
if (request.originalURL) {
this.lastFullUrl = request.originalURL;
}
} }
toggleExpanded(state: boolean) { toggleExpanded(state: boolean) {
@ -165,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());
} }
@ -178,8 +191,32 @@ 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') {
let types_of_data: string[] = [];
if (this.exposesOrigin()) {
types_of_data.push(`część ${noun} historii przeglądania`);
}
if (this.hasMarkedCookies()) {
types_of_data.push('unikalne ID z cookies');
}
if (types_of_data.length > 1) {
types_of_data[types_of_data.length - 1] =
'oraz ' + types_of_data[types_of_data.length - 1];
}
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

@ -48,7 +48,6 @@ export class SaferEmitter extends EventEmitter {
Reflect.apply(listener, this, args); Reflect.apply(listener, this, args);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
debugger;
} }
}); });
} }

View File

@ -1,4 +1,3 @@
import { EventEmitter } from 'events';
import ExtendedRequest, { HAREntry } from './extended-request'; import ExtendedRequest, { HAREntry } from './extended-request';
import { SaferEmitter } from './safer-emitter'; import { SaferEmitter } from './safer-emitter';
@ -34,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;
@ -59,11 +64,10 @@ export class StolenDataEntry extends SaferEmitter {
getPriority() { getPriority() {
let priority = 0; let priority = 0;
priority += Math.min(this.value.length, 50); priority += Math.min(this.value.length, 50);
const url = new URL(this.request.originalURL); if (this.value.includes(this.request.originalHost)) {
if (this.value.includes(url.host)) {
priority += 100; priority += 100;
} }
if (this.value.includes(url.pathname)) { if (this.request.originalPathname && this.value.includes(this.request.originalPathname)) {
priority += 100; priority += 100;
} }
if (this.source === 'cookie') { if (this.source === 'cookie') {
@ -133,7 +137,7 @@ export class StolenDataEntry extends SaferEmitter {
} else if (value === null) { } else if (value === null) {
return 'null'; return 'null';
} else { } else {
return value.toString(); return (value as any).toString();
} }
} }
@ -238,10 +242,14 @@ export class StolenDataEntry extends SaferEmitter {
} }
exposesPath() { exposesPath() {
const pathname = this.request.originalPathname;
if (pathname === null) {
return false;
}
return ( return (
this.request.originalPathname !== '/' && this.request.originalPathname !== '/' &&
[this.value, safeDecodeURIComponent(this.value)].some((haystack) => [this.value, safeDecodeURIComponent(this.value)].some((haystack) =>
haystack.includes(this.request.originalPathname) haystack.includes(pathname)
) )
); );
} }
@ -251,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,
};
}
} }

15
test.ts
View File

@ -1,15 +0,0 @@
import ExtendedRequest from "./extended-request";
import { StolenDataEntry } from "./stolen-data-entry";
import { flattenObject, maskString } from "./util";
console.log(flattenObject({ a: { b: { c: [1, 2, 3] } } }));
console.log(maskString("abcdefghijklmnopqrstuvwxyz", 1 / 3, 5));
console.log(maskString("abcdefghijklmnopqrstuvwxyz", 1, 30));
console.log(
StolenDataEntry.parseValue(
`[{"@context":"https://schema.org/","@type":"Product","image":["/medias/sys_master/root/images/h95/h8b/10873724928030/oppo-reno6-black-front.png","/medias/sys_master/root/images/h15/hb9/10873725681694/oppo-reno6-black-side.png","/medias/sys_master/root/images/hf8/h81/10873728729118/oppo-reno6-black-back.png"],"description":"OPPO Reno6 5G bez umowy lub w abonamencie w sklepie Orange. Zamów do domu lub odbierz w salonie w 24h. Transport gratis!","sku":"1100027218","brand":{"@type":"Thing","name":"OPPO"},"offers":{"@type":"Offer","priceCurrency":"PLN","priceValidUntil":"2099-12-31T00:00:00","itemCondition":"https://schema.org/UsedCondition","availability":"https://schema.org/InStock","url":"/esklep/smartfony/oppo/oppo-reno6-5g","seller":{"@type":"Organization","name":"Orange Polska S.A"},"price":"2199"},"name":"OPPO Reno6 5G"},{"@context":"http://schema.org/","@type":"Product","name":"OPPO Reno6 5G","description":"null","offers":{"@type":"Offer","priceCurrency":"PLN","price":"1248.00"}}]`
)
);

View File

@ -8,6 +8,9 @@
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types"], "typeRoots": ["node_modules/@types", "node_modules/web-ext-types"],
"target": "es2019", "target": "es2019",
"outDir": "lib", "outDir": "lib",
"skipLibCheck": true "skipLibCheck": true,
"strictNullChecks": true,
"strict": true,
"alwaysStrict": true
} }
} }

65
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;
@ -36,7 +37,11 @@ export function getshorthost(host: string) {
.replace(/^.*:\/\//, '') .replace(/^.*:\/\//, '')
.replace(/\/.*$/, '') .replace(/\/.*$/, '')
.split('.'); .split('.');
let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; const second_last = parts.at(-2);
if (!second_last) {
throw new Error('url too short?');
}
let lookback = !['co', 'com'].includes(second_last) ? -2 : -3;
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
lookback = -4; // to distinguish between google ads and stats lookback = -4; // to distinguish between google ads and stats
} else if (parts.at(-2) == 'google') { } else if (parts.at(-2) == 'google') {
@ -73,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,
@ -89,7 +94,7 @@ export async function getTabByID(id: number) {
} }
export function parseToObject(str: unknown): Record<string | symbol, unknown> { export function parseToObject(str: unknown): Record<string | symbol, unknown> {
let result: Record<string | symbol, unknown>; let result: Record<string | symbol, unknown> = {};
let original_string: string; let original_string: string;
if (typeof str === 'string') { if (typeof str === 'string') {
original_string = str; original_string = str;
@ -97,6 +102,8 @@ export function parseToObject(str: unknown): Record<string | symbol, unknown> {
} else if (typeof str == 'object') { } else if (typeof str == 'object') {
result = str as Record<string | symbol, unknown>; result = str as Record<string | symbol, unknown>;
original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str);
} else {
return result;
} }
result[Symbol.for('originalString')] = original_string; result[Symbol.for('originalString')] = original_string;
return result; return result;
@ -149,9 +156,13 @@ export function getDate() {
} }
export function toBase64(file: File): Promise<string> { export function toBase64(file: File): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
const FR = new FileReader(); const FR = new FileReader();
FR.addEventListener('load', (e) => { FR.addEventListener('load', (e) => {
const target = e.target;
if (!target) {
return reject('File missing?');
}
resolve(e.target.result as string); resolve(e.target.result as string);
}); });
FR.readAsDataURL(file); FR.readAsDataURL(file);
@ -199,7 +210,8 @@ export function isBase64JSON(s: unknown): s is string {
export function flattenObject( export function flattenObject(
obj: unknown, obj: unknown,
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(), parser: (to_parse: { toString: () => string }) => string | Record<string, unknown> = (id) =>
id.toString(),
key = '', key = '',
ret = [] as [string, string][], ret = [] as [string, string][],
parsed = false parsed = false
@ -220,7 +232,12 @@ export function flattenObject(
flattenObject(value, parser, prefix + subkey, ret); flattenObject(value, parser, prefix + subkey, ret);
} }
} else if (!parsed) { } else if (!parsed) {
flattenObject(parser(obj), parser, key, ret, true); try {
flattenObject(parser(obj as { toString: () => string }), parser, key, ret, true);
} catch (e) {
//emergency case, mostly for just type safety
ret.push([key, JSON.stringify(obj)]);
}
} else if (typeof obj === 'string') { } else if (typeof obj === 'string') {
ret.push([key, obj]); ret.push([key, obj]);
} else { } else {
@ -231,7 +248,8 @@ export function flattenObject(
export function flattenObjectEntries( export function flattenObjectEntries(
entries: [string, unknown][], entries: [string, unknown][],
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString() parser: (to_parse: { toString: () => string }) => string | Record<string, unknown> = (id) =>
id.toString()
): [string, string][] { ): [string, string][] {
return flattenObject(Object.fromEntries(entries), parser); return flattenObject(Object.fromEntries(entries), parser);
} }
@ -266,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);
}