Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b0d948f
feat: add blocklistPaths
imblue-dabadee Aug 28, 2025
e85956f
feat: doesURLPathExist
imblue-dabadee Aug 28, 2025
f972d6e
test: doesURLPathExist
imblue-dabadee Aug 28, 2025
7693283
feat: add and removals to blocklistPaths
imblue-dabadee Aug 28, 2025
e6bfad7
refactor: increase addURLToBlocklistPaths readability
imblue-dabadee Aug 29, 2025
ec9d635
fix: add httpPrefix to use URL correctly
imblue-dabadee Aug 29, 2025
1c20cb5
fix: removeURLFromBlocklistPaths respects hierachy
imblue-dabadee Aug 29, 2025
72a42aa
fix: handle for longer than 3 paths being added and removed
imblue-dabadee Aug 29, 2025
7e071ab
tests: applyDiffs
imblue-dabadee Aug 29, 2025
9da68b0
refactor: generic functions and remove helper
imblue-dabadee Aug 29, 2025
8b2a573
lint: fix
imblue-dabadee Aug 29, 2025
37d1401
chore: update the tag names
imblue-dabadee Aug 29, 2025
42d92ed
Merge branch 'main' into feat/check-against-urls-with-path
imblue-dabadee Aug 29, 2025
de20842
Merge branch 'main' into feat/check-against-urls-with-path
imblue-dabadee Aug 29, 2025
7d1f82b
Merge branch 'main' into feat/check-against-urls-with-path
imblue-dabadee Sep 15, 2025
7e9b3a5
feat: whitelist support
imblue-dabadee Sep 15, 2025
4854c2b
test: update snapshot
imblue-dabadee Sep 15, 2025
0dc4d5b
fix: add only hostname with paths to whitelist
imblue-dabadee Sep 15, 2025
a929ba9
test: bypass whitelistPaths
imblue-dabadee Sep 15, 2025
095f60a
fix: test only checks hostnames with apths
imblue-dabadee Sep 15, 2025
5bd9365
test: test for whitelistPaths
imblue-dabadee Sep 15, 2025
cb623ef
feat: PathTrie
imblue-dabadee Sep 17, 2025
1179b55
test: PathTrie
imblue-dabadee Sep 17, 2025
ef1e114
chore: update types for blocklistPaths to Trie
imblue-dabadee Sep 17, 2025
a6b535b
chore: remove old insertion/deletion/exists methods
imblue-dabadee Sep 17, 2025
cbfa2a5
test: update to new structure
imblue-dabadee Sep 17, 2025
a98edbb
refactor: getHostnameAndPathComponents
imblue-dabadee Sep 18, 2025
80927ec
fix: separate blocklist entries
imblue-dabadee Sep 18, 2025
be8202d
test: separateBlocklistEntries
imblue-dabadee Sep 18, 2025
f283e11
test: getHostnameAndPathComponents
imblue-dabadee Sep 18, 2025
22b1ad1
test(fix): updateStalelist unit test
imblue-dabadee Sep 18, 2025
d337406
test: subdomain case getHostnameAndPathComponents
imblue-dabadee Sep 18, 2025
a593944
test(fix): update the structure of blocklistPaths
imblue-dabadee Sep 18, 2025
907dd00
test: updateStalelist blocklist separation case
imblue-dabadee Sep 18, 2025
8578fac
test(fix): add mocks
imblue-dabadee Sep 18, 2025
277ada7
test(fix): add url to blocklistPaths before calling bypass
imblue-dabadee Sep 18, 2025
6b4742a
fix: check that blocklistPaths is empty or undefined
imblue-dabadee Sep 18, 2025
09fbe61
refactor: remove old funcs
imblue-dabadee Sep 18, 2025
598b78a
test: check tests path-based blocking version
imblue-dabadee Sep 18, 2025
4f98f31
test: isPathBlocked
imblue-dabadee Sep 18, 2025
f8eed59
fix: update getDefaultPhishingDetectorConfig
imblue-dabadee Sep 18, 2025
2cd8146
fix: revert processConfigs
imblue-dabadee Sep 18, 2025
03714bc
fix: remove blocklist separation in getDefaultPhishingDetectorConfig
imblue-dabadee Sep 18, 2025
80882f7
chore: redundant return variable
imblue-dabadee Sep 18, 2025
5de9179
test: update tests
imblue-dabadee Sep 18, 2025
ed9a836
fix: lint issues
imblue-dabadee Sep 18, 2025
9368c52
Merge branch 'main' into feat/check-against-urls-with-path
imblue-dabadee Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions eslint-warning-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,23 +297,17 @@
"jsdoc/tag-lines": 1
},
"packages/phishing-controller/src/PhishingController.ts": {
"jsdoc/check-tag-names": 38,
"jsdoc/check-tag-names": 39,
"jsdoc/tag-lines": 1
},
"packages/phishing-controller/src/PhishingDetector.ts": {
"@typescript-eslint/no-unused-vars": 1,
"@typescript-eslint/prefer-readonly": 2,
"jsdoc/tag-lines": 2
},
"packages/phishing-controller/src/tests/utils.ts": {
"@typescript-eslint/no-unused-vars": 1
},
"packages/phishing-controller/src/utils.test.ts": {
"import-x/namespace": 5
},
"packages/phishing-controller/src/utils.ts": {
"@typescript-eslint/no-unsafe-enum-comparison": 1,
"@typescript-eslint/no-unused-vars": 1
"@typescript-eslint/no-unsafe-enum-comparison": 1
},
"packages/polling-controller/src/AbstractPollingController.ts": {
"@typescript-eslint/prefer-readonly": 1
Expand Down
225 changes: 225 additions & 0 deletions packages/phishing-controller/src/PathTrie.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import {
deleteFromTrie,
isTerminalPath,
insertToTrie,
type PathTrie,
} from './PathTrie';

const emptyPathTrie: PathTrie = {};

describe('PathTrie', () => {
describe('insertToTrie', () => {
let pathTrie: PathTrie;

beforeEach(() => {
pathTrie = {};
});

it('inserts a URL to the path trie', () => {
insertToTrie('example.com/path1/path2', pathTrie);

expect(pathTrie).toStrictEqual({
'example.com': {
path1: {
path2: {},
},
},
});
});

it('inserts sibling path', () => {
insertToTrie('example.com/path1', pathTrie);
insertToTrie('example.com/path2', pathTrie);

expect(pathTrie).toStrictEqual({
'example.com': {
path1: {},
path2: {},
},
});
});

it('multiple inserts', () => {
insertToTrie('example.com/path1/path2/path31', pathTrie);
insertToTrie('example.com/path1/path2/path32', pathTrie);
insertToTrie('example.com/path1/path2/path33/path4', pathTrie);
insertToTrie('example.com/path2', pathTrie);

expect(pathTrie).toStrictEqual({
'example.com': {
path1: {
path2: {
path31: {},
path32: {},
path33: {
path4: {},
},
},
},
path2: {},
},
});
});

it('idempotent', () => {
insertToTrie('example.com/path1/path2', pathTrie);
insertToTrie('example.com/path1/path2', pathTrie);

expect(pathTrie).toStrictEqual({
'example.com': {
path1: {
path2: {},
},
},
});
});

it('prunes descendants when adding ancestor', () => {
insertToTrie('example.com/path1/path2/path3', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': {
path1: {
path2: {
path3: {},
},
},
},
});

insertToTrie('example.com/path1', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': {
path1: {},
},
});
});

it('does not insert path1/path2 if path1 exists', () => {
insertToTrie('example.com/path1', pathTrie);
insertToTrie('example.com/path1/path2', pathTrie);

expect(pathTrie).toStrictEqual({
'example.com': {
path1: {},
},
});
});

it('does not insert if no path is provided', () => {
insertToTrie('example.com', pathTrie);

expect(pathTrie).toStrictEqual(emptyPathTrie);
});

it('treats trailing slash as equivalent', () => {
insertToTrie('example.com/path', pathTrie);
insertToTrie('example.com/path/', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': { path: {} },
});
});

it('accepts URLs with a scheme', () => {
insertToTrie('https://example.com/path', pathTrie);
expect(pathTrie).toStrictEqual({ 'example.com': { path: {} } });
});
});

describe('deleteFromTrie', () => {
let pathTrie: PathTrie;

beforeEach(() => {
pathTrie = {
'example.com': {
path11: {
path2: {},
},
path12: {},
},
};
});

it('deletes a path', () => {
deleteFromTrie('example.com/path11/path2', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': {
path12: {},
},
});
});

it('deletes all paths', () => {
deleteFromTrie('example.com/path11/path2', pathTrie);
deleteFromTrie('example.com/path12', pathTrie);
expect(pathTrie).toStrictEqual(emptyPathTrie);
});

it('deletes descendants if the path is not terminal', () => {
deleteFromTrie('example.com/path11', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': {
path12: {},
},
});
});

it('idempotent', () => {
deleteFromTrie('example.com/path11/path2', pathTrie);
deleteFromTrie('example.com/path11/path2', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': {
path12: {},
},
});
});

it('does nothing if the path does not exist within the trie', () => {
deleteFromTrie('example.com/nonexistent', pathTrie);
expect(pathTrie).toStrictEqual(pathTrie);
});

it('does nothing if the hostname does not exist', () => {
deleteFromTrie('nonexistent.com/path11/path2', pathTrie);
expect(pathTrie).toStrictEqual(pathTrie);
});

it('does nothing if no path is provided', () => {
deleteFromTrie('example.com', pathTrie);
expect(pathTrie).toStrictEqual(pathTrie);
});

it('deletes with a scheme', () => {
deleteFromTrie('https://example.com/path11/path2', pathTrie);
expect(pathTrie).toStrictEqual({
'example.com': {
path12: {},
},
});
});
});

describe('isTerminalPath', () => {
let pathTrie: PathTrie;

beforeEach(() => {
pathTrie = {
'example.com': {
path11: {
path2: {},
},
},
};
});

it.each([
['terminal path', 'example.com/path11/path2', true],
['ancestor path', 'example.com/path11', false],
['non-existent path', 'example.com/path11/path3', false],
['no path', 'example.com', false],
['no hostname', 'nonexistent.com/path11/path2', false],
['with a scheme', 'https://example.com/path11/path2', true],
])('returns %s if the path is %s', (_name, path, expected) => {
expect(isTerminalPath(path, pathTrie)).toBe(expected);
});
});
});
125 changes: 125 additions & 0 deletions packages/phishing-controller/src/PathTrie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { getHostnameAndPathComponents } from './utils';

export type PathNode = {
[key: string]: PathNode;
};

export type PathTrie = Record<string, PathNode>;

const isTerminal = (node: PathNode): boolean => {
return Object.keys(node).length === 0;
};

/**
* Insert a URL into the trie.
*
* @param url - The URL to insert into the trie.
* @param pathTrie - The trie to insert the URL into.
*/
export const insertToTrie = (url: string, pathTrie: PathTrie) => {
const { hostname, pathComponents } = getHostnameAndPathComponents(url);

if (pathComponents.length === 0 || !hostname) {
return;
}

const lowerHostname = hostname.toLowerCase();
if (!pathTrie[lowerHostname]) {
pathTrie[lowerHostname] = {} as PathNode;
}

let curr: PathNode = pathTrie[lowerHostname];
for (let i = 0; i < pathComponents.length; i++) {
const pathComponent = pathComponents[i];
const isLast = i === pathComponents.length - 1;
const exists = curr[pathComponent] !== undefined;

if (exists) {
if (!isLast && isTerminal(curr[pathComponent])) {
return;
}

if (isLast) {
// Prune descendants if the current path component is not terminal
if (!isTerminal(curr[pathComponent])) {
curr[pathComponent] = {};
}
return;
}
curr = curr[pathComponent];
continue;
}

if (isLast) {
curr[pathComponent] = {};
return;
}
const next: PathNode = {};
curr[pathComponent] = next;
curr = next;
}
};

/**
* Delete a URL from the trie.
*
* @param url - The URL to delete from the trie.
* @param pathTrie - The trie to delete the URL from.
*/
export const deleteFromTrie = (url: string, pathTrie: PathTrie) => {
const { hostname, pathComponents } = getHostnameAndPathComponents(url);

const lowerHostname = hostname.toLowerCase();
if (pathComponents.length === 0 || !pathTrie[lowerHostname]) {
return;
}

const pathToNode: { node: PathNode; key: string }[] = [
{ node: pathTrie, key: lowerHostname },
];
let curr: PathNode = pathTrie[lowerHostname];
for (const pathComponent of pathComponents) {
if (!curr[pathComponent]) {
return;
}

pathToNode.push({ node: curr, key: pathComponent });
curr = curr[pathComponent];
}

const lastEntry = pathToNode[pathToNode.length - 1];
delete lastEntry.node[lastEntry.key];
for (let i = pathToNode.length - 2; i >= 0; i--) {
const { node, key } = pathToNode[i];
if (isTerminal(node[key])) {
delete node[key];
} else {
break;
}
}
};

/**
* Check if a URL is a terminal path i.e. the last path component is a terminal node.
*
* @param url - The URL to check.
* @param pathTrie - The trie to check the URL in.
* @returns True if the URL is a terminal path, false otherwise.
*/
export const isTerminalPath = (url: string, pathTrie: PathTrie): boolean => {
const { hostname, pathComponents } = getHostnameAndPathComponents(url);

const lowerHostname = hostname.toLowerCase();
if (pathComponents.length === 0 || !hostname || !pathTrie[lowerHostname]) {
return false;
}

let curr: PathNode = pathTrie[lowerHostname];
for (const pathComponent of pathComponents) {
if (!curr[pathComponent]) {
return false;
}
curr = curr[pathComponent];
}
return isTerminal(curr);
};
Loading
Loading