diff --git a/src/lib/cyberNews.ts b/src/lib/cyberNews.ts new file mode 100644 index 0000000..b5d5b5b --- /dev/null +++ b/src/lib/cyberNews.ts @@ -0,0 +1,106 @@ +import axios from "axios"; + +export type NewsItem = { + title: string; + link: string; + source: string; + publishedAt: number; +}; + +const FEEDS: { href: string; source: string }[] = [ + { + href: "/news/thn", + source: "The Hacker News", + }, + { + href: "/news/cisa", + source: "CISA", + }, + { + href: "/news/krebs", + source: "Krebs on Security", + }, + { + href: "/news/bc", + source: "BleepingComputer", + }, +]; + +const MAX_ITEMS = 15; +const REQUEST_TIMEOUT_MS = 10_000; + +function parseDate(value: string | null | undefined): number { + if (!value) return 0; + + const ms = Date.parse(value); + return Number.isNaN(ms) ? 0 : ms; +} + +function textOf(el: Element | undefined | null, tag: string): string { + return el?.getElementsByTagName(tag)[0]?.textContent?.trim() ?? ""; +} + +function dateOf(el: Element): number { + const pubDate = textOf(el, "pubDate"); + if (pubDate) return parseDate(pubDate); + + const dc = el.getElementsByTagName("dc:date")[0]?.textContent?.trim(); + return parseDate(dc); +} + +function parseFeed(xml: string, source: string): NewsItem[] { + const doc = new DOMParser().parseFromString(xml, "text/xml"); + + if (doc.querySelector("parsererror")) return []; + + const items: NewsItem[] = []; + + doc.querySelectorAll("item").forEach((item) => { + const title = textOf(item, "title"); + const link = textOf(item, "link"); + if (!title || !link) return; + + items.push({ + title, + link, + source, + publishedAt: dateOf(item), + }); + }); + + return items; +} + +export async function getCyberNews(): Promise { + const results = await Promise.allSettled( + FEEDS.map(({ href, source }) => + axios + .get(href, { timeout: REQUEST_TIMEOUT_MS }) + .then((res) => parseFeed(res.data, source)), + ), + ); + + const items: NewsItem[] = []; + + results.forEach((result) => { + if (result.status === "fulfilled") { + items.push(...result.value); + } + }); + + if (items.length === 0) { + throw new Error("No cyber news available right now."); + } + + const seen = new Set(); + const deduped = items + .sort((a, b) => b.publishedAt - a.publishedAt) + .filter((item) => { + const key = item.link; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + return deduped.slice(0, MAX_ITEMS); +} diff --git a/vite.config.ts b/vite.config.ts index 779e44c..88e3255 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,46 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; +import type { ProxyOptions } from "vite"; + +const newsProxy: Record = { + "/news/thn": { + target: "https://feeds.feedburner.com", + changeOrigin: true, + rewrite: (p) => p.replace(/^\/news\/thn/, "/TheHackersNews"), + headers: { + "User-Agent": "Mozilla/5.0", + }, + }, + "/news/krebs": { + target: "https://krebsonsecurity.com", + changeOrigin: true, + rewrite: (p) => p.replace(/^\/news\/krebs/, "/feed/"), + headers: { + "User-Agent": "Mozilla/5.0", + }, + }, + "/news/cisa": { + target: "https://www.cisa.gov", + changeOrigin: true, + rewrite: (p) => + p.replace(/^\/news\/cisa/, "/cybersecurity-advisories/all.xml"), + headers: { + "User-Agent": "Mozilla/5.0", + }, + }, + "/news/bc": { + target: "https://www.bleepingcomputer.com", + changeOrigin: true, + rewrite: (p) => p.replace(/^\/news\/bc/, "/feed/"), + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", + Accept: + "application/rss+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.7", + }, + }, +}; const host = process.env.TAURI_DEV_HOST; @@ -24,6 +64,7 @@ export default defineConfig(async () => ({ port: 1420, strictPort: true, host: host || false, + proxy: newsProxy, hmr: host ? { protocol: "ws",