Compare commits

...

6 Commits

20 changed files with 259 additions and 81 deletions

View File

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

View File

@ -19,6 +19,7 @@ export default function EmailContent({
visited_url: string;
clusters: Record<string, RequestCluster>;
}) {
console.log('rendering email!', answers);
const _ = (key: string) => v(key, answers.zaimek);
const problems = deduceProblems(answers, clusters);
const explainers = Array.from(

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> = {
cookies_are_pii: () => (
@ -11,4 +13,18 @@ export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => J
</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>
</>
),
};

View File

@ -1,26 +1,30 @@
import { RequestCluster } from '../../request-cluster';
function generateHostPage(
host: string,
cluster: RequestCluster,
index: number,
all_hosts: string[]
): { title: string; elements: any[]; visibleIf: string } {
function f(name: string, h = host) {
return `${h.replace(/\./g, '_')}|${name}`;
all_clusters: RequestCluster[]
): { title: string; elements: any[]; visibleIf?: string } {
function f(name: string, c = cluster) {
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) {
if (!previous_host) {
if (!previous_cluster) {
return {};
}
return { defaultValueExpression: `{${f(name, previous_host)}}` };
return { defaultValueExpression: `{${f(name, previous_cluster)}}` };
}
const domain = cluster.id;
const danych = cluster.getDataTypeDescription();
return {
title: host,
title: cluster.id,
elements: [
{
type: 'radiogroup',
name: f('present'),
isRequired: true,
title: `Cel ujawnienia danych właścicielowi domeny ${host}`,
title: `Cel ujawnienia danych (${danych}) właścicielowi domeny ${domain}`,
...defaultValue('present'),
visibleIf: '{popup_type} != "none"',
choices: [
@ -56,10 +60,10 @@ function generateHostPage(
'present'
)}} != "not_mentioned" and {${f('present')}} != "not_before_making_a_choice"`,
choices: [
{ value: 'consent', text: 'to zgoda.' },
{ value: 'consent', text: 'to zgoda (art. 6 ust. 1 lit. a RODO).' },
{
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.' },
],
@ -76,7 +80,7 @@ function generateHostPage(
choices: [
{
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',
@ -111,18 +115,18 @@ function generateHostPage(
},
{
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'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = 'vague'`,
placeholder: 'marketing',
defaultValueExpression:
index == 0
? 'marketing'
: `{${f('legitimate_interest_description', previous_host)}}`,
: `{${f('legitimate_interest_description', previous_cluster)}}`,
},
{
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'),
...defaultValue('outside_eu'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = "precise" or {${f(
@ -137,7 +141,7 @@ function generateHostPage(
},
{
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'),
isRequired: true,
...defaultValue('was_processing_necessary'),
@ -154,7 +158,7 @@ function generateHostPage(
};
}
export default function generateSurveyQuestions(hosts: string[]) {
export default function generateSurveyQuestions(clusters: RequestCluster[]) {
return {
showQuestionNumbers: 'off',
showProgressBar: 'top',
@ -393,7 +397,7 @@ export default function generateSurveyQuestions(hosts: string[]) {
},
],
},
...hosts.map(generateHostPage),
...clusters.map(generateHostPage),
],
};
}

View File

@ -50,6 +50,7 @@ export function parseAnswers({
mentions_passive_consent,
rejection_is_hard,
administrator_identity_available_before_choice,
popup_action,
...rest
}: RawAnswers): ParsedAnswers {
return {
@ -62,6 +63,7 @@ export function parseAnswers({
mentions_passive_consent,
rejection_is_hard,
administrator_identity_available_before_choice,
popup_action,
hosts: parseHostAnswers(rest),
} as ParsedAnswers;
}

View File

@ -3,6 +3,9 @@ import { v } from '../verbs';
import { Problem } from './problem';
export default class NoInformationAtAllProblem extends Problem {
qualifies() {
return this.answers.popup_type === 'none';
}
getEmailContent() {
const _ = (word: string) => v(word, this.answers.zaimek);
return (
@ -39,6 +42,7 @@ export default class NoInformationAtAllProblem extends Problem {
})
) {
explainers.push('cookies_are_pii');
explainers.push('responsibility_for_third_parties');
}
return explainers;
}

View File

@ -19,6 +19,7 @@ export abstract class Problem {
abstract getEmailContent(): JSX.Element;
abstract getNecessaryExplainers(): ExplainerKey[];
abstract qualifies(): boolean;
getMarkedClusters() {
return Object.values(this.clusters).filter((c) => c.hasMarks());

View File

@ -0,0 +1,63 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { v } from '../verbs';
import { Problem } from './problem';
export class TransferOutsideEU extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
return [];
}
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() {
const clusters = this.getRelatedClusters();
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Transfer danych osobowych poza Europejski Obszar Gospodarczy</h2>
<p>
Państwa strona przetworzyła {_('moje')} dane osobowe poprzez przesłanie danych
do:
</p>
<ul>
{clusters.map((cluster) => (
<li key={cluster.id}>
Właściciela domeny <strong>{cluster.id}</strong>: (w zakresie:{' '}
{cluster.getDataTypeDescription('mojej')});
</li>
))}
</ul>
<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>
{_('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>
</>
);
}
}

View File

@ -0,0 +1,73 @@
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[] {
return ['responsibility_for_third_parties'];
}
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() {
const clusters = this.getRelatedClusters();
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Przetwarzanie danych osobowych bez podania podstawy prawnej</h2>
<p>Państwa strona przetworzyła {_('moje')} dane osobowe poprzez ujawnienie:</p>
<ul>
{clusters.map((cluster) => (
<li key={cluster.id}>
Właścicielowi domeny <strong>{cluster.id}</strong>:{' '}
{cluster.getDataTypeDescription('mojej')}
</li>
))}
</ul>
<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>
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>
</>
);
}
}

View File

@ -1,25 +1,23 @@
import { RequestCluster } from '../../../request-cluster';
import { wordlist } from '../../../util';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
export class UnlawfulCookieAccess extends Problem {
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
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) => {
const hostAnswers = answers.hosts[cluster.id];
const hostAnswers = this.answers.hosts[cluster.id];
return (
(hostAnswers.present == 'not_mentioned' ||
hostAnswers.present == 'not_before_making_a_choice' ||
['none', 'closed_popup', 'deny_all'].includes(answers.popup_action) ||
answers.popup_type === 'none') &&
['none', 'closed_popup', 'deny_all'].includes(this.answers.popup_action) ||
this.answers.popup_type === 'none') &&
hostAnswers.was_processing_necessary != 'yes'
);
});
@ -76,7 +74,8 @@ export class UnlawfulCookieAccess extends Problem {
<li>
Użytkownik wyraził zgodę na takie przetwarzanie danych <em>po</em> tym, jak
został poinformowany bezpośrednio o celu uzyskania dostępu do tej
informacji;
informacji. Zgodnie z Art. 174 ustawy Prawo Telekomunikacyjne, taka zgoda
musiałaby spełniać warunki zgody ustalone przez RODO;
</li>
<li>
Dostęp do treści plików cookies jest konieczny do dostarczania usługi
@ -163,9 +162,12 @@ export class UnlawfulCookieAccess extends Problem {
''
)}
<p>
{_('Proszę')} o wskazanie, czy być może stosowali Państwo inną podstawę prawną
do takiego przetwarzania {_('moich')} danych osobowych, czy przetwarzali je
państwo bez ważnej podstawy prawnej?
{_('Proszę')} o wskazanie,{' '}
<strong>
czy być może stosowali Państwo inną podstawę prawną do takiego przetwarzania{' '}
{_('moich')} danych osobowych, czy przetwarzali je państwo bez ważnej
podstawy prawnej?
</strong>
</p>
{maybe_unnecessary_hosts.length > 1 ? (
<p>

View File

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

View File

@ -35,9 +35,9 @@ function Report() {
<div {...{ 'data-version': counter }}>
{mode === 'survey' ? (
<Questions
hosts={Object.values(clusters)
.filter((cluster) => cluster.getMarkedRequests().length > 0)
.map((cluster) => cluster.id)}
clusters={Object.values(clusters).filter(
(cluster) => cluster.getMarkedRequests().length > 0
)}
onComplete={(answers) => {
setAnswers(parseAnswers(answers));
setMode('screenshots');

View File

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

View File

@ -8,6 +8,7 @@ const words = {
mnie: ['mnie', 'mnie', 'mnie', 'nas'],
moich: ['moich', 'moich', 'moich', 'naszych'],
moje: ['moje', 'moje', 'moje', 'nasze'],
moja: ['moja', 'moja', 'moja', 'nasza'],
mojej: ['mojej', 'mojej', 'mojej', 'naszej'],
muszę: ['muszę', 'muszę', 'muszę', 'musimy'],
odkliknąłeś: ['odkliknąłeś', 'odkliknęłaś', 'odklikęłoś', 'odkliknęliście'],

View File

@ -145,16 +145,6 @@ const Toolbar = () => {
<section className="summary">
<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['*']}`}>
@ -165,6 +155,16 @@ const Toolbar = () => {
}
</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}
@ -174,18 +174,6 @@ const Toolbar = () => {
</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['*']}`}
@ -194,10 +182,22 @@ const Toolbar = () => {
.map((domain) => domain.id)
.join(', ')}
>
{first_sentence_history}
{first_sentence_cookie}
<strong>{cookieDomainCopy}</strong>
</p>
) : null}
{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>
{exposedOriginDomainCopy || cookieDomainCopy ? (

View File

@ -117,9 +117,10 @@ export default class ExtendedRequest {
async cacheOrigin(): Promise<void> {
let url: string;
if (this.data.tabId && this.data.tabId >= 0) {
const tab = await browser.tabs.get(this.data.tabId);
url = tab.url;
if (this.data.type === 'main_frame') {
url = this.data.url;
} else if (this.data.originUrl) {
url = this.data.originUrl;
} else if (
(this.data as any)?.frameAncestors &&
(this.data as any).frameAncestors[0] !== undefined

View File

@ -3,7 +3,7 @@ import { getshorthost, makeThrottle } from './util';
import { RequestCluster } from './request-cluster';
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.setTitle({
title: 'Rentgen',
@ -35,10 +35,12 @@ export default class Memory extends SaferEmitter {
? browser.browserAction.setBadgeBackgroundColor({ color: '#ff726b' })
: browser.browserAction.setBadgeBackgroundColor({ color: '#ffb900' });
setDomainsNumber(
Object.values(this.getClustersForOrigin(request.origin)).length,
request.tabId
);
if (request.tabId >= 0) {
setDomainsCount(
Object.values(this.getClustersForOrigin(request.origin)).length,
request.tabId
);
}
}
constructor() {

1
package-lock.json generated
View File

@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
"name": "rentgen",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@ -182,4 +182,15 @@ export class RequestCluster extends SaferEmitter {
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');
}
return types_of_data.join(', ');
}
}

View File

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