diff --git a/src/background.ts b/src/background.ts index 1467f03b..40dca386 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,8 +1,8 @@ import { browser, ContextMenus, Extension } from "webextension-polyfill-ts"; -import { Command } from "@/command"; +import { CommandPacker } from "@/command/packer"; +import { CommandRunner } from "@/command/runner"; import { Selector } from "@/selector"; -import { truncate } from "@/truncate"; import { AnalyzerEntry, GeneralSettings, @@ -20,9 +20,9 @@ export async function showNotification(message: string): Promise { }); } -export async function search(command: Command): Promise { +export async function search(runner: CommandRunner): Promise { try { - const url: string = command.search(); + const url: string = runner.search(); if (url !== "") { await browser.tabs.create({ url }); } @@ -32,10 +32,10 @@ export async function search(command: Command): Promise { } } -export async function searchAll(command: Command): Promise { +export async function searchAll(runner: CommandRunner): Promise { try { const states: SearcherStates = await getSearcherStates(); - const urls = command.searchAll(states); + const urls = runner.searchAll(states); for (const url of urls) { await browser.tabs.create({ url }); } @@ -45,10 +45,10 @@ export async function searchAll(command: Command): Promise { } } -export async function scan(command: Command): Promise { +export async function scan(runner: CommandRunner): Promise { const apiKeys = await getApiKeys(); try { - const url: string = await command.scan(apiKeys); + const url: string = await runner.scan(apiKeys); if (url !== "") { await browser.tabs.create({ url }); } @@ -95,9 +95,16 @@ export async function createContextMenus( firstEntry = entry; } + const command: CommandPacker = new CommandPacker( + "search", + entry.query, + entry.type, + name + ); + // it tells action, query, type and target to the listener - const id = `Search ${entry.query} as a ${entry.type} on ${name}`; - const title = `Search ${truncate(entry.query)} on ${name}`; + const id = command.getJSON(); + const title = command.getMessage(); const options = { contexts, id, title }; browser.contextMenus.create(options, createContextMenuErrorHandler); } @@ -106,8 +113,15 @@ export async function createContextMenus( if (firstEntry !== undefined) { const query = firstEntry.query; const type = firstEntry.type; - const id = `Search ${query} as a ${type} on all`; - const title = `Search ${truncate(query)} on all`; + + const command: CommandPacker = new CommandPacker( + "search", + query, + type, + "all" + ); + const id = command.getJSON(); + const title = command.getMessage(); const options = { contexts, id, title }; browser.contextMenus.create(options, createContextMenuErrorHandler); } @@ -116,9 +130,10 @@ export async function createContextMenus( const scannerEntries: AnalyzerEntry[] = selector.getScannerEntries(); for (const entry of scannerEntries) { const name = entry.analyzer.name; - // it tells action/query/type/target to the listener - const id = `Scan ${entry.query} as a ${entry.type} on ${name}`; - const title = `Scan ${truncate(entry.query)} on ${name}`; + + const command = new CommandPacker("scan", entry.query, entry.type, name); + const id = command.getJSON(); + const title = command.getMessage(); const options = { contexts, id, title }; browser.contextMenus.create(options, createContextMenuErrorHandler); } @@ -148,17 +163,17 @@ if (typeof browser !== "undefined" && browser.runtime !== undefined) { // eslint-disable-next-line @typescript-eslint/no-misused-promises browser.contextMenus.onClicked.addListener(async (info, tab_) => { const id: string = info.menuItemId.toString(); - const command = new Command(id); - switch (command.action) { + const runner = new CommandRunner(id); + switch (runner.command.action) { case "search": - if (command.target === "all") { - await searchAll(command); + if (runner.command.target === "all") { + await searchAll(runner); } else { - await search(command); + await search(runner); } break; case "scan": - await scan(command); + await scan(runner); break; } }); diff --git a/src/command/packer.ts b/src/command/packer.ts new file mode 100644 index 00000000..42e95696 --- /dev/null +++ b/src/command/packer.ts @@ -0,0 +1,55 @@ +import { truncate } from "@/truncate"; +import { Command, SearchableType } from "@/types"; + +const capitalize = (s: string) => { + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +export class CommandPacker { + public action: string; + public query: string; + public target: string; + public type: SearchableType; + + public constructor( + action: string, + query: string, + type: SearchableType, + target: string + ) { + this.action = action; + this.query = query; + this.type = type; + this.target = target; + } + + private isAbbreviationType(): boolean { + const abbreviations = ["ip", "asn", "cve", "eth", "url"]; + return abbreviations.includes(this.type); + } + + private getNormalizedType(): string { + if (this.isAbbreviationType()) { + return this.type.toUpperCase(); + } + return this.type; + } + + public getJSON(): string { + const command: Command = { + action: this.action, + query: this.query, + type: this.type, + target: this.target, + }; + return JSON.stringify(command); + } + + public getMessage(): string { + const type = this.getNormalizedType(); + + return `${capitalize(this.action)} ${truncate(this.query)} as ${type} on ${ + this.target + }`; + } +} diff --git a/src/command.ts b/src/command/runner.ts similarity index 81% rename from src/command.ts rename to src/command/runner.ts index e2ae7a87..96c9f422 100644 --- a/src/command.ts +++ b/src/command/runner.ts @@ -2,6 +2,7 @@ import { Selector } from "@/selector"; import { AnalyzerEntry, ApiKeys, + Command, Scanner, ScannerTable, Searcher, @@ -9,19 +10,13 @@ import { SearcherTable, } from "@/types"; -export class Command { - public action: string; - public query: string; - public target: string; - public type: string; +export class CommandRunner { + public command: Command; - public constructor(command: string) { - // command = `Search ${entry.query} as a ${entry.type} on ${name}`; - const parts: string[] = command.split(" "); - this.action = parts[0].toLowerCase(); - this.type = parts[parts.length - 3]; - this.query = parts.slice(1, parts.length - 5).join(" "); - this.target = parts[parts.length - 1]; + public constructor(commandString: string) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const command: Command = JSON.parse(commandString); + this.command = command; } private searcherTable: SearcherTable = { @@ -94,15 +89,15 @@ export class Command { }; public search(): string { - const selector: Selector = new Selector(this.query); + const selector: Selector = new Selector(this.command.query); const entries: AnalyzerEntry[] = selector.getSearcherEntries(); - const entry = entries.find((r) => r.analyzer.name === this.target); + const entry = entries.find((r) => r.analyzer.name === this.command.target); let url = ""; if (entry !== undefined) { const searcher = entry.analyzer as Searcher; - if (this.type in this.searcherTable) { - const fn = this.searcherTable[this.type]; + if (this.command.type in this.searcherTable) { + const fn = this.searcherTable[this.command.type]; url = fn(searcher, entry.query); } } @@ -111,10 +106,10 @@ export class Command { } public searchAll(searcherStates: SearcherStates): string[] { - const selector: Selector = new Selector(this.query); + const selector: Selector = new Selector(this.command.query); const entries: AnalyzerEntry[] = selector .getSearcherEntries() - .filter((entry) => this.type === entry.type); + .filter((entry) => this.command.type === entry.type); const selectedEntries = entries.filter( (entry) => !(entry.analyzer.name in searcherStates) || @@ -123,9 +118,9 @@ export class Command { const urls: string[] = []; for (const entry of selectedEntries) { const searcher = entry.analyzer as Searcher; - if (this.type in this.searcherTable) { + if (this.command.type in this.searcherTable) { try { - const fn = this.searcherTable[this.type]; + const fn = this.searcherTable[this.command.type]; urls.push(fn(searcher, entry.query)); } catch (err) { continue; @@ -157,9 +152,9 @@ export class Command { }; public async scan(apiKeys: ApiKeys): Promise { - const selector: Selector = new Selector(this.query); + const selector: Selector = new Selector(this.command.query); const entries: AnalyzerEntry[] = selector.getScannerEntries(); - const entry = entries.find((r) => r.analyzer.name === this.target); + const entry = entries.find((r) => r.analyzer.name === this.command.target); let url = ""; if (entry !== undefined) { const scanner = entry.analyzer as Scanner; diff --git a/src/types.ts b/src/types.ts index c2c23373..67366706 100644 --- a/src/types.ts +++ b/src/types.ts @@ -97,6 +97,13 @@ export interface Config { searcherStates: SearcherStates; } +export interface Command { + action: string; + query: string; + type: SearchableType; + target: string; +} + export const MD5_LENGTH = 32; export const SHA1_LENGTH = 40; export const SHA256_LENGTH = 64; diff --git a/test/backgroud.spec.ts b/test/backgroud.spec.ts index 99168ad3..45d2568b 100644 --- a/test/backgroud.spec.ts +++ b/test/backgroud.spec.ts @@ -11,7 +11,8 @@ import { searchAll, showNotification, } from "@/background"; -import { Command } from "@/command"; +import { CommandPacker } from "@/command/packer"; +import { CommandRunner } from "@/command/runner"; import { browserMock } from "./browserMock"; import sinon = require("sinon"); @@ -43,10 +44,14 @@ describe("Background script", function () { describe("#search", function () { context("when given a valid input", function () { it("should call chrome.tabs.create()", async function () { - const command = new Command( - "Search https://github.com as a url on Urlscan" + const packer = new CommandPacker( + "search", + "https://github.com", + "url", + "Urlscan" ); - await search(command); + const runner = new CommandRunner(packer.getJSON()); + await search(runner); browserMock.tabs.create.assertCalls([ [ { @@ -72,10 +77,14 @@ describe("Background script", function () { }); }); it("should call chrome.tabs.create", async function () { - const command = new Command( - "Search pub-9383614236930773 as a gaPubID on all" + const packer = new CommandPacker( + "search", + "pub-9383614236930773", + "gaPubID", + "all" ); - await searchAll(command); + const runner = new CommandRunner(packer.getJSON()); + await searchAll(runner); browserMock.tabs.create.assertCalls([ [ { @@ -103,11 +112,16 @@ describe("Background script", function () { }); it("should call chrome.tabs.create()", async function () { - const command = new Command( - "Scan https://www.wikipedia.org/ as a url on Urlscan" + const packer = new CommandPacker( + "scan", + "https://www.wikipedia.org/", + "url", + "Urlscan" ); + const runner = new CommandRunner(packer.getJSON()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const commandStub: sinon.SinonStub = sandbox - .stub(command, "scan") + .stub(runner, "scan") .withArgs({ hybridAnalysisApiKey: "test", urlscanApiKey: "test", @@ -117,7 +131,7 @@ describe("Background script", function () { "https://urlscan.io/entry/ac04bc14-4efe-439d-b356-8384843daf75/" ); - await scan(command); + await scan(runner); browserMock.tabs.create.assertCalls([ [ { @@ -140,11 +154,15 @@ describe("Background script", function () { }); it("should not call chrome.tabs.create()", async function () { - const command = new Command( - "Scan https://www.wikipedia.org/ as a url on Urlscan" + const packer = new CommandPacker( + "scan", + "https://www.wikipedia.org/", + "url", + "Urlscan" ); + const runner = new CommandRunner(packer.getJSON()); - await scan(command); + await scan(runner); browserMock.tabs.create.assertCalls([]); }); } diff --git a/test/command.spec.ts b/test/command.spec.ts deleted file mode 100644 index 98847f9d..00000000 --- a/test/command.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import "mocha"; - -import { expect } from "chai"; -import moxios from "moxios"; - -import { Command } from "@/command"; -import { ApiKeys } from "@/types"; - -describe("Command", function () { - describe("#constructor", function () { - it("should return attributes", function () { - const command = new Command( - "Search https://github.com as a url on Urlscan" - ); - expect(command.action).to.equal("search"); - expect(command.query).to.equal("https://github.com"); - expect(command.target).to.equal("Urlscan"); - }); - }); - - describe("#search", function () { - context("ip", function () { - it("should return a URL for search", function () { - const command = new Command("Search 1.1.1.1 as a ip on Urlscan"); - expect(command.search()).to.equal("https://urlscan.io/ip/1.1.1.1"); - }); - }); - - context("domain", function () { - it("should return a URL for search", function () { - const command = new Command("Search github.com as a domain on Urlscan"); - expect(command.search()).to.equal( - "https://urlscan.io/domain/github.com" - ); - }); - }); - - context("url", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search https://github.com as a url on Urlscan" - ); - expect(command.search()).to.equal( - "https://urlscan.io/search/#page.url%3A%22https%3A%2F%2Fgithub.com%22%20OR%20task.url%3A%22https%3A%2F%2Fgithub.com%22" - ); - }); - }); - - context("hash", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search 726a2eedb9df3d63ec1b4a7d774a799901f1a2b9 as a hash on Pulsedive" - ); - expect(command.search()).to.equal( - "https://pulsedive.com/indicator/?ioc=NzI2YTJlZWRiOWRmM2Q2M2VjMWI0YTdkNzc0YTc5OTkwMWYxYTJiOQ==" - ); - }); - }); - - context("email", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search test@test.com as a email on ViewDNS" - ); - expect(command.search()).to.equal( - "https://viewdns.info/reversewhois/?q=test%40test.com" - ); - }); - }); - - context("cve", function () { - it("should return a URL for search", function () { - const command = new Command("Search CVE-2018-16384 as a cve on Vulmon"); - expect(command.search()).to.equal( - "https://vulmon.com/vulnerabilitydetails?qid=CVE-2018-16384" - ); - }); - }); - - context("btc", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa as a btc on BlockCypher" - ); - expect(command.search()).to.equal( - "https://live.blockcypher.com/btc/address/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa/" - ); - }); - }); - - context("gaTrackID", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search UA-67609351 as a gaTrackID on SpyOnWeb" - ); - expect(command.search()).to.equal("http://spyonweb.com/UA-67609351"); - }); - }); - - context("gaPubID", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search pub-9383614236930773 as a gaPubID on SpyOnWeb" - ); - expect(command.search()).to.equal( - "http://spyonweb.com/pub-9383614236930773" - ); - }); - }); - - context("eth", function () { - it("should return a URL for search", function () { - const command = new Command( - "Search 0x4966db520b0680fc19df5d7774ca96f42e6abd4f as a eth on Blockchair" - ); - expect(command.search()).to.equal( - "https://blockchair.com/ethereum/address/0x4966db520b0680fc19df5d7774ca96f42e6abd4f" - ); - }); - }); - }); - - describe("#searchAll", function () { - context("ip", function () { - it("should return URLs", function () { - const command = new Command("Search 1.1.1.1 as a ip on all"); - const states = { - Urlscan: true, - VirusTotal: true, - }; - const urls = command.searchAll(states); - expect(urls.length).to.be.greaterThan(0); - for (const url of urls) { - expect(url).to.match(/^http/); - } - }); - }); - }); - - describe("#scan", function () { - const apiKeys: ApiKeys = { - hybridAnalysisApiKey: "test", - urlscanApiKey: "test", - virusTotalApiKey: "test", - }; - - beforeEach(() => { - moxios.install(); - moxios.stubRequest("https://urlscan.io/api/v1/scan/", { - response: { - result: "https://urlscan.io/entry/foo/", - }, - status: 200, - }); - }); - - afterEach(() => { - moxios.uninstall(); - }); - - context("ip", function () { - it("should return a URL", async function () { - const command = new Command("Scan 1.1.1.1 as a ip on Urlscan"); - expect(await command.scan(apiKeys)).to.equal( - "https://urlscan.io/entry/foo/loading" - ); - }); - }); - - context("domain", function () { - it("should return a URL", async function () { - const command = new Command("Scan github.com as a domain on Urlscan"); - expect(await command.scan(apiKeys)).to.equal( - "https://urlscan.io/entry/foo/loading" - ); - }); - }); - - context("url", function () { - it("should return a URL for scan", async function () { - const command = new Command( - "Scan https://www.wikipedia.org/ as a url on Urlscan" - ); - expect(await command.scan(apiKeys)).to.equal( - "https://urlscan.io/entry/foo/loading" - ); - }); - }); - }); -}); diff --git a/test/command/command.spec.ts b/test/command/command.spec.ts new file mode 100644 index 00000000..ae7b47e6 --- /dev/null +++ b/test/command/command.spec.ts @@ -0,0 +1,249 @@ +import "mocha"; + +import { expect } from "chai"; +import moxios from "moxios"; + +import { CommandPacker } from "@/command/packer"; +import { CommandRunner } from "@/command/runner"; +import { ApiKeys } from "@/types"; + +describe("Command", function () { + describe("#constructor", function () { + it("should return attributes", function () { + const packer = new CommandPacker( + "search", + "https://github.com", + "url", + "Urlscan" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.command.action).to.equal("search"); + expect(runner.command.query).to.equal("https://github.com"); + expect(runner.command.target).to.equal("Urlscan"); + }); + }); + + describe("#search", function () { + context("ip", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker("search", "1.1.1.1", "ip", "Urlscan"); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal("https://urlscan.io/ip/1.1.1.1"); + }); + }); + + context("domain", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "github.com", + "domain", + "Urlscan" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://urlscan.io/domain/github.com" + ); + }); + }); + + context("url", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "https://github.com", + "url", + "Urlscan" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://urlscan.io/search/#page.url%3A%22https%3A%2F%2Fgithub.com%22%20OR%20task.url%3A%22https%3A%2F%2Fgithub.com%22" + ); + }); + }); + + context("hash", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "726a2eedb9df3d63ec1b4a7d774a799901f1a2b9", + "hash", + "Pulsedive" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://pulsedive.com/indicator/?ioc=NzI2YTJlZWRiOWRmM2Q2M2VjMWI0YTdkNzc0YTc5OTkwMWYxYTJiOQ==" + ); + }); + }); + + context("email", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "test@test.com", + "email", + "ViewDNS" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://viewdns.info/reversewhois/?q=test%40test.com" + ); + }); + }); + + context("cve", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "CVE-2018-16384", + "cve", + "Vulmon" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://vulmon.com/vulnerabilitydetails?qid=CVE-2018-16384" + ); + }); + }); + + context("btc", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "btc", + "BlockCypher" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://live.blockcypher.com/btc/address/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa/" + ); + }); + }); + + context("gaTrackID", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "UA-67609351", + "gaTrackID", + "SpyOnWeb" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal("http://spyonweb.com/UA-67609351"); + }); + }); + + context("gaPubID", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "pub-9383614236930773", + "gaPubID", + "SpyOnWeb" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "http://spyonweb.com/pub-9383614236930773" + ); + }); + }); + + context("eth", function () { + it("should return a URL for search", function () { + const packer = new CommandPacker( + "search", + "0x4966db520b0680fc19df5d7774ca96f42e6abd4f", + "eth", + "Blockchair" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(runner.search()).to.equal( + "https://blockchair.com/ethereum/address/0x4966db520b0680fc19df5d7774ca96f42e6abd4f" + ); + }); + }); + }); + + describe("#searchAll", function () { + context("ip", function () { + it("should return URLs", function () { + const states = { + Urlscan: true, + VirusTotal: true, + }; + + const packer = new CommandPacker("search", "1.1.1.1", "ip", "all"); + const runner = new CommandRunner(packer.getJSON()); + const urls = runner.searchAll(states); + expect(urls.length).to.be.greaterThan(0); + for (const url of urls) { + expect(url).to.match(/^http/); + } + }); + }); + }); + + describe("#scan", function () { + const apiKeys: ApiKeys = { + hybridAnalysisApiKey: "test", + urlscanApiKey: "test", + virusTotalApiKey: "test", + }; + + beforeEach(() => { + moxios.install(); + moxios.stubRequest("https://urlscan.io/api/v1/scan/", { + response: { + result: "https://urlscan.io/entry/foo/", + }, + status: 200, + }); + }); + + afterEach(() => { + moxios.uninstall(); + }); + + context("ip", function () { + it("should return a URL", async function () { + const packer = new CommandPacker("scan", "1.1.1.1", "ip", "Urlscan"); + const runner = new CommandRunner(packer.getJSON()); + expect(await runner.scan(apiKeys)).to.equal( + "https://urlscan.io/entry/foo/loading" + ); + }); + }); + + context("domain", function () { + it("should return a URL", async function () { + const packer = new CommandPacker( + "scan", + "github.com", + "domain", + "Urlscan" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(await runner.scan(apiKeys)).to.equal( + "https://urlscan.io/entry/foo/loading" + ); + }); + }); + + context("url", function () { + it("should return a URL for scan", async function () { + const packer = new CommandPacker( + "scan", + "https://www.wikipedia.org/", + "url", + "Urlscan" + ); + const runner = new CommandRunner(packer.getJSON()); + expect(await runner.scan(apiKeys)).to.equal( + "https://urlscan.io/entry/foo/loading" + ); + }); + }); + }); +}); diff --git a/test/command/packer.spec.ts b/test/command/packer.spec.ts new file mode 100644 index 00000000..bf104c1a --- /dev/null +++ b/test/command/packer.spec.ts @@ -0,0 +1,14 @@ +import "mocha"; + +import { expect } from "chai"; + +import { CommandPacker } from "@/command/packer"; + +describe("CommandPacker", function () { + describe("#getMessage()", function () { + it("should return a string", function () { + const packer = new CommandPacker("search", "1.1.1.1", "ip", "Urlscan"); + expect(packer.getMessage()).to.equal("Search 1.1.1.1 as IP on Urlscan"); + }); + }); +});