Merge pull request 'Problem: nieznany cel. Dodanie narzędzi diagnostycznych' (#99) from unknown-purpose into develop

Reviewed-on: #99
This commit is contained in:
Arkadiusz Wieczorek 2022-09-12 09:47:52 +02:00
commit 1106e86b41
14 changed files with 362 additions and 12 deletions

View File

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

View File

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

View File

@ -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

@ -0,0 +1,109 @@
import { RequestCluster } from '../../../request-cluster';
import { dataLocationToText, wordlist } from '../../../util';
import { ExplainerKey } from '../explainers';
import { v } from '../verbs';
import { Problem } from './problem';
export class UnknownPurposes extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
return [];
}
isHostAffected(host: string) {
const answers = this.answers.hosts[host];
if (!answers) {
return false;
}
return (
['not_mentioned', 'not_before_making_a_choice'].includes(answers.present) &&
['no', 'not_sure'].includes(answers.was_processing_necessary) &&
(this.clusters[host].hasCookies() || this.clusters[host].exposesOrigin())
);
}
qualifies(): boolean {
return Object.keys(this.answers.hosts).some((host) => this.isHostAffected(host));
}
getAffectedClusters(): RequestCluster[] {
return Object.keys(this.answers.hosts)
.filter((host) => this.isHostAffected(host))
.map((host) => this.clusters[host]);
}
getEmailContent({ mode, tone }: { mode: 'email' | 'report'; tone: 'official' | 'polite' }) {
const _ = (key: string) => v(key, this.answers.zaimek);
const affected_clusters = this.getAffectedClusters();
const has_history = affected_clusters.some((cluster) => cluster.exposesOrigin());
const has_cookies = affected_clusters.some((cluster) => cluster.hasCookies());
return (
<>
<h2>Cele przetwarzania danych</h2>
<p>
Państwa strona{' '}
{mode == 'email'
? `ujawniła dane ${_('mnie')} dotyczące`
: 'ujawnia dane dotyczące użytkowników'}{' '}
w zakresie{' '}
{wordlist([
...(has_cookies ? ['treści plików cookies'] : []),
...(has_history
? [
mode === 'email'
? `części ${_('mojej')} historii przeglądania`
: `części historii przeglądania`,
]
: []),
])}{' '}
podmiotom, które właścicielami nastepujących domen:
</p>
<ul>
{affected_clusters.map((cluster, index) => {
const locations = cluster.exposesOriginWhere();
return (
<li>
<strong>{cluster.id}</strong>:{' '}
{wordlist([
...(cluster.hasCookies() ? ['treść plików cookies'] : []),
...(cluster.exposesOrigin()
? [
(mode === 'email'
? `część ${_('mojej')} historii przeglądania`
: `część historii przeglądania użytkownika`) +
' (' +
wordlist(
locations.map((l) => dataLocationToText(l))
) +
')',
]
: []),
])}
{index === affected_clusters.length - 1 ? '.' : ';'}
</li>
);
})}
</ul>
{mode === 'email' ? (
tone === 'official' ? (
<p>
Proszę o wskazanie, jakie cele takiego przetwarzania danych, które
mnie dotyczą.
</p>
) : (
<p>
Apeluję o umieszczenie informacji na temat na Państwa stronie, aby jej
użytkownicy mogli podejmować w pełni świadome wybory dotyczące
przetwarzania danych ich dotyczących.
</p>
)
) : (
<p>
<strong>Zalecenie</strong>: warto dodać informacje o tym, jakie cele
ujawniania wyżej opisanych danych wyżej opisanym podmiotom trzecim.
</p>
)}
</>
);
}
}

View File

@ -5,6 +5,17 @@
font-family: 'OpenSans' !important; font-family: 'OpenSans' !important;
} }
#app {
min-height: 100vh;
display: flex;
flex-flow: column;
}
#main-section {
flex-grow: 1;
margin-bottom: 20px; // to contain diag section
}
html { html {
font-size: 1rem; font-size: 1rem;
} }
@ -289,3 +300,9 @@ h1 {
color: $ultra-black-color; color: $ultra-black-color;
} }
} }
.diag-toolbox {
position: fixed;
bottom: 10px;
left: 10px;
}

View File

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

View File

@ -104,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;
@ -130,6 +128,10 @@ export default function StolenDataCluster({
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);
}} }}

13
diag.html Normal file
View File

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

65
diag.tsx Normal file
View File

@ -0,0 +1,65 @@
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import EmailContent from './components/report-window/email-content';
import { makeFakeClusters } from './components/report-window/fake-clusters';
class ErrorBoundary extends React.Component<any, { hasError: boolean; error: any }> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: any) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
function Diag() {
const [json, setjson] = React.useState(
JSON.stringify({ answers: { hosts: {} }, visited_url: '', fake_clusters_data: {} })
);
const { answers, visited_url, fake_clusters_data } = JSON.parse(json);
const fake_clusters = makeFakeClusters(fake_clusters_data);
return (
<div style={{ display: 'grid', gridTemplateColumns: '50% 50%', minHeight: '100vh' }}>
<div>
<textarea
style={{ width: 'calc(100% - 50px)', height: '100%' }}
value={json}
onChange={(e) => {
setjson(e.target.value);
}}
></textarea>
</div>
<div>
<EmailContent
{...{
answers,
visited_url,
clusters: fake_clusters,
scrRequestPath: '/screenshots',
downloadFiles: () => {
alert('download!');
},
user_role: 'user',
}}
/>
</div>
</div>
);
}
ReactDOM.render(
<ErrorBoundary>
<Diag />
</ErrorBoundary>,
document.getElementById('app')
);

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { FakeRequestClusterData } from './components/report-window/fake-clusters';
import ExtendedRequest from './extended-request'; import ExtendedRequest from './extended-request';
import { SaferEmitter } from './safer-emitter'; import { SaferEmitter } from './safer-emitter';
import { Sources, StolenDataEntry } from './stolen-data-entry'; import { DataLocation, Sources, StolenDataEntry } from './stolen-data-entry';
import { allSubhosts, isSameURL, reduceConcat, unique } from './util'; import { allSubhosts, isSameURL, reduceConcat, unique } from './util';
@ -171,7 +172,13 @@ export class RequestCluster extends SaferEmitter {
return this.requests.map((request) => request.getMarkedEntries()).reduce(reduceConcat, []); return this.requests.map((request) => request.getMarkedEntries()).reduce(reduceConcat, []);
} }
exposesOrigin() { exposesOriginWhere(): DataLocation[] {
return this.requests
.map((request) => request.exposesOriginWhere())
.filter((l) => l !== null) as DataLocation[];
}
exposesOrigin(): boolean {
return this.requests.some((request) => request.exposesOrigin()); return this.requests.some((request) => request.exposesOrigin());
} }
@ -203,4 +210,15 @@ export class RequestCluster extends SaferEmitter {
} }
return types_of_data.join(', '); return types_of_data.join(', ');
} }
makeDataForFake(): FakeRequestClusterData {
return {
id: this.id,
hasCookies: this.hasCookies(),
hasMarkedCookies: this.hasMarkedCookies(),
hasMarks: this.hasMarks(),
exposesOriginWhere: this.exposesOriginWhere(),
exposesOrigin: this.exposesOrigin(),
};
}
} }

View File

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

34
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;
@ -283,8 +284,37 @@ export function normalizeForClassname(string: string) {
} }
export function wordlist(words: string[]) { export function wordlist(words: string[]) {
return words.reduce( return Array.from(new Set(words)).reduce(
(acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, (acc, word, i) =>
`${acc}${
i > 0 ? (i < words.length - 1 ? ', ' : Math.random() > 0.5 ? ' i ' : ' oraz ') : ''
}${word}`,
'' ''
); );
} }
const source_to_word: Record<Sources, string> = {
cookie: 'plik cookie o nazwie',
pathname: 'fragment ścieżki w URL',
queryparams: 'query params w URL o nazwie',
header: 'nagłówek HTTP',
request_body: 'body zapytania HTTP, pod kluczem',
};
export function dataLocationToText(l: DataLocation) {
return `${source_to_word[l.source]} ${l.key}`;
}
export function downloadText(filename: string, text: string) {
// https://stackoverflow.com/questions/45831191/generate-and-download-file-from-js
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}