diff --git a/server/.env.example b/server/.env.example index 5499224..c83f793 100644 --- a/server/.env.example +++ b/server/.env.example @@ -4,4 +4,6 @@ DB_USER= DB_PASSWORD= DB_NAME= PORT=3000 - +SSL_MODE=require +VIRUSTOTAL_API_KEY= +VIRUSTOTAL_BASE_URL=https://www.virustotal.com/api/v3 \ No newline at end of file diff --git a/server/bun.lock b/server/bun.lock index 500740f..eceed05 100644 --- a/server/bun.lock +++ b/server/bun.lock @@ -1,6 +1,6 @@ { "lockfileVersion": 1, - "configVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "server", @@ -143,7 +143,7 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], diff --git a/server/src/constants/virustotal.provider.ts b/server/src/constants/virustotal.provider.ts new file mode 100644 index 0000000..8761cdc --- /dev/null +++ b/server/src/constants/virustotal.provider.ts @@ -0,0 +1,135 @@ +import type { ThreatIntelProvider, IocType } from "./provider.interface"; + +// 1 : virustotal response------------------------------------ +interface VirusTotalResponse { + data: { + attributes: { + last_analysis_stats: { + malicious: number; + suspicious: number; + undetected: number; + harmless: number; + timeout: number; + }; + reputation?: number; + tags?: string[]; + }; + }; +} + +// 2 : normalized response------------------------------------------------------------ +interface NormalizedResponse { + provider_name: string; + verdict: "benign" | "suspicious" | "malicious"; + score: number; + tags?: string[]; + confidence?: number; + summary?: string; +} + +class VirusTotalProvider implements ThreatIntelProvider { + readonly name = "virustotal"; + readonly supportedIocTypes: ReadonlyArray = [ + "ip", + "domain", + "url", + "hash", + ]; + + private apiKey = process.env.VIRUSTOTAL_API_KEY; + private baseUrl = + process.env.VIRUSTOTAL_BASE_URL || "https://www.virustotal.com/api/v3"; + + async query(ioc: string, type: IocType): Promise { + if (!this.apiKey) { + return this.fail("VirusTotal API key not configured"); + } + + try { + const endpoint = this.getEndpoint(ioc, type); + if (!endpoint) { + return this.fail("Unsupported IOC type"); + } + const response = await fetch(`${this.baseUrl}${endpoint}`, { + headers: { + "x-apikey": this.apiKey, + accept: "application/json", + }, + }); + + if (response.status === 429) { + return this.fail("Rate limit exceeded"); + } + + if (!response.ok) { + return this.fail(`API error: ${response.status}`); + } + const data = (await response.json()) as VirusTotalResponse; + return this.normalizeResponse(data); + } catch { + return this.fail("VirusTotal query failed"); + } + } + //----get endpoint based on ioc type------------------------------------------------------- + private getEndpoint(ioc: string, type: IocType): string | null { + switch (type) { + case "ip": + return `/ip_addresses/${ioc}`; + case "domain": + return `/domains/${ioc}`; + case "hash": + return `/files/${ioc}`; + case "url": + return `/urls/${this.encodeUrl(ioc)}`; + default: + return null; + } + } + + private encodeUrl(url: string): string { + return Buffer.from(url).toString("base64url"); + } + + //----normalize response--------------------------------------------------------------------- + private normalizeResponse(data: VirusTotalResponse): NormalizedResponse { + const stats = data.data?.attributes?.last_analysis_stats; + if (!stats) { + return this.fail("Incomplete VirusTotal response"); + } + const total = + stats.malicious + + stats.suspicious + + stats.undetected + + stats.harmless + + stats.timeout; + const maliciousCount = stats.malicious; + const suspiciousCount = stats.suspicious; + + let verdict: NormalizedResponse["verdict"] = "benign"; + if (maliciousCount > 0) verdict = "malicious"; + else if (suspiciousCount > 0) verdict = "suspicious"; + + const score = total > 0 ? Math.round((maliciousCount / total) * 100) : 0; + return { + provider_name: this.name, + verdict, + score, + tags: data.data.attributes.tags, + confidence: Math.min(100, total), + summary: `Malicious: ${maliciousCount}, Suspicious: ${suspiciousCount}, Harmless: ${stats.harmless}, Undetected: ${stats.undetected}, Timeout: ${stats.timeout}`, + }; + } + + //FAILURE handler-------------------------------------------------------------------------------------------- + private fail(reason: string): NormalizedResponse { + return { + provider_name: this.name, + verdict: "benign", + score: 0, + confidence: 0, + summary: reason, + }; + } +} + +export { VirusTotalProvider }; diff --git a/server/src/db/index.ts b/server/src/db/index.ts index 5b5397d..c98dcaa 100644 --- a/server/src/db/index.ts +++ b/server/src/db/index.ts @@ -1,28 +1,28 @@ -import { Pool } from 'pg'; +import { Pool } from "pg"; const pool = new Pool({ - host: process.env.DB_HOST, - port: process.env.DB_PORT, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - max: 10, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || "5432"), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + ssl: + process.env.SSL_MODE === "require" ? { rejectUnauthorized: false } : false, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, }); async function testConnection(): Promise { - try { - const client = await pool.connect(); - console.log('PostgreSQL connected successfully'); - client.release(); - } catch (error) { - console.error('PostgreSQL connection failed:', error); - throw error; - } + try { + const client = await pool.connect(); + console.log("PostgreSQL connected successfully"); + client.release(); + } catch (error) { + console.error("PostgreSQL connection failed:", error); + throw error; + } } export default pool; export { testConnection }; - -