Skip to content

Commit 97a1e0c

Browse files
committed
feat: Integrate 7 threat intelligence feeds with auto-sync
- OpenPhish: GitHub feed (8-15k URLs) - PhishTank: Community verified (requires API key) - PhishStats: Real-time detection (20 req/min) - URLhaus: Malware distribution URLs (CRITICAL) - Google Safe Browsing: Real-time lookup (10k/day) - AbuseIPDB: IP reputation (1000/day) - Spamhaus DROP: Malicious netblocks Features: - Vercel Cron: Auto-sync every 6 hours - Admin API: Manual sync control - VirusTotal: PRO-only (64+ char key) to avoid quota - Database: ThreatFeedLog tracking, Blocklist metadata - Expected: 100k+ phishing domains after first sync
1 parent e7fb60a commit 97a1e0c

7 files changed

Lines changed: 845 additions & 28 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { NextResponse } from 'next/server'
2+
import { getServerSession } from 'next-auth'
3+
import { authOptions } from '@/app/lib/auth'
4+
import {
5+
syncOpenPhish,
6+
syncPhishTank,
7+
syncPhishStats,
8+
syncURLhaus,
9+
syncSpamhausDROP,
10+
syncAllThreatFeeds
11+
} from '@/app/lib/threatFeeds'
12+
13+
export const dynamic = 'force-dynamic'
14+
export const maxDuration = 60 // Vercel Pro: 60s timeout
15+
16+
export async function POST(request: Request) {
17+
try {
18+
const session = await getServerSession(authOptions)
19+
20+
// Admin only
21+
if (!session?.user || session.user.role !== 'ADMIN') {
22+
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
23+
}
24+
25+
const { source } = await request.json()
26+
27+
let result
28+
29+
switch (source) {
30+
case 'openphish':
31+
result = await syncOpenPhish()
32+
break
33+
case 'phishtank':
34+
result = await syncPhishTank()
35+
break
36+
case 'phishstats':
37+
result = await syncPhishStats(100)
38+
break
39+
case 'urlhaus':
40+
result = await syncURLhaus()
41+
break
42+
case 'spamhaus':
43+
result = await syncSpamhausDROP()
44+
break
45+
case 'all':
46+
result = await syncAllThreatFeeds()
47+
break
48+
default:
49+
return NextResponse.json({ error: 'Invalid source' }, { status: 400 })
50+
}
51+
52+
return NextResponse.json({
53+
success: true,
54+
source,
55+
result
56+
})
57+
} catch (error: any) {
58+
console.error('Threat feed sync error:', error)
59+
return NextResponse.json(
60+
{ success: false, error: error.message },
61+
{ status: 500 }
62+
)
63+
}
64+
}
65+
66+
export async function GET() {
67+
try {
68+
const session = await getServerSession(authOptions)
69+
70+
if (!session?.user || session.user.role !== 'ADMIN') {
71+
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
72+
}
73+
74+
// Return status of all feeds
75+
return NextResponse.json({
76+
feeds: [
77+
{
78+
id: 'openphish',
79+
name: 'OpenPhish',
80+
description: 'Public phishing feed from GitHub',
81+
url: 'https://raw.githubusercontent.com/openphish/public_feed/refs/heads/main/feed.txt',
82+
free: true,
83+
requiresApiKey: false
84+
},
85+
{
86+
id: 'phishtank',
87+
name: 'PhishTank',
88+
description: 'Community verified phishing database',
89+
url: 'https://www.phishtank.com',
90+
free: true,
91+
requiresApiKey: true,
92+
configured: !!process.env.PHISHTANK_API_KEY
93+
},
94+
{
95+
id: 'phishstats',
96+
name: 'PhishStats',
97+
description: 'REST API for phishing URLs',
98+
url: 'https://phishstats.info',
99+
free: true,
100+
requiresApiKey: false,
101+
rateLimit: '20 requests/minute'
102+
},
103+
{
104+
id: 'urlhaus',
105+
name: 'URLhaus',
106+
description: 'Malware distribution URLs',
107+
url: 'https://urlhaus.abuse.ch',
108+
free: true,
109+
requiresApiKey: false
110+
},
111+
{
112+
id: 'spamhaus',
113+
name: 'Spamhaus DROP',
114+
description: 'Malicious netblock list',
115+
url: 'https://www.spamhaus.org/drop/',
116+
free: true,
117+
requiresApiKey: false
118+
}
119+
],
120+
enrichment: [
121+
{
122+
id: 'google-safe-browsing',
123+
name: 'Google Safe Browsing',
124+
description: 'URL threat lookup',
125+
free: true,
126+
requiresApiKey: true,
127+
configured: !!process.env.GOOGLE_SAFE_BROWSING_API_KEY
128+
},
129+
{
130+
id: 'abuseipdb',
131+
name: 'AbuseIPDB',
132+
description: 'IP reputation (1000 checks/day)',
133+
free: true,
134+
requiresApiKey: true,
135+
configured: !!process.env.ABUSEIPDB_API_KEY
136+
}
137+
]
138+
})
139+
} catch (error: any) {
140+
return NextResponse.json({ error: error.message }, { status: 500 })
141+
}
142+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { NextResponse } from 'next/server'
2+
import { syncAllThreatFeeds } from '@/app/lib/threatFeeds'
3+
4+
export const dynamic = 'force-dynamic'
5+
export const maxDuration = 300 // 5 minutes for Vercel Pro
6+
7+
/**
8+
* Vercel Cron Job - Automated Threat Feed Sync
9+
* Schedule: Every 6 hours
10+
* Runs: 00:00, 06:00, 12:00, 18:00 UTC daily
11+
*/
12+
export async function GET(request: Request) {
13+
try {
14+
// Verify this is a Vercel Cron request
15+
const authHeader = request.headers.get('authorization')
16+
17+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
18+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
19+
}
20+
21+
console.log('[CRON] Starting scheduled threat feed sync...')
22+
23+
const result = await syncAllThreatFeeds()
24+
25+
console.log('[CRON] Sync completed:', result)
26+
27+
return NextResponse.json({
28+
success: true,
29+
message: 'Threat feeds synchronized successfully',
30+
result
31+
})
32+
} catch (error: any) {
33+
console.error('[CRON] Sync failed:', error)
34+
return NextResponse.json(
35+
{ success: false, error: error.message },
36+
{ status: 500 }
37+
)
38+
}
39+
}

app/lib/externalSources.ts

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { prisma } from './db'
7+
import { checkGoogleSafeBrowsing } from './threatFeeds'
78

89
// Vietnamese scam domains from community reports
910
const VN_SCAM_DOMAINS = [
@@ -178,11 +179,13 @@ export async function syncExternalData() {
178179
return { addedScam, addedTrusted }
179180
}
180181

181-
// Check domain against external sources (for future API integration)
182-
export async function checkExternalSources(domain: string): Promise<{
182+
// Check domain against external sources
183+
export async function checkExternalSources(domain: string, fullUrl?: string): Promise<{
183184
isKnownScam: boolean
184185
sources: string[]
185186
details?: string
187+
googleSafeBrowsing?: any
188+
abuseIPDB?: any
186189
virusTotal?: {
187190
detected: boolean
188191
stats: {
@@ -216,38 +219,62 @@ export async function checkExternalSources(domain: string): Promise<{
216219
}
217220
}
218221

219-
// Check VirusTotal
222+
// Check VirusTotal (PRO version only - deep analysis)
220223
let virusTotalData
221-
try {
222-
console.log('[VirusTotal] Checking:', domain)
223-
const vtResult = await checkVirusTotal(`https://${domain}`)
224-
console.log('[VirusTotal] Result:', vtResult)
225-
if (vtResult.notFound) {
226-
// URL never scanned - indicate this in UI
227-
virusTotalData = {
228-
detected: false,
229-
stats: null,
230-
notFound: true,
231-
}
232-
} else if (vtResult.stats) {
233-
virusTotalData = {
234-
detected: vtResult.detected,
235-
stats: vtResult.stats,
236-
notFound: false,
224+
const vtApiKey = process.env.VIRUSTOTAL_API_KEY_PRO || process.env.VIRUSTOTAL_API_KEY
225+
226+
if (vtApiKey && vtApiKey.length > 64) {
227+
// Only use VirusTotal if PRO key is configured (64+ chars)
228+
try {
229+
console.log('[VirusTotal PRO] Deep analysis:', domain)
230+
const vtResult = await checkVirusTotal(`https://${domain}`)
231+
console.log('[VirusTotal PRO] Result:', vtResult)
232+
if (vtResult.notFound) {
233+
virusTotalData = {
234+
detected: false,
235+
stats: null,
236+
notFound: true,
237+
}
238+
} else if (vtResult.stats) {
239+
virusTotalData = {
240+
detected: vtResult.detected,
241+
stats: vtResult.stats,
242+
notFound: false,
243+
}
244+
if (vtResult.detected) {
245+
sources.push(
246+
`VirusTotal PRO: ${vtResult.stats.malicious} engines phát hiện nguy hiểm`
247+
)
248+
}
237249
}
238-
if (vtResult.detected) {
239-
sources.push(
240-
`VirusTotal: ${vtResult.stats.malicious} engines phát hiện nguy hiểm`
241-
)
250+
} catch (error) {
251+
console.error('[VirusTotal PRO] Error:', error)
252+
}
253+
}
254+
255+
// Check Google Safe Browsing (free, fast lookup)
256+
let googleSafeBrowsingData
257+
if (fullUrl) {
258+
try {
259+
const gsbResult = await checkGoogleSafeBrowsing(fullUrl)
260+
googleSafeBrowsingData = gsbResult
261+
if (!gsbResult.safe) {
262+
sources.push(`Google Safe Browsing: ${gsbResult.threats?.join(', ') || 'Threat detected'}`)
242263
}
264+
} catch (error) {
265+
console.error('[Google Safe Browsing] Error:', error)
243266
}
244-
} catch (error) {
245-
console.error('[VirusTotal] Error:', error)
246267
}
268+
269+
// Check AbuseIPDB for IP reputation (if we have IP)
270+
let abuseIPDBData
271+
// Note: IP extraction would happen in websiteAnalyzer.ts, this is placeholder
247272

248273
return {
249274
isKnownScam: sources.length > 0,
250275
sources,
276+
googleSafeBrowsing: googleSafeBrowsingData,
277+
abuseIPDB: abuseIPDBData,
251278
virusTotal: virusTotalData,
252279
}
253280
}

0 commit comments

Comments
 (0)