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