Catalyst / admin/SAynapse-Horizon 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / SAynapse-Horizon

public

Self Hosted Cyber Threat Intelligence Hub

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
SAynapse-Horizon / Synapse-Horizon-v2 / backend / src / sources / rss.js 2718 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { XMLParser } from 'fast-xml-parser'
import { makeIoc, fetchWithTimeout, ok, SEVERITY } from '../normalize.js'

// RSS parser — public security advisory feeds (no auth). Feeds are configurable
// via the RSS_FEEDS env var (comma-separated URLs).
const DEFAULT_FEEDS = [
  { url: 'https://www.cisa.gov/cybersecurity-advisories/all.xml', name: 'CISA Advisories' },
  { url: 'https://feeds.feedburner.com/TheHackersNews', name: 'The Hacker News' },
  { url: 'https://www.bleepingcomputer.com/feed/', name: 'BleepingComputer' },
]
const PER_FEED = 6

const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' })
const HIGH_SIGNAL = /zero-day|0-day|wiper|ransomware|supply.chain|actively exploited|critical/i

export const meta = { key: 'rss', requiresKey: false }

export function configured() {
  return true
}

function feeds() {
  const env = process.env.RSS_FEEDS
  if (!env) return DEFAULT_FEEDS
  return env
    .split(',')
    .map((u) => u.trim())
    .filter(Boolean)
    .map((url) => ({ url, name: new URL(url).host }))
}

const asArray = (v) => (Array.isArray(v) ? v : v ? [v] : [])
const strip = (s) => String(s || '').replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim()

export async function fetchSource() {
  const iocs = []

  for (const feed of feeds()) {
    try {
      const res = await fetchWithTimeout(feed.url)
      const xml = await res.text()
      const doc = parser.parse(xml)
      // Support both RSS (<rss><channel><item>) and Atom (<feed><entry>).
      const items = asArray(doc?.rss?.channel?.item).length
        ? asArray(doc.rss.channel.item)
        : asArray(doc?.feed?.entry)

      for (const item of items.slice(0, PER_FEED)) {
        const title = strip(item.title?.['#text'] ?? item.title)
        if (!title) continue
        const link =
          item.link?.['@_href'] || (typeof item.link === 'string' ? item.link : '') || ''
        const published = item.pubDate || item.published || item.updated
        const critical = HIGH_SIGNAL.test(title)
        iocs.push(
          makeIoc('rss', {
            detectedAt: published,
            severity: critical ? SEVERITY.HIGH : SEVERITY.LOW,
            category: 'Advisory',
            indicator: title,
            indicatorType: 'advisory',
            target: feed.name,
            confidence: critical ? 70 : 55,
            mitre: [],
            headline: `Advisory: "${title}" published via the ${feed.name} feed`,
            raw: { title, link, published, source: feed.name },
            stix: { type: 'report', name: title, labels: ['threat-report'] },
          })
        )
      }
    } catch {
      // Skip an individual unreachable feed.
    }
  }

  return ok(iocs)
}