From 60f17a16e22b1a1578cb019069cc9f6748992850 Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Sat, 18 May 2024 16:54:47 +0900 Subject: [PATCH 1/5] refactor: use ResultAsync --- src/background/scan.ts | 13 +++++--- src/command/runner.ts | 40 ++++++++---------------- src/scanner/base.ts | 19 ++++++------ src/scanner/browserling.ts | 9 +++--- src/scanner/hybridanalysis.ts | 45 +++++++++++++++------------ src/scanner/urlscan.ts | 58 +++++++++++++++++++---------------- src/scanner/virustotal.ts | 43 +++++++++++++++----------- src/types.ts | 13 ++++---- 8 files changed, 125 insertions(+), 115 deletions(-) diff --git a/src/background/scan.ts b/src/background/scan.ts index 9a1609c3..716ccbe5 100644 --- a/src/background/scan.ts +++ b/src/background/scan.ts @@ -2,10 +2,13 @@ import { showNotification } from "~/background/notification"; import type { CommandRunner } from "~/command/runner"; export async function scan(runner: CommandRunner): Promise { - const res = await runner.scan(); - if (res.isOk()) { - await chrome.tabs.create({ url: res.value }); - } else { - showNotification(res.error); + const resultAsync = runner.scan(); + const result = await resultAsync; + + if (result.isOk()) { + await chrome.tabs.create({ url: result.value }); + return; } + + showNotification(result.error); } diff --git a/src/command/runner.ts b/src/command/runner.ts index 932071a7..b157e418 100644 --- a/src/command/runner.ts +++ b/src/command/runner.ts @@ -1,4 +1,4 @@ -import { err, ok, Result } from "neverthrow"; +import { err, errAsync, ok, okAsync, Result, ResultAsync } from "neverthrow"; import type { OptionsType } from "~/schemas"; import { Selector } from "~/selector"; @@ -102,44 +102,35 @@ export class CommandRunner { } private scannerMap: ScannerMap = { - ip: async ( - scanner: Scanner, - query: string, - ): Promise> => { + ip: (scanner: Scanner, query: string) => { return scanner.scanByIP(query); }, - domain: async ( - scanner: Scanner, - query: string, - ): Promise> => { + domain: (scanner: Scanner, query: string) => { return scanner.scanByDomain(query); }, - url: async ( - scanner: Scanner, - query: string, - ): Promise> => { + url: (scanner: Scanner, query: string) => { return scanner.scanByURL(query); }, }; - public async scan(): Promise> { - const getSlot = () => { + public scan(): ResultAsync { + const getSlot = (): ResultAsync => { const selector: Selector = new Selector(this.command.query); const slots: SelectorSlot[] = selector.getScannerSlots(); const slot = slots.find((s) => s.analyzer.name === this.command.name); if (!slot) { - return err(`Slot for ${this.command.name} is missing`); + return errAsync(`Slot for ${this.command.name} is missing`); } if (!isScanner(slot.analyzer)) { - return err(`${slot.analyzer.name} is not a scanner`); + return errAsync(`${slot.analyzer.name} is not a scanner`); } - return ok(slot); + return okAsync(slot); }; - const scan = async (slot: SelectorSlot) => { + const scan = (slot: SelectorSlot): ResultAsync => { const scanner = slot.analyzer as Scanner; switch (scanner.name) { @@ -158,17 +149,12 @@ export class CommandRunner { if (slot.type in this.scannerMap) { const fn = this.scannerMap[slot.type]; - return await fn(scanner, slot.query); + return fn(scanner, slot.query); } - return err("Something goes wrong"); + return errAsync("Something goes wrong"); }; - const result = await getSlot().asyncMap(scan); - - if (result.isErr()) { - return err(result.error); - } - return result.value; + return getSlot().andThen(scan); } } diff --git a/src/scanner/base.ts b/src/scanner/base.ts index 7b4f933f..59511695 100644 --- a/src/scanner/base.ts +++ b/src/scanner/base.ts @@ -1,4 +1,4 @@ -import { err, Result } from "neverthrow"; +import { errAsync, ResultAsync } from "neverthrow"; import type { ScannableType, Scanner } from "~/types"; @@ -6,29 +6,30 @@ export class Base implements Scanner { public baseURL: string; public name: string; public supportedTypes: ScannableType[] = []; - public apiKey: string | undefined = undefined; + public apiKey?: string = undefined; + public apiKeyRequired: boolean = true; public constructor() { this.baseURL = "http://example.com"; this.name = "Base"; } - public setAPIKey(apiKey: string | undefined): void { + public setAPIKey(apiKey?: string): void { this.apiKey = apiKey; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async scanByURL(url: string): Promise> { - return err("Not implemented"); + public scanByURL(url: string): ResultAsync { + return errAsync("Not implemented"); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async scanByIP(url: string): Promise> { - return err("Not implemented"); + public scanByIP(url: string): ResultAsync { + return errAsync("Not implemented"); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async scanByDomain(url: string): Promise> { - return err("Not implemented"); + public scanByDomain(url: string): ResultAsync { + return errAsync("Not implemented"); } } diff --git a/src/scanner/browserling.ts b/src/scanner/browserling.ts index ad515f99..ae61a7b8 100644 --- a/src/scanner/browserling.ts +++ b/src/scanner/browserling.ts @@ -1,4 +1,4 @@ -import { ok, Result } from "neverthrow"; +import { okAsync } from "neverthrow"; import type { ScannableType } from "~/types"; import { buildURL } from "~/utils"; @@ -9,7 +9,8 @@ export class Browserling extends Base { public baseURL: string; public name: string; public supportedTypes: ScannableType[] = ["url"]; - public apiKey: string | undefined = undefined; + public apiKey?: string = undefined; + public apiKeyRequired: boolean = false; public constructor() { super(); @@ -17,8 +18,8 @@ export class Browserling extends Base { this.name = "Browserling"; } - public async scanByURL(url: string): Promise> { - return ok( + scanByURL(url: string) { + return okAsync( buildURL(this.baseURL, `/browse/win/7/ie/11/${encodeURIComponent(url)}`), ); } diff --git a/src/scanner/hybridanalysis.ts b/src/scanner/hybridanalysis.ts index e6dde0de..f8de0f96 100644 --- a/src/scanner/hybridanalysis.ts +++ b/src/scanner/hybridanalysis.ts @@ -1,4 +1,4 @@ -import { err, ok, Result } from "neverthrow"; +import { errAsync, ResultAsync } from "neverthrow"; import { z } from "zod"; import type { ScannableType } from "~/types"; @@ -17,7 +17,7 @@ export class HybridAnalysis extends Base { public baseURL: string; public name: string; public supportedTypes: ScannableType[] = ["url"]; - public apiKey: string | undefined = undefined; + public apiKey?: string = undefined; public constructor() { super(); @@ -25,13 +25,13 @@ export class HybridAnalysis extends Base { this.name = "HybridAnalysis"; } - public setAPIKey(apiKey: string | undefined): void { + setAPIKey(apiKey: string): void { this.apiKey = apiKey; } - public async scanByURL(url: string): Promise> { - if (this.apiKey === undefined) { - return err("Please set your HybridAnalysis API key via the option."); + scanByURL(url: string) { + if (!this.apiKey) { + return errAsync("Please set your HybridAnalysis API key via the option."); } const formData = new FormData(); @@ -43,21 +43,28 @@ export class HybridAnalysis extends Base { "user-agent": "Falcon Sandbox", }; - const res = await fetch(`${this.baseURL}/api/v2/quick-scan/url`, { - method: "POST", - headers, - body: formData, - }); + const scan = async () => { + const res = await fetch(`${this.baseURL}/api/v2/quick-scan/url`, { + method: "POST", + headers, + body: formData, + }); - const data = await res.json(); + const data = await res.json(); - if (!res.ok) { - const parsed = ErrorResponse.parse(data); - return err(parsed.message); - } + if (!res.ok) { + const parsed = ErrorResponse.parse(data); + throw Error(parsed.message); + } + + const parsed = Response.parse(data); + const sha256: string = parsed.sha256; + return `https://www.hybrid-analysis.com/sample/${sha256}/`; + }; - const parsed = Response.parse(data); - const sha256: string = parsed.sha256; - return ok(`https://www.hybrid-analysis.com/sample/${sha256}/`); + return ResultAsync.fromThrowable( + scan, + (err: unknown) => (err as Error).message, + )(); } } diff --git a/src/scanner/urlscan.ts b/src/scanner/urlscan.ts index bcddae3a..de7734d8 100644 --- a/src/scanner/urlscan.ts +++ b/src/scanner/urlscan.ts @@ -1,4 +1,4 @@ -import { err, ok, Result } from "neverthrow"; +import { errAsync, ResultAsync } from "neverthrow"; import { z } from "zod"; import type { ScannableType } from "~/types"; @@ -18,7 +18,7 @@ export class URLScan extends Base { public baseURL: string; public name: string; public supportedTypes: ScannableType[] = ["ip", "domain", "url"]; - public apiKey: string | undefined = undefined; + public apiKey?: string = undefined; public constructor() { super(); @@ -26,28 +26,25 @@ export class URLScan extends Base { this.name = "urlscan.io"; } - public setAPIKey(apiKey: string | undefined): void { + public setAPIKey(apiKey: string): void { this.apiKey = apiKey; } - public async scanByIP(ip: string): Promise> { - return await this.scan(ip); + scanByIP(ip: string) { + return this.scan(ip); } - public async scanByDomain(domain: string): Promise> { - return await this.scan(domain); + scanByDomain(domain: string) { + return this.scan(domain); } - public async scanByURL(url: string): Promise> { - return await this.scan(url); + scanByURL(url: string) { + return this.scan(url); } - private async scan( - query: string, - isPublic = true, - ): Promise> { - if (this.apiKey === undefined) { - return err("Please set your urlscan.io API key via the option."); + private scan(query: string, isPublic = true) { + if (!this.apiKey) { + return errAsync("Please set your urlscan.io API key via the option."); } const body = JSON.stringify({ @@ -59,20 +56,27 @@ export class URLScan extends Base { "content-type": "application/json", }; - const res = await fetch(`${this.baseURL}/api/v1/scan/`, { - method: "POST", - headers, - body, - }); + const scan = async () => { + const res = await fetch(`${this.baseURL}/api/v1/scan/`, { + method: "POST", + headers, + body, + }); - const data = await res.json(); + const data = await res.json(); - if (!res.ok) { - const parsed = ErrorResponse.parse(data); - return err(parsed.message); - } + if (!res.ok) { + const parsed = ErrorResponse.parse(data); + throw new Error(parsed.message); + } + + const parsed = Response.parse(data); + return `${parsed.result}loading`; + }; - const parsed = Response.parse(data); - return ok(`${parsed.result}loading`); + return ResultAsync.fromThrowable( + scan, + (err: unknown) => (err as Error).message, + )(); } } diff --git a/src/scanner/virustotal.ts b/src/scanner/virustotal.ts index 2dda6937..baec8af4 100644 --- a/src/scanner/virustotal.ts +++ b/src/scanner/virustotal.ts @@ -1,4 +1,4 @@ -import { err, ok, Result } from "neverthrow"; +import { errAsync, ResultAsync } from "neverthrow"; import { z } from "zod"; import type { ScannableType } from "~/types"; @@ -27,7 +27,7 @@ export class VirusTotal extends Base { public baseURL: string; public name: string; public supportedTypes: ScannableType[] = ["url"]; - public apiKey: string | undefined = undefined; + public apiKey?: string = undefined; public constructor() { super(); @@ -35,7 +35,7 @@ export class VirusTotal extends Base { this.name = "VirusTotal"; } - public setAPIKey(apiKey: string | undefined): void { + public setAPIKey(apiKey: string): void { this.apiKey = apiKey; } @@ -47,9 +47,9 @@ export class VirusTotal extends Base { return buildURL(this.baseURL, `/gui/url/${sha256}/details`); } - public async scanByURL(url: string): Promise> { - if (this.apiKey === undefined) { - return err("Please set your VirusTotal API key via the option."); + scanByURL(url: string) { + if (!this.apiKey) { + return errAsync("Please set your VirusTotal API key via the option."); } const formData = new FormData(); @@ -59,20 +59,27 @@ export class VirusTotal extends Base { "x-apikey": this.apiKey, }; - const res = await fetch(buildURL(this.baseURL, "/api/v3/urls"), { - method: "POST", - headers, - body: formData, - }); + const scan = async () => { + const res = await fetch(buildURL(this.baseURL, "/api/v3/urls"), { + method: "POST", + headers, + body: formData, + }); - const data = await res.json(); + const data = await res.json(); - if (!res.ok) { - const parsed = ErrorResponse.parse(data); - return err(parsed.error.message); - } + if (!res.ok) { + const parsed = ErrorResponse.parse(data); + throw new Error(parsed.error.message); + } + + const parsed = Response.parse(data); + return this.permaLink(parsed.data.id); + }; - const parsed = Response.parse(data); - return ok(this.permaLink(parsed.data.id)); + return ResultAsync.fromThrowable( + scan, + (err: unknown) => (err as Error).message, + )(); } } diff --git a/src/types.ts b/src/types.ts index 800fed2f..585c3397 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Result } from "neverthrow"; +import { Result, ResultAsync } from "neverthrow"; import type { CommandActionType, OptionsType, SearchableType } from "~/schemas"; @@ -44,10 +44,11 @@ export interface Scanner { name: string; supportedTypes: ScannableType[]; apiKey?: string; - setAPIKey(apiKey: string | undefined): void; - scanByIP(query: string): Promise>; - scanByDomain(query: string): Promise>; - scanByURL(query: string): Promise>; + apiKeyRequired: boolean; + setAPIKey(apiKey?: string): void; + scanByIP(query: string): ResultAsync; + scanByDomain(query: string): ResultAsync; + scanByURL(query: string): ResultAsync; } export interface SearchFuncWrapper { @@ -79,7 +80,7 @@ export interface ScannerMap { [name: string]: ( scanner: Scanner, query: string, - ) => Promise>; + ) => ResultAsync; } export interface Command { From 57a8131e335b7d73c022fcf764d6e64df772c91f Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Sat, 18 May 2024 16:55:10 +0900 Subject: [PATCH 2/5] refactor: console.debug in callback --- src/background/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/background/index.ts b/src/background/index.ts index d62e78c6..54de8d6d 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -31,11 +31,11 @@ export function createContextMenus(text: string, options: OptionsType): void { // it tells action, query, type and target to the listener const id = commandToID(command); const title = commandToMessage(command); - chrome.contextMenus.create({ contexts, id, title }); - - if (options.debug) { - console.debug(`Mitaka: context menu:${id} created`); - } + chrome.contextMenus.create({ contexts, id, title }, () => { + if (options.debug) { + console.debug(`Mitaka: context menu:${id} created`); + } + }); } } From 4a02943b2596fe31250f766cb3d148c5dc942f99 Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Sat, 18 May 2024 16:55:34 +0900 Subject: [PATCH 3/5] refactor: prefer early-return --- src/background/search.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/background/search.ts b/src/background/search.ts index e8068dc8..14932486 100644 --- a/src/background/search.ts +++ b/src/background/search.ts @@ -6,9 +6,10 @@ export async function searchAll(runner: CommandRunner): Promise { for (const result of results) { if (result.isOk()) { await chrome.tabs.create({ url: result.value }); - } else { - showNotification(result.error); + continue; } + + showNotification(result.error); } } @@ -16,7 +17,8 @@ export async function search(runner: CommandRunner): Promise { const result = runner.search(); if (result.isOk()) { await chrome.tabs.create({ url: result.value }); - } else { - showNotification(result.error); + return; } + + showNotification(result.error); } From 885d76dabefc74c7443e44a5d2b94da72559660a Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Sat, 18 May 2024 16:59:27 +0900 Subject: [PATCH 4/5] refactor: prefer shorten notation --- src/options/index.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/options/index.vue b/src/options/index.vue index 9ed41c54..7b14b4ef 100644 --- a/src/options/index.vue +++ b/src/options/index.vue @@ -20,8 +20,8 @@ import { getFaviconURL } from "../utils"; const isInitialized = ref(false); const synchedAt = ref(); -const searchableType = ref(undefined); -const scannableType = ref(undefined); +const searchableType = ref(); +const scannableType = ref(); const options = reactive({ debug: false, @@ -243,10 +243,7 @@ watch(options, async (newOptions) => {
-
+
Date: Sat, 18 May 2024 16:59:54 +0900 Subject: [PATCH 5/5] refactor: define default options as const --- src/selector.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/selector.ts b/src/selector.ts index 7a133af3..864e73e1 100644 --- a/src/selector.ts +++ b/src/selector.ts @@ -30,6 +30,8 @@ import type { SelectorSlot, } from "~/types"; +const defaultOptions = OptionsSchema.parse({}); + export class Selector { protected input: string; protected options: OptionsType; @@ -37,10 +39,7 @@ export class Selector { protected scanners: Scanner[]; protected searchers: Searcher[]; - public constructor( - input: string, - options: OptionsType = OptionsSchema.parse({}), - ) { + public constructor(input: string, options: OptionsType = defaultOptions) { let normalized = options.refang ? refang(input) : input; normalized = options.punycode ? unicodeToASCII(normalized, {