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 / src / components / DetailsDrawer.jsx 7571 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
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { useEffect } from 'react'
import { X, Crosshair, Boxes, Braces, Gauge, Fingerprint, Copy } from 'lucide-react'
import { SOURCE_MAP } from '../data/sources'
import { severityMeta, accentChip, confidenceMeta } from '../utils/theme'
import { formatUtc } from '../utils/format'

// Right-hand drill-down drawer: deep technical telemetry for one indicator.
export default function DetailsDrawer({ row, onClose }) {
  useEffect(() => {
    if (!row) return
    const onKey = (e) => e.key === 'Escape' && onClose()
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [row, onClose])

  if (!row) return null

  const sev = severityMeta(row.severity)
  const src = SOURCE_MAP[row.source]
  const conf = confidenceMeta(row.confidence)

  return (
    <div className="fixed inset-0 z-50 flex justify-end">
      <div
        className="absolute inset-0 bg-slate-950/70 backdrop-blur-sm"
        onClick={onClose}
        aria-hidden
      />
      <aside className="relative flex h-full w-full max-w-xl animate-drawer-in flex-col border-l border-slate-800 bg-slate-900 shadow-2xl">
        {/* Header */}
        <div className="flex items-start justify-between gap-3 border-b border-slate-800 px-5 py-4">
          <div className="min-w-0">
            <div className="flex items-center gap-2">
              <span className={`inline-flex rounded px-1.5 py-0.5 text-[10px] font-bold tracking-wide ${sev.badge}`}>
                {sev.label}
              </span>
              <span className={`inline-flex rounded px-1.5 py-0.5 text-[10px] font-semibold ${accentChip(src?.accent)}`}>
                {src?.name}
              </span>
              <span className="text-[11px] text-slate-500">{src?.provider}</span>
            </div>
            <h3 className="mt-2 text-sm font-medium leading-snug text-slate-100">{row.headline}</h3>
          </div>
          <button
            onClick={onClose}
            className="shrink-0 rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
            aria-label="Close details"
          >
            <X className="h-4 w-4" />
          </button>
        </div>

        {/* Body */}
        <div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-5 py-4">
          {/* Confidence + core attributes */}
          <div className="grid grid-cols-2 gap-3">
            <Panel icon={Gauge} title="Confidence Score">
              <div className="flex items-baseline gap-2">
                <span className={`text-2xl font-semibold ${conf.text}`}>{row.confidence}</span>
                <span className="text-xs text-slate-500">/ 100 · {conf.label}</span>
              </div>
              <div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-slate-800">
                <div className={`h-full rounded-full ${conf.bar}`} style={{ width: `${row.confidence}%` }} />
              </div>
            </Panel>
            <Panel icon={Fingerprint} title="Category">
              <p className="text-sm font-medium text-slate-200">{row.category}</p>
              <p className="mt-1 truncate text-[11px] text-slate-500" title={row.target}>
                {row.target}
              </p>
            </Panel>
          </div>

          {/* Indicator */}
          <Panel icon={Crosshair} title="Primary Indicator">
            <div className="flex items-center gap-2">
              <code className="min-w-0 flex-1 truncate rounded bg-slate-950/70 px-2 py-1.5 font-mono text-xs text-emerald-300 ring-1 ring-inset ring-slate-800">
                {row.indicator}
              </code>
              <CopyButton value={row.indicator} />
            </div>
            <dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
              <Field label="Indicator Type" value={row.indicatorType} mono />
              <Field label="Detected (UTC)" value={formatUtc(row.detectedAt)} mono />
              <Field label="Ingested (UTC)" value={formatUtc(row.ingestedAt)} mono />
              <Field label="Source Provider" value={src?.provider} />
            </dl>
          </Panel>

          {/* STIX / TAXII */}
          <Panel icon={Fingerprint} title="STIX / TAXII Attributes">
            <dl className="grid grid-cols-1 gap-y-2 text-xs">
              <Field label="Object Type" value={row.stix.type} mono />
              {row.stix.pattern && <Field label="Pattern" value={row.stix.pattern} mono wrap />}
              {row.stix.pattern_type && <Field label="Pattern Type" value={row.stix.pattern_type} mono />}
              {row.stix.labels && <Field label="Labels" value={row.stix.labels.join(', ')} mono />}
              {row.stix.name && <Field label="Name" value={row.stix.name} />}
            </dl>
          </Panel>

          {/* MITRE ATT&CK */}
          <Panel icon={Boxes} title="MITRE ATT&CK Mappings">
            <ul className="space-y-1.5">
              {row.mitre.map((t) => (
                <li
                  key={t.id}
                  className="flex items-center gap-2.5 rounded-md border border-slate-800 bg-slate-950/40 px-2.5 py-1.5"
                >
                  <span className="rounded bg-rose-500/10 px-1.5 py-0.5 font-mono text-[11px] font-semibold text-rose-300 ring-1 ring-inset ring-rose-500/25">
                    {t.id}
                  </span>
                  <span className="truncate text-xs text-slate-300">{t.name}</span>
                </li>
              ))}
            </ul>
          </Panel>

          {/* Raw payload */}
          <Panel icon={Braces} title="Raw Source Payload">
            <div className="relative">
              <div className="absolute right-2 top-2 z-10">
                <CopyButton value={JSON.stringify(row.raw, null, 2)} />
              </div>
              <pre className="max-h-72 overflow-auto rounded-lg bg-slate-950/80 p-3 font-mono text-[11px] leading-relaxed text-slate-300 ring-1 ring-inset ring-slate-800">
{JSON.stringify(row.raw, null, 2)}
              </pre>
            </div>
          </Panel>
        </div>

        <div className="border-t border-slate-800 px-5 py-3 text-center text-[10px] uppercase tracking-widest text-slate-600">
          Synapse-Horizon · Indicator {row.id}
        </div>
      </aside>
    </div>
  )
}

function Panel({ icon: Icon, title, children }) {
  return (
    <div className="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
      <div className="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-slate-400">
        <Icon className="h-3.5 w-3.5 text-slate-500" />
        {title}
      </div>
      {children}
    </div>
  )
}

function Field({ label, value, mono, wrap }) {
  return (
    <div className="min-w-0">
      <dt className="text-[10px] uppercase tracking-wide text-slate-500">{label}</dt>
      <dd className={`text-slate-300 ${mono ? 'font-mono text-[11px]' : 'text-xs'} ${wrap ? 'break-all' : 'truncate'}`}>
        {value || '—'}
      </dd>
    </div>
  )
}

function CopyButton({ value }) {
  const copy = () => {
    try {
      navigator.clipboard?.writeText(value)
    } catch {
      /* clipboard unavailable — non-fatal in the demo context */
    }
  }
  return (
    <button
      onClick={copy}
      className="shrink-0 rounded-md border border-slate-700 bg-slate-800/60 p-1.5 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
      aria-label="Copy to clipboard"
      title="Copy"
    >
      <Copy className="h-3.5 w-3.5" />
    </button>
  )
}