Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rework client configuration to make DX better #38

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 34 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,50 @@ A JavaScript client for [Google Safe Browsing](https://safebrowsing.google.com)

## APIs

### threatMatches.find
### Find threat entries

Finds the threat entries that match the Safe Browsing lists.

```ts
import { GoogleSafeBrowsing } from '@hckhanh/google-safe-browsing'

const client = new GoogleSafeBrowsing('apiKey')
// Initialize Google Safe Browsing client with API key and required identifiers
// clientId: Unique identifier for your application instance
// clientVersion: Current version of your application
const client = new GoogleSafeBrowsing('apiKey', {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider documenting the purpose and requirements for clientId and clientVersion configuration parameters

Adding a brief explanation of these configuration parameters would help developers understand how to properly set up the client.

// Initialize Google Safe Browsing client with API key and required identifiers
// clientId: Unique identifier for your application instance
// clientVersion: Current version of your application
const client = new GoogleSafeBrowsing('apiKey', {

clientId: 'uniqueClientId',
clientVersion: '1.0.0',
})

const result = await client.findThreatMatches({
client: {
clientId: 'uniqueClientId',
clientVersion: '1.0.0',
},
threatInfo: {
threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
platformTypes: ['ALL_PLATFORMS'],
threatEntryTypes: ['URL'],
threatEntries: [
{ url: 'http://malware.testing.google.test/testing/malware/' },
],
},
threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
platformTypes: ['ALL_PLATFORMS'],
threatEntryTypes: ['URL'],
threatEntries: [
{ url: 'http://malware.testing.google.test/testing/malware/' },
],
})

const hasRisk = result.matches !== undefined && result.matches.length > 0
const hasRisk = result.matches?.length > 0
```

### Find threat entries from urls

Finds the threat entries that match the Safe Browsing lists from the input urls

```ts
import { GoogleSafeBrowsing } from '@hckhanh/google-safe-browsing'

const client = new GoogleSafeBrowsing('apiKey', {
clientId: 'uniqueClientId',
clientVersion: '1.0.0',
})

const result = await client.findThreatMatchesFromUrls([
'http://malware.testing.google.test/testing/malware/',
])

const hasRisk = result.matches?.length > 0
```

## Release Notes
Expand Down
42 changes: 30 additions & 12 deletions src/GoogleSafeBrowsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,22 @@ import { GoogleSafeBrowsing } from './GoogleSafeBrowsing.js'

const client = new GoogleSafeBrowsing(
process.env.GOOGLE_SAFE_BROWSING_API_KEY as string,
{
clientId: 'khanh.id',
clientVersion: '2.0.0',
},
)

describe('GoogleSafeBrowsing', () => {
it('should detect malicious link', async () => {
await expect(
client.findThreatMatches({
client: {
clientId: 'khanh.id',
clientVersion: '2.0.0',
},
threatInfo: {
threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
platformTypes: ['ALL_PLATFORMS'],
threatEntryTypes: ['URL'],
threatEntries: [
{ url: 'http://malware.testing.google.test/testing/malware/' },
],
},
threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
platformTypes: ['ALL_PLATFORMS'],
threatEntryTypes: ['URL'],
threatEntries: [
{ url: 'http://malware.testing.google.test/testing/malware/' },
],
}),
).resolves.toEqual({
matches: [
Expand All @@ -37,4 +35,24 @@ describe('GoogleSafeBrowsing', () => {
],
})
})

it('should detect malicious link from urls', async () => {
await expect(
client.findThreatMatchesFromUrls([
'http://malware.testing.google.test/testing/malware/',
]),
).resolves.toEqual({
matches: [
{
threatType: 'MALWARE',
platformType: 'ANY_PLATFORM',
threat: {
url: 'http://malware.testing.google.test/testing/malware/',
},
cacheDuration: '300s',
threatEntryType: 'URL',
},
],
})
})
})
97 changes: 64 additions & 33 deletions src/GoogleSafeBrowsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export class GoogleSafeBrowsing {
*/
private readonly apiKey: string

/**
* The client metadata.
*/
private readonly client: ClientInfo

/**
* Represents the endpoint URL of a network request or API call.
*/
Expand All @@ -32,15 +37,21 @@ export class GoogleSafeBrowsing {
* Constructs an instance of the Class with the specified API key and optional endpoint.
*
* @param apiKey The API key used for authentication; must be a non-empty string.
* @param client The client metadata.
* @param [endpoint=DEFAULT_ENDPOINT] The optional endpoint URL.
* @throws {Error} If the `apiKey` is an empty string or only contains whitespace.
*/
constructor(apiKey: string, endpoint: string = DEFAULT_ENDPOINT) {
constructor(
apiKey: string,
client: ClientInfo,
endpoint: string = DEFAULT_ENDPOINT,
) {
if (!apiKey.trim()) {
throw new Error('API key is required')
}

this.apiKey = apiKey
this.client = client
hckhanh marked this conversation as resolved.
Show resolved Hide resolved
this.endpoint = endpoint
}

Expand All @@ -49,31 +60,28 @@ export class GoogleSafeBrowsing {
*
* @example
* ```ts
* const client = new GoogleSafeBrowsing('apiKey')
* const client = new GoogleSafeBrowsing('apiKey', {
* clientId: 'uniqueClientId',
* clientVersion: '1.0.0',
* })
* const result = await client.findThreatMatches({
* client: {
* clientId: 'uniqueClientId',
* clientVersion: '1.0.0',
* },
* threatInfo: {
* threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
* platformTypes: ['ALL_PLATFORMS'],
* threatEntryTypes: ['URL'],
* threatEntries: [
* { url: 'http://malware.testing.google.test/testing/malware/' },
* ],
* },
* threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
* platformTypes: ['ALL_PLATFORMS'],
* threatEntryTypes: ['URL'],
* threatEntries: [
* { url: 'http://malware.testing.google.test/testing/malware/' },
* ],
* })
*
* const hasRisk = result.matches !== undefined && result.matches.length > 0
* const hasRisk = result.matches?.length > 0
* ```
*
* @param request The request object containing the parameters for finding threat matches.
* @param threatInfo The lists and entries to be checked for matches.
*
* @return A promise that resolves to the response object containing the list of {@link ThreatMatch}.
*/
async findThreatMatches(
request: FindThreatMatchesRequest,
threatInfo: ThreatInfo,
): Promise<FindThreatMatchesResponse> {
const res = await fetch(
`${this.endpoint}/threatMatches:find?key=${this.apiKey}`,
Expand All @@ -83,7 +91,7 @@ export class GoogleSafeBrowsing {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, br',
},
body: JSON.stringify(request),
body: JSON.stringify({ client: this.client, threatInfo }),
},
)

Expand All @@ -92,27 +100,50 @@ export class GoogleSafeBrowsing {
throw new Error('Rate limit exceeded for Google Safe Browsing API')
}

throw new Error(`API request failed with status ${res.status}`)
const errorBody = await res.text()
throw new Error(
`API request failed with status ${res.status}: ${errorBody}`,
)
}

return res.json() as Promise<FindThreatMatchesResponse>
return res.json()
}
}

/**
* Represents a request to find threat matches.
* This type is used to encapsulate the necessary information
* for searching and identifying threats in specified lists and entries.
*/
export interface FindThreatMatchesRequest {
/**
* The client metadata.
*/
client: ClientInfo
/**
* The lists and entries to be checked for matches.
* Finds threat matches from urls using Google Safe Browsing API.
*
* @example
* ```ts
* const client = new GoogleSafeBrowsing('apiKey', {
* clientId: 'uniqueClientId',
* clientVersion: '1.0.0',
* })
* const result = await client.findThreatMatchesFromUrls([
* 'http://malware.testing.google.test/testing/malware/'
* ])
*
* const hasRisk = result.matches?.length > 0
* ```
*
* @param urls The list of urls to be checked for matches.
*
* @return A promise that resolves to the response object containing the list of {@link ThreatMatch}.
*/
threatInfo: ThreatInfo
async findThreatMatchesFromUrls(
urls: string[],
): Promise<FindThreatMatchesResponse> {
return this.findThreatMatches({
threatTypes: [
'MALWARE',
'UNWANTED_SOFTWARE',
'SOCIAL_ENGINEERING',
'POTENTIALLY_HARMFUL_APPLICATION',
],
platformTypes: ['ANY_PLATFORM'],
threatEntryTypes: ['URL'],
threatEntries: urls.map((url) => ({ url })),
})
}
}

/**
Expand Down
Loading