Skip to content

Commit 9bbe3d3

Browse files
authored
Merge pull request #197 from ShipSecAI/betterclever/absuseipdb
feat(worker): Implement AbuseIPDB component
2 parents 4171703 + aabb705 commit 9bbe3d3

File tree

4 files changed

+479
-0
lines changed

4 files changed

+479
-0
lines changed

worker/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import './security/atlassian-offboarding';
5555
import './security/trufflehog';
5656
import './security/terminal-demo';
5757
import './security/virustotal';
58+
import './security/abuseipdb';
5859

5960
// GitHub components
6061
import './github/connection-provider';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Optional integration test for AbuseIPDB component.
3+
* Requires a valid AbuseIPDB API key.
4+
* Enable by setting RUN_ABUSEDB_TESTS=1 and providing ABUSEIPDB_API_KEY.
5+
*/
6+
import { describe, expect, test, beforeEach } from 'bun:test';
7+
import { componentRegistry, createExecutionContext, type ExecutionContext } from '@shipsec/component-sdk';
8+
import '../../index'; // Ensure registry is populated
9+
10+
interface AbuseIPDBOutput {
11+
ipAddress: string;
12+
isPublic?: boolean;
13+
ipVersion?: number;
14+
isWhitelisted?: boolean;
15+
abuseConfidenceScore: number;
16+
countryCode?: string;
17+
usageType?: string;
18+
isp?: string;
19+
domain?: string;
20+
hostnames?: string[];
21+
totalReports?: number;
22+
numDistinctUsers?: number;
23+
lastReportedAt?: string;
24+
reports?: Record<string, unknown>[];
25+
full_report: Record<string, unknown>;
26+
}
27+
28+
const shouldRunIntegration =
29+
process.env.RUN_ABUSEDB_TESTS === '1' && !!process.env.ABUSEIPDB_API_KEY;
30+
31+
(shouldRunIntegration ? describe : describe.skip)('AbuseIPDB Integration', () => {
32+
let context: ExecutionContext;
33+
34+
beforeEach(async () => {
35+
context = createExecutionContext({
36+
runId: 'test-run',
37+
componentRef: 'abuseipdb-integration-test',
38+
});
39+
});
40+
41+
test('checks a known IP address', async () => {
42+
const component = componentRegistry.get('security.abuseipdb.check');
43+
expect(component).toBeDefined();
44+
45+
// 1.1.1.1 is Cloudflare DNS and should exist in AbuseIPDB
46+
const ipToCheck = '1.1.1.1';
47+
48+
const params = {
49+
ipAddress: ipToCheck,
50+
apiKey: process.env.ABUSEIPDB_API_KEY!,
51+
maxAgeInDays: 90,
52+
verbose: true
53+
};
54+
55+
const result = await component!.execute(params, context) as AbuseIPDBOutput;
56+
57+
expect(result.ipAddress).toBe(ipToCheck);
58+
expect(typeof result.abuseConfidenceScore).toBe('number');
59+
expect(result.full_report).toBeDefined();
60+
});
61+
62+
test('checks a known malicious IP address', async () => {
63+
const component = componentRegistry.get('security.abuseipdb.check');
64+
expect(component).toBeDefined();
65+
66+
// Using Google DNS as a safe, known IP
67+
const ipToCheck = '8.8.8.8';
68+
69+
const params = {
70+
ipAddress: ipToCheck,
71+
apiKey: process.env.ABUSEIPDB_API_KEY!,
72+
maxAgeInDays: 90,
73+
verbose: false
74+
};
75+
76+
const result = await component!.execute(params, context) as AbuseIPDBOutput;
77+
78+
expect(result.ipAddress).toBe(ipToCheck);
79+
expect(typeof result.abuseConfidenceScore).toBe('number');
80+
expect(result.abuseConfidenceScore).toBeGreaterThanOrEqual(0);
81+
expect(result.abuseConfidenceScore).toBeLessThanOrEqual(100);
82+
});
83+
});
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { describe, it, expect, beforeAll, afterEach, vi } from 'bun:test';
2+
import * as sdk from '@shipsec/component-sdk';
3+
import { componentRegistry } from '../../index';
4+
import { definition } from '../abuseipdb';
5+
6+
interface AbuseIPDBOutput {
7+
ipAddress: string;
8+
isPublic?: boolean;
9+
ipVersion?: number;
10+
isWhitelisted?: boolean;
11+
abuseConfidenceScore: number;
12+
countryCode?: string;
13+
usageType?: string;
14+
isp?: string;
15+
domain?: string;
16+
hostnames?: string[];
17+
totalReports?: number;
18+
numDistinctUsers?: number;
19+
lastReportedAt?: string;
20+
reports?: Record<string, unknown>[];
21+
full_report: Record<string, unknown>;
22+
}
23+
24+
describe('abuseipdb component', () => {
25+
beforeAll(async () => {
26+
// Ensure registry is populated
27+
await import('../../index');
28+
});
29+
30+
afterEach(() => {
31+
vi.restoreAllMocks();
32+
});
33+
34+
it('should be registered with correct metadata', () => {
35+
const component = componentRegistry.get('security.abuseipdb.check');
36+
expect(component).toBeDefined();
37+
expect(component!.label).toBe('AbuseIPDB Check');
38+
expect(component!.category).toBe('security');
39+
});
40+
41+
it('should have parameters defined in metadata', () => {
42+
const component = componentRegistry.get('security.abuseipdb.check');
43+
expect(component).toBeDefined();
44+
expect(component!.metadata?.parameters).toBeDefined();
45+
expect(component!.metadata?.parameters).toHaveLength(2);
46+
});
47+
48+
it('should execute successfully with valid input', async () => {
49+
const component = componentRegistry.get('security.abuseipdb.check');
50+
if (!component) throw new Error('Component not registered');
51+
52+
const context = sdk.createExecutionContext({
53+
runId: 'test-run',
54+
componentRef: 'abuseipdb-test',
55+
});
56+
57+
const params = {
58+
ipAddress: '127.0.0.1',
59+
apiKey: 'test-key',
60+
maxAgeInDays: 90,
61+
verbose: false
62+
};
63+
64+
const mockResponse = {
65+
data: {
66+
ipAddress: '127.0.0.1',
67+
isPublic: true,
68+
ipVersion: 4,
69+
isWhitelisted: false,
70+
abuseConfidenceScore: 100,
71+
countryCode: 'US',
72+
usageType: 'Data Center',
73+
isp: 'Test ISP',
74+
domain: 'example.com',
75+
totalReports: 10,
76+
numDistinctUsers: 5,
77+
lastReportedAt: '2023-01-01T00:00:00Z'
78+
}
79+
};
80+
81+
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify(mockResponse), {
82+
status: 200,
83+
headers: { 'Content-Type': 'application/json' }
84+
}));
85+
86+
const result = await component.execute(params, context) as AbuseIPDBOutput;
87+
88+
expect(fetchSpy).toHaveBeenCalled();
89+
const callArgs = fetchSpy.mock.calls[0];
90+
expect(callArgs[0]).toContain('https://api.abuseipdb.com/api/v2/check');
91+
expect(callArgs[0]).toContain('ipAddress=127.0.0.1');
92+
93+
expect(result.ipAddress).toBe('127.0.0.1');
94+
expect(result.abuseConfidenceScore).toBe(100);
95+
expect(result.isp).toBe('Test ISP');
96+
expect(result.full_report).toEqual(mockResponse);
97+
});
98+
99+
it('should handle 404', async () => {
100+
const component = componentRegistry.get('security.abuseipdb.check');
101+
if (!component) throw new Error('Component not registered');
102+
103+
const context = sdk.createExecutionContext({
104+
runId: 'test-run',
105+
componentRef: 'abuseipdb-test',
106+
});
107+
108+
const params = {
109+
ipAddress: '0.0.0.0',
110+
apiKey: 'test-key',
111+
maxAgeInDays: 90,
112+
verbose: false
113+
};
114+
115+
vi.spyOn(global, 'fetch').mockResolvedValue(new Response(null, {
116+
status: 404,
117+
}));
118+
119+
const result = await component.execute(params, context) as AbuseIPDBOutput;
120+
expect(result.abuseConfidenceScore).toBe(0);
121+
expect(result.full_report.error).toBe('Not Found');
122+
});
123+
124+
it('should throw error on failure', async () => {
125+
const component = componentRegistry.get('security.abuseipdb.check');
126+
if (!component) throw new Error('Component not registered');
127+
128+
const context = sdk.createExecutionContext({
129+
runId: 'test-run',
130+
componentRef: 'abuseipdb-test',
131+
});
132+
133+
const params = {
134+
ipAddress: '1.1.1.1',
135+
apiKey: 'test-key',
136+
maxAgeInDays: 90,
137+
verbose: false
138+
};
139+
140+
vi.spyOn(global, 'fetch').mockResolvedValue(new Response('Unauthorized', {
141+
status: 401,
142+
statusText: 'Unauthorized'
143+
}));
144+
145+
await expect(component.execute(params, context)).rejects.toThrow();
146+
});
147+
148+
it('should throw ValidationError when ipAddress is missing', async () => {
149+
const component = componentRegistry.get('security.abuseipdb.check');
150+
if (!component) throw new Error('Component not registered');
151+
152+
const context = sdk.createExecutionContext({
153+
runId: 'test-run',
154+
componentRef: 'abuseipdb-test',
155+
});
156+
157+
const params = {
158+
ipAddress: '',
159+
apiKey: 'test-key',
160+
maxAgeInDays: 90,
161+
verbose: false
162+
};
163+
164+
await expect(component.execute(params, context)).rejects.toThrow('IP Address is required');
165+
});
166+
167+
it('should throw ConfigurationError when apiKey is missing', async () => {
168+
const component = componentRegistry.get('security.abuseipdb.check');
169+
if (!component) throw new Error('Component not registered');
170+
171+
const context = sdk.createExecutionContext({
172+
runId: 'test-run',
173+
componentRef: 'abuseipdb-test',
174+
});
175+
176+
const params = {
177+
ipAddress: '1.1.1.1',
178+
apiKey: '',
179+
maxAgeInDays: 90,
180+
verbose: false
181+
};
182+
183+
await expect(component.execute(params, context)).rejects.toThrow('AbuseIPDB API Key is required');
184+
});
185+
186+
it('should include verbose parameter in request when enabled', async () => {
187+
const component = componentRegistry.get('security.abuseipdb.check');
188+
if (!component) throw new Error('Component not registered');
189+
190+
const context = sdk.createExecutionContext({
191+
runId: 'test-run',
192+
componentRef: 'abuseipdb-test',
193+
});
194+
195+
const params = {
196+
ipAddress: '8.8.8.8',
197+
apiKey: 'test-key',
198+
maxAgeInDays: 30,
199+
verbose: true
200+
};
201+
202+
const mockResponse = {
203+
data: {
204+
ipAddress: '8.8.8.8',
205+
abuseConfidenceScore: 0,
206+
reports: []
207+
}
208+
};
209+
210+
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify(mockResponse), {
211+
status: 200,
212+
headers: { 'Content-Type': 'application/json' }
213+
}));
214+
215+
await component.execute(params, context);
216+
217+
const callUrl = fetchSpy.mock.calls[0][0] as string;
218+
expect(callUrl).toContain('verbose=true');
219+
expect(callUrl).toContain('maxAgeInDays=30');
220+
});
221+
});

0 commit comments

Comments
 (0)