Flatten the stolen data
This commit is contained in:
parent
c9f3876cf4
commit
68078546fa
|
@ -1,5 +1,10 @@
|
||||||
import { StolenDataEntry } from "./stolen-data-entry";
|
import { StolenDataEntry } from "./stolen-data-entry";
|
||||||
import { getshorthost, parseCookie, Request } from "./util";
|
import {
|
||||||
|
flattenObjectEntries,
|
||||||
|
getshorthost,
|
||||||
|
parseCookie,
|
||||||
|
Request,
|
||||||
|
} from "./util";
|
||||||
|
|
||||||
type NameValue = { name: string; value: string };
|
type NameValue = { name: string; value: string };
|
||||||
|
|
||||||
|
@ -151,9 +156,13 @@ export default class ExtendedRequest {
|
||||||
if (!this.hasCookie() || this.getCookie() === undefined) {
|
if (!this.hasCookie() || this.getCookie() === undefined) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Object.entries(parseCookie(this.getCookie()))
|
return flattenObjectEntries(
|
||||||
|
Object.entries(parseCookie(this.getCookie()))
|
||||||
.map(([key, value]) => [key, value || ""])
|
.map(([key, value]) => [key, value || ""])
|
||||||
.map(([key, value]) => new StolenDataEntry(this, "cookie", key, value));
|
.map(([key, value]) => {
|
||||||
|
return [key, StolenDataEntry.parseValue(value)];
|
||||||
|
})
|
||||||
|
).map(([key, value]) => new StolenDataEntry(this, "cookie", key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
hasReferer() {
|
hasReferer() {
|
||||||
|
@ -174,30 +183,33 @@ export default class ExtendedRequest {
|
||||||
if (!path.includes(";")) {
|
if (!path.includes(";")) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return path
|
return flattenObjectEntries(
|
||||||
|
path
|
||||||
.split(";")
|
.split(";")
|
||||||
.map((e) => e.split("="))
|
.map((e) => e.split("="))
|
||||||
.map(([key, value]) => [key, value || ""])
|
.map(([key, value]) => [key, value || ""])
|
||||||
.map(
|
.map(([key, value]) => {
|
||||||
([key, value]) =>
|
return [key, StolenDataEntry.parseValue(decodeURIComponent(value))];
|
||||||
new StolenDataEntry(this, "pathname", key, decodeURIComponent(value))
|
})
|
||||||
);
|
).map(([key, value]) => new StolenDataEntry(this, "pathname", key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryParams(): StolenDataEntry[] {
|
getQueryParams(): StolenDataEntry[] {
|
||||||
const url = new URL(this.data.url);
|
const url = new URL(this.data.url);
|
||||||
return Array.from((url.searchParams as any).entries())
|
return flattenObjectEntries(
|
||||||
|
Array.from((url.searchParams as any).entries())
|
||||||
.map(([key, value]) => [key, value || ""])
|
.map(([key, value]) => [key, value || ""])
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
try {
|
return [key, StolenDataEntry.parseValue(decodeURIComponent(value))];
|
||||||
value = decodeURIComponent(value);
|
})
|
||||||
} catch (e) {}
|
).map(([key, value]) => {
|
||||||
return new StolenDataEntry(this, "queryparams", key, value);
|
return new StolenDataEntry(this, "queryparams", key, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeadersData(): StolenDataEntry[] {
|
getHeadersData(): StolenDataEntry[] {
|
||||||
return this.data.requestHeaders
|
return flattenObjectEntries(
|
||||||
|
this.data.requestHeaders
|
||||||
.filter((header) => {
|
.filter((header) => {
|
||||||
for (const regex of whitelisted_cookies) {
|
for (const regex of whitelisted_cookies) {
|
||||||
if (regex.test(header.name)) {
|
if (regex.test(header.name)) {
|
||||||
|
@ -206,18 +218,21 @@ export default class ExtendedRequest {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(
|
.map((header) => {
|
||||||
(header) =>
|
return [
|
||||||
new StolenDataEntry(this, "header", header.name, header.value)
|
header.name,
|
||||||
);
|
StolenDataEntry.parseValue(decodeURIComponent(header.value)),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
).map(([key, value]) => new StolenDataEntry(this, "header", key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMark() {
|
hasMark() {
|
||||||
return this.stolenData.some((data) => data.hasMark());
|
return this.stolenData.some((data) => data.isMarked);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMarkedEntries() {
|
getMarkedEntries() {
|
||||||
return this.stolenData.filter((data) => data.hasMark());
|
return this.stolenData.filter((data) => data.isMarked);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHost() {
|
getHost() {
|
||||||
|
|
32
mark.ts
32
mark.ts
|
@ -1,32 +0,0 @@
|
||||||
import { Classifications, StolenDataEntry } from "./stolen-data-entry";
|
|
||||||
|
|
||||||
export default class Mark {
|
|
||||||
classification: keyof typeof Classifications;
|
|
||||||
constructor(public entry: StolenDataEntry, public key: string) {
|
|
||||||
this.classification = entry.classification;
|
|
||||||
}
|
|
||||||
|
|
||||||
getParsedValue() {
|
|
||||||
return this.entry.getParsedValue(this.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
get shorthost() {
|
|
||||||
return this.entry.request.shorthost;
|
|
||||||
}
|
|
||||||
|
|
||||||
get source() {
|
|
||||||
return this.entry.source;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.entry.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get originalURL() {
|
|
||||||
return this.entry.request.originalURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
get valuePreview(): string {
|
|
||||||
return this.entry.getValuePreview(this.key);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,8 +4,9 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "email-template-harsh.js",
|
"main": "email-template-harsh.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx esbuild sidebar/sidebar.tsx report-window/report-window.tsx --bundle background.ts --bundle --outdir=./lib",
|
"build": "npx esbuild sidebar/sidebar.tsx test.ts report-window/report-window.tsx --bundle background.ts --bundle --outdir=./lib",
|
||||||
"watch": "npm run build -- --watch"
|
"watch": "npm run build -- --watch",
|
||||||
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -25,18 +25,11 @@ export default function DomainSummary({
|
||||||
Właściciel domeny <strong>{cluster.id}</strong> otrzymał:{" "}
|
Właściciel domeny <strong>{cluster.id}</strong> otrzymał:{" "}
|
||||||
<ul>
|
<ul>
|
||||||
<li>Mój adres IP</li>
|
<li>Mój adres IP</li>
|
||||||
{cluster.getRepresentativeMarks().map((mark) => (
|
{cluster.getRepresentativeStolenData().map((entry) => (
|
||||||
<li>
|
<li>
|
||||||
{emailClassifications[mark.classification]}{" "}
|
{emailClassifications[entry.classification]}{" "}
|
||||||
{emailSources[mark.source]} (nazwa: <code>{mark.name}</code>,{" "}
|
{emailSources[entry.source]} (nazwa: <code>{entry.name}</code>,{" "}
|
||||||
{mark.key ? (
|
wartość: <code>{entry.getValuePreview()}</code>)
|
||||||
<>
|
|
||||||
pozycja <code>{mark.key}</code>,
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
wartość: <code>{mark.valuePreview}</code>)
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Mark from "../mark";
|
|
||||||
import { RequestCluster } from "../request-cluster";
|
import { RequestCluster } from "../request-cluster";
|
||||||
|
import { StolenDataEntry } from "../stolen-data-entry";
|
||||||
import { getDate, toBase64 } from "../util";
|
import { getDate, toBase64 } from "../util";
|
||||||
import DomainSummary from "./domain-summary";
|
import DomainSummary from "./domain-summary";
|
||||||
|
|
||||||
type PopupState = "not_clicked" | "clicked_but_no_reject_all";
|
type PopupState = "not_clicked" | "clicked_but_no_reject_all";
|
||||||
|
|
||||||
export default function EmailTemplate({
|
export default function EmailTemplate({
|
||||||
marks,
|
entries,
|
||||||
clusters,
|
clusters,
|
||||||
}: {
|
}: {
|
||||||
marks: Mark[];
|
entries: StolenDataEntry[];
|
||||||
clusters: Record<string, RequestCluster>;
|
clusters: Record<string, RequestCluster>;
|
||||||
version: number;
|
version: number;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
@ -68,8 +68,8 @@ export default function EmailTemplate({
|
||||||
) : null}
|
) : null}
|
||||||
<p>
|
<p>
|
||||||
Dzień dobry, w dniu {getDate()} odwiedziłem stronę{" "}
|
Dzień dobry, w dniu {getDate()} odwiedziłem stronę{" "}
|
||||||
{marks[0].originalURL}. Strona ta wysłała moje dane osobowe do podmiotów
|
{entries[0].request.originalURL}. Strona ta wysłała moje dane osobowe do
|
||||||
trzecich - bez mojej zgody.{" "}
|
podmiotów trzecich - bez mojej zgody.{" "}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.values(clusters)
|
{Object.values(clusters)
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { HAREntry } from "../extended-request";
|
import { HAREntry } from "../extended-request";
|
||||||
import Mark from "../mark";
|
import { StolenDataEntry } from "../stolen-data-entry";
|
||||||
import { getshorthost, unique } from "../util";
|
import { getshorthost, unique } from "../util";
|
||||||
|
|
||||||
function handleNewFile(
|
function handleNewFile(
|
||||||
element: HTMLInputElement,
|
element: HTMLInputElement,
|
||||||
marks: Mark[],
|
entries: StolenDataEntry[],
|
||||||
setFiltered: (Blob) => void
|
setFiltered: (Blob) => void
|
||||||
): void {
|
): void {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener("load", () => {
|
reader.addEventListener("load", () => {
|
||||||
const content = JSON.parse(reader.result as string);
|
const content = JSON.parse(reader.result as string);
|
||||||
content.log.entries = content.log.entries.filter((har_entry: HAREntry) =>
|
content.log.entries = content.log.entries.filter((har_entry: HAREntry) =>
|
||||||
marks.some((mark) => mark.entry.matchesHAREntry(har_entry))
|
entries.some((entry) => entry.matchesHAREntry(har_entry))
|
||||||
);
|
);
|
||||||
setFiltered(
|
setFiltered(
|
||||||
new Blob([JSON.stringify(content)], { type: "application/json" })
|
new Blob([JSON.stringify(content)], { type: "application/json" })
|
||||||
|
@ -21,8 +21,8 @@ function handleNewFile(
|
||||||
reader.readAsText(element.files[0]);
|
reader.readAsText(element.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFakeHAR(marks: Mark[]) {
|
function generateFakeHAR(entries: StolenDataEntry[]) {
|
||||||
const requests = marks.map((mark) => mark.entry.request);
|
const requests = entries.map((entry) => entry.request);
|
||||||
return {
|
return {
|
||||||
log: {
|
log: {
|
||||||
version: "1.2",
|
version: "1.2",
|
||||||
|
@ -50,12 +50,16 @@ function generateFakeHAR(marks: Mark[]) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HARConverter({ marks }: { marks: Mark[] }) {
|
export default function HARConverter({
|
||||||
|
entries,
|
||||||
|
}: {
|
||||||
|
entries: StolenDataEntry[];
|
||||||
|
}) {
|
||||||
const [filtered, setFiltered] = useState<Blob | null>(null);
|
const [filtered, setFiltered] = useState<Blob | null>(null);
|
||||||
const [filename, setFilename] = useState("");
|
const [filename, setFilename] = useState("");
|
||||||
const [fakeHAR, setFakeHAR] = useState<ReturnType<typeof generateFakeHAR>>();
|
const [fakeHAR, setFakeHAR] = useState<ReturnType<typeof generateFakeHAR>>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFakeHAR(generateFakeHAR(marks));
|
setFakeHAR(generateFakeHAR(entries));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -65,7 +69,7 @@ export default function HARConverter({ marks }: { marks: Mark[] }) {
|
||||||
accept=".har"
|
accept=".har"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFilename(e.target.files[0].name);
|
setFilename(e.target.files[0].name);
|
||||||
handleNewFile(e.target, marks, setFiltered);
|
handleNewFile(e.target, entries, setFiltered);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{(filtered && (
|
{(filtered && (
|
||||||
|
@ -82,7 +86,7 @@ export default function HARConverter({ marks }: { marks: Mark[] }) {
|
||||||
new Blob([JSON.stringify(fakeHAR)], { type: "application/json" })
|
new Blob([JSON.stringify(fakeHAR)], { type: "application/json" })
|
||||||
)}
|
)}
|
||||||
download={`${getshorthost(
|
download={`${getshorthost(
|
||||||
marks[0].originalURL
|
entries[0].request.originalURL
|
||||||
)}-${new Date().toJSON()}-trimmed.har`}
|
)}-${new Date().toJSON()}-trimmed.har`}
|
||||||
>
|
>
|
||||||
Pobierz "zfałszowany" HAR
|
Pobierz "zfałszowany" HAR
|
||||||
|
|
|
@ -15,9 +15,10 @@ function Report() {
|
||||||
setCounter((c) => c + 1);
|
setCounter((c) => c + 1);
|
||||||
}
|
}
|
||||||
const clusters = getMemory().getClustersForOrigin(origin);
|
const clusters = getMemory().getClustersForOrigin(origin);
|
||||||
const marks = Object.values(clusters)
|
const entries = Object.values(clusters)
|
||||||
.map((cluster) => cluster.getRepresentativeMarks())
|
.map((cluster) => cluster.getRepresentativeStolenData())
|
||||||
.reduce(reduceConcat, []);
|
.reduce(reduceConcat, [])
|
||||||
|
.filter((entry) => entry.isMarked);
|
||||||
return (
|
return (
|
||||||
<div {...{ "data-version": counter }}>
|
<div {...{ "data-version": counter }}>
|
||||||
<h1>Generuj treść maila dla {origin}</h1>
|
<h1>Generuj treść maila dla {origin}</h1>
|
||||||
|
@ -31,37 +32,36 @@ function Report() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{marks.map((mark) => (
|
{entries.map((entry) => (
|
||||||
<tr
|
<tr
|
||||||
key={mark.entry.request.originalURL + ";" + mark.key}
|
key={entry.id}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
mark.classification == "id" ? "yellow" : "white",
|
entry.classification == "id" ? "yellow" : "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td>{mark.shorthost}</td>
|
<td>{entry.request.shorthost}</td>
|
||||||
<td style={{ overflowWrap: "anywhere" }}>
|
<td style={{ overflowWrap: "anywhere" }}>
|
||||||
{mark.source}:{mark.name}
|
{entry.source}:{entry.name}
|
||||||
{mark.key}
|
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
width: "400px",
|
width: "400px",
|
||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
backgroundColor: mark.entry.isRelatedToID()
|
backgroundColor: entry.isRelatedToID()
|
||||||
? "#ffff0054"
|
? "#ffff0054"
|
||||||
: "white",
|
: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mark.valuePreview}
|
{entry.getValuePreview()}
|
||||||
{/* always gonna have
|
{/* always gonna have
|
||||||
one key, because unwrapEntry is called above */}
|
one key, because unwrapEntry is called above */}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
value={mark.classification}
|
value={entry.classification}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
mark.classification = e.target
|
entry.classification = e.target
|
||||||
.value as keyof typeof Classifications;
|
.value as keyof typeof Classifications;
|
||||||
refresh();
|
refresh();
|
||||||
}}
|
}}
|
||||||
|
@ -81,8 +81,8 @@ function Report() {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<EmailTemplate {...{ marks, clusters, version: counter }} />
|
<EmailTemplate {...{ entries, clusters, version: counter }} />
|
||||||
<HARConverter {...{ marks }} />
|
<HARConverter {...{ entries }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import ExtendedRequest from "./extended-request";
|
import ExtendedRequest from "./extended-request";
|
||||||
import {
|
import { Sources, StolenDataEntry } from "./stolen-data-entry";
|
||||||
MergedStolenDataEntry,
|
|
||||||
Sources,
|
|
||||||
StolenDataEntry,
|
|
||||||
} from "./stolen-data-entry";
|
|
||||||
|
|
||||||
import { allSubhosts, isSameURL, reduceConcat, unique } from "./util";
|
import { allSubhosts, isSameURL, reduceConcat, unique } from "./util";
|
||||||
|
|
||||||
|
@ -34,13 +30,16 @@ export class RequestCluster extends EventEmitter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStolenData(filter: {
|
getRepresentativeStolenData(
|
||||||
|
filter: {
|
||||||
minValueLength: number;
|
minValueLength: number;
|
||||||
cookiesOnly: boolean;
|
cookiesOnly: boolean;
|
||||||
cookiesOrOriginOnly: boolean;
|
cookiesOrOriginOnly: boolean;
|
||||||
}): MergedStolenDataEntry[] {
|
} = { minValueLength: 0, cookiesOnly: false, cookiesOrOriginOnly: false }
|
||||||
|
): StolenDataEntry[] {
|
||||||
return this.requests
|
return this.requests
|
||||||
.map((request) => request.stolenData)
|
.map((request) => request.stolenData)
|
||||||
|
|
||||||
.reduce((a, b) => a.concat(b), [])
|
.reduce((a, b) => a.concat(b), [])
|
||||||
.filter((entry) => {
|
.filter((entry) => {
|
||||||
return entry.value.length >= filter.minValueLength;
|
return entry.value.length >= filter.minValueLength;
|
||||||
|
@ -52,33 +51,64 @@ export class RequestCluster extends EventEmitter {
|
||||||
entry.source === "cookie" ||
|
entry.source === "cookie" ||
|
||||||
entry.classification === "history"
|
entry.classification === "history"
|
||||||
)
|
)
|
||||||
.sort((entryA, entryB) => (entryA.name > entryB.name ? -1 : 1))
|
.sort((entry1, entry2) => {
|
||||||
.filter((element, index, array) => {
|
if (entry1.value > entry2.value) {
|
||||||
// remove duplicates by name/value
|
return -1;
|
||||||
|
} else if (entry1.value < entry2.value) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
const indexA = source_priority.indexOf(entry1.source);
|
||||||
|
const indexB = source_priority.indexOf(entry2.source);
|
||||||
|
if (indexA < indexB) {
|
||||||
|
return -1;
|
||||||
|
} else if (indexA > indexB) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return entry1.value.length > entry2.value.length ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((_, index, array) => {
|
||||||
|
// removing value duplicates
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
element.name != array[index - 1].name ||
|
array[index].getValuePreview() ===
|
||||||
element.value != array[index - 1].value
|
array[index - 1].getValuePreview() ||
|
||||||
|
(array[index].classification === "history" &&
|
||||||
|
array[index - 1].classification === "history") || // if they're both history, then the first one is the longest
|
||||||
|
isSameURL(array[index].value, array[index - 1].value)
|
||||||
) {
|
) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((entryA, entryB) => (entryA.value > entryB.value ? -1 : 1))
|
.sort((entry1, entry2) => {
|
||||||
.reduce(
|
if (entry1.name < entry2.name) {
|
||||||
(acc: MergedStolenDataEntry[], entry: StolenDataEntry) => {
|
return -1;
|
||||||
// group by value
|
} else if (entry1.name > entry2.name) {
|
||||||
const last_entry = acc.slice(-1)[0];
|
return 1;
|
||||||
if (last_entry.hasValue(entry.value)) {
|
|
||||||
last_entry.mergeWith(entry);
|
|
||||||
} else {
|
} else {
|
||||||
acc.push(new MergedStolenDataEntry([entry]));
|
if (entry1.value.length > entry2.value.length) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return acc;
|
}
|
||||||
},
|
})
|
||||||
[new MergedStolenDataEntry([])] as MergedStolenDataEntry[]
|
.filter((_, index, array) => {
|
||||||
)
|
// removing name duplicates, keeping only the first - which is the longest. Some data loss may occur.
|
||||||
|
if (index == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (array[index].name === array[index - 1].name) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
.sort((entry1, entry2) =>
|
.sort((entry1, entry2) =>
|
||||||
entry1.getPriority() > entry2.getPriority() ? -1 : 1
|
entry1.getPriority() > entry2.getPriority() ? -1 : 1
|
||||||
);
|
);
|
||||||
|
@ -125,49 +155,4 @@ export class RequestCluster extends EventEmitter {
|
||||||
exposesOrigin() {
|
exposesOrigin() {
|
||||||
return this.requests.some((request) => request.exposesOrigin());
|
return this.requests.some((request) => request.exposesOrigin());
|
||||||
}
|
}
|
||||||
|
|
||||||
getMarks() {
|
|
||||||
return this.requests
|
|
||||||
.map((request) => request.getMarkedEntries())
|
|
||||||
.reduce(reduceConcat, [])
|
|
||||||
.map((entry) => entry.marks)
|
|
||||||
.reduce(reduceConcat, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRepresentativeMarks() {
|
|
||||||
// removes duplicates so the email/HAR file is shorter
|
|
||||||
return this.getMarks()
|
|
||||||
.sort((markA, markB) => {
|
|
||||||
if (markA.entry.value > markB.entry.value) {
|
|
||||||
return -1;
|
|
||||||
} else if (markA.entry.value < markB.entry.value) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
const indexA = source_priority.indexOf(markA.source);
|
|
||||||
const indexB = source_priority.indexOf(markB.source);
|
|
||||||
if (indexA < indexB) {
|
|
||||||
return -1;
|
|
||||||
} else if (indexA > indexB) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return markA.entry.value.length > markB.entry.value.length ? -1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((_, index, array) => {
|
|
||||||
if (index == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
array[index].valuePreview === array[index - 1].valuePreview ||
|
|
||||||
(array[index].classification === "history" &&
|
|
||||||
array[index - 1].classification === "history") || // if they're both history, then the first one is the longest
|
|
||||||
isSameURL(array[index].entry.value, array[index - 1].entry.value)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,36 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getMemory } from "../memory";
|
import { getMemory } from "../memory";
|
||||||
import { RequestCluster } from "../request-cluster";
|
import { RequestCluster } from "../request-cluster";
|
||||||
import { MergedStolenDataEntry, Sources } from "../stolen-data-entry";
|
import { Sources, StolenDataEntry } from "../stolen-data-entry";
|
||||||
|
|
||||||
import { hyphenate, useEmitter } from "../util";
|
import { useEmitter } from "../util";
|
||||||
|
|
||||||
const MAX_STRING_VALUE_LENGTH = 100;
|
const MAX_STRING_VALUE_LENGTH = 100;
|
||||||
|
|
||||||
function StolenDataValueTable({
|
|
||||||
entry,
|
|
||||||
prefixKey = "",
|
|
||||||
}: {
|
|
||||||
entry: MergedStolenDataEntry;
|
|
||||||
prefixKey: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{entry.getDecodingsApplied().includes("base64") ? (
|
|
||||||
<span style={{ color: "white", backgroundColor: "green" }}>
|
|
||||||
"base64"
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{Object.keys(entry.getParsedValues(prefixKey)[0]).map((key) => {
|
|
||||||
const subkey = `${prefixKey}.${key}`;
|
|
||||||
return (
|
|
||||||
<tr key={`${prefixKey}.${key}`}>
|
|
||||||
<th
|
|
||||||
onClick={(e) => {
|
|
||||||
entry.toggleMark(subkey);
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
border: entry.hasMark(subkey)
|
|
||||||
? "2px solid red"
|
|
||||||
: "2px solid transparent",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hyphenate(key)}
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<StolenDataValue entry={entry} prefixKey={subkey} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StolenDataValue({
|
function StolenDataValue({
|
||||||
entry,
|
entry,
|
||||||
prefixKey = "",
|
|
||||||
}: {
|
}: {
|
||||||
entry: MergedStolenDataEntry;
|
entry: StolenDataEntry;
|
||||||
prefixKey?: string;
|
prefixKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const [version] = useEmitter(entry);
|
const [version] = useEmitter(entry);
|
||||||
const value = entry.getParsedValues(prefixKey)[0];
|
|
||||||
let body = null;
|
let body = null;
|
||||||
if (!value) {
|
if (!entry.value) {
|
||||||
body = <></>;
|
body = <></>;
|
||||||
} else if (typeof value === "string") {
|
} else {
|
||||||
const content = entry.getParsedValues(prefixKey)[0] as string;
|
|
||||||
body = (
|
body = (
|
||||||
<div
|
<div data-version={version}>
|
||||||
style={{
|
{entry.value.slice(0, MAX_STRING_VALUE_LENGTH)}{" "}
|
||||||
border: entry.hasMark(prefixKey)
|
{entry.value.length > MAX_STRING_VALUE_LENGTH ? "(...)" : ""}
|
||||||
? "2px solid red"
|
|
||||||
: "2px solid transparent",
|
|
||||||
}}
|
|
||||||
data-version={version}
|
|
||||||
>
|
|
||||||
{content.slice(0, MAX_STRING_VALUE_LENGTH)}{" "}
|
|
||||||
{content.length > MAX_STRING_VALUE_LENGTH ? "(...)" : ""}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
body = <StolenDataValueTable entry={entry} prefixKey={prefixKey} />;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
entry.toggleMark(prefixKey);
|
entry.toggleMark();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
data-marks={entry.getMarkedValues().join(", ")}
|
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,27 +48,32 @@ function StolenDataRow({
|
||||||
entry,
|
entry,
|
||||||
cluster,
|
cluster,
|
||||||
}: {
|
}: {
|
||||||
entry: MergedStolenDataEntry;
|
entry: StolenDataEntry;
|
||||||
cluster: RequestCluster;
|
cluster: RequestCluster;
|
||||||
}) {
|
}) {
|
||||||
const [version] = useEmitter(entry);
|
const [version] = useEmitter(entry);
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={origin + cluster.id + entry.getUniqueKey()}
|
|
||||||
data-key={origin + cluster.id + entry.getUniqueKey()}
|
data-key={origin + cluster.id + entry.getUniqueKey()}
|
||||||
data-version={version}
|
data-version={version}
|
||||||
>
|
>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={entry.isMarked}
|
||||||
|
onChange={() => entry.toggleMark()}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<th
|
<th
|
||||||
style={{
|
style={{
|
||||||
width: "100px",
|
width: "100px",
|
||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
border: entry.hasMark("") ? "2px solid red" : "2px solid transparent",
|
|
||||||
}}
|
}}
|
||||||
onClick={() => entry.toggleMark("")}
|
onClick={() => entry.toggleMark()}
|
||||||
>
|
>
|
||||||
{entry.getNames().map(hyphenate).join(", ")}
|
{entry.name}
|
||||||
</th>
|
</th>
|
||||||
<td>{entry.getSources().map((source) => icons[source])}</td>
|
<td>{[entry.source].map((source) => icons[source])}</td>
|
||||||
<td style={{ wordWrap: "anywhere" as any }}>
|
<td style={{ wordWrap: "anywhere" as any }}>
|
||||||
<StolenDataValue entry={entry} />
|
<StolenDataValue entry={entry} />
|
||||||
</td>
|
</td>
|
||||||
|
@ -172,13 +117,17 @@ export default function StolenDataCluster({
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{cluster
|
{cluster
|
||||||
.getStolenData({ minValueLength, cookiesOnly, cookiesOrOriginOnly })
|
.getRepresentativeStolenData({
|
||||||
|
minValueLength,
|
||||||
|
cookiesOnly,
|
||||||
|
cookiesOrOriginOnly,
|
||||||
|
})
|
||||||
.map((entry) => (
|
.map((entry) => (
|
||||||
<StolenDataRow
|
<StolenDataRow
|
||||||
{...{
|
{...{
|
||||||
entry,
|
entry,
|
||||||
cluster,
|
cluster,
|
||||||
key: origin + cluster.id + entry.getUniqueKey(),
|
key: entry.id,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { TCModel } from "@iabtcf/core";
|
// import { TCModel } from "@iabtcf/core";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import ExtendedRequest, { HAREntry } from "./extended-request";
|
import ExtendedRequest, { HAREntry } from "./extended-request";
|
||||||
import Mark from "./mark";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getshorthost,
|
getshorthost,
|
||||||
|
@ -9,8 +8,6 @@ import {
|
||||||
isJSONObject,
|
isJSONObject,
|
||||||
isURL,
|
isURL,
|
||||||
parseToObject,
|
parseToObject,
|
||||||
reduceConcat,
|
|
||||||
unique,
|
|
||||||
} from "./util";
|
} from "./util";
|
||||||
|
|
||||||
export type Sources = "cookie" | "pathname" | "queryparams" | "header";
|
export type Sources = "cookie" | "pathname" | "queryparams" | "header";
|
||||||
|
@ -35,9 +32,9 @@ export type DecodingSchema = "base64";
|
||||||
|
|
||||||
export class StolenDataEntry extends EventEmitter {
|
export class StolenDataEntry extends EventEmitter {
|
||||||
public isIAB = false;
|
public isIAB = false;
|
||||||
public iab: TCModel | null = null;
|
// public iab: TCModel | null = null;
|
||||||
public id: number;
|
public id: number;
|
||||||
public marks: Mark[] = [];
|
private marked = false;
|
||||||
public classification: keyof typeof Classifications;
|
public classification: keyof typeof Classifications;
|
||||||
public decoding_applied: DecodingSchema = null;
|
public decoding_applied: DecodingSchema = null;
|
||||||
|
|
||||||
|
@ -77,8 +74,8 @@ export class StolenDataEntry extends EventEmitter {
|
||||||
return priority;
|
return priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeWith(entry: StolenDataEntry): MergedStolenDataEntry {
|
get isMarked() {
|
||||||
return new MergedStolenDataEntry([this, entry]);
|
return this.marked;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValue(value: string) {
|
hasValue(value: string) {
|
||||||
|
@ -86,6 +83,9 @@ export class StolenDataEntry extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseValue(value: unknown): string | Record<string, unknown> {
|
static parseValue(value: unknown): string | Record<string, unknown> {
|
||||||
|
if (isBase64JSON(value)) {
|
||||||
|
return StolenDataEntry.parseValue({ base64: JSON.parse(atob(value)) });
|
||||||
|
}
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -144,29 +144,21 @@ export class StolenDataEntry extends EventEmitter {
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
addMark(key: string) {
|
mark() {
|
||||||
this.marks.push(new Mark(this, key));
|
this.marked = true;
|
||||||
this.emit("change");
|
this.emit("change");
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMark(key?: string) {
|
unmark() {
|
||||||
if (key) {
|
this.marked = false;
|
||||||
return this.marks.some((k) => k.key == key);
|
|
||||||
} else {
|
|
||||||
return this.marks.length > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeMark(key: string) {
|
|
||||||
this.marks = this.marks.filter((mark) => mark.key != key);
|
|
||||||
this.emit("change");
|
this.emit("change");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMark(key: string) {
|
toggleMark() {
|
||||||
if (this.hasMark(key)) {
|
if (this.marked) {
|
||||||
this.removeMark(key);
|
this.unmark();
|
||||||
} else {
|
} else {
|
||||||
this.addMark(key);
|
this.mark();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,7 +191,6 @@ export class StolenDataEntry extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
getValuePreview(key = ""): string {
|
getValuePreview(key = ""): string {
|
||||||
console.log("getValuePreview", key, this.getParsedValue(key));
|
|
||||||
const value = this.getParsedValue(key);
|
const value = this.getParsedValue(key);
|
||||||
const str =
|
const str =
|
||||||
typeof value === "object" && value[Symbol.for("originalString")]
|
typeof value === "object" && value[Symbol.for("originalString")]
|
||||||
|
@ -218,90 +209,8 @@ export class StolenDataEntry extends EventEmitter {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class MergedStolenDataEntry extends EventEmitter {
|
|
||||||
constructor(public entries: StolenDataEntry[]) {
|
|
||||||
super();
|
|
||||||
const all_marks = unique(
|
|
||||||
entries.map((entry) => entry.marks).reduce(reduceConcat, [])
|
|
||||||
);
|
|
||||||
for (const entry of entries) {
|
|
||||||
entry.marks = all_marks;
|
|
||||||
}
|
|
||||||
// getMemory().emit("change"); // to trigger render
|
|
||||||
}
|
|
||||||
|
|
||||||
on(event: string, listener: () => void) {
|
|
||||||
for (const entry of this.entries) {
|
|
||||||
entry.on(event, listener);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeListener(event: string, listener: () => void) {
|
|
||||||
for (const entry of this.entries) {
|
|
||||||
entry.removeListener(event, listener);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasValue(value: string) {
|
|
||||||
return this.entries.some((entry) => entry.value === value);
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeWith(entry: StolenDataEntry) {
|
|
||||||
this.entries.push(entry);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPriority() {
|
|
||||||
return Math.max(...this.entries.map((entry) => entry.getPriority()));
|
|
||||||
}
|
|
||||||
|
|
||||||
getUniqueKey() {
|
getUniqueKey() {
|
||||||
return `${this.getNames().join(":")};${this.entries
|
return this.request.shorthost + ";" + this.name + ";" + this.value;
|
||||||
.map((e) => e.id)
|
|
||||||
.join(":")};`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNames(): string[] {
|
|
||||||
return unique(this.entries.map((e) => e.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
getSources(): string[] {
|
|
||||||
return unique(this.entries.map((e) => e.source));
|
|
||||||
}
|
|
||||||
|
|
||||||
getValues() {
|
|
||||||
return unique(this.entries.map((e) => e.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
getParsedValues(key_path: string) {
|
|
||||||
return Array.from(
|
|
||||||
new Set(this.entries.map((e) => e.getParsedValue(key_path)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMark(key: string) {
|
|
||||||
this.entries.forEach((entry) => entry.addMark(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
getMarkedValues() {
|
|
||||||
return this.entries
|
|
||||||
.map((entry) => entry.marks)
|
|
||||||
.reduce((a, b) => a.concat(b), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMark(key: string): boolean {
|
|
||||||
return this.entries.some((entry) => entry.hasMark(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMark(key: string): void {
|
|
||||||
this.entries.forEach((entry) => entry.toggleMark(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
getDecodingsApplied(): DecodingSchema[] {
|
|
||||||
return unique(this.entries.map((entry) => entry.decoding_applied));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
test.ts
Normal file
3
test.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { flattenObject } from "./util";
|
||||||
|
|
||||||
|
console.log(flattenObject({ a: { b: { c: [1, 2, 3] } } }));
|
28
util.ts
28
util.ts
|
@ -183,3 +183,31 @@ export function isBase64(s: string): boolean {
|
||||||
export function isBase64JSON(s: unknown): s is string {
|
export function isBase64JSON(s: unknown): s is string {
|
||||||
return typeof s === "string" && isBase64(s) && isJSONObject(atob(s));
|
return typeof s === "string" && isBase64(s) && isJSONObject(atob(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flattenObject(
|
||||||
|
obj: Record<string, unknown>
|
||||||
|
): [string, string][] {
|
||||||
|
const ret: [string, string][] = [];
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const value = obj[key];
|
||||||
|
if (value === null) {
|
||||||
|
ret.push([key, "null"]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof value === "object") {
|
||||||
|
const flattened = flattenObject(value as Record<string, unknown>);
|
||||||
|
for (const [subkey, subvalue] of flattened) {
|
||||||
|
ret.push([`${key}.${subkey}`, subvalue]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret.push([key, value.toString()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenObjectEntries(
|
||||||
|
entries: [string, unknown][]
|
||||||
|
): [string, string][] {
|
||||||
|
return flattenObject(Object.fromEntries(entries));
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user