// Copyright 2022 Luca Casonato. All rights reserved. MIT license. /** * Safe Browsing API Client for Deno * ================================= * * Enables client applications to check web resources (most commonly URLs) against Google-generated lists of unsafe web resources. The Safe Browsing APIs are for non-commercial use only. If you need to use APIs to detect malicious URLs for commercial purposes – meaning “for sale or revenue-generating purposes” – please refer to the Web Risk API. * * Docs: https://developers.google.com/safe-browsing/ * Source: https://googleapis.deno.dev/v1/safebrowsing:v5.ts */ import { auth, CredentialsClient, GoogleAuth, request } from "/_/base@v1/mod.ts"; export { auth, GoogleAuth }; export type { CredentialsClient }; /** * Enables client applications to check web resources (most commonly URLs) * against Google-generated lists of unsafe web resources. The Safe Browsing * APIs are for non-commercial use only. If you need to use APIs to detect * malicious URLs for commercial purposes – meaning “for sale or * revenue-generating purposes” – please refer to the Web Risk API. */ export class SafeBrowsing { #client: CredentialsClient | undefined; #baseUrl: string; constructor(client?: CredentialsClient, baseUrl: string = "https://safebrowsing.googleapis.com/") { this.#client = client; this.#baseUrl = baseUrl; } /** * Search for full hashes matching the specified prefixes. This is a custom * method as defined by https://google.aip.dev/136 (the custom method refers * to this method having a custom name within Google's general API development * nomenclature; it does not refer to using a custom HTTP method). * */ async hashesSearch(opts: HashesSearchOptions = {}): Promise { opts = serializeHashesSearchOptions(opts); const url = new URL(`${this.#baseUrl}v5/hashes:search`); if (opts.hashPrefixes !== undefined) { url.searchParams.append("hashPrefixes", String(opts.hashPrefixes)); } const data = await request(url.href, { client: this.#client, method: "GET", }); return deserializeGoogleSecuritySafebrowsingV5SearchHashesResponse(data); } } /** * The full hash identified with one or more matches. */ export interface GoogleSecuritySafebrowsingV5FullHash { /** * The matching full hash. This is the SHA256 hash. The length will be * exactly 32 bytes. */ fullHash?: Uint8Array; /** * Unordered list. A repeated field identifying the details relevant to this * full hash. */ fullHashDetails?: GoogleSecuritySafebrowsingV5FullHashFullHashDetail[]; } function serializeGoogleSecuritySafebrowsingV5FullHash(data: any): GoogleSecuritySafebrowsingV5FullHash { return { ...data, fullHash: data["fullHash"] !== undefined ? encodeBase64(data["fullHash"]) : undefined, }; } function deserializeGoogleSecuritySafebrowsingV5FullHash(data: any): GoogleSecuritySafebrowsingV5FullHash { return { ...data, fullHash: data["fullHash"] !== undefined ? decodeBase64(data["fullHash"] as string) : undefined, }; } /** * Details about a matching full hash. An important note about forward * compatibility: new threat types and threat attributes may be added by the * server at any time; those additions are considered minor version changes. It * is Google's policy not to expose minor version numbers in APIs (see * https://cloud.google.com/apis/design/versioning for the versioning policy), * so clients MUST be prepared to receive `FullHashDetail` messages containing * `ThreatType` enum values or `ThreatAttribute` enum values that are considered * invalid by the client. Therefore, it is the client's responsibility to check * for the validity of all `ThreatType` and `ThreatAttribute` enum values; if * any value is considered invalid, the client MUST disregard the entire * `FullHashDetail` message. */ export interface GoogleSecuritySafebrowsingV5FullHashFullHashDetail { /** * Unordered list. Additional attributes about those full hashes. This may be * empty. */ attributes?: | "THREAT_ATTRIBUTE_UNSPECIFIED" | "CANARY" | "FRAME_ONLY"[]; /** * The type of threat. This field will never be empty. */ threatType?: | "THREAT_TYPE_UNSPECIFIED" | "MALWARE" | "SOCIAL_ENGINEERING" | "UNWANTED_SOFTWARE" | "POTENTIALLY_HARMFUL_APPLICATION"; } /** * The response returned after searching threat hashes. If nothing is found, * the server will return an OK status (HTTP status code 200) with the * `full_hashes` field empty, rather than returning a NOT_FOUND status (HTTP * status code 404). **What's new in V5**: There is a separation between * `FullHash` and `FullHashDetail`. In the case when a hash represents a site * having multiple threats (e.g. both MALWARE and SOCIAL_ENGINEERING), the full * hash does not need to be sent twice as in V4. Furthermore, the cache duration * has been simplified into a single `cache_duration` field. */ export interface GoogleSecuritySafebrowsingV5SearchHashesResponse { /** * The client-side cache duration. The client MUST add this duration to the * current time to determine the expiration time. The expiration time then * applies to every hash prefix queried by the client in the request, * regardless of how many full hashes are returned in the response. Even if * the server returns no full hashes for a particular hash prefix, this fact * MUST also be cached by the client. If and only if the field `full_hashes` * is empty, the client MAY increase the `cache_duration` to determine a new * expiration that is later than that specified by the server. In any case, * the increased cache duration must not be longer than 24 hours. Important: * the client MUST NOT assume that the server will return the same cache * duration for all responses. The server MAY choose different cache durations * for different responses depending on the situation. */ cacheDuration?: number /* Duration */; /** * Unordered list. The unordered list of full hashes found. */ fullHashes?: GoogleSecuritySafebrowsingV5FullHash[]; } function serializeGoogleSecuritySafebrowsingV5SearchHashesResponse(data: any): GoogleSecuritySafebrowsingV5SearchHashesResponse { return { ...data, cacheDuration: data["cacheDuration"] !== undefined ? data["cacheDuration"] : undefined, fullHashes: data["fullHashes"] !== undefined ? data["fullHashes"].map((item: any) => (serializeGoogleSecuritySafebrowsingV5FullHash(item))) : undefined, }; } function deserializeGoogleSecuritySafebrowsingV5SearchHashesResponse(data: any): GoogleSecuritySafebrowsingV5SearchHashesResponse { return { ...data, cacheDuration: data["cacheDuration"] !== undefined ? data["cacheDuration"] : undefined, fullHashes: data["fullHashes"] !== undefined ? data["fullHashes"].map((item: any) => (deserializeGoogleSecuritySafebrowsingV5FullHash(item))) : undefined, }; } /** * Additional options for SafeBrowsing#hashesSearch. */ export interface HashesSearchOptions { /** * Required. The hash prefixes to be looked up. Clients MUST NOT send more * than 1000 hash prefixes. However, following the URL processing procedure, * clients SHOULD NOT need to send more than 30 hash prefixes. Currently each * hash prefix is required to be exactly 4 bytes long. This MAY be relaxed in * the future. */ hashPrefixes?: Uint8Array; } function serializeHashesSearchOptions(data: any): HashesSearchOptions { return { ...data, hashPrefixes: data["hashPrefixes"] !== undefined ? encodeBase64(data["hashPrefixes"]) : undefined, }; } function deserializeHashesSearchOptions(data: any): HashesSearchOptions { return { ...data, hashPrefixes: data["hashPrefixes"] !== undefined ? decodeBase64(data["hashPrefixes"] as string) : undefined, }; } function decodeBase64(b64: string): Uint8Array { const binString = atob(b64); const size = binString.length; const bytes = new Uint8Array(size); for (let i = 0; i < size; i++) { bytes[i] = binString.charCodeAt(i); } return bytes; } const base64abc = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"]; /** * CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 * Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation * @param data */ function encodeBase64(uint8: Uint8Array): string { let result = "", i; const l = uint8.length; for (i = 2; i < l; i += 3) { result += base64abc[uint8[i - 2] >> 2]; result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)]; result += base64abc[uint8[i] & 0x3f]; } if (i === l + 1) { // 1 octet yet to write result += base64abc[uint8[i - 2] >> 2]; result += base64abc[(uint8[i - 2] & 0x03) << 4]; result += "=="; } if (i === l) { // 2 octets yet to write result += base64abc[uint8[i - 2] >> 2]; result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; result += base64abc[(uint8[i - 1] & 0x0f) << 2]; result += "="; } return result; }