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 / server.js 3186 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
78
79
80
81
82
83
84
85
86
87
88
89
import express from 'express'
import cors from 'cors'
import { runSources, describeSources, SOURCE_KEYS } from './sources/index.js'

const PORT = Number(process.env.PORT) || 4000
const app = express()
app.use(cors())
app.use(express.json())

// -----------------------------------------------------------------------------
// In-memory cache. Holds the last successful pull per source plus a merged view.
// -----------------------------------------------------------------------------
const cache = {
  bySource: {}, // key -> { iocs, status, ts }
  generatedAt: null,
}

const mergedIocs = () =>
  Object.values(cache.bySource)
    .flatMap((s) => s.iocs)
    .sort((a, b) => new Date(b.detectedAt) - new Date(a.detectedAt))

const statusMap = () => {
  const out = {}
  for (const s of describeSources()) {
    const cached = cache.bySource[s.key]
    out[s.key] = {
      requiresKey: s.requiresKey,
      configured: s.configured,
      status: cached?.status?.status || (s.configured ? 'idle' : 'unconfigured'),
      count: cached?.iocs?.length || 0,
      message: cached?.status?.message || null,
      lastPull: cached?.ts || null,
    }
  }
  return out
}

const snapshot = () => ({
  generatedAt: cache.generatedAt,
  total: mergedIocs().length,
  sources: statusMap(),
  iocs: mergedIocs(),
})

/** Pull the given sources live and fold the results into the cache. */
async function sync(keys = SOURCE_KEYS) {
  const { iocs, status } = await runSources(keys)
  const ts = new Date().toISOString()
  const grouped = {}
  for (const key of keys) grouped[key] = []
  for (const ioc of iocs) (grouped[ioc.source] ||= []).push(ioc)

  for (const key of keys) {
    cache.bySource[key] = { iocs: grouped[key] || [], status: status[key] || { status: 'idle' }, ts }
  }
  cache.generatedAt = ts
  return snapshot()
}

// -----------------------------------------------------------------------------
// Routes
// -----------------------------------------------------------------------------
app.get('/api/health', (_req, res) => res.json({ ok: true, uptime: process.uptime() }))

app.get('/api/sources', (_req, res) => res.json({ sources: statusMap() }))

app.get('/api/feeds', (_req, res) => res.json(snapshot()))

app.post('/api/sync', async (req, res) => {
  const requested = Array.isArray(req.body?.sources) ? req.body.sources : SOURCE_KEYS
  const keys = requested.filter((k) => SOURCE_KEYS.includes(k))
  try {
    const result = await sync(keys.length ? keys : SOURCE_KEYS)
    res.json(result)
  } catch (err) {
    res.status(500).json({ error: String(err?.message || err) })
  }
})

app.listen(PORT, '0.0.0.0', () => {
  const configured = describeSources().filter((s) => s.configured).map((s) => s.key)
  console.log(`[synapse-horizon-api] listening on :${PORT}`)
  console.log(`[synapse-horizon-api] configured sources: ${configured.join(', ') || '(none — set API keys or HORIZON_DEMO=1)'}`)
  // Warm the cache in the background so /api/feeds has data on first load.
  sync()
    .then((s) => console.log(`[synapse-horizon-api] initial sync complete — ${s.total} IOCs`))
    .catch((e) => console.error('[synapse-horizon-api] initial sync failed:', e.message))
})