import React, { useEffect, useRef, useState } from 'react'; import { X, Download, AlertTriangle, Trash2, Eye } from '../icons'; import api from '../utils/api'; import { useEscapeKey } from '../hooks/useEscapeKey'; import { useFocusTrap } from '../hooks/useFocusTrap'; export interface CanaryTokenRow { uuid: string; kind: 'http' | 'dns' | 'aws_passive'; decky_name: string; // Set when the token targets a MazeNET topology decky. Null/absent // for fleet tokens. Drives the "scope" badge in the list and the // topology jump-link in the drawer. topology_id: string | null; blob_uuid: string | null; instrumenter: string | null; generator: string | null; placement_path: string; callback_token: string; placed_at: string; last_triggered_at: string | null; trigger_count: number; created_by: string; state: 'planted' | 'revoked' | 'failed'; last_error: string | null; } interface CanaryTrigger { uuid: string; token_uuid: string; occurred_at: string; src_ip: string; user_agent: string | null; request_path: string | null; dns_qname: string | null; headers: Record; attacker_id: string | null; } interface Props { token: CanaryTokenRow; onClose: () => void; onRevoked: (uuid: string) => void; } const Row: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
{label}
{value ?? }
); function fmt(iso: string | null): string { if (!iso) return '—'; const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } const STATE_COLOR: Record = { planted: '#00ff88', revoked: 'var(--dim-color)', failed: '#ff5555', }; const KIND_LABEL: Record = { http: 'HTTP CALLBACK', dns: 'DNS CALLBACK', aws_passive: 'AWS PASSIVE', }; const CanaryTokenDrawer: React.FC = ({ token, onClose, onRevoked }) => { const panelRef = useRef(null); useEscapeKey(onClose, true); useFocusTrap(panelRef, true); useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, []); const [triggers, setTriggers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [downloading, setDownloading] = useState(false); const [revoking, setRevoking] = useState(false); useEffect(() => { let cancelled = false; setLoading(true); setError(null); api.get(`/canary/tokens/${encodeURIComponent(token.uuid)}/triggers?limit=200`) .then((res) => { if (!cancelled) setTriggers(res.data.triggers || []); }) .catch((err) => { if (cancelled) return; const status = err?.response?.status; setError( status === 403 ? 'Viewer role required.' : status === 404 ? 'Token has been deleted.' : 'Failed to load triggers.' ); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [token.uuid]); const handleDownloadPreview = async () => { setDownloading(true); setError(null); try { const res = await api.get( `/canary/tokens/${encodeURIComponent(token.uuid)}/preview`, { responseType: 'blob' }, ); const blobUrl = URL.createObjectURL(res.data); const a = document.createElement('a'); a.href = blobUrl; a.download = token.placement_path.split('/').pop() || `canary-${token.callback_token}.bin`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); } catch (err: any) { const status = err?.response?.status; setError( status === 403 ? 'Admin role required to preview.' : status === 409 ? 'Token has no preview-able bytes (passive aws_creds, or blob deleted).' : 'Preview failed.' ); } finally { setDownloading(false); } }; const handleRevoke = async () => { if (!window.confirm(`Revoke canary token on ${token.decky_name}? This unlinks the file and stops the slug from resolving.`)) return; setRevoking(true); setError(null); try { await api.delete(`/canary/tokens/${encodeURIComponent(token.uuid)}`); onRevoked(token.uuid); } catch (err: any) { const status = err?.response?.status; setError( status === 403 ? 'Admin role required to revoke.' : status === 404 ? 'Token already gone.' : 'Revoke failed.' ); } finally { setRevoking(false); } }; const previewable = token.kind !== 'aws_passive'; const callbackUrl = token.kind === 'http' ? `/c/${token.callback_token}` : token.kind === 'dns' ? `${token.callback_token}.` : '— (passive bait, no callback)'; return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'flex-end', zIndex: 1000, }} >
CANARY TOKEN · {token.decky_name}
{token.placement_path}
{token.state.toUpperCase()} · {KIND_LABEL[token.kind]} · {token.trigger_count} {token.trigger_count === 1 ? 'hit' : 'hits'} {token.state === 'failed' && token.last_error && · {token.last_error}}
{previewable && ( )} {token.state === 'planted' && ( )}
{error && (
{error}
)}

METADATA

{token.uuid}} /> topology · {token.topology_id.slice(0, 8)}… ) : ( fleet )} /> {token.callback_token}} /> {callbackUrl}} />

CALLBACK HISTORY ({triggers.length}{triggers.length === 200 ? '+' : ''})

{loading &&
loading…
} {!loading && triggers.length === 0 && (
No callbacks yet. The slug will start firing if the artifact gets exfiltrated and opened.
)}
{triggers.map((t) => (
{t.src_ip} {fmt(t.occurred_at)}
{t.user_agent && (
UA · {t.user_agent}
)} {t.request_path && (
HTTP · {t.request_path}
)} {t.dns_qname && (
DNS · {t.dns_qname}
)} {t.attacker_id && (
attacker · {t.attacker_id}
)}
))}
); }; export default CanaryTokenDrawer;